デッドレターキュー:分散システムにおけるポイズンメッセージの処理
メッセージのポイズニングによるキューのブロックを防止する
デッドレターキュー(DLQ)は、コンシューマーが処理できないメッセージを捕捉する安全ネットです。これにより、1つの破損したペイロードがキュー内のその後のすべてのメッセージをブロックしたり、静かにドロップしたりすることが防がれます。
メッセージ駆動型のシステムは、最終的に処理できないメッセージを受信します。フォーマット不正のペイロード、コンシューマー側で変更されたスキーマ、リトライしても失敗するダウンストリーム呼び出しなどです。デッドレターキューがない場合、そのメッセージはキューの先頭に永遠にブロックされるか、静かに破棄されます。どちらも失敗について把握できる状態よりも悪い結果です。
DLQは見えない失敗を見えて検査可能な状態に変えます。メッセージを隔離し、アラートを送り、修復して再送信するか、永久的に破棄するかを、偶然ではなく意図的に決定する場所を提供します。

実装の詳細はブローカーによって異なりますが、根本的なパターンはどこでも同じです:配信試行回数、閾値、そしてその閾値を超えたメッセージの宛先です。このガイドでは、DLQの実際の動作、ポイズンメッセージと一時的な障害の見分け方、リトライ versus 破棄の判断基準、そして根本原因を修正した後で安全に再送信する方法を解説します。このパターンが位置する統合パターン全体の文脈については、App Architectureをご覧ください。
デッドレターキューとは
デッドレターキューは、ブローカーまたはコンシューマーによって、メッセージの処理が失敗するたびに別の通常のキューにルーティングされる、通常のキューです。これは特別な構造ではありません。RabbitMQのデッドレターキューは、通常の交換機能にバインドされた通常のキューであり、SQSのDLQも標準またはFIFOキューです。キューが「DLQ」と見なされるのは、他に失敗したメッセージをそこへ向けているという理由だけです。
各ブローカーはリダイレクトを以下のように実装しています:
- Amazon SQS は
maxReceiveCountを持つリドライブポリシーを使用します。メッセージが削除されることなくその回数受信されると、SQSは設定されたdeadLetterTargetArnにメッセージを移動します。AWSは明示的に、DLQのメッセージ保持期間をソースキューよりも長く保つことを推奨しています。なぜなら、元のエンキュー時刻(移動時刻ではない)が依然として有効期限を管理するからです。 - RabbitMQ では、
requeue=falseで拒否された場合、メッセージごとのTTLが期限切れになった場合、キューが長さ制限に達した場合、またはクォラムキューがdelivery-limitを超えた場合に、メッセージはデッドレター化されます。これはx-dead-letter-exchange(およびオプションでx-dead-letter-routing-key)キュー引数で構成され、RabbitMQはx-deathヘッダーを付加して、理由、元のキュー、およびその発生回数を記録します。 - Apache Kafka にはブローカーネイティブなDLQはありません。Kafkaはオフセットのみを追跡し、「失敗した」メッセージの概念はありません。デッドレタートピックパターンは、コンシューマー、Kafka Streamsトポロジ、またはKafka Connectコネクタ内で構築するものです。一般的には、Spring Kafkaの
@RetryableTopicやDeadLetterPublishingRecovererが行うように、最終的なDLTの前にリトライトピック階層と組み合わせて使用されます。 - Azure Service Bus は、メッセージの配信カウントが
MaxDeliveryCount(デフォルトは10)を超えた場合に自動的にデッドレター化し、TTLExpiredException、HeaderSizeExceeded、MaxTransferHopCountExceededなどのシステム理由の場合にもデッドレター化します。これらはすべてメッセージのDeadLetterReasonプロパティに記録されます。
ブローカーやストリーミングプラットフォームが、信頼性パターンではなく運用上の観点からどのように連携するかについてのより広い視点については、Apache Kafka Quickstart と RabbitMQ on AWS EKS vs SQS が、これらのブローカーを実行するインフラストラクチャの側面をカバーしています。
ポイズンメッセージ
ポイズンメッセージとは、コンシューマーがリトライを試みる回数にかかわらず、決して成功しないメッセージのことです。フォーマット不正のJSONペイロード、プロデューサーによって名前が変更されたスキーマフィールド、ビジネスルール違反、特定の入力で常にスローするバグなどが該当します。これは一時的な障害とは異なります。一時的な障害では、メッセージ自体は正常ですが、環境が一時的に正常ではない場合(ダウンストリームのタイムアウト、データベース接続の断絶、レート制限の応答など)です。
両方の障害タイプを同じように扱うことは、DLQで最も一般的なミスです。最初の失敗でデッドレター化すると、リトライで成功するはずの一時的なエラーを罰することになります。ポイズンメッセージを諦める前に何十回もリトライすると、計算リソースを浪費し、順序付きキューやパーティション上でその後に続く関連しないメッセージの遅延を引き起こし、ログを同じスタックトレースで溢れさせることになります。
2つを分離するのに役立ついくつかの検出シグナルがあります:
- 例外の種類。 逆シリアル化エラー、バリデーションエラー、
ClassCastExceptionスタイルの障害は、ほぼ常に恒久的なものです。Spring KafkaのDefaultErrorHandlerは、特定の例外を致命的として明示的に扱い、リトライ予算を使い切る前にリトライをスキップします。 - 無変動の繰り返しカウント。 RabbitMQの
x-deathヘッダー配列により、メッセージが何回デッドレター化され、その理由が正確に分かります。カウントが増加し、すべてのサイクルで同じx-first-death-reasonを持つメッセージは、ポイズンであり、不運ではありません。 - レプリカ間での一貫した失敗。 すべてのコンシューマーインスタンスが周囲のメッセージには成功しながら、同じメッセージで失敗する場合、問題なのはインフラストラクチャではなくメッセージ自体です。
コードレベルでリトライ可能な障害と不可能な障害を区別する方法(DLQポリシーに依存する同じ分類)については、Go Error Handling Architecture: Boundaries and Patterns をご覧ください。
リトライ versus 破棄
すべてのDLQの背後にある核心的なポリシー決定は、リトライの閾値です。メッセージが隔離される前に何度配信試行が許されるかです。これを低く設定しすぎると、一時的なダウンストリームの一時的な問題の後には成功するはずのメッセージをデッドレター化してしまいます。高く設定しすぎると、ポイズンメッセージが長い間メインキューに留まり、ワーカーの容量を消費し、順序付きシステムではキューに積まれているその後のすべてのメッセージをブロックします。
主要なブローカーにわたる現在のガイダンスは、類似した数値に収束しています:
| ブローカー | メカニズム | 一般的な閾値 |
|---|---|---|
| Amazon SQS | リドライブポリシー内の maxReceiveCount |
混合された一時的/恒久的なワークロードの場合 3〜5 |
| RabbitMQ (クォラムキュー) | delivery-limit ポリシー引数 |
3〜5、キューごとに調整 |
| Azure Service Bus | MaxDeliveryCount |
デフォルト10、レイテンシー敏感なキューではしばしば減らされる |
| Kafka (リトライトピック経由) | リトライカウントヘッダー + リトライトピック階層 | 最終的なDLTの前に3〜4回のリトライトピックホップ |
多くのチームが着地点とする実用的な中間点は次のとおりです:保守的に(2〜3回の試行)始め、本番環境での実際の障害の混在を観察し、リトライのほとんどが数回で解決されるキューに対してのみ閾値を上げます。リトライカウントは、ダウンストリームの障害がリトライストームにならないように、試行間の指数関数的バックオフとジッターとペアにします。これはバックオフとサーキットブレーカーの設計でカバーされているのと同じ規律です。サーキットブレーカー at the integration boundary はこれを補完します:これは、キュー内のすべてのメッセージが個別に障害を発見し、一つずつデッドレター化するのではなく、ヘルスチェックの取れていない依存関係へのリクエスト送信を停止します。
メッセージがDLQに入った後、「破棄」は依然として意図的なアクションであるべきであり、無視してはいけません。DLQ自体に保持期間を設定します(調査用に十分長い期間。AWSはDLQの保持期間がソースキューを超えることを推奨しています。RabbitMQのDLQでは1週間が一般的な下限です)。そして、失敗が静かに期限切れになるのではなく、三角分類(トリアージ)されるように、DLQの深さと年齢でアラートを送ります。検査されることなくDLQから期限切れになるメッセージは、失うことを決定せずに失われたと判断されたメッセージです。
ここでは、重複の可能性が生じうる場所と同じくらい、アイデンポテンシー(冪等性)が重要です。DLQからメインキューへとリドライブされたメッセージは、機能的には重複配信です。もしあなたのコンシューマーが同じメッセージに対して2回実行するのが安全でない場合、DLQからのリドライブは、避けようとしていたのと同じ重複副作用バグを作り出す可能性があります。リドライブを安全にするためのコンシューマー側のパターンについては、Idempotency in Distributed Systems That Actually Works をご覧ください。
リプレイ戦略
メッセージを正しくDLQから出すことは、入れることとは別の規律です。
- まず根本原因を修正する。 リプレイ前にコンシューマーの修正をデプロイすることは、クリーンなリカバリーと、同じ失敗でキューを再度ポイズン化する違いを生みます。
- 意図的にリドライブし、自動的に行わない。 SQS は、オンデマンドでメッセージを元のキュー(または別の宛先)に戻すリドライブソース機能をサポートしています。RabbitMQ と Kafka では、同等のコンシューマーまたはツールを自分で構築する必要があります。いずれにせよ、リプレイはオペレーターがトリガーするアクションとして扱い、何がいつリプレイされたかの記録を残します。
- 必要な場所で順序を維持する。 Kafkaの場合、デッドレタートピックはソーストピックと同じ少なくとも数のパーティションを持ち、元のメッセージキーを保持する必要があります。そうすることで、リプレイされたメッセージが正しいパーティションに戻り、キーごとの順序が維持されます。
- リトライ試行を制限する。 修正とリプレイサイクルの後に再び失敗するメッセージは一時的なものではありません。それをDLQ内で無限にループさせるのではなく、恒久的なアーカイブ(データベーステーブル、オブジェクトストレージバケット)へルーティングします。RabbitMQ自体のドキュメントは、デッドレター化されたメッセージがさらにTTLベースのデッドレター化が無効になる前に、キュー間で限定された回数(16回)のみルーティング可能であると警告しています。
- DLQがそれ自体にデッドレター化しないようにする。 DLQ自体に
x-dead-letter-exchange(RabbitMQ) または同じチェーンを指すリドライブポリシー (SQS) があり、リプレイ失敗が発生すると無限ループを引き起こす可能性があります。DLQのデッドレター設定を空にするか、厳密に終端のアーカイブを指すようにします。 - 存在だけでなく量でもアラートを送る。 DLQ内の単一のメッセージはデータポイントですが、突然のスパイクはインシデントです。DLQの深さとメッセージの年齢を、他のすべてのものと同じアラートパイプラインに接続します。Modern Alerting Systems Design for Observability Teams は、DLQアラートに直接適用されるルーティングとノイズ低減のプラクティスを扱っています。
もしあなたのワークフローが単一のメッセージではなく、マルチステップの長時間実行プロセスに関わる場合、同じデッドレター思考がワークフロー層にも適用されます。saga’s compensation logic は、ステップが一時的ではなく恒久的に失敗した場合に、「隔離、検査、決定」の規律を必要とします。そして、イベント自体がデータベース書き込みから発生する場合、transactional outbox pattern はすでにリレーワーカーにデッドレター処理を組み込んでいるため、パターンはブローカーよりも1層上で現れます。
大きな picture における DLQ の位置づけ
デッドレターキューは、失敗を消し去るものではありません。代わりに、失敗を耐性のあるものにし、静かではなく検証可能にします。それは、一時的なケースではバックオフ付きのリトライ、リドライブが安全であるためのアイデンポテンシーのコンシューマー、そして struggling な依存関係がメインキュー(そして最終的にはDLQ)に同じ失敗を数千回送り込まないようにするサーキットブレーカーと併せて使用すると最も効果を発揮します。DLQの閾値、保持期間、アラートを、 untouched にしておくデフォルトではなく、第一級の構成決定として扱ってください。そうすれば、デッドレターはデータが静かに消える場所ではなく、診断ツールとなります。