怪しい看板の3階
火曜日の午後2時。都内の雑居ビル。
スマホの地図アプリと、Slackのスクリーンショットを交互に見ながら、狭い階段を上がった。2週間前、社内Slackの#tech-helpチャンネルに流れた匿名の投稿。「レガシーコードの構造問題ならLCIに相談してみてください。雑居ビルの3階、怪しい看板のところです。腕は確かです」。投稿者は不明。「レガシー・コード・インベスティゲーション」でGoogle検索したら、住所と「コード問題、解決します。LCI」だけの簡素なウェブサイトが出てきた。
2階の踊り場を過ぎた。3階に小さなプレートが1枚。「レガシー・コード・インベスティゲーション(LCI)」。看板というより表札。
——本当に怪しい。
ドアの前に立つ。ノックした。返答がない。もう一度ノックする。
中から低い声。「……開いているよ」
ドアを開けた。まず匂い。接着剤の有機溶剤と、甘ったるいエナジードリンク。デスクにモニタが3台。足元にエナジードリンク缶が積み上がっている。ホワイトボードに何か書いてあるが、ドアからでは読めない。
デスクの端で——男が椅子に座り、木製の帆船模型を組み立てていた。右手にピンセット、左手に薄い木の板。木くずが散っている。
「……隔壁が合わないな。0.3ミリ厚すぎる」
わたしのことは見ていない。
「あの——Slackで紹介されて来たんですが。LCIの——」
男がようやく顔を上げた。わたしを見る。2秒。それから椅子を回した。
「ワトソン君。遅かったね」
「——え?」
「座りたまえ」
「……わたし、ワトソンではないんですが」
完全に無視された。椅子を指している。
2秒待った。反応はない。……まあいいか。訂正しても聞いていないし。争うほどのことではない。
椅子に座る。鞄からノートPCと方眼ノートを取り出した。
「ロック……さん、ですか?」
「ロックだ。探偵だ」
探偵。一瞬ノートPCの手が止まった。処理して、流した。
「先月末、うちのECサイトが30分間止まりました」
ノートPCを開いた。自分で描いたシステム構成図を映す。3つの機能——注文API、商品検索API、レポートバッチ——が1つのDBコネクションプール(max: 20)から矢印で接続されている。手描き。色分けしてある。
「月末のレポートバッチが走ったら、注文も検索も全部止まりました。原因はコネクションプールの枯渇です。バッチが20本中15本を占有して」
ロックさんは図を覗き込んでいた。同時に手元の船模型をいじっている。集中していないように見えるが——どうだろう。
「コネクションプールの上限が20で、バッチが15本使うと残りが5本。秒間80リクエストを5本では捌けません」
ロックさんが船模型の部品を置いた。構成図を見ている。
「20本のコネクション。注文、検索、バッチ。——全員が同じ部屋にいるわけだ」
「はい。バッチの同時実行数を減らせば当面は凌げますが——」
「対症療法だ。バッチの数を減らしても、部屋は1つのままだ。別の何かが暴走すれば、また全員が道連れになる」
少し間を置いた。
「……はい。それで、根本的にどうすればいいのかがわからなくて」
隔壁のない船
ロックさんが船模型の船体を持ち上げた。わたしに見せる。船体の内部は1つの空洞。仕切りがない。
「これを見たまえ。この船には、まだ隔壁が入っていない。船体の中は1つの空間だ。——もし、ここに穴が開いたら?」
「水が入って……全体が沈みます」
「君のシステムは——この船だ。20本のコネクションが1つの部屋にある。1つの機能が暴走すれば、全員が溺れる」
ノートPCで現在のコードを見せた。
Beforeコード: 全機能が共有するコネクションプール
| |
3つの機能が、すべて同じconnection_poolからコネクションを取得している。壁がない。1つの船体に穴が開いたら、全部が沈む構造。
方眼ノートに図を描いた。1つの大きな四角——コネクションプール:20——に、3つの矢印が入っている。バッチの矢印だけ太くした。接続を大量に使うから。
「レポート1件で平均5秒。月末に50件が並行で走ると——」
「15本のコネクションが5秒間占有される。その間に来た注文リクエストは——5本のコネクションの空き待ちだ」
「コネクション取得のタイムアウトが60秒に設定してあって——」
「60秒待って、結局取れなければ503。——ワトソン君、60秒は長い」
「……はい。その60秒の間に新しいリクエストがさらに溜まって——」
「雪崩だ」
30分の全館停電。社長がSlackに「EC止まってるぞ」と書き込んだのを見て、わたしがバッチをkillして仮復旧した。あの30分間のことを考えると、声が固くなる。
沈まない船の設計図
ロックさんがピンセットで薄い木の板を1枚取った。接着剤を塗り、船体の内側に嵌める。
「この1枚の板が——隔壁だ。これで、船体が2つの部屋に分かれた」
「図にしていいですか?」
ノートを取り出した。
「コネクションプールを分ける……ということですか」
3つのプールに分けた図を描いた。注文API:8本、検索API:8本、バッチ:4本。合計20本。——描いた後、自分の図を見て眉をひそめた。
「でも、分けたら全体の効率が落ちませんか? バッチが走らない日は4本が遊んでいます。注文が混んでも使えない」
ロックさんが船模型を置いた。
「船の隔壁も、貨物室の容積を減らす。隔壁の板の分だけ、荷物が積めなくなる。——しかし、ワトソン君。1箇所の穴で船を失うのと、貨物室が少し狭くなるのと、どちらが高くつく?」
3秒の沈黙。図を見ている。
「……30分の全館停電の方が、はるかに高いです」
「そうだ。Bulkheadの本質は——効率を少し犠牲にして、壊滅を防ぐことだ。完璧な効率は、完璧な脆さの裏返しでもある」
Rate Limitingとの違い
ロックさんがBulkheadのコード概念を説明している間に、ノートに「セマフォ」と書いた。その横に「Rate Limiting?」と疑問符付きで。
「ロックさん、1つ聞いていいですか。これは……セマフォですよね? コネクションの数を数えて、上限に達したら止める。Rate Limitingとは違うんですか?」
ロックさんが椅子を少し回した。
「いい質問だ。——Rate Limitingは『入口の門番』だ。1秒間に何人入れるかを制御する。時間あたりの流量の制限」
「Bulkheadは?」
「Bulkheadは『部屋の壁』だ。入ってきた人を、どの部屋にどれだけ入れるかを制御する。同時に存在できる数の制限」
ノートに描いた。入口に門番、中に壁。
「門番は……入る速度を制御。壁は……中の配分を制御」
「そうだ。門番がいても、壁がなければ——全員が同じ部屋に殺到する。壁があっても、門番がいなければ——際限なく人が入ってきて、全ての部屋が溢れる」
図を見ている。ゆっくりと。
「……両方要る、ということですか」
「二重の防衛線だ。入口と内部。——どちらか一方では足りない」
Afterコード: Bulkheadクラス
ロックさんがホワイトボードに向かった。コードを書き始める。
| |
「図にしていいですか?」
ノートにBulkheadオブジェクトの箱を描いた。中に「max: 8」「active: 3」と書く。「あと5本空いている」。
execute()の中身を追った。呼ばれるとまずカウンタをチェックする。上限に達していたら即座にcroak——例外を投げる。達していなければカウンタを+1して、渡されたコードを実行する。evalで包んでいるから、コード内で例外が発生しても必ずカウンタを-1してから再送出する。リソースのリークがない。
BulkheadRegistry
| |
「Registryがあると、機能名で引けるんですね。注文は注文用、検索は検索用……」
ロックさんがうなずいた。
隔壁の粒度
ノートの図を発展させた。3つのプール(注文/検索/バッチ)を描いた後、注文APIの中をさらに分割する線を描きかけて、手を止めた。
「隔壁の粒度はどう決めるんですか? いま3つに分けましたけど——注文APIの中でも、決済呼び出しと在庫照会は分けた方がいいですか?」
「ワトソン君、船の話をしよう。タイタニック号を知っているね?」
「……隔壁があったのに沈んだ船、ですか」
「16の水密区画があった。設計上は4区画の浸水に耐えられるはずだった。——しかし氷山は6区画を破った。隔壁の上端が低すぎて、1つの区画から次の区画へ水が溢れた」
「隔壁が多ければ安全というわけではない……」
「逆もまた真だ。隔壁が少なすぎれば、1室の浸水で沈む。——粒度の正解は、障害の影響範囲で決める」
ノートに戻る。
「注文APIの中で、決済と在庫照会を分ける必要があるかどうかは——」
「どちらかが遅延したとき、もう一方を巻き添えにしたくないなら、分ける。巻き添えでも許容できるなら、分けない。——ビジネスの判断だ」
「決済が止まるのと、在庫照会が止まるのとでは……決済が止まる方が致命的です」
「ならば決済は独立した隔壁を持つべきだ。在庫照会は注文APIの残りと同居でもいい。——隔壁の数は、守りたいものの数で決まる」
即時拒否の原則
「制限に達したらどうなるんですか? 処理を待たせますか? 即座に断りますか?」
「2つの方針がある。ワトソン君、ノートに描きたまえ」
素直にペンを取った。
「1つ目:即座に拒否。Bulkheadが満杯なら、リクエストを例外で返す。呼び出し元はすぐに別の手を打てる」
描きながら、口が先に動いた。
「待ち時間ゼロ。でも、一時的なピークでも容赦なく弾く……」
「2つ目:一定時間だけ待つ。Bulkheadに空きが出るまで最大N秒待つ。空きが出なければ拒否」
「待つ場合……スレッドが待機している間、そのスレッド自体がリソースを消費しますよね。Bulkheadで守ろうとしているリソースとは別の場所でリソースが詰まる」
ロックさんが少しだけ目を見開いた。すぐに戻った。
「……そうだ。待機そのものがリソースを消費する。隔壁の手前に行列ができて、その行列がシステムを圧迫する——本末転倒だ」
「じゃあ、即座に拒否が基本で——待つのは特殊なケースだけ、ですか」
「正解だ。隔壁の原則は速やかな失敗。空きがなければ即座に『ここは満室だ』と断る。——断り方さえ誠実であれば、客は別の店に行ける」
Afterコード: 機能別Bulkheadで隔離
| |
各メソッドが最初にやることが変わった。connection_pool->get_connectionではなく、bulkhead_registry->get('order')。まずBulkheadを取得し、そのexecute()の中でコネクションを使う。バッチ用Bulkheadが4接続の上限に達しても、注文用Bulkheadの8接続は無傷。隔壁が浸水を閉じ込めている。
バッチの失敗とビジネス判断
ノートの図を見返した。バッチ用Bulkhead——4接続。
「Bulkheadで隔離すれば、バッチの暴走は注文APIに影響しなくなる。——でも、バッチ自体が失敗するのは許容していいんですか?」
「全館停電で全レポートが生成されないのと、4接続の範囲内で優先度の高いレポートだけ先に生成されるのと——どちらが望ましい?」
「……後者です。でも、それは——」
「技術の問題ではなく?」
「経理チームと話す必要があります。どのレポートを先に出すか、優先順位を決めないと」
「Bulkheadは壁を立てるだけだ。壁の向こうに何を入れるかは——」
「ビジネスが決める」
ロックさんが船模型の隔壁を1枚嵌めた。
「そうだ」
classDiagram
class Bulkhead {
-String name
-Int max_concurrent
-Int _active_count
+execute(CodeRef code) Any
+available() Int
+is_full() Bool
}
class BulkheadRegistry {
-HashRef~Bulkhead~ _bulkheads
+register(name, max_concurrent) Bulkhead
+get(String name) Bulkhead
}
class App_Service {
-ConnectionPool connection_pool
-BulkheadRegistry bulkhead_registry
+process_order(order) Result
+search_products(query) Result
+generate_report(params) Result
}
BulkheadRegistry "1" --> "*" Bulkhead : manages
App_Service --> BulkheadRegistry : uses
App_Service --> Bulkhead : executes through
水密区画の完成
ロックさんが船模型に最後の隔壁を嵌め終えた。机に置く。3つの区画が隔壁で仕切られている。
「ワトソン君。この船は——もう1箇所の穴では沈まない」
自分のノートの図を見た。3つのBulkheadで区切られたシステム構成図。
「わたしのシステムも……バッチが暴走しても、注文と検索は止まらない」
「ただし——」
「隔壁だけでは完全ではない……ですよね。隔壁の中でリソースが枯渇したら、Circuit Breakerで遮断して。入口にはRate Limitingで流量を制御して」
ロックさんがわたしを見た。船模型から手を離す。
「——そうだ」
少し驚いたように見えた。わたしが自分で言ったからだろうか。Slackで見かけた「Rate Limiting」の話と、今日学んだBulkhead。入口の門番と、内部の壁。2つで1組の防衛線——。
「ワトソン君」
「はい」
——あれ、今「はい」と答えた。……まあ、いいか。
報酬
荷物をまとめた。立ち上がる。
「ロックさん、あの——お代というか、報酬は……」
ロックさんが船模型の横に置いてある接着剤のチューブを手に取った。
「これだ。このエポキシ樹脂——木と木を分子レベルで結合させる。二度と離れない。いい接着剤だ」
「……接着剤、ですか」
「同じものを1本。次に来るときに持ってきたまえ」
鞄にノートPCをしまいながら、接着剤のメーカー名と品番をスマホで撮影した。
「……わかりました」
報酬が接着剤。Slackの投稿者は「腕は確か」とは言っていたが、「変な人」とは言っていなかった。いや——「怪しい看板」とは言っていたか。
ドアを開けて廊下に出た。振り返らない。
階段を降りながら、スマホのノートアプリを開く。今日描いた図の写真を見返した。3つのBulkheadで区切られたシステム構成図。その横に、指でメモを書き足す。「Rate Limiting — 入口の門番?」。ロックさんが最後に言った「二重の防衛線」のことが気になっている。
雑居ビルの出口を出た。4月の午後の日差し。スマホをポケットにしまい、会社に戻る方向に歩き出す。
月末まであと3週間。今度の月末は——全館停電にはならない。
探偵の調査報告書
| 容疑(アンチパターン) | 真実(パターン) | 証拠(効果) |
|---|---|---|
| 全機能が共有するリソースプール | Bulkhead(機能別リソース隔離) | 1機能の暴走が他機能を道連れにしない |
| 粒度を考慮しない隔壁設計 | 障害影響範囲に基づく粒度決定 | 守るべきものの数で隔壁数を決める |
| 制限到達時の無限待機 | 即時拒否(Fail Fast) | 待機によるリソース消費の二次被害を防ぐ |
| 隔壁単独での過信 | CB + Rate Limiting との組み合わせ | 入口の門番と内部の壁で二重の防衛線 |
推理のステップ
- 共有リソースプールの特定 — 全機能が同一のDBコネクションプールを使っていないか確認する
- 機能ごとの使用量調査 — 各機能が通常時・ピーク時にどれだけリソースを消費するか把握する
- Bulkheadオブジェクトの設計 —
name(機能名)とmax_concurrent(最大同時実行数)を持つMooクラスを作成する - BulkheadRegistryの導入 — 複数のBulkheadを名前で管理する仕組みを作る
- 粒度の決定 — 「この機能が止まったとき、他の何を巻き添えにしたくないか」をビジネス観点で判断する
- 制限到達時の振る舞い決定 — 即時拒否を基本とし、呼び出し元で適切にエラーハンドリングする
- 他のパターンとの組み合わせ検討 — Circuit Breaker(障害検出・遮断)、Rate Limiting(入口の流量制御)との多層防衛
ロックより
隔壁のない船は、たった1つの穴で沈む。だが、隔壁を入れた船は——穴が開いた区画だけが水を受け入れ、残りの区画は浮力を保つ。
君のシステムも同じだ。全てのリソースを1つの部屋に入れている限り、1つの暴走が全てを道連れにする。壁を立てたまえ。完璧な効率を手放す代わりに、壊滅しないシステムを手に入れるんだ。
それから——壁を立てたら、入口の門番のことも忘れないでくれたまえ。壁だけでは、際限なく人が入ってきて、全ての部屋が溢れる。内部の壁と入口の門番。二重の防衛線が、船を浮かせ続ける。
