【AWS SQS】DLQを放置して痛い目を見た話

に公開

Dead Letter Queue(DLQ)とは?

  • 通常処理で配信しきれなかったメッセージを格納するキュー
  • 継続的にエラーが発生する(成功しない)メッセージ
  • 再送回数が上限に達したメッセージ などが該当

DLQを放置して、障害対応が地獄になっていた・・・

数年前にいたとある現場の話です。

ECSワーカー(ECS on EC2)でSQSをポーリングする処理基盤を、途中から運用することになりました。

簡単なフロー

  • Web/API → SQSに投入
  • ECSワーカーがポーリングして実行
  • 失敗はDLQへ

いわゆる「失敗に強い」構成を目指した……はずでした。

障害対応時

…が、実際の障害対応でこうなりました↓

  • DLQに溜まっているのは分かる
  • しかし 何が原因で失敗したのか追えない
  • どの処理?どのタイミング?が分からない
  • 「とりあえず戻す」と同じ失敗を繰り返して、ノイズだけ増える

結論:DLQを作って満足してしまっていた

なぜDLQ放置が辛いのか

DLQは「失敗したキュー」ですが、正確にはこうです。

DLQは 失敗メッセージを隔離して、正常系を止めずに、調査と再処理を可能にするため の仕組み

(公式ドキュメント)

https://docs.aws.amazon.com/ja_jp/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-dead-letter-queues.html

つまり、DLQに落ちた瞬間に必要なのは次の3つ。

  1. 追える(調査できること)
  2. 戻せる(再処理できること)
  3. 壊さない(冪等性があること)

この3つが欠けると、「DLQがあるのに辛い」になります。

前提:今回の構成(ECS on EC2 / ポーリング)

  • ECS on EC2 でワーカーを稼働
  • ワーカーがSQSをポーリングして処理
  • 成功:DeleteMessage
  • 失敗:削除しない → 再配信 → 一定回数でDLQ

処理時間は平均 2分程度/ジョブ

“失敗に強い”を成立させる4つの柱

キュー構成でよくある「失敗に強い」といわれる理由は何なのか?を整理します。

1) 非同期化(ピークを吸収)

  • APIを軽くして、重い処理をワーカーへ逃がせる
  • 負荷ピーク時でもサービスが落ちにくくなる

2) リトライ(仕組みで再試行)

  • 失敗時に DeleteMessage しなければ、SQSが再配信してくれる

3) 隔離(DLQで毒メッセージを止めない)

  • 永遠に失敗する入力が混ざっても、DLQに逃がして正常系を止めない

4) 冪等性(再試行が怖くない)

  • ここがないと、リトライ=二重実行の地雷になる

修正:DLQ運用の最低ラインを作る

(A) 監視:DLQのメッセージ数をアラートにする

DLQは放置すると「あとで気づく」になりがちです(笑)

最低限:

  • DLQの ApproximateNumberOfMessagesVisible > 0 でアラート
  • 可能なら「増加率」も(急増はデグレや外部障害のサイン)

“DLQが気づいたら溜まってた” ではなく
“DLQが溜まったら都度確認できる状態” が運用のスタート!

(公式:SQS CloudWatch metrics)

https://docs.aws.amazon.com/ja_jp/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-available-cloudwatch-metrics.html


(B) DLQの1件からログまで辿れるようにする

DLQが辛い最大の理由は「辿れない」ことです。

メッセージに最低限入れると運用が変わります。

  • job_id(ジョブの一意ID)
  • tenant_id(マルチテナントなら)
  • resource_id(対象ID)
  • trace_id(ログ/トレースと紐付け)

障害対応の速度は、だいたい「相関IDがあるか」で決まる。


(C) DLQが出た時に「何をするか」を決める

DLQを見ても、人によって対応がブレると回りません。

障害対応の基本かもですが、実際に発生した際どう動くかを決めておくと、いざという時に迅速かつ落ち着いて対処できます。

最低限のRunbook例

  1. DLQ件数と増加速度を確認
  2. 影響範囲(tenant_id / 機能)を把握
  3. (B)で設定した job_id でログ検索 → 失敗理由を分類
  4. 分類に応じて対処
    • 一時障害:復旧後に再投入
    • 恒久障害:入力修正 / 仕様修正 / データ修正
  5. 再投入する場合は「再投入条件」を満たしてから実施
  6. 再投入後、成功率とDLQ増加が止まったことを確認

…など


(D) 再処理(リドライブ):戻す手順を用意する

DLQからの復旧手段として、DLQのメッセージを元のキューへ戻す(再投入する)運用があります。
DLQは「再処理する前提」なので、戻し方を決めておきます。

考え方

  • 原因が未解決のまま戻さない
  • 戻した後に「成功したか」を検証する

冪等性(迷うならジョブ台帳方式)

DLQ運用と相性が良いのは ジョブ台帳(ledger)方式です。

ジョブ台帳方式とは

  • メッセージに job_id を必ず入れる
  • DBに job_id一意制約 で保存する
  • すでに成功済みなら安全にスキップできる

メリット:

  • DLQからの再投入が怖くない(同じジョブが2回走っても壊さない)
  • 失敗理由や試行回数も記録できる → 調査が速い

失敗の分類(DLQ運用が一気に楽になる)

繰り返しになりますが、DLQの苦労は「全部同じ失敗」に見えることです。
運用上はこの2種類に分けるだけで、かなり楽になります。
(サービスや現場によってルールは変わりますが…)

一時障害(Transient)

  • 外部APIの瞬断
  • DBの一時的な負荷
  • ネットワーク揺らぎ

→ リトライが効く。復旧後に再投入しやすい。

恒久障害(Permanent)

  • 入力不正(データフォーマット等の形式エラー)
  • 存在しないID
  • 権限不足

→ リトライしても無駄!早めに隔離して原因修正に寄せる。

まとめ

いかがでしたでしょうか。

1つの現場の例なのでどこまで参考になるかは分かりませんが、
DLQの調査に時間を要してしまっている人のお役に立てれば幸いです!

参考

Amazon SQS best practices

https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-best-practices.html?utm_source=chatgpt.com

Discussion