VOLUME 02 / CHAPTER 02

親切な再試行が、障害を広げる

タイムアウト、再試行、サーキットブレーカー、キューが、それぞれ何を制御するかを分ける。部分失敗から業務の結果を守る設計を考える。

出典照合日: · 独自解説

架空事例:青葉機材が予約、契約確認、通知を連携させる。要求数、待ち時間、再試行回数は計算のための仮定で、製品の実測値ではない。

遅い相手へ、もう一度頼む

契約管理サービスの応答が遅くなった。予約システムは、一時的な失敗なら再送すれば回復できるように作られていた。一回目が失敗すると、すぐに二回目を送る。二回目も失敗すると、三回目を送る。

営業のブラウザにも、同じ親切な仕組みがあった。応答がなければ、予約システムへ再送する。契約管理サービス内部のデータアクセスにも、失敗した問い合わせを再実行する処理がある。

一つの要求を救うための動作が、相手への仕事を増やしていた。

説明のため、直列に呼び出す四つの層があり、各層が初回を含め最大三回試行するとする。最下層の失敗がすべて上へ返り、各層が独立に試行を繰り返す場合、最下層に届く回数は最大で 3×3×3×3=81回 になる。これは設定と呼出し構造を単純化した上限の例である。三回の「再試行」なら初回を加えて四回になるため、言葉の数え方も揃えなければならない。

相手が過負荷で遅いなら、仕事を増やすことで回復を妨げる可能性がある。再試行は常に安全な追加機能ではなく、資源を使って成功の機会を買う操作である。1

タイムアウトは、相手の処理を消さない

呼出し元が二秒で待つのをやめても、呼出し先がその時点で止まるとは限らない。契約確認や予約確定が、その後も進む場合がある。呼出し元から見える「失敗」と、呼出し先に残る業務上の結果が分かれる。

したがって、タイムアウトした要求を新しい操作として再実行する前に、処理の性質を確かめる必要がある。参照の再実行と、予約を新しく確定する操作の再実行は、同じ危険を持たない。結果が届かなかっただけなのに別の予約を追加すれば、二重確約が起こり得る。既存テキスト第7章の冪等性は、この結果不明の問題から読むと意味が明確になる。1

キャンセルを伝える仕組みがあっても、伝えたこととキャンセルが完了したことは別である。既に確定した外部の処理を、通信を切るだけでなかったことにはできない。処理結果を照会する、同一操作を識別する、業務上の取消しを別に実行する、といった設計が必要になる。

待ち時間の設定にも範囲がある。接続を確立するまでの時間、接続後の応答待ち、要求全体の期限は同じではない。ある設定がDNSの名前解決や暗号化接続の確立まで含むかは、利用する実装の仕様を確認する。AWSの解説も、タイムアウトが覆う範囲を取り違える問題を挙げている。2

回数だけでなく、要求全体の残り時間を見る

利用者への回答を全体で五秒以内に終える必要があるとする。各外部呼出しへ五秒のタイムアウトを設定すると、二つを直列に呼ぶだけで全体の期限を超え得る。さらに再試行を加えれば、利用者が待つ時間は増える。

本事例では、要求の開始からの期限を持ち、各段階で残り時間を確認する案を考える。残り時間がほとんどないのに、長い外部処理を新たに始めても、求められる期限内に結果を返せない。再試行するかどうかも、その操作が再実行可能か、回復する種類の失敗か、残り時間があるかを合わせて判断する。

入力が業務ルールを満たさないという不成立は、同じ内容を直ちに再送しても解消しない。一時的な通信障害は、時間を置けば成功する可能性がある。HTTPの4xxと5xxの大分類だけで、すべての再試行可否を機械的に決めない。コード固有の意味、APIの契約、結果が確定したかを読む必要がある。

指数バックオフは、試行の間隔を広げる方法である。全員が同じ間隔で一斉に再送すれば、再び負荷が集中する。そこで待ち時間へばらつきを加えるジッターを使う。ただし、間隔を広げることは総試行回数の上限を決めることとは違う。回数、期限、負荷の予算も別に定める。12

再試行と遮断は、逆向きの判断

一時的な失敗へもう一度呼び出すのが再試行である。一方、失敗が続く依存先へ今は呼び出さないと判断するのがサーキットブレーカーである。

代表的なモデルでは、Closedで呼出しを通し、失敗が所定条件を満たすとOpenで遮断する。その後、Half-Openで限られた呼出しを試し、回復しているか判断する。これは、相手の修復を実行する機能ではない。障害中に呼出しを続けることで、待ちや失敗が拡大するのを抑えるための制御である。3

遮断すれば必ず利用者が助かるわけでもない。予約確定に必要な契約情報を得られない場合、正当な確約はできなくなる。本事例では、その依頼を確認中として保存し、後で再確認する業務を候補にする。古い結果を使って確約する案を採るなら、その古さを許容する業務上の根拠が別に必要である。

ブレーカーの状態をどの単位で共有するかも影響する。一つの依存先だけが不調なのに、無関係な機能への呼出しまで止めてしまえば、障害範囲を自ら広げる。一方、呼出し元が多数あり、それぞれ独立に試験的な呼出しを行うなら、回復直後に集中する可能性がある。状態の名前を覚えるだけでは、この設計は決まらない。

隔離するのは、コードだけではない

