Featured image of post コードドクター【Chain of Responsibility】多段塞栓症候群〜暴走アラートの連鎖分離術〜

コードドクター【Chain of Responsibility】多段塞栓症候群〜暴走アラートの連鎖分離術〜

深夜2時。サーバールームの空調の音だけが低く唸っている。

俺のモニターには、赤い文字が3行並んでいた。CPU、メモリ、ネットワーク——3つのアラートが同時に発火している。いや、正確には「暴発」だ。CPU使用率は50%程度で、本来なら警告にもならないはずの値。なのに、PagerDutyが鳴り、Slackが荒れ、俺の携帯が振動し続けている。

「くそ……また誤爆か」

俺はキーボードを叩いて黙らせながら、18年間育ててきた監視スクリプトを睨みつけた。こいつが最近、俺の言うことを聞かなくなった。新しいアラート種別を足すたびに、別のアラートが暴れ出す。今夜も、SSL証明書の監視を追加しただけで、なぜかCPUアラートの閾値判定がおかしくなった。

俺の名前は黒崎鉄男。42歳。インフラエンジニア歴18年。自社の監視基盤をPerlで一から構築した男だ。「黒崎のスクリプト」は社内では半ば伝説になっている——良い意味でも、悪い意味でも。

同僚の山田が、深夜にもかかわらずチャットで送ってきたメッセージ。「黒崎さん、知り合いにコードの専門医がいるんですけど、往診してもらえませんか」。俺は「医者に診せるようなコードじゃねえ」と返したが、山田は引かなかった。「もう3週連続で深夜の誤爆アラートですよね。チーム全員寝不足です」

……その一言で、渋々承諾した。

往診

「……失礼します」

背後でドアが開く音がした。振り返ると、黒い鞄を持った長身の男と、白衣の女性が立っていた。

「どちら様だ」

俺は椅子を回転させて正面を向いた。

「大丈夫ですよ、ここはコード診療所です……あ、いえ、往診ですね」

助手のナナコと名乗ったその人が、柔らかく微笑む。隣の男は何も言わない。無言で俺のデスクを見回している。

「おい、勝手に——」

男は俺の言葉を無視して、モニターの前にもう1脚の椅子を引き寄せ、どかっと座った。そしてスクリプトのソースコードを開き始める。

「……すみません、先生はこういう方なんです。お邪魔しますね」

ナナコが申し訳なさそうに頭を下げる。

俺は呆れたが、同僚の紹介だから追い出すわけにもいかない。だが、自分が18年間育てたコードを他人にじろじろ見られるのは、正直気分が良くなかった。

「言っとくがな。俺のスクリプトは18年間、一度も本番障害を出してない」

ドクターと呼ばれた男は、画面をスクロールしながら——予想外のことを言った。

「18年……よく保った」

俺は少しだけ面食らった。てっきり開口一番「ひどいコードだ」とでも言われると身構えていたのだ。

「だろ? 俺のスクリプトは——」

「だが、もう限界だ。」

触診:多段塞栓症候群

ドクターの指が画面の一箇所を指した。

俺の監視スクリプトの心臓部。check_alert サブルーチン。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 患部:AlertMonitor.pm — check_alert()
sub check_alert ($self, $alert) {
    my $type     = $alert->{type}     // 'unknown';
    my $severity = $alert->{severity} // 0;
    my $source   = $alert->{source}   // 'unknown';
    my $message  = $alert->{message}  // '';

    if ($type eq 'cpu' && $severity >= 90) {
        $self->_notify_pagerduty($alert);
        $self->_log_alert($alert, 'CRITICAL', 'pagerduty');
    }
    elsif ($type eq 'cpu' && $severity >= 70) {
        $self->_notify_slack($alert, '#alerts');
        $self->_log_alert($alert, 'WARNING', 'slack');
    }
    elsif ($type eq 'cpu' && $severity >= 50) {
        $self->_log_alert($alert, 'INFO', 'log_only');
    }
    elsif ($type eq 'memory' && $severity >= 95) {
        $self->_notify_pagerduty($alert);
        $self->_notify_slack($alert, '#alerts');
        $self->_log_alert($alert, 'CRITICAL', 'pagerduty+slack');
    }
    # ... 以下、memory, disk, network, process, ssl_cert と
    # 合計35分岐が延々と続く ...
    else {
        $self->_log_alert($alert, 'UNKNOWN', 'log_only');
    }
}

ドクターが画面の端から端までスクロールした。35個の elsif が、延々と連なっている。

「……多段塞栓」

「え?」

