デッドレターと毒メッセージの処理設計
処理に必ず失敗するメッセージが無限リトライでキューを詰まらせる前に隔離する設計を原理から。DLQ退避・redrive・リトライ上限と指数バックオフの組み立て方が掴めます。
- 1.毒メッセージは何度処理しても必ず失敗するため、無限リトライがワーカーとキューを占有しヘッドオブラインブロッキングを起こす。リトライ回数に上限を設け、超えたらDLQへ隔離して本流を流し続けるのが原則。
- 2.リトライは即時連打せず指数バックオフ+ジッタで間隔を広げ、一時障害(ネットワーク瞬断・下流の過負荷)の回復を待つ。ただし恒久障害(不正ペイロード・スキーマ不整合)にバックオフは無意味なので、両者を区別して扱う。
- 3.DLQは終着点ではなく退避所。原因を可視化・調査し、修正後にredrive(再投入)で本流へ戻す救済フローと、無限ループを防ぐ再投入回数の追跡まで含めて初めて設計が完結する。
毒メッセージが本流を止める仕組み
非同期メッセージング(キュー、ストリーム、Pub/Sub)の信頼性は、配信失敗時のリトライに支えられています。コンシューマが処理に失敗してメッセージを ack しなければ、ブローカは「未処理」とみなして再配信する。一時的なネットワーク瞬断や下流の一過性エラーなら、再配信でいずれ成功します。これが at-least-once 配信の根幹です(/devops/message-queue-delivery-semantics/)。
問題は、何度処理しても必ず失敗するメッセージ——いわゆる毒メッセージ(poison message)——が混ざったときです。原因はデシリアライズできない壊れたペイロード、スキーマ不整合、参照先が消えた外部キー、コードのバグが踏む特定データなど。これらは再配信しても結果は変わりません。リトライが永遠に成功しないまま、ブローカはそのメッセージを延々と再配信し続けます。
毒メッセージの無限リトライが起こすこと
1. ワーカーが毒メッセージの処理に時間と資源を費やし続ける
2. ack されないので可視性タイムアウト後に何度も再配信される
3. 順序保証つきキュー/パーティションでは、その1件が
後続をブロックする(ヘッドオブラインブロッキング)
4. リトライのCPU・下流呼び出しが正常メッセージの容量を奪う
とくに深刻なのがヘッドオブラインブロッキングです。順序保証のあるキューや、Kafka のような単一パーティション内 FIFO のストリームでは、先頭の1件が処理できないとその後ろの全メッセージが進めません。健全なメッセージが何百万件積まれていても、毒メッセージ1件がオフセットを進ませず、パーティション全体が停止する。無限リトライは資源を食うだけでなく、システムの前進そのものを止めるのです。
リトライは一時障害には特効薬ですが、恒久障害には毒です。「失敗したら再送」を無条件に適用すると、絶対に成功しないメッセージへ無限に資源を注ぎ込む。リトライ設計の出発点は「この失敗は時間が解決するのか、しないのか」を分けることです。前者にだけリトライし、後者は早期に隔離する——この区別が毒メッセージ対策の核心です。
一時障害と恒久障害を分ける
リトライ設計の第一歩は、失敗を二種類に分類することです。
| 分類 | 例 | リトライの是非 | 正しい扱い |
|---|---|---|---|
| 一時障害(transient) | ネットワーク瞬断、下流の過負荷、ロック競合、タイムアウト | 有効(時間が解決する) | バックオフして再試行 |
| 恒久障害(permanent) | デシリアライズ不可、スキーマ不整合、不正な必須項目、業務ルール違反 | 無意味(何度やっても失敗) | リトライせず即DLQへ隔離 |
理想は、コンシューマが例外の種類でこの二つを見分けることです。HTTP 5xx や接続エラーは一時障害として再試行し、400 相当の検証エラーやパースエラーは恒久障害として即座に DLQ へ送る(fail fast)。恒久障害を律儀にリトライ上限まで再試行するのは、確実に失敗する処理に資源を払い、隔離を遅らせるだけです。
ただし現実には両者の判別は完璧にはできません。下流の 500 が一過性なのか恒久バグなのかは外からは分かりません。そこで実務では「判別できる恒久エラーは即隔離、判別できないものは上限つきでリトライし、上限超過で隔離」という二段構えを取ります。リトライ回数の上限こそが、判別不能な毒メッセージに対する最後の安全弁です。
リトライ回数の上限と指数バックオフ
判別できない失敗には、**リトライ回数の上限(maximum receive count / max delivery attempts)を設けます。ブローカは各メッセージの配信試行回数を数え、上限を超えたら本流のキューから外してデッドレターキュー(DLQ)**へ移します。SQS の maxReceiveCount と redrive policy、RabbitMQ の dead-letter exchange、Kafka なら再試行トピックと DLT(dead letter topic)が、それぞれこの役割を担います。
上限を決めたら、次は再試行の間隔です。失敗直後に即リトライ(連打)するのは最悪です。一時障害の典型は下流の過負荷であり、回復していない相手へ即座に再送すれば、負荷を上乗せして回復をさらに遅らせます。そこで間隔を試行ごとに広げる指数バックオフを使います。
指数バックオフ(base=200ms, factor=2 の例)
試行1失敗 → 200ms 待つ
試行2失敗 → 400ms 待つ
試行3失敗 → 800ms 待つ
試行4失敗 → 1600ms 待つ
... 上限 max_attempts に達したら DLQ へ
待機時間 = min(base * factor^(attempt-1), cap)
さらに重要なのが**ジッタ(揺らぎ)**です。同じ障害で大量のメッセージが同時に失敗すると、指数バックオフだけでは全件が同じ時刻に足並みを揃えて再送され、回復しかけた下流を周期的な波が再び叩く(サンダリングハード)。各待機時間に乱数の揺らぎを加えて再送タイミングを散らすことで、波を平らにします。指数バックオフとジッタの組み合わせ、そしてリトライ予算でリトライ自体の総量を抑える原理は、同期 RPC のリトライと完全に同じ系譜です(/devops/retry-backoff-jitter/)。
キューにおけるバックオフは、しばしば**可視性タイムアウト(visibility timeout)**や per-message delay で実装されます。メッセージを ack せず、次に見えるようになるまでの時間を試行回数に応じて延ばすことでバックオフを表現します。ただし可視性タイムアウトを長く取りすぎると、毒メッセージが DLQ へ落ちるまでの総時間(上限回数 × 平均待機)が膨らみ、隔離が遅れます。上限回数とバックオフの伸び方は、最悪ケースの隔離までの所要時間から逆算して決めるべきです。
順序保証つきキューでヘッドオブラインブロッキングを避けたい場合、即時リトライをその場で繰り返すと先頭が長時間ブロックされます。そこで「失敗したメッセージはいったん別の再試行キューへ退避させ、本流のオフセットは先へ進める」という設計が有効です。Kafka の再試行トピック方式がこれで、失敗メッセージを retry-5s・retry-30s のような遅延トピックへ送り、本パーティションの前進を止めません。順序保証と引き換えに前進性を取る判断です。
DLQ は終着点ではなく退避所
リトライ上限を超えたメッセージは DLQ へ移ります。ここで陥りがちな誤解が、DLQ をゴミ箱だと思うことです。DLQ は「捨てる場所」ではなく「人間が原因を調べ、修正し、本流へ戻すまで安全に保管する退避所」です。設計はここから本番です。
DLQ にメッセージが溜まっても何の処理もしなければ、それは処理されなかった注文・支払い・イベントが消えていくのと同じです。「失敗は DLQ に入るから安心」ではなく、DLQ の滞留件数・流入レートを必ずメトリクス化し、件数が増えたらアラートを上げる。さらに DLQ にもメッセージの保持期間(retention)があり、期間を過ぎればブローカが自動削除します。調査が retention に間に合わなければ、毒メッセージもろとも本来救えたデータまで失います。DLQ は監視対象であって、放置対象ではありません。
DLQ を退避所として機能させるには、最低限これらが要ります。
- 死因の保全:なぜ失敗したかを後から追えるよう、最後の例外・スタックトレース・失敗時刻・試行回数・元のキュー名をメッセージ属性やメタデータに付けて DLQ へ送る。本文だけ移して死因を捨てると、調査が始められません。
- 可観測性:DLQ の
ApproximateNumberOfMessages、流入レート、毒メッセージのパターン(同一スキーマ・同一発信元への偏り)を可視化する。流入の急増は、上流のデプロイ事故やスキーマ変更の事故を示す早期シグナルになります。 - 分類と原因究明:DLQ のメッセージを死因ごとに束ねる。「壊れたペイロード」「下流が長時間ダウン」「特定バージョンのバグ」では取るべき手当てが違います。
redrive:本流へ戻す救済フロー
原因を直したら、退避したメッセージを本流へ戻します。これが**redrive(再投入)**です。SQS なら DLQ redrive、RabbitMQ なら shovel/再 publish、Kafka なら DLT から元トピックへの再送で実現します。救済フローの典型はこうです。
救済(redrive)フロー
1. アラートで DLQ への流入を検知
2. 死因メタデータから根本原因を特定(バグ/スキーマ/下流障害)
3. 原因を修正(コード修正・デプロイ、下流の復旧、データ補正)
4. DLQ から本流(または専用の再処理キュー)へ redrive
5. 再処理が成功することを確認、DLQ の件数が減ることを監視
redrive 設計には三つの落とし穴があります。
第一に、修正前に redrive してはいけません。原因が直っていないまま戻せば、メッセージは再び同じ失敗を辿り、リトライ上限を消費してまた DLQ へ戻る。DLQ と本流を往復するだけの無駄なループになります。redrive は「原因が解消された」という前提を必ず満たしてから行います。
第二に、redrive の回数自体を追跡する必要があります。「DLQ から戻す → また失敗 → また DLQ」を無制限に許すと無限ループになりかねません。redrive 回数をメタデータに刻み、一定回数を超えたものは自動再投入の対象から外し、人手の判断(手動補正・破棄)に回します。
第三に、再処理は冪等でなければなりません。redrive されたメッセージは、最初の失敗時点で副作用(一部の DB 更新、外部 API 呼び出し)を起こしているかもしれません。これを冪等性なしで再処理すると、二重課金・二重発送が起きます。コンシューマは「同じメッセージを複数回処理しても結果が一度きりと同じ」になるよう設計しておくことが、redrive を安全にする前提条件です(/devops/idempotency-exactly-once/)。送信側でメッセージ生成自体を確実にする outbox 等と組み合わせると、生成から消費まで一貫した信頼性になります(/devops/transactional-outbox/)。
下流が長時間ダウンしているとき、リトライ上限つきとはいえ、流れ込む全メッセージが上限まで試行されてから DLQ へ落ちます。結果、復旧可能だったメッセージが大量に DLQ へ溜まり、復旧後に巨大な redrive 作業が要る。下流の障害を検知したらサーキットブレーカを開いて消費自体を一時停止し、メッセージをキューに留めておけば、DLQ への不要な流入を防げます。「個々の失敗はリトライ+DLQ、下流全体の障害はブレーカで消費停止」と層を分けるのが定石です(/devops/circuit-breaker-bulkhead/)。
試験・面接で問われる勘所
毒メッセージは何度処理しても必ず失敗するメッセージで、無限リトライが資源を食い、順序保証キューではヘッドオブラインブロッキングを起こす——この因果を即答できること。対策はリトライ回数の上限を設け、超えたらDLQへ隔離して本流の前進を守る。リトライ間隔は指数バックオフ+ジッタで広げ、一時障害の回復を待ちつつサンダリングハードを防ぐ。一時障害と恒久障害を分け、恒久障害はリトライせず即隔離(fail fast)。DLQは終着点でなく退避所で、死因の保全・監視・原因修正後のredrive(再投入)まで含めて設計が完結する。redriveは冪等性と再投入回数の追跡が前提、という三点を押さえること。
まとめ
- 毒メッセージは何度処理しても失敗するため、無限リトライがワーカーとキューの容量を奪い、順序保証キューでは1件が後続全体を止めるヘッドオブラインブロッキングを招く。
- 対策の核心は失敗の分類。一時障害はバックオフして再試行、恒久障害は判別できしだい即DLQへ隔離(fail fast)。判別不能なものはリトライ回数の上限を最後の安全弁にする。
- 再試行間隔は指数バックオフ+ジッタで広げ、回復していない下流への連打とサンダリングハードを避ける。順序保証下では別の再試行キューへ退避し本流の前進を止めない。
- DLQはゴミ箱ではなく退避所。死因メタデータの保全・滞留件数の監視・retention 超過による消失防止が必須。放置されたDLQは静かなデータ損失。
- redrive(再投入)は原因修正後にのみ行い、再投入回数を追跡して無限ループを防ぎ、再処理は冪等に設計する。下流全体の障害はサーキットブレーカで消費を止め、DLQへの不要な大量流入を防ぐ。
DevOps/インフラ Article
デッドレターと毒メッセージの処理設計を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
メッセージキュー
比較で見る軸
難易度: advanced / カテゴリ: DevOps/インフラ / タグ数: 6
導入後に効く点
リトライは即時連打せず指数バックオフ+ジッタで間隔を広げ、一時障害(ネットワーク瞬断・下流の過負荷)の回復を待つ。ただし恒久障害(不正ペイロード・スキーマ不整合)にバックオフは無意味なので、両者を区別して扱う。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- DevOps/インフラ
- タグ数
- 6
判断チェックリスト
- 自社の用途が「メッセージキュー / デッドレター」に近いか確認する。
- 強みである「毒メッセージは何度処理しても必ず失敗するため、無限リトライがワーカーとキューを占有しヘッドオブラインブロッキングを起こす。リトライ回数に上限を設け、超えたらDLQへ隔離して本流を流し続けるのが原則。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。