Microservicesで障害が起きたサービスを切り離すためのCircuit Breaker

Microservicesのような分散システムでは、各サービスが独立して動いている。(はず。) 仮にどこかのサービスに障害が起こった際も、全体は自律的に動き続けるべきである。

サービスが独立しているということは、別のサービスで障害が起きていることを、すべてのサービスは知らないはずである。 そのため、サービス間通信を行う際は、ネットワーク遅延などに備えリトライ処理を実装すべきだし、 呼び出される側もAPIでRate Limitを実装するべきだ。

しかし、例えばタイムアウト時間を経過するまで多数のリクエストが来続けて、スレッドプールを使い果たし、 システムリソースが枯渇してノードが停止、依存サービスもまとめて死ぬ、ということもあり得る。

このようなケースでシステムはどう動くべきか。タイムアウト時間を短くすればcancelされてスレッドの枯渇は防げるかもしれないが、 正常時のリクエストも失敗しやすくなってしまうかもしれない。 望ましいのは、操作がすぐに失敗し、成功しそうならちゃんと呼び出す、というものだ。

Circuit Breakerパターンは、このような、分散システムでのエラー処理に解決策を提供する。 Circuit Breakerは失敗する可能性のあるリクエストにおけるプロキシとして(実態はサービス内の組み込みロジックであるが) 動作する。つまり、Microservicesにおける各サービスは、自分自身でCircuit Breakerを実装する。 プロキシは、最近発生した障害の数を監視し、その情報を使って、リクエストを送信するか、すぐに失敗させるかを決定する。

Circuit Breakerは、次のような状態を持つState Machineである。

  • Closed
    • 正常な状態。いつもClosedであって欲しい。この状態のときは、リクエストは常に正常に送信される。
    • リクエストを送信して、それが失敗したときは、失敗の回数を内部に保持し、事前に設定したある閾値を超えることで、自身のStateをOpenに切り替える。
  • Open
    • 最も異常な状態。この状態のときは、リクエストを常に失敗させる。
    • Open Stateはタイムアウトタイマーを保持しており、その期限が切れると自身のStateをHalf-Openに切り替える。
  • Half-Open
    • ClosedとOpenの中間。操作が失敗すると自身のStateをOpenに、事前に設定したある回数以上連続で成功するとClosedに切り替える。

Circuit Breakerの実装で必要なことがいくつかある。

  • エラーハンドリング
    • Circuit Breakerを使ってリクエストを送信するサービスは、操作が行えないときのハンドリングを実装する必要がある。機能を一時的に低下させる、代替サービスをリクエストするなど。
  • 例外の種類を判定する
    • サーキットブレーカーは、帰ってきたエラーによってはTrip(Open Stateへの移行)を遅らせるよう調整すべきかもしれない。例えば、ノードが完全に壊れているときと、一時的な過負荷によるタイムアウトのち外など。
  • ロギング
    • Circuit Breakerは失敗/成功をロギングすべきだ。
  • 回復性
    • Circuit Breakerは、適切に自身のStateを移行できる必要がある。復旧しているのにClosedにならない、まだ完全に復旧していないのにClosedのまま、などの不適切なしきい値を設定してしまっては、Circuit Breakerを用意した意味がない。
  • 同時実行
    • 例えば複数のgoroutineが同時にCircuit Breakerにアクセスした際も、ブロッキングや、オーバーヘッドが発生しないようなmutex構造を備えているべきだ。

Goでは、go-kit/kit/circuitbreakerや、afex/hystrix-goなどがメジャーなライブラリだ。が、 circuit breaker自体はシンプルな動きだし、自分たちのオペレーションするMicroservicesに組み込みやすいような形のCircuit Breakerを自前実装してもいいと思う。