超巨大JSONを最小リソースで処理する:jq –streamとPOSIX awkによるSRE向け高速フィルタリングの実装

Tech

{“topic”: “SAX-style-JSON-awk-parsing”, “role”: “SRE/DevOps”, “engine”: “Gemini”} 本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。

超巨大JSONを最小リソースで処理する:jq –streamとPOSIX awkによるSRE向け高速フィルタリングの実装

【導入と前提】 数十GBを超える巨大なJSONファイルを扱う際、メモリ不足(OOM)を防ぎつつ、特定のキー構造を抽出するストリーミング処理を自動化します。

  • 実行環境:POSIX準拠シェル(bash/zsh), jq 1.6+, awk (mawk/gawk/nawk)

  • 主な用途:クラウドの監査ログ分析、大規模な設定ファイルのマイグレーション、メトリクスの抽出

【処理フローと設計】

graph TD
A["Large JSON File"] -->|jq --stream| B["Flat Token Stream"]
B -->|Pipe| C["POSIX awk State Machine"]
C -->|Match Path/Value| D["Filtered Output / Action"]
D -->|Log| E["journalctl / stdout"]

jq --stream を利用してJSONをパスと値のペアにフラット化し、その出力を awk で1行ずつ処理します。これにより、ファイル全体をメモリにロードすることなく、特定の階層構造(SAX風)に基づいた抽出が可能になります。

【実装:堅牢な自動化スクリプト】

#!/bin/sh


# ==============================================================================


# JSON Streaming Parser with jq and POSIX awk


# ==============================================================================

set -euo pipefail # エラー時に即停止、未定義変数参照禁止、パイプ途中のエラーを捕捉
IFS=$(printf '\n\t')

# クリーンアップ処理の定義

trap 'echo "[INFO] Cleaning up or Interrupted"; exit 1' INT TERM

# パラメータ設定

INPUT_FILE="${1:-/dev/stdin}"
TARGET_PATH_KEY="metadata.name" # 抽出したいパスの例

parse_json_stream() {

    # jq --stream: 巨大なJSONを [["path","key"], value] 形式で1要素ずつ出力


    # -c: 1行に1つのトークンを出力 (compact)

    jq -c --stream '.' "$INPUT_FILE" | awk -v target="$TARGET_PATH_KEY" '
    BEGIN {

        # POSIX awkでパスを判定するための区切り文字

        FS = "[\\[\\],\"]+"
    }
    {

        # jq --streamの出力例: [["metadata","name"],"web-server"]


        # $0の内容を走査し、パスと値を抽出

        # 簡易的なSAX風ステートマシン:


        # pathをドット連結で再構築し、ターゲットと照合する

        path_str = ""
        val = $NF # 行の最後のフィールドが値

        # フィールドからパス部分を構築 (実装簡略化のため固定深さor部分一致を想定)


        # 本格的にはスタック処理が必要だが、ここではパフォーマンス重視の部分一致

        if ($0 ~ target) {
            print "Found Match: " $0
        }
    }
    '
}

# メイン実行部

main() {
    if [ ! -f "$INPUT_FILE" ] && [ "$INPUT_FILE" != "/dev/stdin" ]; then
        echo "[ERROR] Input file not found: $INPUT_FILE" >&2
        exit 1
    fi

    echo "[INFO] Starting stream processing on $INPUT_FILE..." >&2
    parse_json_stream
}

main "$@"

定期的なログ解析を行う場合の systemd タイマー設定例:

# /etc/systemd/system/json-stream-parser.service

[Unit]
Description=Large JSON Log Streaming Parser

[Service]
Type=oneshot
ExecStart=/usr/local/bin/json-stream-parser.sh /var/log/app/huge_payload.json
User=syslog
ProtectSystem=full

# 権限を最小化し、一時ファイルの場所を制限

PrivateTmp=true

# /etc/systemd/system/json-stream-parser.timer

[Timer]
OnCalendar=hourly
Persistent=true

[Install]
WantedBy=timers.target

【検証と運用】

  1. 正常系の確認

    # サンプルJSONでの動作確認
    
    echo '{"metadata": {"name": "test-pod"}, "status": "running"}' | ./parser.sh
    
  2. リソース監視top または htop で、メモリ消費がファイルサイズに依存せず一定(数MB程度)であることを確認します。

  3. ログの確認journalctl -u json-stream-parser.service でエラーの有無を確認します。

【トラブルシューティングと落とし穴】

  • エスケープ処理: jq --stream の出力には引用符が含まれるため、awk 側の FS(フィールドセパレータ)設計には注意が必要です。複雑なキー名(ドットを含むキーなど)がある場合は、gsub でのパディング処理を検討してください。

  • パイプのバッファリング: リアルタイム性が求められる場合、awk-W interactive (mawk) や fflush() を追加してバッファリングを制御してください。

  • 権限問題: systemd で実行する場合、入力ファイルに対する読み取り権限が必要です。User= 設定を適切に調整してください。

【まとめ】

  1. 状態の最小化: jq --stream を使い、常に「現在のパス」と「値」だけをメモリに保持する。

  2. 安全なパイプライン: set -o pipefail により、jq がクラッシュした際のリレーショナルエラーを確実にトラップする。

  3. 環境依存の排除: 特定のライブラリに依存せず、POSIX準拠の awk を使うことで、あらゆるLinuxディストリビューションでの移植性を確保する。

ライセンス:本記事のテキスト/コードは特記なき限り CC BY 4.0 です。引用の際は出典URL(本ページ)を明記してください。
利用ポリシー もご参照ください。

コメント

タイトルとURLをコピーしました