部分故障とリトライ — 「応答がない」の正体

ネットワークの向こうから返事が来ない。サーバが死んだのか、遅いだけなのか、返事だけが消えたのか — 送った側には区別できません。この「部分故障」とつきあうための3点セット、タイムアウト・リトライ・冪等性を、事故の再現アニメで体感します。

1. 「応答がない」の3つの真実

1台のコンピュータの中なら、関数を呼べば「成功する」か「エラーになる」かのどちらかです。ところがネットワーク越しでは第3の結果 — 沈黙 — があります。下のアニメは、まったく違う3つの現実が、送信側からは同じ「返事がない」にしか見えないことを示しています。

同じ沈黙、違う真実 — 送信側からは区別できない
3つのシナリオが同時に進みます。①はサーバ停止で処理されず、②は実は処理成功(返事が遅いだけ)、③も処理成功(応答だけ消失)。しかしクライアント側の表示は3つとも「⏰ タイムアウト」— 処理されたかどうか、送信側は知りようがないのです。
POINT — 部分故障とタイムアウトの正しい理解 分散システムでは全体が一度に壊れるのではなく、一部だけが、しかも「壊れたと確かめられない」形で壊れる。これが部分故障。タイムアウトは故障の証拠ではなく、「これ以上待たない」という見切りにすぎない。

2. とりあえず再送 — が事故を起こす

返事がないなら、もう一度送ればいい? 多くの場合はそれで正解です。しかし相手が実は1回目を処理していた場合、再送は二重実行になります。送金でそれをやると…

再現: 二重送金事故 — ACKが消えただけだったのに
1回目の送金は成功していたのに、その確認応答(ACK)だけが消えました。クライアントはタイムアウトして再送 → サーバは新しい依頼だと思ってもう一度引き落とし。画面には「成功」と出ているのに、残高は2回分減っています。
注意 — 副作用のある操作を無邪気に再送しない 「送金」「注文確定」「メール送信」のような操作は、1回目が実は成功していたかもしれないという前提で扱う。何も対策せずにリトライを入れると、リトライの数だけ実行される可能性がある。

3. 冪等性 — 同じリクエストは何度届いても1回分

解決策は、リクエストに一意なID(冪等キー)を付けることです。サーバは処理済みのIDを覚えておき、同じIDが再び届いたら処理はせず、保存しておいた結果だけを返す。こうすれば再送は何回でも安全になります。この「何度実行しても結果が1回分」という性質を冪等(べきとう)と呼びます。

冪等キーの効果 — 同じ事故シナリオで比較する
前のデモと同じ「ACK消失 → 再送」のシナリオ。冪等キーありでは、サーバが処理済み表で ID:8321 を見つけ、引き落としをせずに保存済みの返事だけを返します。ボタンで「なし」に切り替えて、結末の違いを見比べてください。
f(f(x)) = f(x) 何回適用しても結果が変わらない性質=冪等。「残高を9万円にする」は冪等、「残高から1万円引く」は冪等でない — 後者は冪等キーで守る

4. みんな同時に再送すると「嵐」になる

リトライにはもう1つの罠があります。サーバが一瞬つまずいたとき、全クライアントが同じタイミングで再送すると、回復しかけたサーバに一斉射撃を浴びせて再び倒してしまう — リトライストームです。対策は、待ち時間を試行のたびに倍にする指数バックオフと、ランダムなずらし(ジッタ)の組み合わせ。

リトライストーム vs 指数バックオフ+ジッタ
36台のクライアントが、直後に復旧するサーバへ一斉にアクセスします。全員一斉リトライでは毎回まとまって押し寄せてサーバを過負荷に叩き落とし続け、いつまでも回復しません。指数バックオフ+ジッタに切り替えると、再送がばらけて処理能力の範囲に収まり、全員が成功に到達します。
待ち時間 = 基本値 × 2試行回数 + ジッタ 例: 0.4秒 → 0.8秒 → 1.6秒 → 3.2秒…と倍々に引きつつ、ランダムなゆらぎで各クライアントの再送時刻をばらけさせる
一歩先へ — 実世界のリトライ設計 TCP は再送とバックオフをプロトコル自体に組み込んでいる。Stripe の決済APIは Idempotency-Key ヘッダで再送を安全にし、AWS の SDK は指数バックオフ+ジッタが標準動作だ。「正確に1回だけ届ける(exactly once)」を通信だけで保証するのは極めて難しいため、実務では「少なくとも1回届ける + 受け手が冪等」という組み合わせで作るのが定石になっている。

5. まとめ