RabbitMQのプリフェッチ設定を極め、コンシューマのパフォーマンスを最適化する

RabbitMQのプリフェッチを調整して、コンシューマがメッセージを溜め込まず、遅い処理を隠さずに、ビジー状態を維持できるようにします。

RabbitMQのプリフェッチ設定を極め、コンシューマのパフォーマンスを最適化する

RabbitMQのプリフェッチは、小さな設定でありながらすべてを変えるものです。これは、RabbitMQがコンシューマに一度に保持させる未承認メッセージの数を制御します。低く設定しすぎると、高速なコンシューマが次の配信を待つ時間が長くなります。高く設定しすぎると、低速なコンシューマが静かに作業を溜め込み、レイテンシが増加し、キューの深さのグラフが嘘をつくようになります。

プリフェッチを考える便利な方法は、未完了の作業です。プリフェッチが20の場合、コンシューマは20個のメッセージを配信されたがまだ承認していない状態を保持できます。それらのメッセージはキュー内でreadyではなくなります。それらはunackedとなり、コンシューマがack、nack、reject、または切断するまでコンシューマに留まります。

つまり、プリフェッチは単なるスループットのつまみではありません。それは公平性のつまみであり、メモリのつまみであり、障害回復のつまみでもあります。

RabbitMQにおけるbasic.qosの動作

コンシューマはbasic.qosでプリフェッチを設定します。ほとんどのクライアントライブラリではprefetch_countを設定します。prefetch_sizeはほとんど使用されず、通常はゼロのままです。

PythonとPikaの場合:

channel.basic_qos(prefetch_count=10)
channel.basic_consume(
    queue="jobs",
    on_message_callback=handle_message,
    auto_ack=False,
)

Node.jsとamqplibの場合:

await channel.prefetch(10);
await channel.consume("jobs", async (msg) => {
  try {
    await handleMessage(msg.content);
    channel.ack(msg);
  } catch (err) {
    channel.nack(msg, false, false);
  }
}, { noAck: false });

手動確認が重要です。自動確認を使用する場合、RabbitMQはメッセージが配信されるとすぐに完了したと見なします。プリフェッチは、管理する未確認のウィンドウがないため、同じように処理の信頼性を保護しなくなります。

RabbitMQは、AMQPの元の文言がチャネル指向であるにもかかわらず、現代の使用ではデフォルトでコンシューマごとにプリフェッチを適用します。一部のクライアントはglobalフラグを公開しています。注意して使用してください。共有チャネルまたは接続全体の制限は、コンシューマ間で混乱を招く相互作用を生み出す可能性があります。ほとんどのサービスは、各コンシューマが独自のチャネルと独自のプリフェッチカウントを持っている場合、推論が容易になります。

プリフェッチがレイテンシを変える理由

2つのコンシューマを持つキューを想像してください。コンシューマAは100個のメッセージのバッチを受け取り、その後低速な外部APIにヒットします。コンシューマBは正常で高速ですが、それらの100個のメッセージはすでにAに割り当てられています。RabbitMQは、Aがそれらを拒否するか、そのチャネルが閉じない限り、それらをBに与えません。

キューの観点からは、それらのメッセージは準備ができていません。ユーザーの観点からは、それらは遅延しています。これが、高いプリフェッチがブローカーのグラフではシステムを良く見せながら、実際のレイテンシを悪化させる理由です。

低いプリフェッチは、RabbitMQに作業をより公平に分散する機会を多く与えます。高いプリフェッチは、コンシューマにより多くのローカル作業とより少ないブローカーラウンドトリップを与えます。どちらも常に正しいわけではありません。

意味のある開始値

低速なジョブの場合は、小さく始めてください。各メッセージがサードパーティのAPIを呼び出したり、複数のデータベース行を書き込んだり、CPU負荷の高い変換を行ったりする場合は、prefetch_count=1から10を試してください。失敗した、または低速なコンシューマが保持する作業量を少なくしたい場合です。

数十から数百ミリ秒かかり、安定したワーカーで実行される中程度のジョブの場合、10、20、50などの値が一般的な開始点です。より高い値にする前に測定してください。

ブローカーとコンシューマが低レイテンシネットワーク上にある非常に高速なハンドラの場合、より高いプリフェッチはラウンドトリップを減らし、スループットを向上させることができます。その場合でも、ベンチマークが5分間良く見えたからといって、巨大な数を選択しないでください。コンシューマのメモリとテールレイテンシを監視してください。

簡単な経験則として、プリフェッチをコンシューマが短いウィンドウで快適に保持できる作業量に合わせてサイズ設定することです。ワーカーが1秒あたり約20メッセージを処理し、約1秒のローカルバッファリング作業で問題ない場合、20近くのプリフェッチは妥当な実験です。

プリフェッチが高すぎるかどうかを判断する方法

プリフェッチはおそらく高すぎます:

  • messages_unacknowledgedがアクティブなコンシューマと比較して大きい場合。
  • 一部のコンシューマが多くの未確認メッセージを持っている一方で、他のコンシューマがアイドル状態である場合。
  • messages_readyが低い場合でも、メッセージのレイテンシが高い場合。
  • バースト中にコンシューマのメモリが上昇する場合。
  • コンシューマのクラッシュが大量の再配信を引き起こす場合。

