大規模JSONストリームの高速解析:POSIX awkによるメモリ効率的なSAX風パーサーの実装
【導入と前提】
大規模なJSONファイルをメモリ消費を抑えつつ高速に処理するため、POSIX準拠awkを用いたストリーミング解析手法を実装し、SRE業務の軽量化を図ります。
OS: Linux (Ubuntu/RHEL) または BSD系
ツール: POSIX awk, Bash 4.0+, curl (データ取得用)
前提:
jqがインストールされていない制限環境や、数GB超のJSONを1パスで処理したい状況を想定。
【処理フローと設計】
graph TD A["JSON Stream/File"] --> B["Pre-processor: Tokenizer"] B --> C["State Machine: POSIX awk"] C --> D["SAX Events: Path/Value Output"] D --> E["Log/Metrics Aggregator"]
JSONの構造を再帰的に読み込むのではなく、トークン({, }, [, ], :, ,)を区切り文字として扱い、スタック構造をawkの配列で管理することで、現在のパス(Path)と値(Value)をストリーム出力します。
【実装:堅牢な自動化スクリプト】
#!/bin/bash
# ==============================================================================
# JSON Streaming Parser (SAX-style) using POSIX awk
# ==============================================================================
set -euo pipefail
trap 'echo "Error at line $LINENO"; exit 1' ERR
# 一時ファイルのクリーンアップ設定
TMP_DATA=$(mktemp /tmp/json_stream.XXXXXX)
trap 'rm -f "$TMP_DATA"' EXIT
# --- JSON Streaming Parser Logic (POSIX awk) ---
# このawkスクリプトは、JSONをトークン化し、現在の階層パスと値を出力します。
parse_json() {
awk '
BEGIN {
FS = " "
RS = "([ \t\n\r]*[:\22,][ \t\n\r]*|[ \t\n\r]*[\{\}\[\]][ \t\n\r]*)"
# RT (Record Terminator) はGNU awk拡張だが、POSIXでは
# 文字単位の処理または巧妙なgsubで代用可能。ここでは汎用性を重視。
}
{
# トークンの種類に応じたスタック管理
token = RT
gsub(/[ \t\n\r]/, "", token)
if (token == "{") {
depth++
stack[depth] = "OBJECT"
} else if (token == "[") {
depth++
stack[depth] = "ARRAY"
array_idx[depth] = 0
} else if (token == "}" || token == "]") {
delete stack[depth]
delete array_idx[depth]
depth--
} else if (token == ":") {
# 直前のレコードがキー
current_key = $0
gsub(/^\"|\"$/, "", current_key)
} else if (token == "," || token == "}" || token == "]") {
# 値の確定
val = $0
if (length(val) > 0) {
gsub(/^[ \t]+|[ \t]+$/, "", val)
# パスの生成と出力
path = ""
for (i=1; i<=depth; i++) {
path = path "/" (stack[i] == "ARRAY" ? array_idx[i] : current_key)
}
print path "\t" val
}
if (stack[depth] == "ARRAY") array_idx[depth]++
}
}'
}
# --- 実行セクション ---
# サンプルとして公開APIからデータを取得しストリーム処理
SOURCE_URL="https://api.github.com/repos/kubernetes/kubernetes/releases/latest"
echo "Starting JSON stream processing..."
curl -sL "${SOURCE_URL}" | \
sed 's/\"/\"/g' | \
parse_json
# 補足:systemdタイマーで実行する場合のユニット例
# [Service]
# ExecStart=/usr/local/bin/json_parser.sh
# StandardOutput=append:/var/log/json_metrics.log
【検証と運用】
正常系の確認: スクリプトを実行し、
path/to/key [TAB] valueの形式で出力されることを確認します。./json_parser.sh | grep "tag_name"
ログ確認方法: systemdで運用している場合は
journalctlを使用します。journalctl -u json-parser.service -f
メモリ使用量の監視:
/usr/bin/time -vを用いて、入力サイズに関わらずRSS(常駐セットサイズ)が一定(数MB程度)に保たれていることを確認してください。
【トラブルシューティングと落とし穴】
エスケープ文字の扱い: POSIX awk単体ではバックスラッシュによるエスケープ(
\"等)の完全なパースが困難です。実運用ではsed 's/\\"//g'等で事前に正規化するか、より複雑な正規表現が必要です。GNU awk依存: 上記スクリプトで使用している
RT変数はGNU awk (gawk) の拡張機能です。純粋なPOSIX awk環境(mawk等)では、RSを1文字(例:RS="[{}[\],:]")に設定し、1文字ずつ評価するループを組む必要があります。巨大な文字列: 1つの値(文字列)が極端に大きい場合、awkのバッファサイズ制限(環境に依存)に抵触する可能性があります。
【まとめ】
運用の冪等性と堅牢性を維持するための3つのポイント:
ステートレスな設計: awk内で状態(depth/stack)を管理し、1行(1トークン)ごとに処理を完結させ、中断後の再開を容易にする。
標準入出力の活用:
curl | awk | loggerのパイプラインを維持し、一時ファイルへの依存を最小化してディスクI/O競合を防ぐ。シグナルハンドリング:
set -eとtrapによる確実な終了処理を実装し、異常終了時に不完全なデータが後続工程に流れないようにする。

