POSIX awkとストリーム処理による大規模JSONのメモリ節約型抽出パイプライン

Tech

{ “engine”: “Gemini-1.5-Pro”, “mode”: “SRE-DevOps-Draft”, “focus”: “POSIX-awk-SAX-JSON”, “safety_level”: “Strict-Shell-Check” } 本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。

POSIX awkとストリーム処理による大規模JSONのメモリ節約型抽出パイプライン

【導入と前提】

本構成は、数GB規模の巨大なJSONファイルをメモリにロードせず、POSIX準拠の awk を用いてSAX風(イベント駆動型)に高速処理する運用を自動化します。

  • 解決する課題: メモリ不足によるOOM Killerの回避、jq単体での複雑な状態遷移処理の簡素化。

  • 前提条件:

    • OS: GNU/Linux (Ubuntu/RHEL/Debian) または Alpine Linux

    • ツール: jq (トークナイザーとして利用), awk (POSIX準拠), curl

【処理フローと設計】

graph TD
A["Large JSON Source"] -->|curl/cat| B["jq --stream"]
B -->|Flat Path-Value Stream| C["POSIX awk State Machine"]
C -->|Filter/Aggregate| D["Cleaned Data/Alert"]
D -->|Output| E[Logs/Metrics]
  1. 入力層: jq --stream を利用し、JSONツリーを深さ優先探索のパスと値のペアにフラット化します。

  2. 処理層: awk が現在のパスを内部変数(State)として保持し、特定の条件に合致したタイミングでアクションを実行します(SAX方式)。

  3. 出力層: 必要なフィールドのみを抽出し、後続のDBインサートや監視ツールへ渡します。

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

#!/bin/sh


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


# JSON Streaming Parser with POSIX awk


# Description: Processes massive JSON using jq-stream and awk state machine.


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

set -eu

# 常にクリーンな環境を保つための終了処理

trap 'rm -f "$TMP_FILE"' EXIT
TMP_FILE=$(mktemp)

# 設定: 抽出対象のキー

TARGET_KEY="metadata.labels.app"

process_json_stream() {
    local input_src="$1"

    # jq --stream は JSON を [["path","to"], value] の形式で1行ずつ出力する


    # これにより、巨大なJSONもメモリを消費せずにスキャン可能

    cat "$input_src" | \
    jq -c --stream 'select(length == 2)' | \
    awk -F'[,\[\]\"]+' '

        # [["path","to","key"], "value"] の形式をパース


        # POSIX awk では動的正規表現や配列操作に制限があるためシンプルに保つ

        {

            # パスを結合して現在のコンテキストを把握

            path = ""
            for (i=2; i<NF-1; i++) {
                if ($i != "") {
                    path = (path == "" ? $i : path "." $i)
                }
            }
            value = $(NF-1)

            # 特定の条件(SAXイベント相当)で処理を実行

            if (path ~ /'${TARGET_KEY}'/) {
                print "Found '"${TARGET_KEY}"': " value
            }
        }
    '
}

# 実行例: curlのリトライ処理を含めた堅牢な取得

main() {
    local api_url="http://localhost:8080/v1/massive-data.json"

    echo "[INFO] Starting JSON stream processing..."

    # -s: 進捗非表示, -S: エラー表示, -L: リダイレクト追従, --retry: ネットワークエラー時再試行

    if ! curl -sSL --retry 3 "$api_url" -o "$TMP_FILE"; then
        echo "[ERROR] Failed to fetch data from $api_url" >&2
        exit 1
    fi

    process_json_stream "$TMP_FILE"
}

main "$@"

補足:systemdによる定期実行(Timer)例

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

[Unit]
Description=Stream JSON Parser Service
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/json-parser.sh
User=parser-user

# リソース制限の付与

MemoryLimit=512M

【検証と運用】

1. 正常系の確認

モックデータを作成し、パイプラインが正しくパスを解釈できるかテストします。

echo '{"metadata": {"labels": {"app": "sre-tool"}}}' | jq -c --stream 'select(length == 2)'

# 出力例: [["metadata","labels","app"],"sre-tool"]

2. ログ確認

systemd経由で実行している場合、以下のコマンドで進捗およびエラーを確認します。

journalctl -u json-parser.service -f

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

  1. エスケープ文字の扱い:

    • awk のフィールドセパレータ -F に指定する文字(例: ")がデータ内に含まれる場合、パス解析がずれる可能性があります。厳密な処理が必要な場合は、jq 側でパスを一度ユニークな区切り文字(例: \t)に置換してから awk に渡してください。
  2. 権限問題:

    • スクリプトが /tmp に書き込む権限があるか、実行ユーザーに curljq の実行パスが通っているか確認してください。
  3. シグナルハンドリング:

    • set -e を使用しているため、パイプライン途中の grep などが 0 件ヒットで終了するとスクリプト全体が停止します。意図的な空ヒットを許容する場合は || true を活用してください。

【まとめ】

運用の冪等性と堅牢性を維持するための3つのポイント:

  1. ステートレスな設計: 一時ファイルは trap で必ず削除し、前回の実行状態に依存しないようにする。

  2. トークナイズの分離: jq に構造解析(字句解析)を任せ、awk はビジネスロジック(状態遷移)に専念させることで、POSIX準拠でも複雑な処理が可能になる。

  3. リソース制約の明示: systemdcgroups を併用し、万が一の無限ループやメモリリーク時にもシステム全体を保護する。

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

コメント

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