ナナコが横から穏やかに補足した。

「黒崎さん、先生がおっしゃっているのは、このコードの血管——つまり処理フローですね——に、血栓が35箇所も詰まっているということです。新しい血栓が1つ増えるたびに、隣の血栓が連鎖的に不安定になるんですよ」

「血栓? これは条件分岐だ。ロジックだ」

俺は反射的に反論した。だが、ナナコは穏やかに続けた。

「ええ、ロジックです。でも、黒崎さん自身がおっしゃっていましたよね? 『新しい種別を足すと別のアラートが暴発する』って。それが連鎖的な塞栓反応なんです」

図星だった。今夜まさに、SSL証明書の分岐を足しただけでCPUの閾値判定がずれた。

多段塞栓症候群。

ドクターが短く宣告した。

「……随分と大げさな名前だな」

俺はそう言いながらも、内心では認めざるを得なかった。18年かけて育てた自慢のスクリプトが——血管の詰まった老体だと、この男は言っているのだ。

処方箋:責任の連鎖

ドクターが俺の端末のキーボードに手を伸ばした。

「おい、俺の端末だぞ」

牽制したが、ドクターは気にしない。新しいファイルを開き、最初の数行を書き始めた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 処方:AlertHandler.pm — 基底クラス
package AlertHandler;
use v5.36;

sub new ($class, %args) {
    return bless {
        next_handler => undef,
        log          => $args{log} // [],
    }, $class;
}

sub set_next ($self, $handler) {
    $self->{next_handler} = $handler;
    $handler->{log} = $self->{log};
    return $handler;
}

sub handle ($self, $alert) {
    if ($self->{next_handler}) {
        return $self->{next_handler}->handle($alert);
    }
    $self->_log('UNKNOWN', 'log_only', $alert);
    return;
}

「パッケージなんか切らなくても——」

「責任の連鎖。」

ドクターが短く遮った。

ナナコが俺に向き直る。「先生が作ろうとしているのは、Chain of Responsibility というパターンです。黒崎さん、工場の組み立てラインって見たことありますか?」

「……ベルトコンベアのやつか」

「そうです。各ステーションには担当者がいて、自分の担当作業が必要なら処理する。必要なければ、そのまま次のステーションに流す。今の黒崎さんのコードは、一人の担当者が全工程を見ている状態なんです」

「……それが35分岐になった理由か」

ドクターは黙々とキーボードを叩き続けた。CPUアラート専用のハンドラが姿を現す。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 処方:AlertHandler::CpuAlert — CPU担当ハンドラ
package AlertHandler::CpuAlert;
use v5.36;
use parent -norequire, 'AlertHandler';

sub handle ($self, $alert) {
    return $self->SUPER::handle($alert)
        unless $alert->{type} eq 'cpu';

    if ($alert->{severity} >= 90) {
        $self->_notify_pagerduty($alert);
        $self->_log('CRITICAL', 'pagerduty', $alert);
    }
    elsif ($alert->{severity} >= 70) {
        $self->_notify_slack($alert, '#alerts');
        $self->_log('WARNING', 'slack', $alert);
    }
    else {
        $self->_log('INFO', 'log_only', $alert);
    }
    return 1;
}

俺は画面を食い入るように見つめた。

自分の担当じゃなければ、SUPER::handle で次のハンドラに回す。自分の担当なら、重症度に応じて適切に処理する。それだけだ。

深夜のオフィスで、ドクターが書いたコードを食い入るように見つめる黒崎。ナナコがモニターを指し示しながら説明している

「……短いな。1つのハンドラが」

「各自の責務。」

ドクターの指は止まらない。メモリ、ディスク、ネットワーク、プロセス、SSL証明書——それぞれのハンドラが、独立したファイルとして次々に生まれていく。

外科手術:連鎖の構築

全てのハンドラが書き終わると、ドクターは最後のピースに取りかかった。チェーンの組み立てだ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 処方:チェーンの組み立て
my $cpu     = AlertHandler::CpuAlert->new();
my $memory  = AlertHandler::MemoryAlert->new();
my $disk    = AlertHandler::DiskAlert->new();
my $network = AlertHandler::NetworkAlert->new();
my $process = AlertHandler::ProcessAlert->new();
my $ssl     = AlertHandler::SslCertAlert->new();

# ベルトコンベアの順番を決める
$cpu->set_next($memory)
    ->set_next($disk)
    ->set_next($network)
    ->set_next($process)
    ->set_next($ssl);

# アラートを流す — 先頭から順に、担当者が処理する
$cpu->handle($alert);

