自動化本地 LLM 推論伺服器存活監控 #
GPUStack 會偵測到「看似正在執行,但無法進行推論」的狀態。我們建置了一套系統,每 5 分鐘執行 2 階段健康檢查+Slack 通知+Zabbix 整合。
問題出在哪裡? #
前一篇文章介紹的 BASTION(以本地 LLM 建置自動分析基礎設施日誌的系統)是一套每 15 分鐘使用本地 LLM(Qwen2.5-14B)自動分析基礎設施日誌的機制。
這套機制的單點故障(single point of failure)就是 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",並以 max_tokens: 5 接收回應。由於只需確認推論是否可用,我們使用最少的 token 數量。
結束代碼(Exit code)設計 #
我們將腳本的結束代碼設計成 3 種數值。
| 結束代碼 | 意義 | Slack 通知 | Zabbix 觸發條件 |
|---|---|---|---|
| 0 | 正常 | 僅在復原時通知 | — |
| 1 | API 停止 | CRITICAL(紅色) | 嚴重故障 |
| 2 | 模型停止 | WARNING(橘色) | 警告 |
我們將 1 與 2 分開,是因為應對方式不同。若是 Exit 1,就需要檢查 GPUStack 處理程序本身;若是 Exit 2,只要查看 GPUStack 主控台上的模型狀態即可。收到通知的人,光憑結束代碼就能理解接下來該做什麼。
連續告警抑制 #
以每 5 分鐘的間隔執行,意味著若 GPUStack 停止 30 分鐘,同一則 CRITICAL 通知就會送出 6 次。到了第三次,就沒有人會再去看 Slack 了。
為了防止這種情況,我們將前一次的狀態儲存到檔案中,並只在狀態改變時才發送通知。
| 狀態轉換 | 通知 |
|---|---|
| ok → api_down | 發送 CRITICAL 通知 |
| ok → model_down | 發送 WARNING 通知 |
| api_down → ok | 發送 RECOVERED 通知 |
| api_down → api_down | 不通知 |
| model_down → model_down | 不通知 |
在復原時發送 RECOVERED 通知也同樣重要。收到 CRITICAL 通知的人,不必再開啟主控台去確認「現在還沒好,還是已經修復了」。
實作 #
全部功能都以單一個 shell 腳本完成。它與其他 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",即代表推論成功;若沒有,則視為模型停止並進行處理。
狀態持久化 #
# Read previous state STATUS_FILE="/tmp/gpustack-health-status" PREV_STATUS="ok" [ -f "$STATUS_FILE" ] && PREV_STATUS=$(cat "$STATUS_FILE") # Save current state echo "ok" > "$STATUS_FILE" # or "api_down" or "model_down"
只需將狀態寫入單一個檔案即可,不需要複雜的資料庫。即使重啟後也一樣:第一次執行時因為沒有檔案,會以 ok 作為預設值;若有異常,則在第一次執行時就會發送通知。
cron 註冊 #
*/5 * * * * bash /opt/oc-seclogs/gpustack-healthcheck.sh >> /var/log/gpustack-healthcheck.log 2>&1
我們將間隔設定為 5 分鐘,比 BASTION 的定期分析間隔(15 分鐘)更短。因為若在 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 通知即時性很高,但缺乏「過去一個月的正常運作時間」或「回應時間的趨勢」這類長期觀點。這正是 Zabbix 的強項,因此我們也註冊了 UserParameter,讓 Zabbix 一併進行監控。
UserParameter #
我們在 Zabbix Agent 的設定檔中新增了 3 個 UserParameter。
| Key | 內容 | 資料型別 |
|---|---|---|
gpustack.health | 健康檢查腳本的結束代碼(0/1/2) | 整數 |
gpustack.response_time | API 回應時間(毫秒) | 整數 |
gpustack.models | 模型清單 JSON | 文字 |
gpustack.health 會呼叫健康檢查腳本本身並回傳結束代碼。透過在 cron 與 Zabbix 中重複使用同一個腳本,可避免判斷邏輯重複撰寫。
觸發條件(Trigger) #
我們設定了 3 個等級的觸發條件。
| 嚴重程度 | 條件 | 意義 |
|---|---|---|
| 嚴重故障 | gpustack.health = 1 | API 停止。LLM 推論完全中斷。 |
| 警告 | gpustack.health = 2 | API 存活,但模型無法進行推論。 |
| 資訊 | gpustack.response_time > 5000 | API 回應耗時 5 秒以上。GPU 高負載的徵兆。 |
第三項「回應延遲」觸發條件,可作為演變成 CRITICAL 之前的前兆偵測。若在圖表中看到回應時間逐漸增加,就能在發生 OOM 之前,先調整模型的批次大小或並行請求數量。
cron 與 Zabbix 的角色分工 #
Zabbix(每 5 分鐘):儲存歷史紀錄、回應時間圖表、升級(escalation)、故障歷史。
同時運作兩者可確保冗餘性。即使 cron 停止,Zabbix 仍會捕捉到;即使 Zabbix 停止運作,cron 依然會發送通知。
營運成果 #
自開始營運以來,我們偵測到 2 起 GPUStack 模型停止回應的案例。這兩起案例都是 CHECK1 通過但 CHECK2 偵測到異常(Exit 2)。查看 GPUStack 主控台後,發現模型處於「Error」狀態。GPUStack 的自動重啟功能重新載入了模型,幾分鐘後便收到 RECOVERED 通知,確認已復原。
若沒有健康檢查,下一次的定期分析(最多 15 分鐘後)就會失敗,而且需要更長的時間,人才會察覺 Slack 沒有再發送通知。
總結 #
我們透過 shell 腳本+cron+Zabbix,將本地 LLM 推論伺服器(GPUStack)的健康監控自動化。
有三個設計重點值得一提。2 階段檢查(分別檢查 API 回應與實際推論,提升偵測準確度)、連續告警抑制(僅在狀態改變時才通知,防止倦怠)、cron 與 Zabbix 併用(同時達成即時通知與長期趨勢分析)。
由於整套機制都以單一個 shell 腳本完成,同樣的做法也可套用在其他提供 OpenAI 相容 API 的 LLM 伺服器監控上,例如 vLLM 或 Ollama。
BASTION 是一項在封閉環境中實現 AI 安全監控的服務。