/var/www/yatta47.log

/var/www/yatta47.log

やったのログ置場です。スクラップみたいな短編が多いかと。

Kafka Producerのチューニングパラメータをシーケンス図で理解する

Kafka Producerのチューニングパラメータをシーケンス図で理解する

Kafka Producerのパラメータは数十個あるんですが、send() からACK受信までの内部シーケンスに沿って整理すると、どのパラメータがどの段階に効くかが一気に見えてきます。パラメータ一覧をフラットに眺めるより、シーケンス図1枚に紐付けた方が圧倒的にわかりやすいです。

パラメータ一覧を眺めていても迷子になる

Kafka Producerのパラメータ設定について調べていたんですよ。公式ドキュメントを開いたらパラメータが数十個並んでて、正直どこから手をつけていいかわからなかった。

で、ConfluentやAutoMQのドキュメントを読み込んでいくうちに、パラメータを一覧でフラットに見てるから迷子になるんだと気づいたんですよね。

Producerの内部処理をシーケンスとして捉えて、「この設定は処理のこの段階に効く」と紐付けると、チューニングの方向性が一気にクリアになりました。

Producerの内部処理フロー

send() を呼んでからブローカーにメッセージが届くまで、Producerの中では2つのスレッドが動いています。アプリケーションスレッドとSenderスレッド(バックグラウンドI/O)です。

sequenceDiagram
    participant App as アプリケーション
    participant Acc as RecordAccumulator
    participant Sender as Senderスレッド
    participant Broker as Broker

    App->>Acc: send() — メッセージ格納
    Note over Acc: パーティション別バッファ<br/>buffer.memory で上限管理

    loop バッチ判定
        Sender->>Acc: batch.size に達した?<br/>linger.ms 経過した?
    end

    Acc->>Sender: バッチ ready
    Note over Sender: compression.type で圧縮

    Sender->>Broker: バッチ送信<br/>(max.in.flight.requests.per.connection)
    Broker-->>Sender: ACK(acks: 0 / 1 / all)

    alt ACK成功
        Sender-->>App: Callback(成功)
    else ACK失敗 or タイムアウト
        Sender->>Broker: リトライ<br/>(retries / retry.backoff.ms)
    end

ここが面白いところで、send() は非同期なんですよね。呼んだ瞬間にアプリケーションスレッドには制御が返ってくる。

実際のネットワーク送信はSenderスレッドが裏でやってる。だからアプリ側の send() のレスポンスタイムと、メッセージがブローカーに届くまでの時間は別物なんですよ。

RecordAccumulatorはパーティションごとにバッチバッファを持っていて、同じパーティション宛のメッセージを効率的にまとめてくれます。

各パラメータの役割と設定の考え方

batch.size と linger.ms(バッチ段階)

この2つはペアで動きます。バッチが batch.size(デフォルト16KB)に達するか、linger.ms(デフォルト5ms)が経過するか、先に満たした方でバッチが送信される仕組みです。

flowchart TD
    A[メッセージ到着] --> B[バッチバッファに格納]
    B --> C{batch.size に到達?}
    B --> D{linger.ms 経過?}
    C -- Yes --> E[バッチ送信]
    D -- Yes --> E
    C -- No --> D

batch.sizeを大きくすると、1バッチに多くのメッセージが入ってリクエスト数が減るのでスループットが上がります。ただしバッチが埋まるまで待つ分レイテンシは上がる。

linger.msを大きくすると、待ち時間が増えてバッチが大きくなるのでスループットが上がる。こちらも待ち時間分レイテンシは上がります。

これ、知らなかったんですが、linger.msを0にしても必ずしもレイテンシが下がらないんですよね。1件ずつリクエストを飛ばすとブローカー側のリクエストキューが伸びて、結局待ち時間が増える。

AutoMQのベンチマークでは linger.ms=0 で平均27.5ms、linger.ms=5 で平均7.5msという結果が出ています。待った方が速いという逆説的な結果です。

acks(ACK段階)

メッセージがブローカーに届いたことをどのレベルで確認するかの設定です。

動作 トレードオフ
0 ACKを待たない レイテンシ最小。メッセージ消失のリスクあり
1 リーダーの書き込み完了を待つ バランス型。リーダー障害時に消失の可能性
all 全In-Sync Replicaの書き込み完了を待つ レイテンシ最大。消失リスク最小

デフォルトは all(Kafka 3.0以降)。acks=all でISR数が多いと、全レプリカの書き込み完了を待つ分ACK待ちの時間が長くなります。

Producerは max.in.flight.requests.per.connection(デフォルト5)まで並行送信できますが、ACK遅延がこの枠を埋め尽くすとスループットが下がるんですよね。

compression.type(圧縮段階)

バッチ単位で圧縮をかける設定です。

方式 圧縮率 CPU負荷 用途
none -- -- デフォルト
gzip 高い 高い サイズ重視
snappy 中程度 低い バランス型
lz4 中程度 最小 スループット重視で推奨
zstd 高い 中程度 圧縮率とCPUのバランス

圧縮するとネットワーク転送量が減ってスループットが上がる一方、圧縮処理のCPU負荷でレイテンシが上がります。

スループット重視ならlz4が効率的です。

buffer.memory(バッファ段階)

Producerがメッセージを一時的に保持するバッファの合計サイズで、デフォルトは32MB。バッファが満杯になると send() がブロックされ、max.block.ms(デフォルト60秒)を超えると TimeoutException で失敗します。

通常はデフォルトで問題ないですが、高スループット環境でProducerが詰まる場合は増やす必要があります。

注意点

Confluentのチューニングガイドでは、スループット最適化のケースとして batch.size の引き上げ、linger.ms の増加、compression.type=lz4acks=1 の組み合わせが挙げられています。

ただし、具体的な数値はベンチマーク環境(クラスタ構成、ネットワーク、メッセージサイズ)によって大きく変わるので、自分の環境で計測するのが前提です。

ユースケースに合わせた判断が必要なんですよね。

  • ログ収集やバッチ処理 → スループット寄り(batch.size大きめ、linger.ms長め、lz4圧縮)
  • イベント駆動やリアルタイム処理 → レイテンシ寄り(batch.size小さめ、linger.ms短め、acks=1)
  • 金融取引やオーダー管理 → 耐久性寄り(acks=all、リトライ設定そのまま)

あと、linger.msを0にすれば最速、というのは直感的に正しそうで実は罠です。

前述のベンチマークのとおり、適度にバッチングした方がブローカーへの負荷が分散されて結果的にレイテンシも下がるケースがあります。

まとめ

Kafka Producerのパラメータが多くて迷う原因は、パラメータ一覧をフラットに見ているからだったんですよね。send() からACK受信までのシーケンスに紐付けると、各パラメータが「どの段階の振る舞いを制御しているか」が見えて、チューニングの方向性が立てやすくなります。

この「処理シーケンスに設定を紐付ける」アプローチは、Kafkaに限らずNginxやデータベースなど設定パラメータが多いミドルウェア全般に使えるなと思いました。

参考