俺は思わず唸った。

「……set_next のメソッドチェーンで繋いでいくのか。ベルトコンベアそのものだな」

ナナコが頷く。「そうです。そして一番大事なのは、新しいアラート種別を追加したい時です。黒崎さん、今までならどうしていましたか?」

「あの35分岐のどこかに、新しい elsif を——」

言いかけて、俺は気づいた。

「……新しいハンドラを1つ書いて、チェーンに繋ぐだけ、か」

「ご名答です。他のハンドラには一切触りません。CPUのロジックも、メモリのロジックも、SSL証明書のロジックも——お互いの存在すら知らないんです」

俺は18年間、一人で全部見ようとしていたのか。工場で例えるなら、品質管理も検品も梱包も発送も、全部一人でやっていた状態だ。そりゃ、ラインが詰まるわけだ。

術後経過:暴発しないアラート

ドクターがテストを実行した。

CPUクリティカル——PagerDutyに通知。正常。メモリ警告——Slackに通知。正常。未知のアラート種別——チェーン終端でUNKNOWNとしてログ記録。正常。

そして、俺が今夜苦しめられたシナリオ。SSL証明書のアラートを追加した状態で、CPUアラートを流す。

CPUのハンドラが処理して——終了。SSLのハンドラには届きもしない。当たり前だ。CPUの担当者がちゃんと処理したんだから、その先に流す必要がない。

「……暴発しない」

俺はモニターを凝視した。

「暴発しないぞ」

ドクターがコードを書き終え、ふと手を伸ばした先にあったのは——俺の缶コーヒーだ。俺がさっきまで飲んでいたやつだ。ドクターは集中が切れたのか、無意識にそれを掴んで一口飲んだ。

「……おい。お前、俺のコーヒー飲んだだろ」

ドクターが手元を見下ろした。缶を見て、少し間があって——何事もなかったようにそのまま俺に返した。

ナナコが慌てて割り込む。「あっ……すみません、先生は集中すると周りが見えなくなるんです」

いや、謝るのはこいつだろ。だが、なんというか——缶コーヒーを分け合う間柄になったような、妙な連帯感が芽生えていた。絶対に違うんだろうが。

ドクターが立ち上がり、鞄を整え始めた。帰り支度だ。

「おい」

ドクターが振り返る。

「……ありがとよ、は言わねえぞ。だが、お前のコード……悔しいが、俺のより読みやすい」

「感謝は、このコードに。」

あいつらが帰った後、俺は一人で端末に向き合った。チェーンの構造を眺める。

18年間、俺が育てたスクリプトは確かに生きていた。だが、成長痛を抱えていた。俺はそれを「反抗期」だと思っていたが——あいつは「血管が詰まっている」と見抜いた。悔しいが、的確だ。

俺はキーボードに手を置く。次のアラート種別、何から書こうか。今度は——一人の担当者じゃなく、チームで。


処方箋まとめ

症状適用すべき経過観察
条件分岐(if-elsif)が10個以上連なっている
新しい処理を追加すると既存の処理が壊れる
処理の順序や組み合わせを動的に変更したい
リクエストを複数の処理ステップに段階的に通したい
条件分岐が3個以下で、今後増える予定がない
処理の順序が固定で、変更の必要がない

治療のステップ

  1. Handler(基底クラス)の定義: handle メソッドと set_next メソッドを持つ抽象ハンドラを作成する。
  2. ConcreteHandler(具体ハンドラ)の実装: 各責務(CPU、メモリ、ディスク等)ごとにサブクラスを作り、自分の担当かどうかを判断するロジックを実装する。
  3. チェーンの組み立て: set_next のメソッドチェーンでハンドラを連結し、処理の流れを構築する。
  4. 委譲の実装: 自分の担当でなければ SUPER::handle で次のハンドラに処理を渡す。チェーン終端ではデフォルト処理を実行する。
  5. 拡張: 新しい種別の追加は、新しいハンドラクラスを1つ作成してチェーンに追加するのみ。既存ハンドラの修正は不要。

助手より

18年間、おひとりで監視基盤を守り続けてきた黒崎さんの技術力と責任感は、本当に素晴らしいものです。Chain of Responsibility パターンは、黒崎さんのスクリプトを否定するものではありません。成長したシステムに合わせて、責任を「分担」する仕組みを導入しただけなんです。これからは、新しいアラート種別が増えても安心してくださいね。ハンドラを1つ追加するだけで、他を壊すことなく対応できますから。ぐっすり眠れる夜が戻ることを、お祈りしています。

——ナナコ

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。