TT Lab
はじめる
学ぶ 学習パス コース

システム間連携 (EAI)

リトライはタダではない

TT Labで続きを見る

一言でいうと

リトライは、複数の層で重なった瞬間に掛け算になり、回復中の相手をもう一度倒してしまうので、1つの層でだけ行い、指数バックオフにジッターを混ぜる必要があります。

リトライはなぜ障害を大きくするのか

相手システムが少し不安定になります。こちら側でリトライをかけます。よい設計のようです。ところが、こんな状況を考えてみましょう。

게이트웨이:   재시도 4회
  → 서비스A:  재시도 4회
    → 서비스B: 재시도 4회

このコードブロックの韓国語は、ゲートウェイがリトライ4回、サービスAがリトライ4回、サービスBがリトライ4回を示しています。

ユーザーがボタンを1回押しただけなのに、最終システムには4 × 4 × 4 = 64回のリクエストが行きます。回復中だった相手は、この爆撃で再び倒れます。そして、この波は、リトライの間隔ごとに繰り返されます。

リトライは1つの層でだけ行うという原則が、ここから出てきます。複数の層でリトライすると、指数的に増幅されます。

指数バックオフとジッター

固定間隔のリトライには悪い点が2つあります。早すぎる再試行になり、みんなで一斉に叩きます。

1차 실패 → 1초 대기 → 2차 → 2초 → 3차 → 4초 → 4차 → 8초 ... (상한 30초)

このコードブロックの韓国語は、1回目の失敗→1秒待機→2回目→2秒→3回目→4秒→4回目→8秒と続き、上限は30秒、という意味です。

指数バックオフは、最初の問題を解決します。ところが、2つ目が残ります。

1万個のクライアントが同じルールに従うと、まったく同じ瞬間にリトライします。 回復中のサーバーは、その瞬間に再び倒れます。そして、その波が、2秒、4秒、8秒ごとに繰り返されます。

これがthundering herdです。解決策はジッター(jitter)です。計算された待機時間に、ランダム性を混ぜます。

sleep = random(계산값 × 0.5, 계산값)      # full jitter 계열

このコードブロックの韓国語は、計算値(バックオフで計算された待機時間)を表し、コメントは、full jitter系を意味します。

ジッターのないバックオフは、半分しかやっていないのと同じです。そして、ジッターがなくても、普段はうまく動くので、大きな障害が起きたときに初めて発見されます。

リトライしてはいけないエラー

この区別がないと、リトライは害になります。

エラーの種類 リトライするか 理由
接続拒否、タイムアウト O 一時的である可能性が高いです
HTTP 500、502、503、504 O サーバー側の一時的なエラーです
HTTP 400(形式エラー) X 100回送っても100回失敗します
HTTP 401/403(認証/権限) X 資格が変わるまでは通りません
HTTP 404 X 対象がありません
HTTP 409(重複) X すでに処理されたという意味かもしれません
HTTP 429(上限超過) 条件付き Retry-Afterを尊重します

429を無視してすぐにリトライすることは、特に悪いです。相手が「ゆっくり来てください」と言っているのに、より速く行くようなものです。多くのAPIが、この場合、遮断で対応します。

DLQ: 諦め方を知る必要がある

最大試行回数を超えたメッセージは、DLQ(Dead Letter Queue)に送ります。無限のリトライはキューを塞ぎ、塞がったキューは全体の停止です。

DLQに入れるときに、原本だけを入れてはいけません。あとで人が見る必要があるからです。

{
  "msg_id": "M-20260819-000123",
  "original": { ...원본 전문... },
  "reason": "upstream 503 after 5 attempts",
  "attempts": 5,
  "first_failed_at": "2026-08-19T02:11:03+09:00",
  "last_failed_at": "2026-08-19T02:12:47+09:00",
  "trace_id": "a1b2c3d4"
}

このコードブロックの韓国語は、原本の電文を表します。

そして、DLQは監視の対象です。件数が0でなければ、誰かが見る必要があります。DLQを作っておいて誰も見ていないシステムが、本当に多いです。それは、メッセージを黙って捨てるのと同じです。

再処理の設計

