リトライ回数は「失敗のやり直し」ではなく負荷の事前決定|どの層が・何回まで・どの間隔で
外部のサービスと連携するコードを書いていると、リトライ回数を入力する欄に出会います。3か、5か。とりあえず3にして、次に進む。
このとき決めているのは「失敗したら何回やり直すか」だと感じますが、実際に決めているのは別のことです。相手のサービスが落ちかけているとき、そこへ送るリクエストを何倍にするか。これを平常時に、落ち着いた気分で決めています。
平常時には、この設定は何も起こしません。相手が正常なら1回で成功するので、リトライ回数が3でも10でも挙動は同じです。設定の意味が現れるのは、相手が苦しいときだけです。だから見直しのきっかけが生まれにくい——これは筆者の見立てですが、リトライ設定が根拠なく埋まりがちな理由の一つだと考えています。
この記事では、Google SRE本の第21章 Handling Overloadと、AWS Architecture BlogのExponential Backoff And Jitter(Marc Brooker、2015年3月4日、2023年5月に更新の注記あり)を材料に、この判断を4つの論点に分けます。
30秒で要点
- リトライ設定は「やり直しの回数」ではなく、障害時に相手へかける負荷の倍率を決めている
- Google SRE本は、リクエスト単位の上限3回に加えて、クライアント単位で「リトライが占める比率10%未満」という予算を併用している
- 回数の上限だけでは最悪ケースで3倍になりうる。比率の予算を重ねると一般的なケースで1.1倍に収まるとされる
- リトライするのは、拒否している層の直上の層だけ。複数の層が再試行すると組み合わせ爆発になる
- ジッター(待ち時間のばらつき)が無い指数バックオフは、AWSのシミュレーションで最も悪い結果だった
| 用語 | 意味 |
|---|---|
| リトライ | 失敗したリクエストを、もう一度送ること |
| API | ソフトウェア同士がやり取りするための窓口。ここでは外部サービスとの連携口を指す |
| 指数バックオフ | 再試行のたびに待ち時間を倍々に伸ばしていく方式 |
| ジッター | 待ち時間にランダムなばらつきを加えること。再試行が同時刻に集中するのを防ぐ |
| リトライ予算 | 再試行に使ってよい量の上限。回数で持つ場合と、全リクエストに占める比率で持つ場合がある |
| 層 | クライアント、フロントエンド、バックエンド、データベースのように、呼び出しが通過する段階のこと |
| 冪等(べきとう)性 | 同じ操作を何度実行しても、結果が1回実行したときと変わらない性質 |
論点1:その回数は、障害時に負荷を何倍にするか
Google SRE本の第21章は、リクエスト単位で最大3回のリトライ予算を置き、3回失敗したら呼び出し元に失敗を返す、と説明しています。
理由の説明が興味深いところです。3回とも過負荷のタスクに当たったのなら、データセンター全体が過負荷である可能性が高く、もう一度試しても助かる見込みは薄い——という理屈です。「3回やればだいたい成功する」ではなく、「3回失敗するような状況では、4回目に意味がない」という向きの判断になっています。
ここで注意が必要なのは、この3という数字がGoogleの社内システムを前提にしていることです。一般的なAPI連携の推奨値として持ち出せる値ではありません。持ち帰れるのは数字ではなく、回数を決めるときに「何回で成功するか」ではなく「何回失敗したら状況が変わっていると判断するか」を考えるという向きのほうです。
論点2:回数の上限だけでは足りない
同章は、回数の上限だけを置いた場合の問題も述べています。リクエスト単位で3回まで許すと、最悪のケースではリクエスト数が3倍に増えうる、と。
相手が過負荷で落ちかけているときに、送られてくる量が3倍になる。これは助けにならないどころか、悪化させます。
そこで同章が併用しているのが、クライアント単位のリトライ予算です。各クライアントが「自分の出したリクエストのうち、リトライが占める比率」を追跡し、その比率が10%を下回っているあいだだけ再試行する、という仕組みです。
この10%の予算を重ねると、一般的なケースでは増加が1.1倍に収まるとされています。3倍と1.1倍では、相手にとっての意味がまったく違います。
分母と分子をはっきりさせておきます。分母はそのクライアントが出した全リクエスト数、分子はそのうちリトライだったものの数、単位は比率です。回数の上限は1リクエストごとに効き、比率の予算はクライアント全体に効きます。両方あって初めて、「ある1件が粘る」ことと「全体として押し寄せる」ことを別々に抑えられます。
この値もGoogleの社内前提のものです。ただし、「回数だけでなく比率でも上限を持つ」という構造そのものは、自社の環境に移して考えられます。
論点3:リトライするのは、拒否している層の直上だけ
3つ目は、回数よりも見落とされやすい論点です。
Google SRE本は、リクエストのリトライは拒否している層の直上の層でのみ行うべきだとしています。例として、データベースのフロントエンドから返った失敗は、その直上にあるバックエンドだけが再試行すべきであり、複数の層が再試行すれば組み合わせ爆発になる、と説明されています。
そして、あるリクエストが処理できず再試行すべきでもないと判断した場合には、「overloaded; don't retry」というエラーを使い、連鎖的なリトライの爆発を避けるとしています。再試行するなという情報を、エラーとして下流へ伝えるわけです。
実務で確かめる価値があるのはここです。1つのリクエストが通る経路に、リトライの仕組みがいくつ入っているか。HTTPクライアントの設定、アプリケーション側の再試行、ジョブスケジューラの再実行——それぞれ別の人が別のタイミングで入れたものが、同じ経路上に重なっていることがありえます。3つの層がそれぞれ3回ずつ再試行すれば、最悪で27回になります。
これは「よくあること」だと主張する根拠を持っていません。ただ、重なっていないことを確認した記憶があるかと問われて答えられないなら、確認する価値はあります。
論点4:間隔にばらつきを持たせる
4つ目は待ち時間です。指数バックオフ——再試行のたびに待ち時間を倍々に伸ばす方式——は広く使われていますが、それだけでは足りません。
AWS Architecture Blogの記事は、100のクライアントが競合する状況のシミュレーションで、ジッター無しの指数バックオフが「明らかな敗者」だったと報告しています。作業量も所要時間も、ジッターを加えた方式より悪かった、という結果です。
なぜそうなるのかは、記事の結果から次のように読めます(ここは本記事による整理です)。同時に失敗したクライアントは、同じ計算式で待てば、同じタイミングで一斉に再試行します。待ち時間を倍々に伸ばしても、波が来る間隔が伸びるだけで、波そのものは残ります。ジッターは、この波をならすために入れるものだと理解できます。
同記事では、ジッターを加えたことで100クライアント競合時の呼び出し回数が半分以下になったと報告されています。方式ごとの比較では、Equal JitterがFull Jitterよりやや多くの作業をし、はるかに時間がかかったとされ、DecorrelatedとFullの優劣ははっきりしないものの、Fullのほうが作業は少なく時間はやや長い、とまとめられています。いずれもクライアントの作業量とサーバー負荷を大きく減らす、という評価です。
なお、この「半分以下」は、楽観的並行性制御の動きを単純にシミュレーションした条件下での値です。一般的な削減率として持ち出せる数字ではありません。
なぜ、この設定は根拠なく埋まるのか
ここからは筆者の見立てです。
リトライの設定値には、平常時に結果を返してくれないという性質があります。回数を3から5に変えても、相手が正常なら何も変わりません。試して確かめることができない設定は、「とりあえず妥当そうな値」で埋まり、埋まったまま動き続けます。
もう一つは、リトライが善意の機能に見えることです。「失敗しても諦めずにもう一度試す」は、素直に読めば丁寧な実装に聞こえます。それが相手にとっては負荷の増加だという視点は、自分のコードを見ているかぎり出てきにくいものです。
この2つが重なると、リトライ設定は「決めた覚えのある人がいない値」になります。障害が起きて初めて、その値が何を意味していたかが分かる——という順序になりやすいのだと考えています。
確かなことと、まだ確かではないこと
一次資料で確認したこと
- リクエスト単位で最大3回のリトライ予算を置き、3回失敗したら呼び出し元に失敗を返す(Google SRE本 第21章)
- クライアント単位でリトライ比率を追跡し、10%未満のときだけ再試行する予算を併用する(同)
- 回数の上限だけでは最悪ケースで3倍になりうるが、10%の比率予算を重ねると一般的なケースで1.1倍に収まる(同)
- リトライは拒否している層の直上の層のみで行い、「overloaded; don't retry」で連鎖を止める(同)
- ジッター無しの指数バックオフは、100クライアント競合のシミュレーションで作業量・所要時間ともに最も悪かった(AWS Architecture Blog)
- ジッターを加えると同条件で呼び出し回数が半分以下になった(同)
この記事では扱わないこと
- 特定のライブラリやSDKの既定のリトライ回数・挙動(確認していない)
- どのジッター方式を選ぶべきかの一般的な推奨(出典は特定条件下の比較)
- 冪等性の設計の詳細(冪等性とは?「もう一度送っていい処理か」を決めていないAPIの弱点で扱っています)
前提として置いておくこと
3回・10%・1.1倍という数値は、いずれもGoogleの社内システムを前提にした値です。自社の推奨値として転記できるものではありません。移せるのは「回数と比率の両方で上限を持つ」「リトライする層を1つに限る」「間隔にばらつきを持たせる」という構造のほうです。
4つの論点を、1つの連携について書き出す
いま動いている外部連携を1つ選び、次の4つを書き出してみてください。コードを変える前に、書けるかどうかを確かめる作業です。
- どの層がリトライしているか:そのリクエストが通る経路(クライアントのライブラリ、自社のアプリケーション、ジョブの再実行)のうち、再試行の仕組みが入っているのはどこか。2つ以上あるなら、なぜ2つ必要かを説明できるか
- 何回までか:それぞれの上限。掛け算すると、最悪で何回になるか
- 比率の上限があるか:全リクエストのうちリトライが占める比率に、上限が設けられているか。無いなら、障害時に相手へ何倍の量が向かうかを計算できるか
- 間隔にばらつきがあるか:待ち時間がランダム性を持っているか。固定間隔や、ジッター無しの指数バックオフになっていないか
4つのうち書けないものがあれば、そこが「決めていなかった」ところです。書けないことが分かるだけで十分で、この時点で設定を変える必要はありません。障害時に何が起きるかを言葉にできる状態になってから、変えるかどうかを決めれば間に合います。
外部サービスからの通知を受け取る側の設計はWebhook入門、AIのAPIを呼ぶ場合の再試行の線引きはAPI経由でAIを活用する方法で扱っています(どちらの記事のコード例もジッターを含まないので、本記事の論点4を補いとして読んでください)。エラーの種類ごとの判断はHTTPステータスコードとは?301・302・404・410の使い分け、再送とキャッシュの関係はno-cacheは「キャッシュするな」ではないが近い話題です。
この記事で扱ったような判断を、自社の状況に照らして整理したい場合は、無料診断ツールで状況を言語化するところから始めることができます。
状況を整理する(15分)