巨大なJSONログを低メモリで高速集計する:POSIX awkとjq –streamによるSAX風処理の自動化

Tech

{ “status”: “draft”, “topic”: “POSIX-compliant AWK and jq streaming for memory-efficient JSON parsing”, “tags”: [“SRE”, “Bash”, “jq”, “awk”, “Performance”, “DevOps”], “engine”: “Gemini 1.5 Pro” } 大規模なJSONファイルを扱う際、メモリ不足(OOM)を回避しつつ高速にフィルタリング・集計を行うための「SAX風(ストリーム指向)」処理を実装します。

  1. RESEARCH-FIRST:

    • jq --stream オプションは、JSONをリーフノードのパスと値のペアに分解して出力する。これにより、巨大なオブジェクト全体をメモリに載せる必要がなくなる。

    • awk はこのストリーム形式(パスの配列と値)を1行ずつ読み込み、ステートマシンとして動作させることで、特定の階層のデータを抽出できる。

    • POSIX準拠の awk 記述にこだわり、移植性を確保。

  2. PLAN:

    • set -euo pipefail による堅牢なシェルスクリプト。

    • jq --stream -> awk (SAXパーサー) -> output のパイプライン。

    • 一時ファイル管理のための trap 処理。

    • systemdタイマーによる定期実行例。

  3. 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"

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

  1. パイプのバッファリング問題: リアルタイム性が求められる場合、awkjq のバッファリングにより出力が遅延することがあります。その場合は stdbuf -oL をパイプラインに挿入してください。

  2. 特殊文字のハンドリング: awkgsub で簡易的にパースしていますが、JSONの値にカンマやクォートが含まれる場合、正規表現の厳密な定義が必要です。より複雑な構造には jq 内で値をエンコードしてから awk に渡す手法が有効です。

  3. 書き込み権限: 出力先(/var/log/app/)のディレクトリ権限を事前に chown で実行ユーザーに付与しておく必要があります。

【まとめ】

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

  1. ステートレスな設計: スクリプト自体に過去の状態を持たせず、常に SOURCE_URL からのストリームを正とする。

  2. リソースの局所化: mktemptrap を組み合わせ、異常終了時でもゴミファイル(一時ファイル)を残さない。

  3. リソースの隔離: systemdMemoryMax を活用し、万が一のメモリリーク時もシステム全体のハングアップを防止する。

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

コメント

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