リトライはタダではない
一言でいうと
リトライは、複数の層で重なった瞬間に掛け算になり、回復中の相手をもう一度倒してしまうので、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に溜まったものを、どう戻すか。
- 分類: 理由別に分けます。システムのエラーか、データのエラーか
- 判断: 直して再投入するか、ソースに再送を依頼するか
- 安全確認: 再処理が冪等か。そうでなければ、重複処理になります
- 実行: 少量から。全部を一度に入れると、同じ理由でまた詰まります
- 記録: いつ誰が何件を再処理したか
項目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つの韓国語コメントは、順に、冪等キーであり制約が最後の防衛線であること、最初の応答をそのまま保管することを述べています。
冪等キーを何にするかが、設計の核心です。
order_noだけにする方法は危険です。同じ注文に対する修正電文もありうるからです송신시스템 + 전문번호(韓国語で、送信システムと電文番号を意味します)は良い方法です。電文番号が送信側で一意であれば송신시스템 + 전문번호 + 일자(韓国語で、送信システム、電文番号、日付を意味します)は、電文番号が日単位で循環する場合に使います
そして必ず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つの経路
最後に、実務で重複がどこから来るかを整理しておきましょう。
- リトライ: タイムアウト後の再送(応答だけが失われた場合)
- キューのat-least-once: ブローカーがackを受け取れず、再配信
- 運用者による手動の再送: 障害後の「とりあえずもう一度送ってください」
- 送信側のバッチの再実行: 失敗したバッチを最初から回し直す
4つ目が最も大きいです。バッチが半分処理して死んだのに、全体を回し直すと、半分が重複です。そのため、バッチも再開地点を記録するか、受信側の冪等性に頼る必要があります。たいてい、後者が現実的です。
現場での姿
最もよく見る形は、誰もリトライを設計していないのに、リトライが3重にかかっている状況です。ゲートウェイのデフォルト値、HTTPクライアントライブラリのデフォルト値、そしてこちらが組んだコード。それぞれは合理的な3–4回ですが、掛け合わせると数十回になります。そのため、障害対応のときに、「こちらは1回しか送っていないのに、相手のログには40件記録されている」という会話が交わされます。
次によくあるのは、リトライしてはいけないエラーをリトライすることです。形式エラーや認証失敗は、100回送っても同じ結果なのに、リトライロジックがステータスコードを区別しないと、その無意味なリトライが相手のログを埋め、こちらのキューを塞ぎます。
そして最後に、諦め方を知っている必要があります。DLQに移すときに、理由・試行回数・最初の失敗時刻を一緒に残さないと、数週間後にそのファイルを開いた人が、これを再投入してよいかを判断できません。