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

EAI 中間層をつくる

早く断るのが親切だ

TT Labで続きを見る

一言でいうと

遅い対象1つがハブ全体を引きずり下ろすことを、連鎖障害と呼びます。防ぐ方法は、2つを重ねることです。死んでいる対象は呼び出さずにすぐ拒否し(サーキットブレーカー)、生きてはいるが遅い対象には、同時に送れる量を対象ごとに絞ります(バルクヘッド)。どちらも「待たせるより早く拒否する」という同じ原理、つまりバックプレッシャーです。

なぜ必要なのか

勘定系が止まったとします。ハブはリクエストごとに勘定系へ接続し、レスポンスを待ちます。モジュール4のタイムアウトが3秒なら、毎秒100件が入ってくる間に、ハブには待機中のスレッドが300個溜まります。スレッドごとにメモリとソケットを握っているので、ハブが先に息詰まり、勘定系とまったく関係のない保険請求の取引までが、ハブの前で行列に並びます。勘定系1つの障害が、ハブを経由して、すべての取引の障害になります。

タイムアウトだけでは足りません。タイムアウトは「1件をどれだけ待つか」に答えるだけで、「死んでいるとわかっている対象を呼び続けるのか」には答えません。そして、死にかけている対象にリクエストを注ぎ続けると、その対象が回復する機会も奪ってしまいます。

どう動くのか

サーキットブレーカー。Martin Fowlerの記事が整理しているとおり、保護する呼び出しを包むオブジェクトが失敗を数え、基準に達したら回路を開きます。開いている間は呼び出さず、即座にエラーを返します。Fowlerは、このパターンがMichael Nygardの書籍『Release It!』で広く知られるようになったと書いています。状態は3つです。

状態 呼び出し 遷移
CLOSED する 連続失敗が基準に達したら → OPEN
OPEN しない(すぐE904) リトライ時間が経過したら → HALF_OPEN
HALF_OPEN 試験呼び出しを1件だけ 成功 → CLOSED、失敗 → OPEN(時間を最初からやり直し)

半開(HALF_OPEN)が核心です。回路を永遠に開いたままにすると、対象が回復しても気づけません。時間が経ったからといって一斉にすべて閉じると、たった今生き返った対象に、溜まったリクエストが殺到して、再び殺してしまいます。1件で探りを入れ、その1件が成功したら閉じます。複数のスレッドが同時に尋ねても、試験呼び出しは1件でなければならないので、状態の変更はロックの中で行います。

何を失敗として数えるか。残高不足(B201)は、勘定系が健全に答えたものです。これを失敗として数えると、月末に残高不足が集中するだけで、正常な勘定系へ向かう道を自分で断ち切ることになります。失敗とは、対象が処理できなかったと言ったもの(E500)、答えがなかったもの(E901)、接続すらできなかったもの(E902)で、システムエラーだけを数えます。

バルクヘッド。船の隔壁のように、1つの区画に水が溜まっても、ほかの区画は乾いたままにしておく設計です。対象ごとに同時処理数の上限(セマフォ)を別々に置きます。勘定系の枠が3なら、勘定系へ向かうリクエストは同時に3件までしか入れず、4件目からは待たずに過負荷(E905)として拒否します。請求には別の枠があるので、勘定系が遅くても、請求の取引は自分の枠で素早く通り抜けます。上限を超えたリクエストを行列に並べて待たせると、結局スレッドが溜まり、最初の問題に戻ります。早く拒否すれば、呼び出し元(チャネル)が、リトライ・代替経路・案内文言のなかから選べます。

サーキットとリトライ。リトライは一時的な失敗を乗り越えるために使い、サーキットは継続的な失敗で呼び出しを止めるために使います。リトライする側がサーキットの拒否(E904)までリトライすると、サーキットが開いた意味がありません。そのため拒否コードを別に設け、呼び出し元はE904・E905にすぐリトライしません。

数字はどう決めるか。連続失敗の基準が低すぎると、一時的な揺らぎでも回路が開き、高すぎると、死んでいる対象を長く呼び続けます。リトライ時間は、対象が通常回復するのにかかる時間より短いと、半開の試験が失敗ばかりを続け、長すぎると、回復したあともしばらく拒否し続けます。同時処理数の上限は、対象が耐えられる同時実行数(コネクションプールのサイズなど)を超えないように決めます。正解の数字はなく、対象の普段のメトリクスを見て決めたあと、遷移の記録を見ながら直します。

状態遷移を記録します。サーキットが開いたという事実は、障害のシグナルです。遷移(CLOSED→OPEN、OPEN→HALF_OPEN、HALF_OPEN→CLOSED)を、時刻と対象とともに残せば、「勘定系が14:02に開き、14:05に閉じた」が1行で見えます。本番では、この遷移をアラートにつなげます。

現場での姿

最もよくあるミスは、サーキットを1つだけ置くことです。ハブ全体にブレーカー1つをかけると、勘定系の障害で、請求・カードの取引までE904を受け取ります。分離ではなく全面遮断です。2つ目は、同時処理数の上限を、スレッドプール1つだけで設けることです。プール全体が対象1つに占領されれば、結果はバルクヘッドがないのと同じです。3つ目は、業務エラーを失敗として数えるサーキットです。4つ目は、半開なしで「30秒後に自動で閉じる」ように作ったものです。閉じた瞬間に、溜まったリクエストが一斉に殺到します。最後に、サーキットが開いたという記録がなければ、運用者は「E904がなぜ出るのですか」という問い合わせを受けて初めて気づきます。

次のラボですること

breaker.pyに状態機械を作り、採点ツールがフェイククロックで時間を進めて遷移を確認します。そのあと、2つの対象(勘定系・請求)へ中継する骨組み(relay_base.py)に組み込みます。死んでいる対象にはすぐE904、同時実行の上限を超えたらE905、対象ごとに分けて分離、状態遷移の記録、業務エラーは失敗として数えない、の順です。採点ツールは、勘定系フィクスチャの呼び出し回数と同時処理の最大値で確認します。