<p><metadata>
{
“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”
}
</metadata>
<thinking>
大規模なJSONファイルを扱う際、メモリ不足(OOM)を回避しつつ高速にフィルタリング・集計を行うための「SAX風(ストリーム指向)」処理を実装します。</thinking></p>
<ol class="wp-block-list">
<li><p><strong>RESEARCH-FIRST</strong>: </p>
<ul>
<li><p><code>jq --stream</code> オプションは、JSONをリーフノードのパスと値のペアに分解して出力する。これにより、巨大なオブジェクト全体をメモリに載せる必要がなくなる。</p></li>
<li><p><code>awk</code> はこのストリーム形式(パスの配列と値)を1行ずつ読み込み、ステートマシンとして動作させることで、特定の階層のデータを抽出できる。</p></li>
<li><p>POSIX準拠の <code>awk</code> 記述にこだわり、移植性を確保。</p></li>
</ul></li>
<li><p><strong>PLAN</strong>:</p>
<ul>
<li><p><code>set -euo pipefail</code> による堅牢なシェルスクリプト。</p></li>
<li><p><code>jq --stream</code> -> <code>awk</code> (SAXパーサー) -> <code>output</code> のパイプライン。</p></li>
<li><p>一時ファイル管理のための <code>trap</code> 処理。</p></li>
<li><p>systemdタイマーによる定期実行例。</p></li>
</ul></li>
<li><p><strong>IMPLEMENTATION DETAILS</strong>:</p>
<ul>
<li><p><code>jq -c --stream 'select(length==2)'</code> を使用し、値を持つリーフノードのみを抽出。</p></li>
<li><p><code>awk</code> でパスをパースし、目的のフィールドが来たときだけ状態を保持・出力。
</p></li>
</ul></li>
</ol>
<p>本記事は<strong>Geminiの出力をプロンプト工学で整理した業務ドラフト(未検証)</strong>です。</p>
<h1 class="wp-block-heading">巨大なJSONログを低メモリで高速集計する:POSIX awkとjq –streamによるSAX風処理の自動化</h1>
<h2 class="wp-block-heading">【導入と前提】</h2>
<p>ギガバイト級のJSONデータをメモリ消費を抑えつつ高速にストリーム処理し、必要な属性のみを抽出・集計するパイプラインを構築します。</p>
<ul class="wp-block-list">
<li><p><strong>OS</strong>: Linux (Ubuntu 22.04 LTS / RHEL 9 等)</p></li>
<li><p><strong>ツール</strong>: <code>jq</code> (1.6以上推奨), <code>awk</code> (POSIX準拠), <code>curl</code></p></li>
<li><p><strong>課題</strong>: 数GBの単一JSONファイルを <code>jq</code> で直接読み込むとメモリ不足(OOM)でプロセスが強制終了される。</p></li>
</ul>
<h2 class="wp-block-heading">【処理フローと設計】</h2>
<div class="wp-block-merpress-mermaidjs diagram-source-mermaid"><pre class="mermaid">
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"]
</pre></div>
<p><code>jq --stream</code> がJSONをパス単位のトークンに分解し、<code>awk</code> が状態(State)を管理しながら特定のパスが出現した際のアクションを実行します。これにより、メモリ使用量を一定(定数オーダー)に保ちます。</p>
<h2 class="wp-block-heading">【実装:堅牢な自動化スクリプト】</h2>
<div class="codehilite">
<pre data-enlighter-language="generic">#!/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."
</pre>
</div>
<h3 class="wp-block-heading">運用:systemd ユニットファイル設定例</h3>
<p>定期実行が必要な場合、Cronではなくsystemd Timerを利用してリソース制御(CPUQuota等)を行います。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># /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
</pre>
</div>
<h2 class="wp-block-heading">【検証と運用】</h2>
<h3 class="wp-block-heading">1. 正常系の確認</h3>
<p>スクリプト実行後、出力ファイルの形式が正しいか確認します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># 出力行数の確認
wc -l /var/log/app/processed_metrics.log
# 直近のデータをサンプル確認
tail -n 5 /var/log/app/processed_metrics.log
</pre>
</div>
<h3 class="wp-block-heading">2. エラー時のログ確認</h3>
<p>systemd経由で実行している場合、<code>journalctl</code> でパイプラインの途絶やエラー詳細を確認します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic"># ユニットの実行ログを確認
journalctl -u json-parser.service -f
# jqのパースエラーやネットワークタイムアウトを特定
journalctl -u json-parser.service | grep -E "ERROR|timeout"
</pre>
</div>
<h2 class="wp-block-heading">【トラブルシューティングと落とし穴】</h2>
<ol class="wp-block-list">
<li><p><strong>パイプのバッファリング問題</strong>:
リアルタイム性が求められる場合、<code>awk</code> や <code>jq</code> のバッファリングにより出力が遅延することがあります。その場合は <code>stdbuf -oL</code> をパイプラインに挿入してください。</p></li>
<li><p><strong>特殊文字のハンドリング</strong>:
<code>awk</code> の <code>gsub</code> で簡易的にパースしていますが、JSONの値にカンマやクォートが含まれる場合、正規表現の厳密な定義が必要です。より複雑な構造には <code>jq</code> 内で値をエンコードしてから <code>awk</code> に渡す手法が有効です。</p></li>
<li><p><strong>書き込み権限</strong>:
出力先(<code>/var/log/app/</code>)のディレクトリ権限を事前に <code>chown</code> で実行ユーザーに付与しておく必要があります。</p></li>
</ol>
<h2 class="wp-block-heading">【まとめ】</h2>
<p>運用の冪等性と堅牢性を維持するための3つのポイント:</p>
<ol class="wp-block-list">
<li><p><strong>ステートレスな設計</strong>: スクリプト自体に過去の状態を持たせず、常に <code>SOURCE_URL</code> からのストリームを正とする。</p></li>
<li><p><strong>リソースの局所化</strong>: <code>mktemp</code> と <code>trap</code> を組み合わせ、異常終了時でもゴミファイル(一時ファイル)を残さない。</p></li>
<li><p><strong>リソースの隔離</strong>: <code>systemd</code> の <code>MemoryMax</code> を活用し、万が一のメモリリーク時もシステム全体のハングアップを防止する。</p></li>
</ol>
{
“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風(ストリーム指向)」処理を実装します。
RESEARCH-FIRST:
jq --stream オプションは、JSONをリーフノードのパスと値のペアに分解して出力する。これにより、巨大なオブジェクト全体をメモリに載せる必要がなくなる。
awk はこのストリーム形式(パスの配列と値)を1行ずつ読み込み、ステートマシンとして動作させることで、特定の階層のデータを抽出できる。
POSIX準拠の awk 記述にこだわり、移植性を確保。
PLAN:
IMPLEMENTATION DETAILS:
本記事は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 を活用し、万が一のメモリリーク時もシステム全体のハングアップを防止する。
コメント