最後のポイントは見落とされがちです。ワーカーが1,000の未確認メッセージを保持してクラッシュした場合、RabbitMQはそれらのメッセージを再配信できます。これは正しい動作ですが、ハンドラが冪等でない場合、ダウンストリームシステムに重複した圧力を生み出す可能性があります。

プリフェッチを下げることは、多くの場合、公平性と回復動作を改善します。ピークスループットが少し低下するかもしれませんが、ユーザーが実際に感じるレイテンシを改善できます。

プリフェッチが低すぎるかどうかを判断する方法

プリフェッチはおそらく低すぎます:

  • コンシューマのCPUとメモリ使用率が低い一方で、messages_readyが増加し続けている場合。
  • 処理時間が非常に短いが、配信レートが制限されている場合。
  • コンシューマとRabbitMQ間のネットワークレイテンシが顕著な場合。
  • プリフェッチを増やすことで、テールレイテンシやメモリプレッシャーを増加させずにスループットが向上する場合。

典型的な例は、小さなインメモリ計算を行い、すぐにackする高速なワーカーです。prefetch_count=1の場合、次のメッセージを待つ時間が長くなりすぎる可能性があります。プリフェッチを上げることで、小さなローカルバッファを与え、ビジー状態を維持できます。

ダウンストリームのボトルネックを隠さない

プリフェッチの調整は、低速なデータベースを修正しません。作業がどのように分散され、バッファリングされるかを変更することしかできません。すべてのメッセージが同じ過負荷のAPIを待つ場合、より高いプリフェッチは、タイムアウトと再試行を増やしながら、スループットを一時的に良く見せる可能性があります。

コンシューマ内部で測定してください。メッセージのデコード、データベースの待機、外部サービスの呼び出し、ackに費やされた時間のログまたはメトリクスを出力します。RabbitMQは準備済みおよび未確認のカウントを表示できますが、ハンドラが8秒かかる理由を教えてくれません。

ダウンストリームサービスがレート制限されている場合、プリフェッチは多くの場合、高くするのではなく低くする必要があります。キューにバックログを可視化させ、ワーカー内で数千のインフライトコールを隠さないようにします。

プリフェッチと並行処理は異なる

プリフェッチが50だからといって、コンシューマが50個のメッセージを並行して処理するわけではありません。RabbitMQが確認応答を受け取る前に50個のメッセージを配信する可能性があることを意味するだけです。それらが同時に実行されるかどうかは、コンシューマコードに依存します。

プリフェッチ50のシングルスレッドコンシューマは、49個がメモリで待機している間、一度に1つのメッセージを処理する場合があります。並行処理10とプリフェッチ50のワーカープールは、10個のタスクをアクティブにし、40個をバッファリングする場合があります。場合によっては、そのバッファは便利です。場合によっては、それは単なるレイテンシです。

プリフェッチを実際の並行処理に合わせてください。プロセスが5つのハンドラを同時に実行できる場合、プリフェッチ5から20は、500よりも推論が容易です。

順序と公平性のトレードオフ

RabbitMQキューはキュー・レベルで順序を保持しますが、コンシューマの動作は作業が完了する順序を変更する可能性があります。複数のコンシューマと1より大きいプリフェッチがある場合、メッセージ20はメッセージ3よりも前に完了する可能性があります。これは、より高速なワーカーに送られたか、より簡単な作業であったためです。

ほとんどのワークキューでは、完了順序は重要ではありません。アカウント更新、在庫変更、または順番に処理する必要があるワークフローの場合、それは非常に重要になる可能性があります。そのような場合、順序付けキーごとに1つのキューを使用するか、キーでシャーディングするか、プリフェッチを低く保つことが、最大スループットを追求するよりも安全な場合があります。

公平性にも同様のトレードオフがあります。低いプリフェッチは、コンシューマがより頻繁にメッセージを求めて戻ってくるため、RabbitMQが作業をより均等に分配できるようにします。高いプリフェッチは、最初にメッセージを受信したコンシューマに報酬を与えます。メッセージの処理時間が不均一な場合、これは1つのワーカーが低速なジョブの山を保持し、別のワーカーがそのバッチを迅速に完了するという結果につながる可能性があります。

人々が「RabbitMQの負荷分散は不均一だ」と言うとき、プリフェッチは最初に確認すべきものの1つです。ブローカーは、まだ配信されていないメッセージのみをバランスできます。

障害動作が重要

プリフェッチは、コンシューマがダウンしたときに何が起こるかを変更します。prefetch_count=1の場合、チャネルが閉じるときに1つの未確認配信が戻ってきます。prefetch_count=500の場合、数百が一度に戻ってくる可能性があります。コンシューマがクラッシュする前に部分的な副作用を実行した場合、ハンドラが冪等でない限り、それらの再配信は重複した書き込み、重複した電子メール、または重複したAPI呼び出しを引き起こす可能性があります。

