本地LLM推理服务器健康监控自动化实践 #
GPUStack 存在”看起来在运行,但实际无法进行推理”的状态。我们构建了一套系统,以5分钟为间隔运行两阶段健康检查 + Slack 通知 + Zabbix 集成。
问题是什么? #
上一篇文章介绍的 BASTION(用本地LLM自动分析基础设施日志的机制)每15分钟使用本地LLM(Qwen2.5-14B)自动分析基础设施日志。
这套机制的单点故障是 GPUStack(LLM推理服务器)。GPUStack 一旦宕机,日志采集会继续进行,但分析和通知会全部停止。而且要注意到它已经停止需要花费时间。每15分钟到来的 Slack 通知”没有送达”,人工察觉到这一点可能需要好几个小时。
GPUStack 本身具备模型自动重启功能。如果进程崩溃,会自动重启。但仅凭这一点无法检测到某些情况。
• API进程存活,但模型因OOM被卸载
• API返回200,但模型对推理请求没有响应
• 从网络角度看,BASTION 服务器无法访问 GPUStack
换句话说,”进程存活”和”实际能够推理”是两回事。因此需要一套机制,能够定期从外部确认”实际是否能够执行推理”。
设计思路 #
任务很简单:每5分钟调用一次 GPUStack API,正常则什么都不做,异常则通知 Slack。不过在设计上有几个决策点。
两阶段检查 #
我们将健康检查分为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 数。
退出码设计 #
我们将脚本的退出码设计为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。如果只靠进程监控,会将其判定为”正常”。采用两阶段检查的决策在实际运维中得到了验证。
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中复用同一个脚本,避免了判断逻辑的重复。
触发器 #
我们设置了3个级别的触发器。
| 严重程度 | 条件 | 含义 |
|---|---|---|
| 严重故障 | gpustack.health = 1 | API 宕机。LLM推理完全停止。 |
| 警告 | gpustack.health = 2 | API 存活,但模型无法进行推理。 |
| 信息 | gpustack.response_time > 5000 | API 响应耗时5秒以上。是GPU高负载的征兆。 |
第三个”响应延迟”触发器起到了在变为 CRITICAL 之前进行前兆检测的作用。如果在图表中看到响应时间逐渐上升,就可以在发生OOM之前调整模型的批处理大小或并发请求数。
cron 与 Zabbix 的角色分工 #
Zabbix(5分钟间隔):历史记录存储、响应时间图表、告警升级、故障历史。
两者同时运行确保了冗余性。即使cron停止,Zabbix也能捕获;即使Zabbix宕机,cron仍会发出通知。
运维成果 #
自开始运维以来,我们已经检测到2起 GPUStack 模型停止响应的情况。两次都是 CHECK1 通过,CHECK2 检测到了异常(Exit 2)。查看 GPUStack 控制台,模型处于”Error”状态。GPUStack 的自动重启功能重新加载了模型,几分钟后收到了RECOVERED通知,确认已恢复。
如果没有健康检查,下一次定期分析(最长15分钟后)会失败,而人工察觉到Slack没有发送通知则需要花费更长时间。
总结 #
我们使用 shell 脚本 + cron + Zabbix,实现了本地LLM推理服务器(GPUStack)的健康监控自动化。
有3个设计要点值得关注。两阶段检查(将API响应与实际推理分开检测,提高检测精度)、连续告警抑制(仅在状态变化时通知,防止告警疲劳)、cron与Zabbix并用(同时实现即时通知与长期趋势把握)。
由于全部通过一个shell脚本完成,同样的方法也可用于监控 vLLM、Ollama 等其他提供OpenAI兼容API的LLM服务器。
BASTION 是一项在封闭环境中实现AI安全监控的服务。