大規模JSONストリームのメモリ効率的な処理:POSIX awkによるSAX風パーサーの実装

Tech

{ “status”: “production”, “role”: “SRE/DevOps Engineer”, “architecture_pattern”: “SAX-style Event-driven Parsing”, “portability_level”: “POSIX High”, “robustness_features”: [“set -euo pipefail”, “trap cleanup”, “curl retry”] }

本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。

大規模JSONストリームのメモリ効率的な処理:POSIX awkによるSAX風パーサーの実装

【導入と前提】

メモリ制約のある環境で数GB規模のJSONを解析するため、DOMを構築せずストリーム処理で特定フィールドを抽出する基盤を構築します。

  • OS: Linux / BSD (POSIX準拠)

  • ツール: curl, awk (mawk/gawk両対応), sh (bash/zsh推奨)

  • 課題: jqがインストールされていない、または大規模JSONによりjqでOOM(Out of Memory)が発生する環境での代替。

【処理フローと設計】

graph TD
A["Remote API / File"] -->|curl/cat| B["Stream Segmenter"]
B -->|Tokenized Stream| C["POSIX awk State Machine"]
C -->|Match Logic| D["Filtered Output/Event"]
D -->|Log/Metrics| E["Downstream System"]

この設計では、JSON全体をメモリにロードするのではなく、文字列を1文字またはトークン単位で走査し、特定のキー(State)に到達したときのみ値を抽出します。

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

#!/usr/bin/env bash


# JSON Streaming Parser via POSIX awk


# Usage: ./json_parse.sh <URL>

set -euo pipefail
IFS=$'\n\t'

# テンポラリファイルのクリーンアップ処理

cleanup() {
    local exit_code=$?
    trap - EXIT

    # 必要に応じて一時ファイルを削除

    exit "${exit_code}"
}
trap cleanup EXIT INT TERM

# 環境変数設定(デフォルト値)

TARGET_URL="${1:-}"
RETRY_COUNT=3
RETRY_DELAY=2

if [[ -z "${TARGET_URL}" ]]; then
    echo "Usage: $0 <URL>" >&2
    exit 1
fi

# -----------------------------------------------------------------------------


# Core Logic: SAX-style Parser in AWK


# -----------------------------------------------------------------------------


# RS (Record Separator) をトークン境界として活用し、


# キーの階層(Path)をスタックで管理しながら目的の値を抽出する。


# -----------------------------------------------------------------------------

parse_json_stream() {

    # curlオプション: 


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

    curl -L -sS --retry "${RETRY_COUNT}" --retry-delay "${RETRY_DELAY}" "${TARGET_URL}" | \
    awk '
    BEGIN {

        # 文字列、数値、ブール、null、構造子を分離するためのRS設定


        # 簡易的な実装のため、エスケープされたダブルクォート等は考慮外

        RS = "([[:space:]]*:[[:space:]]*|[[:space:]]*,[[:space:]]*|[[:space:]]*[{][[:space:]]*|[[:space:]]*[}][[:space:]]*|[[:space:]]*\\[[[:space:]]*|[[:space:]]*\\][[:space:]]*)"
    }
    {
        token = $0
        gsub(/^[[:space:]"]+|[[:space:]"]+$/, "", token) # クォートと空白の除去

        # RT (Record Terminator) を参照して構造を把握

        if (RT ~ /\{/) {
            depth++; stack[depth] = last_key
        } else if (RT ~ /\}/) {
            depth--
        } else if (RT ~ /:/) {
            last_key = token
        } else if (RT ~ /,|\]/) {

            # 抽出条件: 例としてキーが "id" または "name" のものを出力

            if (last_key == "id" || last_key == "name") {

                # パス情報を付与して出力(SRE向けメトリクス形式)

                printf "path=%s, key=%s, value=%s\n", stack[depth], last_key, token
            }
        }
    }
    '
}

# 実行

parse_json_stream

【検証と運用】

1. 正常系確認

ローカルにモックサーバーを立てるか、公開API(例:GitHub API)を叩いて動作を確認します。

# GitHub APIからリポジトリ名とIDを抽出

./json_parse.sh "https://api.github.com/repos/kubernetes/kubernetes"

2. ログ確認と監視

systemd-cat を併用してログを journalctl に転送することで、パイプラインの破綻を検知できます。

./json_parse.sh "https://api.example.com/stream" 2>&1 | systemd-cat -t json-parser

# 確認コマンド

journalctl -t json-parser -f

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

  1. エスケープ文字の処理:

    • 上記の awk 実装は簡易版であり、\"(エスケープされたダブルクォート)を含む複雑な文字列で誤判定する可能性があります。厳密な要件では gsub によるプリプロセスが必要です。
  2. パイプラインの異常終了:

    • set -o pipefail を忘れると、curl が404エラーを返しても awk が正常終了してしまい、検知が遅れます。
  3. 大容量メモリ消費の回避:

    • awk 内で配列(stack)に巨大なデータを詰め込まないよう注意してください。あくまで「現在のパス(キー名)」のみを保持するのがSAX流のベストプラクティスです。

【まとめ】

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

  1. シグナルハンドリング: trap を用いて、異常終了時でも不完全な一時ファイルやゾンビプロセスを残さない。

  2. ネットワークの不確実性への備え: curl のリトライオプションを活用し、一時的な瞬断によるパイプライン停止を防ぐ。

  3. ステートレスな設計: 入力ストリームのオフセットに依存せず、トークン単位で評価することで、再実行時の冪等性を担保しやすくする。

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

コメント

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