一、健康检查为什么成了"杀手"

在 Kubernetes 集群部署大模型推理服务后,实例正常处理请求却频繁被 kubelet 重启。事件日志原因出奇一致——Liveness probe failed。

大模型推理负载与常规 Web 服务截然不同。一次 Prefill 可能持续数秒,高并发下 GPU 被持续占满。kubelet 的 /healthz 请求进入队列后,若前面排了多个推理任务,探针响应很容易超过 1s 超时。许多团队直接照搬微服务探针配置,连续三次超时即触发重启。对加载 70B 模型需数分钟的 Pod,这是自杀配置。

⚠️ 核心误区是把进程存活等同于服务健康。推理服务健康是多维度的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1:Kubernetes 事件流中频繁出现的探针失败与容器重启

二、从误杀根因到 Probe 分级

误杀来自三个层面错配。探针类型错配最常见:Liveness 本意检测死锁,团队却常把就绪逻辑塞进去。队列繁忙时 Liveness 失败直接重启容器,而非摘流。超时参数错配同样致命:P99 延迟 5 到 10 秒,探针超时却设 1 秒。优雅终止缺失雪上加霜:请求断开、KV Cache 丢失、客户端收 502,形成死亡螺旋。

📊 正确做法是建立探针分级。Startup 确认模型加载完成,Liveness 只做存活检查,Readiness 反映能否接收新请求,需检查队列深度与 GPU 显存。

探针类型 检查目标 失败后果 建议配置
Startup Probe 模型加载完成、服务可访问 不标记为 Ready failureThreshold=30, periodSeconds=10
Liveness Probe 进程未死锁、主线程存活 重启容器 极简 HTTP 200,timeoutSeconds=5
Readiness Probe 是否还能接收新请求 从 Endpoints 摘除 检查队列深度与 GPU 显存

🔍 Readiness 失败只摘流量不杀容器,Liveness 失败才重启。把负载检查放到 Readiness 是第一原则。

[外链图片转存中…(img-7BrQXAbJ-1779153757152)]

图 2:Probe 分级体系与流量控制关系

三、自定义就绪检查实战

Readiness Probe 不能只做简单 ping。生产就绪端点需暴露真实负载状态:排队请求数或 GPU 显存超阈值时返回 503,让负载均衡器把流量切走。

以下是基于 FastAPI 的实现框架。核心逻辑是维护运行时指标,在就绪端点按阈值返回状态。

from fastapi import FastAPI

app = FastAPI()

queue_depth = 0
max_queue_depth = 16
gpu_memory_percent = 0.0
max_gpu_memory_percent = 0.92

@app.get("/health/ready")
async def readiness_probe():
    if queue_depth >= max_queue_depth:
        return {"status": "unready", "reason": "queue_full"}, 503
    if gpu_memory_percent >= max_gpu_memory_percent:
        return {"status": "unready", "reason": "gpu_memory_high"}, 503
    return {"status": "ready"}

@app.get("/health/live")
async def liveness_probe():
    return {"status": "alive"}

💡 不要把就绪检查做成同步推理。若就绪端点需访问 GPU 状态,确保不进入主推理队列。

[外链图片转存中…(img-UVix0X4i-1779153757153)]

图 3:自定义就绪检查端点的核心逻辑实现

四、优雅终止:别让重启变成事故

即使探针配置正确,实例升级或缩容时仍需终止 Pod。Kubernetes 发送 SIGTERM 后给 Pod 宽限期,默认 30 秒。推理服务须在此窗口内停收新请求,并尽量让进行中的推理跑完。

import signal
import sys
import time
from fastapi import Request

shutting_down = False

def handle_sigterm(signum, frame):
    global shutting_down
    shutting_down = True
    time.sleep(15)
    sys.exit(0)

signal.signal(signal.SIGTERM, handle_sigterm)

@app.middleware("http")
async def shutdown_guard(request: Request, call_next):
    if shutting_down and request.url.path != "/health/live":
        return {"status": "shutting_down"}, 503
    return await call_next(request)

⚙️ 生产环境更稳妥的做法是收到 SIGTERM 后立即将 Readiness 设为 false,等待活跃请求归零再退出。terminationGracePeriodSeconds 建议 120 秒以上,必须大于最长单请求推理时间。

🛡️ 关键参数是 terminationGracePeriodSeconds 必须大于最长单请求推理时间。

五、深度思考与趋势判断

当前主流推理框架的健康端点普遍偏简单,多数只提供 /health 返回 200,在生产规模化部署时显得不足。笔者认为推理服务健康模型正从二元存活向多维状态机演进。未来会出现三个趋势:按模型维度拆分健康状态;服务网格与自定义探针深度集成;推理框架内置过载保护,动态就绪反馈替代静态限流。

🎯 核心判断是推理服务的 SRE 体系不能照搬微服务经验。探针设计的本质是负载感知,而非心跳检测。

总结

推理服务被健康检查误杀,根源在探针逻辑与负载特征不匹配。通过建立三级探针体系、自定义就绪检查与优雅终止机制,可将重启频率从每小时数次降到每周数次。

你在部署大模型推理服务时,是否遇到过探针误杀问题?欢迎分享经验。

📌 别忘了点赞收藏,后续持续更新 AI 推理优化的深度解析与实战干货。关注我带你玩转 AI。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