DLQに溜まったものを、どう戻すか。

  1. 分類: 理由別に分けます。システムのエラーか、データのエラーか
  2. 判断: 直して再投入するか、ソースに再送を依頼するか
  3. 安全確認: 再処理が冪等か。そうでなければ、重複処理になります
  4. 実行: 少量から。全部を一度に入れると、同じ理由でまた詰まります
  5. 記録: いつ誰が何件を再処理したか

項目3が核心です。冪等でないインターフェースの再処理は、「やらないより悪い」結果を生むことがあります。二重入金、二重発注。

冪等性: どう作るか

冪等であるとは、同じリクエストを何度処理しても、副作用が1回だけ起きることです。「応答が常に同じ」ではありません。

方法は、結局1つです。リクエストごとに一意のキーを付け、受信側がすでに処理したキーを覚えておきます。

CREATE TABLE inbox_log (
  msg_id     TEXT PRIMARY KEY,      -- ★ 멱등키. 제약이 마지막 방어선
  biz_key    TEXT NOT NULL,
  status     TEXT NOT NULL,
  response   TEXT,                  -- 최초 응답을 그대로 보관
  created_at TEXT NOT NULL
);

このコードブロックの2つの韓国語コメントは、順に、冪等キーであり制約が最後の防衛線であること、最初の応答をそのまま保管することを述べています。

冪等キーを何にするかが、設計の核心です。

そして必ずDB制約(PRIMARY KEY / UNIQUE)で止めましょう。アプリケーションのif 조회 then 없으면 insert(韓国語は順に「照会」「なければ」を意味します)は、同時に2件が入ってくると突破されます。照会と挿入の間に隙間があるからです。制約は、その隙間をなくします。アプリケーションにバグがあっても、制約は突破されません。

保存してから応答する(store and return)

さらに一歩進めて、重複リクエストに最初の応答をそのまま返します。

1. msg_id 로 조회
2. 있으면 → 저장된 response 를 그대로 반환 (처리 안 함)
3. 없으면 → 처리하고, 결과를 response 에 저장

このコードブロックの韓国語は、msg_idで照会する、あれば保存されたresponseをそのまま返す(処理しない)、なければ処理して結果をresponseに保存する、という3つの手順です。

こうすれば、送信側にとって、再送が完全に安全になります。タイムアウトで応答を受け取れなかったときに、そのまま再度送ればよいのです。これがIdempotency-Keyヘッダー規約が行うことであり、決済APIがこの方式を使う理由です。

冪等履歴の保管期間

inbox_logは無限に大きくなります。整理する必要があります。ところが、整理した瞬間に、その区間の重複防御が失われます。

そのため、保管期間は「現実的に再送が来うる最大期間」より長く設定します。相手の再処理ポリシーが「最大7日前のものまで再送」なら、30日間の保管が安全です。そして、整理バッチは期間の条件だけで削除します。「古いものからN件」のような方式は、突然流入が増えたときに、最近のものを消してしまいます。

重複が発生する4つの経路

最後に、実務で重複がどこから来るかを整理しておきましょう。

  1. リトライ: タイムアウト後の再送(応答だけが失われた場合)
  2. キューのat-least-once: ブローカーがackを受け取れず、再配信
  3. 運用者による手動の再送: 障害後の「とりあえずもう一度送ってください」
  4. 送信側のバッチの再実行: 失敗したバッチを最初から回し直す

4つ目が最も大きいです。バッチが半分処理して死んだのに、全体を回し直すと、半分が重複です。そのため、バッチも再開地点を記録するか、受信側の冪等性に頼る必要があります。たいてい、後者が現実的です。

現場での姿

最もよく見る形は、誰もリトライを設計していないのに、リトライが3重にかかっている状況です。ゲートウェイのデフォルト値、HTTPクライアントライブラリのデフォルト値、そしてこちらが組んだコード。それぞれは合理的な3–4回ですが、掛け合わせると数十回になります。そのため、障害対応のときに、「こちらは1回しか送っていないのに、相手のログには40件記録されている」という会話が交わされます。

次によくあるのは、リトライしてはいけないエラーをリトライすることです。形式エラーや認証失敗は、100回送っても同じ結果なのに、リトライロジックがステータスコードを区別しないと、その無意味なリトライが相手のログを埋め、こちらのキューを塞ぎます。

そして最後に、諦め方を知っている必要があります。DLQに移すときに、理由・試行回数・最初の失敗時刻を一緒に残さないと、数週間後にそのファイルを開いた人が、これを再投入してよいかを判断できません。