バルクヘッド
Bulkhead ・ バルクヘッド
リソースを区画に分け、一部の障害が全体の資源を食い潰さないようにする隔離パターン。
概要
バルクヘッド(bulkhead)は、システム内のリソース — スレッドプール、コネクションプール、メモリ、サーバのインスタンスなど — を用途ごとの区画に分割し、ある区画で起きた障害が他の区画の資源まで食い潰さないようにする隔離パターンです。名前は船の「隔壁」に由来します。船体を水密の区画に区切っておけば、一箇所に穴が空いて浸水しても被害はその区画に閉じ、船全体は沈まない。この構造をソフトウェアに持ち込んだのがバルクヘッドです。
登場するのは主に、1つのサービスが複数の依存先を呼び出す場面です。マイクロサービスのように依存先が多いアーキテクチャでは、依存先のどれか1つが不調になることは日常です。そのとき「不調な依存先に関わる処理だけが劣化し、他の処理は普段どおり動く」状態を作れるかどうかが、システム全体の生存率を大きく左右します。バルクヘッドは、その隔離を「そもそもリソースを共有しない」という構造で保証するアプローチです。
なぜ生まれたか
このパターンが必要になった典型的な事故は、「1つの遅い依存先が、健全な処理まで巻き添えにして全体を止める」というものです。あるWebサービスが依存先AとBを呼び出しており、リクエストを処理するワーカースレッドは20本の共有プールだとします。ここでBの応答が極端に遅くなると、Bを呼び出したスレッドは応答待ちのまま返ってきません。新しいリクエストが来るたびに別のスレッドがBの呼び出しに入り、やがて20本すべてがBの応答待ちで占有されます。この瞬間、Aだけを使う処理も、依存先を一切呼ばない処理も、ワーカーが払底しているために一切さばけなくなります。Bという一部品の劣化が、サービス全停止に化けるのです。
タイムアウトを設定していても、この事故は防ぎきれません。タイムアウトが切れるまでの間はスレッドが占有され続けるため、流入が速ければプールは枯渇します。「障害の伝播を時間で区切る」タイムアウトに対して、「障害が使える資源の総量を最初から区切っておく」のがバルクヘッドで、Michael Nygard の『Release It!』(2007年)が安定性パターンの一つとして名付け、Netflix のライブラリ Hystrix が依存先ごとのスレッドプール分離として実装したことで広く知られるようになりました。
詳細
共有プールの何が危ないか — before / after
バルクヘッドの核心は図で見るのが早いです。左が共有プール、右が依存先ごとにプールを分けた構成です。同じ「依存先Bの応答遅延」が起きても、左では全ワーカーがBの待ちに吸い込まれて健全なAまで停止するのに対し、右では被害がB用の区画に閉じ、Aの処理は普段どおり流れ続けます。
重要なのは、右の構成では「B用プールが満杯になったら、Bへの新規リクエストは待たずに即座に失敗させる」点です。上限に達した区画で粘らず速く失敗することで、呼び出し元は代替手段(フォールバック)に切り替えられます。この「素早く失敗して品質を落として生き残る」振る舞いはグレースフルデグラデーションそのものであり、バルクヘッドはその前提となる隔離を提供します。
実装の形はいろいろある
もっとも古典的な実装は、依存先ごとのスレッドプール分離です。Hystrix はこれを標準の動作にしていました。スレッドを丸ごと分けるとコンテキストスイッチのオーバーヘッドが増えるため、後継の Resilience4j などでは「同時実行数の上限をセマフォで区切る」軽量な方式も選べます。スレッドは共有しつつ、依存先Bへの同時呼び出しは最大10まで、と数だけを区画化する考え方です。
コネクションプールの分割も定番です。たとえば同じデータベースを使うオンライン処理と夜間バッチが1つのプールを共有していると、バッチが接続を占有した瞬間にユーザー向けの応答が止まります。用途別にプールを分けておけば、バッチの暴走はバッチ用の区画で止まります。同様に、メッセージキューを重要度別に分ける、ロードバランサ配下のインスタンス群を顧客セグメント別に分ける、といった粒度の大きい区画化もすべてバルクヘッドの応用です。
さらに一般化すると、この発想は基盤レイヤにも現れます。コンテナごとの CPU・メモリ上限や、Kubernetes の resource limits / ResourceQuota は、「1つの暴走したワークロードがノードやクラスタの資源を食い潰さない」ためのバルクヘッドです。マルチテナントの SaaS で特定テナントの過剰な利用が他テナントに波及しないよう、テナントごとにレートリミットや専用リソースを割り当てるテナント分離も同じ系譜にあります。「共有をやめて上限で区切る」という一つの原理が、スレッドからクラスタまでスケールを変えて繰り返されているのです。
仲間のパターンとの組み合わせ
バルクヘッドは単独で使うものではなく、回復性パターンの積み重ねの一段です。まずタイムアウトで1回の呼び出しの待ち時間を区切り、リトライ(指数バックオフ)で一時的な失敗を吸収し、サーキットブレーカーで壊れた依存先への呼び出し自体を遮断する。バルクヘッドはこれらと直交する軸で、「それでも障害が起きたとき、消費される資源の総量」に天井を付けます。サーキットブレーカーが「開くまで」の間もプール枯渇を防げるのは、バルクヘッドがあるからです。
トレードオフも明確です。プールを分割すると、全体としてのリソース利用効率は下がります。共有プール20本なら一時的にAへ18本使えたところが、分割後はA用の10本が上限になる。区画のサイズを小さくしすぎれば平常時から性能の天井になり、大きくしすぎれば隔離の意味が薄れます。依存先ごとの平常時のトラフィックとレイテンシから必要な同時実行数を見積もり、モニタリングで各区画の使用率を観測しながら調整するのが実務の進め方です。区画が実際に障害を閉じ込められるかは、カオスエンジニアリングのように依存先の遅延を意図的に注入して確かめるのが確実です。