通知機能を別のモジュールへ分けた。ところが、通知先の応答が止まると、予約照会まで遅くなった。両者が同じ実行枠や接続資源を使い、通知待ちがそれを占有していたためである。

ソースコードの責務を分けることと、実行時の資源を分けることは違う。Bulkheadの考え方では、実行数や資源のプールを分け、一つの機能の過負荷が別の機能を使えなくすることを抑える。1

ただし、プールを分ければ無制限に安全になるわけではない。全体のCPUやメモリ、同じDBの容量は共有されているかもしれない。予約用の枠を確保した結果、通知側に滞留が生じることもある。どの仕事を優先し、どれだけ待たせ、どの条件で受け付けないかを決める。

待ち行列へ入れ続ければ、入口で拒否しなくて済むように見える。しかし、利用者の期限を過ぎてから実行される仕事に価値があるかは別である。古い要求を処理し続けて、新しい要求がさらに遅れる場合もある。キューの長さだけでなく、待っている時間と業務上の期限を見る。

非同期化で移るもの、残るもの

予約確定後の通知をキューへ送れば、通知の完了を待たずに営業へ結果を返せる。呼出し元と受信側の実行時刻を切り離せる点が利点である。

しかし、仕事が消えたわけではない。受信側が処理できる量より到着する量が多ければ、滞留は増える。仮に毎秒三十件到着し、毎秒二十件処理できる状態が六十秒続き、損失や重複がない単純なモデルなら、待ち件数は六百件増える。この計算は容量の不足を可視化するもので、キューを置くだけでは解消しない。

また、送信したことと、キューへ確実に保存されたことは同じではない。キューが受け付けたことと、受信側の業務処理が完了したことも違う。どの時点を受付成功とするかを決め、それ以降の追跡に必要な識別子を返す必要がある。

配送が少なくとも一回という性質を持つ場合、重複が起こり得る。受信側が通知履歴や業務操作の識別子を使って扱う必要がある。順序も、システム全体で一つの順序が必要なのか、同じ予約の更新間だけでよいのかを区別する。全件の順序を強く揃えるほど、独立に進められる仕事を制限する場合がある。1

一つのメッセージを何度処理しても失敗する場合、無限に再配送していては前へ進めない。再処理不能な仕事を識別し、原因と業務上の対応を確認する経路が必要になる。保管場所を別にしただけで対応が完了したとみなさず、誰が何を確認して再開するかまで決める。

完了の意味を利用者へ返す

営業へ「受け付けました」と返したとき、予約は確定しているのか。確認待ちなのか。通知だけが未完了なのか。この区別が曖昧なら、内部の耐障害性が向上しても、利用者が誤って顧客へ確約する可能性がある。

本事例では、受付番号と処理状態を区別し、結果を確認する方法を提供する。通知の失敗は確定した予約をなかったことにせず、必要な再送として扱う。一方、契約確認が終わっていない依頼は確定と表示しない。内部処理を非同期にすることと、業務上の成功を先に約束することを混同しない。

SAシラバス4-9と5-5の連携・非機能要件は、個別の通信方式の選択で終わらない。4 遅延や部分失敗が起きたときにも、利用者とシステムが同じ結果を理解できることが必要である。

親切な再試行は、条件を決めれば有用な回復手段になる。しかし、誰が何回試し、どの期限でやめ、その後の業務をどう進めるかがなければ、失敗を増幅する。耐障害性は、再実行の数ではなく、失敗しても守る約束から設計する。

知識を答案へ戻す

午前Ⅱ。 再試行は再び呼ぶ、ブレーカーは一時的に呼ばない、Bulkheadは資源の波及を限定する、キューは実行時刻を切り離す。それぞれが保証しない性質も確認する。非同期化は整合性や処理完了の自動保証ではない。

午後Ⅰ。 本文の依存関係と障害時の動作を追う。通知の失敗で予約まで止まるなら、何を直列に待ち、何を共有しているかを特定する。状況を読まずにマイクロサービスや再試行を増やす案を正解にしない。

午後Ⅱ。 どの失敗を想定し、どの機能を継続し、何を保留したかを説明する。本章のように、契約情報が不明な依頼を確約せず保存する判断なら、可用性と業務上の正確性の折合いが見える。

立ち止まって考える:最大三回の試行を、すべての層に置けば安全か

層ごとの試行が掛け合わされ、下流の負荷を増やす可能性がある。初回を含む回数、全体の期限、再実行の安全性、失敗の種類を確認し、再試行を担う場所と予算を定める。単に試行回数を増やすことを耐障害性の向上とみなさない。

出典と照合範囲

[1] コンプリートテキスト第7・13・16・18章:部分失敗、冪等性、再試行、資源隔離、キュー、整合性を基礎とした。四層の試行回数と滞留件数は、本書の仮定から計算した例である。

[2] Amazon Builders’ Library, Timeouts, retries, and backoff with jitter:タイムアウトの適用範囲、再試行の負荷、待ち時間の分散という観点を参照した。製品ごとの現在の既定値を説明するものではない。AWS公開記事

[3] Microsoft Azure Architecture Center, Circuit Breaker pattern:Closed・Open・Half-Openの代表的なモデルと、Retryとの目的の違いを照合した。特定の閾値や共有方式を普遍的な要件としていない。公式文書

[4] IPA「SAシラバス Ver.5.2」:4-9、5-5、8-1、8-4。連携と方式、非機能要件と評価への対応を照合した。公式PDF