ローカルLLM推論サーバーの死活監視を自動化した話 #
GPUStackが「動いているように見えて推論できない」状態を検知する。2段階ヘルスチェック+Slack通知+Zabbix連携を5分間隔で回す仕組みを作りました。
何が問題だったのか #
前回の記事(ローカルLLMでインフラログを自動分析する仕組みを作った話)で紹介したBASTIONは、15分ごとにインフラログをローカルLLM(Qwen2.5-14B)で自動分析する仕組みです。
この仕組みの単一障害点はGPUStack(LLM推論サーバー)です。GPUStackが落ちると、ログの収集は続きますが分析・通知が全部止まります。しかも、止まったことに気づくのが遅い。15分ごとのSlack通知が「来ない」ことに人間が気づくまで、下手をすると数時間かかります。
GPUStack自体にはモデルの自動再起動機能があります。プロセスがクラッシュしたら自動で再起動する。しかし、これだけでは検知できないケースがあります。
・APIプロセスは生きているがモデルがOOMでアンロードされた
・APIは200を返すがモデルが推論リクエストに応答しない
・ネットワーク的にBASTIONサーバーからGPUStackに到達できない
つまり「プロセスの生存」と「推論が実際に動く」は別の問題です。外側から定期的に「本当に推論できるか」を確認する仕組みが必要でした。
設計方針 #
やることはシンプルです。5分ごとにGPUStackのAPIを叩き、正常なら何もしない、異常ならSlackに通知する。ただし、いくつかの設計判断があります。
2段階チェック #
ヘルスチェックを2段階に分けました。
| 段階 | チェック内容 | 何を検知するか | 失敗時 |
|---|---|---|---|
| CHECK1 | /v1/models APIへのHTTPリクエスト | GPUStackプロセスの死活 | Exit 1(CRITICAL) |
| CHECK2 | chat/completions に実推論リクエスト | モデルが実際にロードされて応答可能か | Exit 2(WARNING) |
CHECK1が通ってCHECK2が落ちるケースが重要です。「APIは生きてるけどモデルが推論できない」状態。これはプロセス監視だけでは絶対に拾えません。
CHECK2では "healthcheck" という1単語を送り、max_tokens: 5 で応答を受けます。推論が動くかどうかの確認だけなので、最小限のトークンで済ませています。
終了コードの設計 #
スクリプトの終了コードを3値にしました。
| 終了コード | 意味 | Slack通知 | Zabbixトリガー |
|---|---|---|---|
| 0 | 正常 | 復旧時のみ | — |
| 1 | APIダウン | CRITICAL(赤) | 重度の障害 |
| 2 | モデルダウン | WARNING(橙) | 警告 |
1と2を分けたのは、対処が異なるからです。Exit 1ならGPUStackのプロセス自体を確認する必要があります。Exit 2ならGPUStackダッシュボードでモデルの状態を見ればいい。通知を見た人が次に何をすべきかが、終了コードだけで分かるようにしています。
連続アラート抑制 #
5分間隔で回すと、GPUStackが30分ダウンした場合に同じCRITICAL通知が6回飛びます。3回目あたりで誰もSlackを見なくなります。
これを防ぐために、前回の状態をファイルに保存し、状態が変化した時のみ通知する設計にしました。
| 状態遷移 | 通知 |
|---|---|
| ok → api_down | CRITICAL通知を送信 |
| ok → model_down | WARNING通知を送信 |
| api_down → ok | RECOVERED通知を送信 |
| api_down → api_down | 通知しない |
| model_down → model_down | 通知しない |
復旧時にRECOVERED通知を出すのも重要です。CRITICALを受け取った人が「まだ落ちてるのか、もう直ったのか」を確認するためにダッシュボードを開く手間がなくなります。
実装 #
シェルスクリプト1本で完結します。BASTIONの他のスクリプト(summarize.sh、analyze.sh等)と同じディレクトリに配置し、cronで5分ごとに実行します。
CHECK1: API応答確認 #
MODELS_RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" \
--max-time 15 \
-H "Authorization: Bearer ${API_KEY}" \
"${GPUSTACK_HOST}/v1/models")GPUStackはOpenAI互換APIを提供しているので、/v1/models エンドポイントにGETリクエストを投げるだけです。HTTP 200が返ればAPIは生きています。タイムアウトは15秒。
CHECK2: 実推論テスト #
INFERENCE_RESPONSE=$(curl -s --max-time 15 \
-H "Authorization: Bearer ${API_KEY}" \
-H "Content-Type: application/json" \
"${GPUSTACK_HOST}/v1/chat/completions" \
-d '{"model":"qwen2.5-14b-instruct",
"messages":[{"role":"user","content":"healthcheck"}],
"max_tokens":5,"temperature":0}')レスポンスのJSONに "choices" が含まれていれば推論成功。含まれていなければモデルダウンとして処理します。
状態保存 #
# 前回の状態を読み込み STATUS_FILE="/tmp/gpustack-health-status" PREV_STATUS="ok" [ -f "$STATUS_FILE" ] && PREV_STATUS=$(cat "$STATUS_FILE") # 今回の状態を保存 echo "ok" > "$STATUS_FILE" # or "api_down" or "model_down"
ファイル1つに状態を書くだけ。複雑なデータベースは不要です。再起動しても初回はファイルがないので ok がデフォルトになり、異常があれば1回目の通知が飛びます。
cron登録 #
*/5 * * * * bash /opt/oc-seclogs/gpustack-healthcheck.sh >> /var/log/gpustack-healthcheck.log 2>&1
BASTIONの定期分析(15分間隔)よりも短い5分間隔にしています。LLMが落ちた状態で定期分析が走ると空振りするので、それより前に検知したいためです。
実際にハマったこと #
APIキー認証を見落としていた #
最初に動かしたとき、CHECK1でいきなりHTTP 401が返りました。GPUStackにAPIキー認証が設定されていたのに、curlにAuthorizationヘッダーを入れていなかった。当然のミスですが、ヘルスチェックスクリプトをテスト環境(認証なし)で書いて本番にそのまま持っていくと踏みます。.env ファイルからAPIキーを読み込む形に修正して解決しました。
CHECK1が通ってCHECK2が落ちるケースは実在する #
GPUStackのAPIプロセスは起動しているが、モデルのロードに失敗している状態。/v1/models は200を返すのに、chat/completions は500を返します。プロセス監視だけでは「正常」と判定されてしまう。2段階チェックにした判断は正しかったと運用で確認できました。
Slack通知のフォーマットは「次に何をすべきか」を含める #
最初は「GPUStack APIが応答しません」とだけ出していましたが、通知を受け取った人(=自分ですが)が次にやることは毎回同じ。なので通知本文に確認コマンド(curl -s http://<endpoint>/v1/models)を含めるようにしました。通知を見てターミナルにコピペすればすぐ状況確認できます。
Zabbix連携 #
cronによるSlack通知は即時性が高いですが、「過去1ヶ月の稼働率はどうだったか」「応答時間のトレンドはどうか」といった長期的な視点が欠けます。ここはZabbixの得意分野なので、UserParameterを登録してZabbixからも監視しています。
UserParameter #
Zabbix Agentの設定ファイルに3つのUserParameterを追加しました。
| キー | 内容 | データ型 |
|---|---|---|
gpustack.health | ヘルスチェックスクリプトの終了コード(0/1/2) | 整数 |
gpustack.response_time | API応答時間(ミリ秒) | 整数 |
gpustack.models | モデル一覧JSON | テキスト |
gpustack.health はヘルスチェックスクリプトそのものを呼び出し、終了コードを返します。cronとZabbixで同じスクリプトを使い回すことで、判定ロジックの重複を避けています。
トリガー #
3段階のトリガーを設定しました。
| 深刻度 | 条件 | 意味 |
|---|---|---|
| 重度の障害 | gpustack.health = 1 | APIダウン。LLM推論が完全に停止 |
| 警告 | gpustack.health = 2 | APIは生きているがモデルが推論不能 |
| 情報 | gpustack.response_time > 5000 | API応答に5秒以上。GPU高負荷の兆候 |
3番目の「応答遅延」トリガーは、CRITICALになる前の予兆検知として機能します。応答時間が徐々に伸びているグラフが見えれば、OOMが起きる前にモデルのバッチサイズや同時リクエスト数を調整できます。
cronとZabbixの役割分担 #
Zabbix(5分間隔):ヒストリ保存、応答時間グラフ、エスカレーション、障害履歴。
両方動かすことで冗長性を確保。cronが動かなくてもZabbixが拾い、Zabbixが落ちてもcronが通知する。
運用結果 #
運用を始めてから、GPUStackのモデルが応答しなくなるケースを2回検知しました。どちらもCHECK1は通りCHECK2で検知(Exit 2)。GPUStackのダッシュボードを確認するとモデルが「Error」状態になっていました。GPUStackの自動再起動機能がモデルを再ロードし、数分後にRECOVERED通知が来て復旧確認。
もしヘルスチェックがなければ、次の定期分析(最大15分後)が空振りし、Slackに通知が来ないことに人間が気づくまでさらに時間がかかっていたはずです。
まとめ #
ローカルLLM推論サーバー(GPUStack)の死活監視を、シェルスクリプト+cron+Zabbixで自動化しました。
設計上のポイントは3つ。2段階チェック(API応答と実推論を分けて検知精度を上げる)、連続アラート抑制(状態変化時のみ通知して疲弊を防ぐ)、cronとZabbixの併用(即時通知と長期トレンドの両立)。
シェルスクリプト1本で完結する仕組みなので、GPUStackに限らずvLLMやOllamaなどOpenAI互換APIを提供するLLMサーバーであれば同じアプローチで監視できます。
BASTIONは閉域環境でAIセキュリティ監視を実現するサービスです。