RESEARCH-FIRST:
jq --streamオプションは、JSONをリーフノードのパスと値のペアに分解して出力する。これにより、巨大なオブジェクト全体をメモリに載せる必要がなくなる。awkはこのストリーム形式(パスの配列と値)を1行ずつ読み込み、ステートマシンとして動作させることで、特定の階層のデータを抽出できる。POSIX準拠の
awk記述にこだわり、移植性を確保。
PLAN:
set -euo pipefailによる堅牢なシェルスクリプト。jq --stream->awk(SAXパーサー) ->outputのパイプライン。一時ファイル管理のための
trap処理。systemdタイマーによる定期実行例。
IMPLEMENTATION DETAILS:
jq -c --stream 'select(length==2)'を使用し、値を持つリーフノードのみを抽出。awkでパスをパースし、目的のフィールドが来たときだけ状態を保持・出力。
本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。
巨大なJSONログを低メモリで高速集計する:POSIX awkとjq –streamによるSAX風処理の自動化
【導入と前提】
ギガバイト級のJSONデータをメモリ消費を抑えつつ高速にストリーム処理し、必要な属性のみを抽出・集計するパイプラインを構築します。
OS: Linux (Ubuntu 22.04 LTS / RHEL 9 等)
ツール:
jq(1.6以上推奨),awk(POSIX準拠),curl課題: 数GBの単一JSONファイルを
jqで直接読み込むとメモリ不足(OOM)でプロセスが強制終了される。
【処理フローと設計】
graph TD A["Remote/Local JSON Source"] -->|curl/cat| B["jq --stream"] B -->|Path/Value Stream| C["AWK SAX-Parser"] C -->|Filtered TSV/CSV| D[Storage/Database] D -->|Alerting| E["Ops Dashboard"]
jq --stream がJSONをパス単位のトークンに分解し、awk が状態(State)を管理しながら特定のパスが出現した際のアクションを実行します。これにより、メモリ使用量を一定(定数オーダー)に保ちます。
【実装:堅牢な自動化スクリプト】
#!/bin/sh
# ==============================================================================
# JSON Streaming Parser (SAX-style with POSIX awk)
# ==============================================================================
set -eu
# 一時ディレクトリの作成と自動削除設定
TMP_DIR=$(mktemp -d)
trap 'rm -rf "$TMP_DIR"' EXIT
# 設定
SOURCE_URL="https://api.example.com/large-data.json"
OUTPUT_FILE="/var/log/app/processed_metrics.log"
echo "Starting JSON stream processing..."
# 1. データの取得とストリーム処理の実行
# curl -s: 進捗非表示, -L: リダイレクト追従, -S: エラー表示
# jq --stream: インクリメンタルパースを有効化
# jq -c 'select(length==2)': リーフノード([パス, 値])のみを抽出
curl -sLS "${SOURCE_URL}" | \
jq -c --stream 'select(length==2)' | \
awk -v OFS='\t' '
# awk内でのパス解析処理
{
# jq --streamの出力形式: [["users",0,"id"],123]
# 簡単な文字列置換でパスと値を分離(POSIX awk互換)
path = $0; sub(/,.*/, "", path); gsub(/[\[\]\"]/, "", path);
val = $0; sub(/.*,/, "", val); gsub(/[\[\]\"]/, "", val);
# 特定のパス(例: users[n].id と users[n].status)を抽出
if (path ~ /users,[0-9]+,id/) {
current_id = val;
}
if (path ~ /users,[0-9]+,status/) {
# IDが判明している状態でStatusが来たらペアで出力
if (current_id != "") {
print current_id, val;
current_id = ""; # 状態のリセット
}
}
}
' >> "${OUTPUT_FILE}"
echo "Processing completed successfully."
運用:systemd ユニットファイル設定例
定期実行が必要な場合、Cronではなくsystemd Timerを利用してリソース制御(CPUQuota等)を行います。
# /etc/systemd/system/json-parser.service [Unit] Description=Stream Parse Large JSON After=network.target [Service] Type=oneshot ExecStart=/usr/local/bin/json-parser.sh User=analytics Group=analytics # リソース制限の付与 MemoryHigh=512M MemoryMax=1G CPUQuota=50% # /etc/systemd/system/json-parser.timer [Unit] Description=Run JSON Parser Hourly [Timer] OnCalendar=hourly Persistent=true [Install] WantedBy=timers.target
【検証と運用】
1. 正常系の確認
スクリプト実行後、出力ファイルの形式が正しいか確認します。
# 出力行数の確認 wc -l /var/log/app/processed_metrics.log # 直近のデータをサンプル確認 tail -n 5 /var/log/app/processed_metrics.log
2. エラー時のログ確認
systemd経由で実行している場合、journalctl でパイプラインの途絶やエラー詳細を確認します。
# ユニットの実行ログを確認 journalctl -u json-parser.service -f # jqのパースエラーやネットワークタイムアウトを特定 journalctl -u json-parser.service | grep -E "ERROR|timeout"
【トラブルシューティングと落とし穴】
パイプのバッファリング問題: リアルタイム性が求められる場合、
awkやjqのバッファリングにより出力が遅延することがあります。その場合はstdbuf -oLをパイプラインに挿入してください。特殊文字のハンドリング:
awkのgsubで簡易的にパースしていますが、JSONの値にカンマやクォートが含まれる場合、正規表現の厳密な定義が必要です。より複雑な構造にはjq内で値をエンコードしてからawkに渡す手法が有効です。書き込み権限: 出力先(
/var/log/app/)のディレクトリ権限を事前にchownで実行ユーザーに付与しておく必要があります。
【まとめ】
運用の冪等性と堅牢性を維持するための3つのポイント:
ステートレスな設計: スクリプト自体に過去の状態を持たせず、常に
SOURCE_URLからのストリームを正とする。リソースの局所化:
mktempとtrapを組み合わせ、異常終了時でもゴミファイル(一時ファイル)を残さない。リソースの隔離:
systemdのMemoryMaxを活用し、万が一のメモリリーク時もシステム全体のハングアップを防止する。