これは、高いプリフェッチが間違っているという意味ではありません。それは、高いプリフェッチは冪等なハンドラ、明確な再試行ルール、および再配信率の監視とともに属することを意味します。重複処理が危険な場合は、アプリケーションがそれを処理できるように構築されるまで、未確認のウィンドウを小さく保ちます。

コンシューマのredeliveredフラグを確認してください。これは完全な再試行カウンターではありませんが、メッセージが以前に配信されたことを示す有用なシグナルです。堅牢な再試行制限のために、ヘッダーまたはアプリケーション状態で試行回数を追跡し、使い果たされたメッセージをデッドレターキューにルーティングします。

複数のキューと混合ワークロード

1つのプリフェッチ値がすべてのキューに適合することはほとんどありません。thumbnail.generateemail.sendを消費するサービスは、それぞれに異なる設定が必要になる場合があります。サムネイル生成はCPU負荷が高く、低い並行処理が最適な場合があります。電子メール送信はネットワークに依存し、より多くのインフライトメッセージを許容する場合があります。

単一のプロセスが1つのチャネルで複数のキューを消費する場合、QoS動作の推論が難しくなる可能性があります。意味の異なるワークロードには個別のチャネルを優先してください。これにより、プリフェッチ、監視、および障害処理がより明確になります。

混合メッセージサイズも別の警告サインです。キューに小さなイベントと巨大なペイロードの両方が含まれている場合、カウントベースのプリフェッチはメモリプレッシャーを適切に反映しません。10個の小さなメッセージと10個の大きなメッセージは同じコストではありません。その状況では、ワークロードを分割するか、大きなペイロードをRabbitMQから移動し、代わりに参照を渡します。

キューごとではなく、コンシューマごとの未確認を監視する

キュー・レベルの未確認カウントは未完了の作業があることを示しますが、偏りを隠す可能性があります。1つのコンシューマが未確認メッセージのほとんどを保持し、残りがほぼ空である場合があります。これは多くの場合、高いプリフェッチ、不均一なメッセージコスト、または1つの不健康なワーカーを指しています。

テスト中に管理UI、Prometheus、またはrabbitmqctl list_consumersからコンシューマ・レベルのメトリクスを使用します。分布が不均一な場合、プリフェッチを下げるか、低速なメッセージタイプを分割することで、総スループットがほとんど変わらなくても、実際のレイテンシを改善できます。

デプロイ後にプリフェッチを再検討する

プリフェッチ値は古くなります。ハンドラが1つのデータベース行のみを書き込んでいたときに機能した値は、次のリリースでAPI呼び出し、追加の検証、またはより大きなペイロードが追加された後は間違っている可能性があります。プリフェッチをパフォーマンス設定の一部として扱い、一度設定して忘れる数字ではありません。

コンシューマのリリース後、処理レイテンシ、未確認カウント、再配信、およびコンシューマメモリを以前のバージョンと比較します。レイテンシが上昇しているがCPUが飽和していない場合、ハンドラは外部の何かを待っている可能性があり、より低いプリフェッチがシステムをより公平に保つ可能性があります。CPUが高く、各メッセージがCPUバウンドである場合、ワーカーを追加するか、メッセージごとの作業を減らすことが、プリフェッチを変更するよりも重要になる場合があります。

選択した値の理由をコンシューマ設定の近くに文書化します。将来のメンテナは、prefetch_count=5が公平性、メモリ、順序、ダウンストリームのレート制限、または単なる一時的なデフォルトのために選択されたのかを知っている必要があります。

実際のメッセージ形状でテストする

本番メッセージが大きなJSONペイロードであるか、高価なデータベースルックアップを含む場合、小さな偽のメッセージでプリフェッチを調整しないでください。メッセージサイズとハンドラコストが重要です。

便利なテストループは次のとおりです:

  1. プリフェッチ値を選択します。
  2. 安定した動作が見られるのに十分な時間、現実的なパブリッシュレートを実行します。
  3. messages_readymessages_unacknowledged、コンシューマCPU、コンシューマメモリ、処理レイテンシ、およびエラーレートを監視します。
  4. 1つのコンシューマを強制終了し、いくつのメッセージが再配信されるかを確認します。
  5. プリフェッチを増減して繰り返します。

最適な値は、短いベンチマークスループットが最も高い値であることはほとんどありません。それは、コンシューマをビジー状態に保ち、レイテンシを許容可能に保ち、システムが処理できる方法で失敗する値です。

実用的なデフォルト

まだデータがない場合は、通常のワークキューに対して手動確認とprefetch_count=10から始めてください。低速で高価、または厳密に公平な処理には1を使用してください。測定後、高速で安定したハンドラには20または50を試してください。配信ラウンドトリップがボトルネックであり、コンシューマにメモリの余裕があることをメトリクスが示した場合にのみ、より高い値にしてください。

RabbitMQのプリフェッチ調整は一度限りの設定ではありません。メッセージサイズが変更されたとき、コンシューマコードが変更されたとき、ダウンストリームの依存関係が変更されたとき、またはワーカーインスタンスを追加したときに、再検討してください。適切なプリフェッチ値は、現在の作業の形状に一致する値です。