Claude CodeでKafkaのパーティションキー偏りをレビューさせる手順
プロデューサーのキー選択コードと、パーティション別のオフセット差を突き合わせて偏りの原因を絞る、読み取り専用のレビュー用サブエージェントの作り方です。
Kafkaのパーティション偏りは、コードを読むだけでも、オフセットを眺めるだけでも原因を絞れません。コードにはキーの選び方が、クラスターにはどのパーティションが太っているかが書かれているからです。この2つを突き合わせる作業を、読み取り専用のサブエージェントに任せます。
作るのは kafka-skew-reviewer という1ファイルです。tools でコード読み取りとBashだけに絞り、PreToolUseフックで --reset-offsets や --alter を弾きます。本番に近いブローカーへ向けても、状態を変える操作は通りません。
パーティション偏りが生まれる場所は5つに分かれる
プロデューサーがレコードをどのパーティションへ送るかは、partitioner.class の挙動で決まります。未設定のときの既定の動きは次のとおりです。
- キーがあれば、キーのハッシュでパーティションを選ぶ
- キーもパーティション指定も無ければ、
batch.sizeバイト溜まるまで同じパーティションに送り続け(スティッキー)、溜まったら切り替える
つまり、キーを付けたレコードの行き先はキーの値の分布で決まります。ユーザーIDをキーにすると、同じユーザーのデータは同じパーティションに集まります。順序保証や消費側の局所性が欲しいから、この設計にするわけです。裏返すと、特定のキーに書き込みが集中すれば、そのパーティションだけが太ります。
コードレビューで見る場所を、症状から逆引きできる形にします。
| 疑う箇所 | コードで探す語 | オフセットに出る形 |
|---|---|---|
| 値の種類が少ないキー | コードで探す語ProducerRecord、key=、send( の第2引数 | オフセットに出る形一部のパーティションだけが突出する |
| 一部のキーの流量が桁違い | コードで探す語キーの生成元(テナントID、顧客ID) | オフセットに出る形常に同じ1〜2本が太る |
null や固定文字列のキー | コードで探す語キーの代入箇所、デフォルト値 | オフセットに出る形1本に全件が集まる |
| 独自のパーティショナー | コードで探す語partitioner.class、Partitioner の実装 | オフセットに出る形実装次第で任意の偏り |
RoundRobinPartitioner | コードで探す語同クラス名の指定 | オフセットに出る形新しいバッチ作成時に不均等が出る既知の問題あり(KAFKA-9965) |
partitioner.ignore.keys を true にすると、プロデューサーはキーを使わずにパーティションを選びます。ただし独自パーティショナーを指定しているときは、この設定も partitioner.adaptive.partitioning.enable も効きません。レビューでは「設定が書いてあるのに効いていない」ケースを拾う必要があります。
パーティション数を後から増やした場合は、キーとパーティションの対応そのものが変わります。hash(key) % パーティション数 で決まる以上、増設前と同じキーが別のパーティションへ流れ始めます。新しいパーティションがクライアントに見えるまでは、metadata.max.age.ms の間隔ぶん、不均等が出ることもあります。
先に偏りを数字にする
サブエージェントに任せる前に、人間の手でも偏りを数字にしておくと、報告の当たり外れが判断できます。パーティション別の先頭と末尾のオフセットは kafka-get-offsets.sh で取れます。--time には latest(-1)と earliest(-2)を指定できます。
bin/kafka-get-offsets.sh --bootstrap-server localhost:9092 \
--topic orders --time latest | sort -t: -k2n > latest.txt
bin/kafka-get-offsets.sh --bootstrap-server localhost:9092 \
--topic orders --time earliest | sort -t: -k2n > earliest.txt出力は トピック名:パーティション番号:オフセット の形で、1パーティションが1行です。2つのファイルを並べて引き算すると、いま保持されているオフセットの幅がパーティションごとに出ます。
paste -d: earliest.txt latest.txt | awk -F: '
{ n=$6-$3; s+=n; v[NR]=n; p[NR]=$2 }
END { for (i=1;i<=NR;i++)
printf "partition=%s offsets=%d share=%.1f%%\n", p[i], v[i], 100*v[i]/s }'次の4行は、このawkを架空の入力(4パーティション、うち1本だけ9,000)で動かした結果です。
partition=0 offsets=1000 share=8.3%
partition=1 offsets=980 share=8.2%
partition=2 offsets=9000 share=75.0%
partition=3 offsets=1020 share=8.5%均等なら各25%前後になるはずが、1本が75%を占めています。偏りの有無はここで一目で分かります。
オフセットの差は、件数の目安として読みます。コンパクション(キーごとに最新値だけを残す設定)が有効なトピックでは古いレコードが消えるため、保持幅と実際の流入量が一致しないことがあります。件数そのものが必要なときは、別の指標で裏を取ってください。
コンシューマーグループの遅れを見る kafka-consumer-groups.sh --describe の LAG 列も、パーティション別に出ます。ただしこれは「処理が追いついていない」ことを示す数字で、偏りとは別物です。偏りがあれば遅れも偏りやすいので、2つを並べて読むと、太っているパーティションが遅れている原因かどうかを切り分けられます。
レビュー用サブエージェントを .claude/agents/ に置く
サブエージェントは、YAMLフロントマターと本文のシステムプロンプトで構成します。必須は name と description の2つだけです。tools を省くと、利用可能なツールをすべて継承します。レビュー専用にするなら、省かずに明示します。
---
name: kafka-skew-reviewer
description: Kafkaのパーティション偏りをレビューする。プロデューサーのキー選択コードと、トピックのパーティション別オフセット差を突き合わせ、ホットパーティションの原因を指摘する。偏り・ホットパーティション・キー設計の相談で使う。
tools: Read, Grep, Glob, Bash
model: inherit
maxTurns: 20
hooks:
PreToolUse:
- matcher: "Bash"
hooks:
- type: command
command: "./.claude/scripts/validate-kafka-readonly.sh"
---
あなたはKafkaのキー設計をレビューする担当です。ファイルは変更せず、
Kafkaクラスターの状態も変えません。
手順:
1. Grepでプロデューサーの送信箇所を探す(ProducerRecord、send(、
partitioner.class、partitioner.ignore.keys)。キーの生成元まで追う
2. kafka-get-offsets を --time earliest と --time latest で実行し、
パーティションごとのオフセット幅と全体に占める割合を出す
3. 偏りの大きいパーティションと、コード側のキーの候補を対応づける
4. 次の形式で報告する
報告形式:
- 偏りの数字(パーティション別の割合。最大と平均の比)
- 原因の候補(コードの根拠 ファイル:行 / 数字の根拠 の両方があるものを
「根拠あり」、片方だけなら「仮説」と明記)
- 直した場合の副作用(順序保証、パーティション数、既存データ)
- 確認できなかったこと(接続できない、コードが見つからない等)
数字かコードのどちらかが取れないときは、原因を断定せず、不足分を報告する。フロントマターの項目は、それぞれ次の役割です。
description: Claudeがいつこのサブエージェントへ委任するかの判断材料になる。「偏り」「ホットパーティション」のような依頼側の語を入れておくtools: 許可リスト。EditとWriteを含めないことで、ファイルを書き換えられなくするmaxTurns: エージェント的な往復の上限。上限に達すると、出力は途中経過として返り、Claudeが再開できるhooks: このサブエージェントが動いている間だけ有効なフック。終了すれば片付けられる
サブエージェントは、Claude Code本体のシステムプロンプトを受け取らず、この本文と作業ディレクトリなどの環境情報だけで動きます。プロンプトに手順と報告形式を書いておく理由はここにあります。
落とし穴
新しい agents ディレクトリを作った直後は読み込まれない
.claude/agents/ 内のファイルの追加や編集は、数秒で検知されます。ただし、セッション開始時に存在しなかったディレクトリの最初のファイルだけは、再起動するまで読み込まれません。--add-dir で追加したディレクトリ内の .claude/agents/ も監視対象外です。
プラグインとして配る場合は注意が要ります。プラグイン提供のサブエージェントでは、hooks、mcpServers、permissionMode は無視されます。この記事の構成をそのまま配布するなら、.claude/agents/ にコピーして使う形になります。
フックでKafka CLIを読み取り専用に絞る
tools: Bash を許すと、何でも実行できてしまいます。そこでPreToolUseフックを挟みます。フックには標準入力でJSONが渡され、実行しようとしているコマンドは tool_input.command に入っています。終了コード2で終われば、そのコマンドは実行されず、標準エラーのメッセージがサブエージェントに返ります。
次のスクリプトは、公式の読み取り専用DBリーダーの例を、拒否リストから許可リストへ組み替えたものです。
#!/bin/bash
# .claude/scripts/validate-kafka-readonly.sh
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
[ -z "$COMMAND" ] && exit 0
# コマンドの連結・置換・リダイレクトを持ち込ませない(パイプは後段で見る)
if echo "$COMMAND" | grep -qE ';|&|`|\$\(|>|<'; then
echo "Blocked: 連結・置換・リダイレクトは使えません" >&2
exit 2
fi
# 先頭は読み取り専用の Kafka CLI だけ許可
if ! echo "$COMMAND" | grep -qE '^([./A-Za-z0-9_-]*/)?kafka-(get-offsets|consumer-groups|topics)(\.sh)? '; then
echo "Blocked: kafka-get-offsets / consumer-groups / topics 以外は実行できません" >&2
exit 2
fi
# 状態を変えるオプションを拒否
if echo "$COMMAND" | grep -qE -- '--(reset-offsets|delete|alter|create|execute|delete-offsets)'; then
echo "Blocked: 状態を変えるオプションは使えません" >&2
exit 2
fi
# パイプ先は集計用コマンドだけ
if echo "$COMMAND" | grep -q '|'; then
AFTER=$(echo "$COMMAND" | sed 's/^[^|]*|//')
for part in $(echo "$AFTER" | tr '|' '\n' | awk '{print $1}'); do
case "$part" in sort|head|tail|wc) ;; *)
echo "Blocked: パイプ先は sort / head / tail / wc だけです" >&2; exit 2 ;;
esac
done
fi
exit 0macOSとLinuxでは、スクリプトに実行権限が無いと、フックは何もブロックせずに失敗します。必ず付与してください。
chmod +x ./.claude/scripts/validate-kafka-readonly.shダミーのJSONを流して挙動を確かめます。
echo '{"tool_input":{"command":"kafka-topics.sh --bootstrap-server b:9092 --delete --topic orders"}}' \
| ./.claude/scripts/validate-kafka-readonly.sh; echo "exit=$?"このスクリプトで、--time -2 付きの kafka-get-offsets、--describe 付きの kafka-consumer-groups.sh | sort は終了コード0で通りました。--delete や --reset-offsets、; での連結、rm の実行、curl へのパイプは終了コード2で止まりました。
awk を許可リストに入れていないのは、awk から外部コマンドを呼べるためです。集計が必要なら、前節のように自分で awk を流して数字だけを依頼文に貼ってください。サブエージェントには、各パーティションのオフセット幅を自分で引き算させる手もあります。数十パーティションなら現実的ですが、数百を超えるなら人間側で集計するほうが確実です。
このフックは、正規表現による簡易な門番です。Kafkaの認証が読み取り専用ユーザーで構成されていれば、そちらのほうが堅牢な最終防衛線になります。--command-config に読み取り専用の資格情報を指定するなど、ブローカー側の権限と二重にしてください。
依頼する:名前で呼び、材料を渡す
サブエージェントは、依頼文で名前を挙げると委任されます。確実に動かしたいときは @ でメンションします。
@"kafka-skew-reviewer (agent)" orders トピックの偏りを見て。
ブローカーは localhost:9092、プロデューサーは src/order/publisher.py。@agent-kafka-skew-reviewer と手で打つ形でも解決されます。メンションが決めるのはどのサブエージェントを呼ぶかだけで、サブエージェントに渡るプロンプトは、メンションを受けたClaudeが依頼内容から書き起こします。トピック名、ブローカー、プロデューサーのパスは、依頼文に含めてください。
報告は、システムプロンプトで指定した4項目の順に返るはずです。例えば次のような形になります。表の数字は、前節の架空の入力を使った書式の例です。
| パーティション | 割合 | 判定 |
|---|---|---|
| 2 | 割合75.0% | 判定突出(平均の3倍) |
| 0, 1, 3 | 割合8.2〜8.5% | 判定平均を下回る |
報告のうち、読む価値が高いのは次の2点です。
- 原因が「根拠あり」か「仮説」か。コードの行番号とオフセットの数字が両方揃っていれば根拠あり
- 「直した場合の副作用」の欄。ここが空なら、レビューは途中で止まっている
一方で、Claudeが示す原因は、プロデューサーのコードとオフセット幅という2つの材料からの推論です。キーの実際の値の分布は見えていません。たとえばテナントIDをキーにしている場合、「大口テナントが1社ある」ことまではコードから分かりません。トピックの一部を kafka-console-consumer で抜き出してキーの分布を数える、といった追加調査は、人間が判断して足します。
指摘を受けて直す前に確認すること
偏りの原因がキーだと分かっても、すぐ直せるとは限りません。
偏りへの手の打ち方と引き換えになるもの
キーを変える
同じ顧客のレコードが同じパーティションに入るという前提が崩れます。消費側がキー単位の順序に依存していないかを、先にコードで確かめます。
パーティション数を増やす
運用ドキュメントが挙げる副作用は、増設前後でキーの行き先が変わることです。既存キーの順序保証に影響し、既存データは自動で再配置されません。偏りの原因が1つのキーなら、増やしても解消しないこともあります。
キーを無視する
partitioner.ignore.keys を true にすると均等に近づきますが、キー単位でまとまる性質は失われます。キー単位の集計や順序が要る処理には向きません。
増設については、パーティション数を増やしたときに auto.offset.reset=latest のコンシューマーが、新しいパーティションの先頭を取りこぼす場合がある点にも注意が必要です。運用ドキュメントは、パーティション数が増えうるトピックを読むコンシューマーには by_duration を使う方法を示しています。
レビュー用サブエージェントは、こうした副作用の洗い出しにも使えます。別の文脈でもう一度差分を検証させる設計は、敵対的レビューの記事で扱っています。MCPサーバー経由でトピックやコンシューマーグループを調べたい場合は、Kafka MCPサーバーの記事が参考になります。パーティション設計そのものを別のミドルウェアで考える場合は、PostgreSQLのパーティション化の手順が近い考え方です。
まとめ
偏りのレビューは、コード側の「キーの選び方」と、クラスター側の「パーティション別の太さ」の両方が揃ってはじめて原因の絞り込みになります。サブエージェントには、EditとWriteを外した tools、許可リスト型のPreToolUseフック、根拠と仮説を分ける報告形式の3つを持たせると、安全に任せられます。キーの値の分布のように、コードとオフセットからは見えない材料は、人間が足してください。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。