遅い相手ひとつにハブを引きずらせない
目標
サーキットブレーカーと対象別のバルクヘッドで、遅い、あるいは死んでいる対象1つがハブ全体を引きずり下ろさないようにします。
なぜ重要なのか
タイムアウトは、1件をどれだけ待つかを決めるだけです。死んでいる対象を呼び出し続けると、待機するスレッドが溜まってハブが先に息詰まり、関係のない取引まで一緒に止まります。早く拒否する仕組みが対象ごとに別々にあって初めて、障害が広がりません。
ステップ
/root/eaimw/breaker/breaker.pyにCircuitBreaker(failure_threshold, reset_timeout, clock=time.monotonic)を作成してください。state(CLOSED・OPEN・HALF_OPEN)、allow()、record_success()、record_failure()を持ちます。連続失敗が基準に達したらOPEN、OPENならallow()はFalseです。時刻は必ずclock()で読みます(採点ツールがフェイククロックを渡します)。- OPENで
reset_timeoutが経過したら、allow()が1回だけTrueを返してHALF_OPENになるようにしてください。試験呼び出しが成功したらCLOSED、失敗したら再びOPEN(時間は最初から)です。複数のスレッドが同時に尋ねても、1件だけを許可します(ロック)。 cp /opt/lab/fixtures/eaimw/breaker/relay_base.py /root/eaimw/breaker/relay.pyで始めて、--fail-threshold(既定は5)・--reset-timeout(既定は10)引数を追加し、対象の呼び出しをブレーカーで包んでください。開いていれば呼び出さず、すぐE904を返します。このステップでは、0000以外の結果を失敗として数えます。--max-inflight(既定は10)の同時処理数の上限を設けてください。上限に達していたら、待たずにすぐE905を返します。- ブレーカーと同時処理数の上限を、対象(CORE・CLAIM)ごとに別々に置いてください。勘定系の故障や飽和が、請求の取引に広がってはいけません。
--breaker-log(既定は/root/eaimw/breaker/breaker.log)に、状態が変わるたびに<시각>|<대상>|<FROM>-><TO>(プレースホルダーは時刻と対象です)を1行残してください。- 失敗として数えるものを、システムエラー(E500・E901・E902)に絞ってください。業務上の拒否(B2xx)は、対象が健全に答えたものなので、成功として数えます。
参考
- 同時処理数の上限:
threading.BoundedSemaphore(n)のacquire(blocking=False)がFalseなら、上限に達しています。終わったら必ずrelease()します(try/finally)。 - 状態遷移の記録:
allow()の前後とrecord_*()の前後のstateを比較すれば、変わった瞬間がわかります。 - 勘定系フィクスチャのスイッチ:
curl -XPOST localhost:9201/_ctl -d '{"mode":"fail"}'(500)、"slow"(遅延)、"normal"。統計は/_statsのcalls・max_inflightです。 - よくある間違い: ハブ全体にブレーカー1つだけを置いてしまうこと、上限を超えたリクエストを行列に並べて待たせてしまうこと、HALF_OPENで複数件を許可してしまうこと、
time.time()を直接呼んでフェイククロックを無視してしまうこと。
連続失敗が溜まったら回路を開く
/root/eaimw/breaker/breaker.pyのCircuitBreakerが、連続失敗の基準でOPENになり、OPENならallow()がFalseを返すようにしてください。
成功したら失敗数を0に戻して「連続」を数えます。開いた時刻はtime.time()ではなくself.clock()で記録してください。採点ツールが時計を握ります。
1件で探りを入れて閉じる
reset_timeoutのあとHALF_OPENで試験呼び出しを1件だけ許可し、成功ならCLOSED、失敗なら再びOPENにしてください。
allow()の中で、OPENかつ時間が経過していればHALF_OPENに変え、試験呼び出しがすでに出たかの印を持ちます。2つのスレッドが同時に尋ねても、片方だけがTrueを受け取るよう、ロックの中で判断してください。
開いていれば呼び出さない
relay_base.pyをコピーして対象の呼び出しをブレーカーで包み、開いていればE904ですぐ答えてください(--fail-threshold・--reset-timeout)。
handle_txが包む場所です。allow()がFalseならcall_targetを呼ばずに戻り、呼び出したあとは結果に応じてrecord_success/record_failureを呼びます。
上限を超えたら待たない
--max-inflightの同時処理数の上限を設け、上限に達していたらすぐE905で答えてください。
BoundedSemaphoreのacquire(blocking=False)がFalseなら、上限に達しています。呼び出したあとは、例外が出てもreleaseされるように、try/finallyを使います。
対象ごとにバルクヘッドを分ける
ブレーカーと同時処理数の上限をCORE・CLAIMの対象ごとに別々に置き、勘定系の故障・飽和が請求に広がらないようにしてください。
1つずつ置いていたものを、対象名をキーにした辞書に変えます。隔壁が1つなら、1区画の水が船全体に広がります。
状態遷移を記録する
--breaker-logに、状態が変わるたびに「時刻|対象|FROM->TO」を1行残してください。
allow()の前後、recordの前後のstateを比較し、違っていれば1行書きます。複数のスレッドが書くので、ロックを取って書きます。
業務上の拒否は失敗ではない
失敗として数えるものをE500・E901・E902に絞り、B2xxは成功として数えてください。
残高不足は、勘定系が健全に答えたものです。これを失敗として数えると、月末の業務上の拒否だけで、正常な勘定系へ向かう道を断ち切ってしまいます。