资讯详情

LLM服务健康检查:liveness与readiness分离实践

📅 2026/11/2 12:36:58 | 华诺云谱 👁 阅读
LLM服务健康检查:liveness与readiness分离实践
1. 这不是“加个HTTP接口”就能糊弄的事LLM服务健康检查的本质矛盾你有没有遇到过这样的场景模型服务明明还在响应请求Kubernetes却把它从Service的Endpoint列表里踢掉了或者反过来Pod状态是Running但所有推理请求都卡在503——上游网关反复重试下游用户疯狂刷新页面。这不是偶发故障而是把LLM当普通Web服务对待时必然踩中的坑。liveness和readiness必须分开这句话不是K8s文档里的套话是大模型服务在生产环境里用CPU烧出来的血泪教训。我去年接手一个医疗问答LLM集群初期直接复用旧的Spring Boot健康检查模板/health返回200就认为“活着”/ready也返回200就认为“可服务”。结果上线第三天凌晨监控报警QPS断崖式下跌70%但所有Pod状态全是绿色。排查发现模型加载完成后的warmup阶段耗时长达92秒而readiness探针超时设为30秒、重试次数3次——意味着Pod启动后第90秒就被判定为不可用直接被Service摘除。更讽刺的是liveness探针每10秒检查一次它只管进程没死完全不管GPU显存是否被OOM Killer干掉、CUDA上下文是否已崩溃。这种“假存活真瘫痪”的状态持续了47分钟直到运维手动重启Pod才恢复。核心问题在于LLM服务的生命周期维度远超传统微服务。它有三个不可忽略的物理层状态进程存活OS层面、模型加载完成内存/GPU层面、推理链路就绪业务逻辑层面。liveness只管第一个readiness必须覆盖后两个。而绝大多数团队写的健康检查连第二个维度都没触达。比如用curl -f http://localhost:8000/health这种纯HTTP探针根本测不出模型参数是否成功mmap到GPU显存也测不出tokenizer是否完成vocab加载。真正的健康检查得像外科医生做术前评估——既要听心跳进程又要查血氧显存还得做神经反射测试推理通路。这背后是硬件资源与计算范式的代际差异。传统服务重启几秒内完成LLM服务光是加载一个7B模型到A100上就要20秒以上中间涉及PCIe带宽瓶颈、显存碎片整理、CUDA context初始化。如果你的readiness探针还按“1秒超时、3次重试”的老规矩来等于给K8s下了一道自杀指令。更麻烦的是LLM的“就绪”不是二值开关而是概率性状态第一次推理可能因cache未命中慢到2秒第二次就降到200ms。健康检查必须区分“冷启动就绪”和“热态就绪”前者允许长超时后者必须严苛。所以别再抄网上那些“return {status: ok}”的示例代码了。今天这篇我就用实操过的三个真实案例医疗问答、金融风控、代码生成拆解怎么写出真正扛住生产压力的健康检查。不讲理论只说你在kubectl describe pod时能看到什么、在Prometheus里该埋哪些指标、在日志里怎么定位probe失败的真实原因。毕竟在大模型服务里一个健康的Pod才是比任何SLO承诺都更硬的信用背书。2. 为什么必须拆开写liveness和readiness的物理世界分工2.1 liveness探针只回答一个终极问题——进程死了吗liveness探针存在的唯一意义是告诉Kubernetes“这个容器进程是否还在操作系统层面运行”。它的设计哲学极其朴素只要进程没被SIGKILL干掉、没触发OOM Killer、没卡死在某个系统调用里就认为它“活着”。绝不应该、也不需要去验证业务逻辑。很多团队在这里犯致命错误——把模型加载状态、GPU显存占用率、甚至API响应时间塞进liveness结果导致Pod被反复重启形成恶性循环。举个真实案例某电商推荐LLM服务liveness探针配置为exec: [sh, -c, python -c \import torch; print(torch.cuda.memory_allocated())\]意图检测GPU显存是否泄漏。结果在流量高峰时GPU显存峰值达到92%探针执行时恰好触发OOM KillerK8s立刻杀死Pod并重启。新Pod启动后重复相同过程集群陷入“启动-探针失败-重启”死循环。根本原因在于liveness探针的执行本身会消耗GPU资源而它要检测的恰恰是GPU资源状态——这是典型的自指悖论。正确的liveness实现必须满足三个铁律零副作用不能触发任何业务逻辑不分配GPU显存不加载模型权重不发起网络请求亚秒级完成执行时间必须远小于probe的initialDelaySeconds避免阻塞容器启动仅依赖OS原语只使用kill -0 $PID、cat /proc/$PID/status等系统调用。我们最终采用的方案是创建一个独立的healthd守护进程用Rust编写它只做一件事监听Unix Domain Socket收到连接请求时立即返回OK\n。这个进程与主LLM服务进程同属一个cgroup但完全隔离。liveness探针配置为livenessProbe: exec: command: [sh, -c, echo -n test | nc -U /tmp/healthd.sock -w 1] initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 1 failureThreshold: 3这里的关键细节-w 1设置网络超时为1秒确保即使healthd进程卡死也能快速失败nc命令不解析DNS、不建立TCP连接纯粹测试socket可达性/tmp/healthd.sock路径在initContainer里预创建避免权限问题。实测下来这个探针在A100节点上平均耗时3.2ms失败时能精准捕获进程僵死如glibc malloc deadlock。提示永远不要用HTTP探针做liveness。HTTP server本身可能卡在event loop里但进程仍存活。用socket或文件锁这类OS级原语才能真正触及“进程是否活着”的本质。2.2 readiness探针必须回答三个递进问题——模型加载好了吗推理链路通吗服务质量达标吗readiness探针才是LLM服务健康检查的主战场。它要解决的不是“是否活着”而是“能否安全承接流量”。这需要分层验证每一层失败都对应不同的运维动作验证层级检测目标失败后果探针类型典型耗时L1模型加载权重文件是否完整加载到GPU显存Pod从Service Endpoint移除exec500ms-3sL2推理通路tokenizer能否正常encode/decode模型forward是否返回tensor同上HTTP100ms-500msL3服务质量P95延迟≤800ms错误率0.1%触发自动扩缩容自定义指标实时聚合我们曾用单层HTTP探针/ready试图覆盖全部结果在金融风控场景栽了跟头模型加载完成后/ready返回200但首次推理因CUDA kernel cache未预热耗时2.3秒。K8s立即将其纳入负载均衡导致上游交易系统超时熔断。后来拆分成两级探针L1用exec探针检测GPU显存占用突增nvidia-smi --query-gpumemory.used --formatcsv,noheader,nounits | awk {print $1}阈值设为模型理论显存占用的95%L2用HTTP探针调用轻量级健康端点/v1/health/inference该端点执行一次空输入推理input_ids[1]只验证tensor计算通路不走完整业务逻辑。关键技巧在于L1探针的阈值计算。以Llama-2-7b为例理论显存占用 参数量×2字节 KV Cache×2×序列长度×头数×维度。我们用nvidia-smi dmon -s mu -d 100采集冷启动过程显存变化曲线发现峰值出现在模型权重mmap后、CUDA context初始化前此时显存占用比理论值高12%来自CUDA driver预留。因此L1阈值设为理论值×1.12而非简单取整。这个12%的余量让探针在不同型号GPUA100/V100/L40上都保持稳定。注意readiness探针的failureThreshold必须大于warmup时间除以periodSeconds。例如warmup需90秒periodSeconds10则failureThreshold至少设为1090÷101否则Pod永远无法进入Ready状态。2.3 为什么不能合并——Kubernetes调度器的底层逻辑K8s调度器对liveness和readiness的处理机制截然不同强行合并会破坏调度语义。liveness失败触发的是驱逐evictionKubelet杀死容器进程kube-scheduler重新调度新Pod。readiness失败触发的是摘除removalPod保持Running状态但Service控制器将其从Endpoints列表中剔除不再接收流量。如果把readiness逻辑塞进liveness会导致灾难性后果。某次灰度发布中我们将模型版本校验检查/model/version文件是否匹配加入liveness探针。当新旧模型版本不一致时探针返回非零退出码Kubelet立即杀死容器。结果出现诡异现象Pod状态在Running和Terminating间高频切换etcd里pod对象版本号每分钟增长200。根本原因是K8s的liveness探针失败后会立即触发容器重启流程而重启过程中模型加载又触发版本校验失败形成闭环。最终解决方案是版本校验只放在readiness里失败时Pod保持Running但无流量真正需要强制重启的场景如模型文件损坏由initContainer在启动前校验并失败退出。更深层的调度约束在于K8s的HorizontalPodAutoscalerHPA只读取readiness状态不关心liveness。如果readiness探针包含liveness逻辑HPA扩缩容决策就会被进程存活状态污染。例如当GPU显存泄漏导致推理缓慢时readiness探针因超时失败HPA误判为“负载过高”而扩容结果新Pod同样因显存泄漏快速失效造成资源雪崩。分离后liveness保持稳定进程未死readiness准确反映服务能力HPA才能基于真实负载做决策。3. 实操落地三类LLM服务的健康检查配置模板3.1 医疗问答类LLM高可靠性场景下的分级探针设计医疗问答服务对可用性要求极高SLA 99.95%且模型加载耗时长Llama-3-8b加载需112秒。我们采用三级readiness探针架构L1GPU资源就绪exec探针检测CUDA context是否初始化完成。原理是NVIDIA驱动在context初始化后会在/proc/driver/nvidia/gpus/*/information中写入Model: A100等字段。探针脚本#!/bin/sh # /health/gpu-ready.sh if [ ! -f /proc/driver/nvidia/gpus/0000:00:00.0/information ]; then exit 1 fi # 检查显存占用是否达预期阈值 MEM_USED$(nvidia-smi --query-gpumemory.used --formatcsv,noheader,nounits | head -1 | awk {print $1}) MODEL_MEM_EXPECTED18200 # MB, 计算得出 if [ $MEM_USED -lt $((MODEL_MEM_EXPECTED * 95 / 100)) ]; then exit 1 fi exit 0配置参数initialDelaySeconds: 120,periodSeconds: 15,timeoutSeconds: 3,failureThreshold: 8L2推理链路就绪HTTP探针调用/v1/health/inference端点发送最小化请求{ messages: [{role: user, content: }], max_tokens: 1, temperature: 0 }关键点max_tokens: 1确保只生成一个token避免长文本推理耗时temperature: 0禁用随机性保证结果确定性。响应必须包含choices:[0].text且非空。超时设为500msfailureThreshold3。L3服务质量就绪Prometheus指标探针通过K8s的metrics.k8s.ioAPI查询Pod的custom metricsreadinessProbe: httpGet: path: /health/metrics port: 8080 initialDelaySeconds: 300 periodSeconds: 60/health/metrics端点返回JSON{ p95_latency_ms: 782, error_rate: 0.0003, gpu_util_percent: 65.2 }其中p95_latency_ms ≤ 800且error_rate ≤ 0.001才返回200。这个探针在warmup完成后300秒才启用避免早期抖动误判。实操心得医疗场景必须禁用模型自动warmup。我们发现HuggingFace Transformers的device_mapauto在多GPU时会并发加载导致PCIe带宽打满反而延长warmup时间。改用device_map{: 0}强制单卡加载配合L1探针的120秒initialDelay稳定性提升40%。3.2 金融风控类LLM低延迟场景下的轻量化探针优化金融风控要求首字节延迟P95≤300ms模型为Quantized Llama-2-3bAWQ量化。传统HTTP探针在此场景下成为瓶颈——Python HTTP server的event loop在高并发时响应延迟波动大导致readiness误判。我们转向共享内存探针方案主进程在启动时创建POSIX共享内存段/dev/shm/llm-health写入结构体struct HealthStatus { uint8_t model_loaded; // 0/1 uint64_t last_inference_ns; // 纳秒时间戳 uint32_t p95_latency_us; // 微秒 };readiness探针用C写的轻量级程序直接读取该内存段int shm_fd shm_open(/llm-health, O_RDONLY, 0600); struct HealthStatus* health mmap(NULL, sizeof(struct HealthStatus), PROT_READ, MAP_SHARED, shm_fd, 0); if (health-model_loaded 0 || (clock_gettime(CLOCK_MONOTONIC, ts), ts.tv_sec*1e9ts.tv_nsec - health-last_inference_ns 5e9)) { return 1; // 5秒内无推理则认为失活 }编译成静态链接二进制体积仅124KB执行耗时稳定在8.3μs。K8s配置readinessProbe: exec: command: [/health/shm-probe] initialDelaySeconds: 45 periodSeconds: 5 timeoutSeconds: 1这个方案将探针执行时间从HTTP的12ms降低到0.008ms彻底消除探针自身对延迟指标的污染。上线后P95延迟标准差从±47ms降至±8ms。注意共享内存方案需在SecurityContext中设置privileged: false和capabilities: [IPC_LOCK]否则mmap会失败。我们踩过的坑是忘记在initContainer里预创建shm段导致主进程启动时报错shm_open: No such file or directory。3.3 代码生成类LLM动态扩缩容场景下的自适应探针代码生成服务流量峰谷明显工作日9-18点高峰需配合KPAKnative Pod Autoscaler实现毫秒级伸缩。传统固定周期探针在此场景下效率低下——低峰期每5秒探一次浪费资源高峰期探针积压导致扩缩滞后。我们实现事件驱动探针主进程暴露Unix socket/tmp/llm-probe.sockK8s探针通过nc -U发送PROBE命令服务端立即返回当前状态# probe_server.py import socket import json from datetime import datetime def handle_probe(): status { model_loaded: True, inference_qps: get_current_qps(), # 从metrics exporter获取 gpu_memory_used_percent: get_gpu_mem_percent(), timestamp: datetime.now().isoformat() } return json.dumps(status).encode() # Unix socket server sock socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.bind(/tmp/llm-probe.sock) sock.listen(1) while True: conn, _ sock.accept() conn.sendall(handle_probe()) conn.close()K8s配置启用startupProbe替代initialDelaySecondsstartupProbe: exec: command: [sh, -c, echo PROBE | nc -U /tmp/llm-probe.sock -w 2 | grep -q model_loaded.*true] periodSeconds: 3 timeoutSeconds: 2 failureThreshold: 30 # 最多等待90秒 readinessProbe: exec: command: [sh, -c, echo PROBE | nc -U /tmp/llm-probe.sock -w 1 | jq -r .inference_qps | grep -q ^[0-9]*$] periodSeconds: 1 timeoutSeconds: 1关键创新点在于periodSeconds: 1——探针每秒执行一次但通过-w 1严格限制超时。当服务过载时probe socket响应变慢探针超时失败KPA立即触发缩容。实测在QPS从500骤降至50时Pod数量在8.3秒内完成收缩KPA默认10秒间隔。踩坑记录最初用HTTP probe高峰期probe请求堆积在event loop队列导致readiness状态滞后。改用socket后probe请求由OS内核直接处理完全绕过Python GIL响应时间稳定在120μs以内。4. 常见问题与排查技巧实录从kubectl日志里挖出真相4.1 Probe失败但Pod状态正常先看Kubelet日志当kubectl get pods显示Pod ReadyFalse但kubectl logs pod看不到错误时问题往往在Kubelet层面。执行# 查看节点Kubelet日志需有节点SSH权限 journalctl -u kubelet -n 100 --since 1 hour ago | grep -A 5 -B 5 probe failed典型输出Dec 05 14:22:33 node-01 kubelet[1234]: Readiness probe failed: OCI runtime exec failed: exec failed: unable to start container process: exec: sh: executable file not found in $PATH: unknown这说明你的镜像里没有sh命令。解决方案不是安装bash而是改用busyboxFROM your-llm-image RUN apk add --no-cache busybox HEALTHCHECK --interval10s --timeout3s --start-period30s --retries3 \ CMD busybox wget --spider --timeout3 http://localhost:8000/ready || exit 14.2 GPU显存检测不准用nvidia-smi的原始输出校准nvidia-smi --query-gpumemory.used返回的字符串包含空格和单位直接用于shell比较会出错# 错误写法 MEM$(nvidia-smi --query-gpumemory.used --formatcsv,noheader,nounits) if [ $MEM -gt 18000 ]; then ... # 当MEM 18200 时-gt比较失败正确做法是用awk清洗MEM$(nvidia-smi --query-gpumemory.used --formatcsv,noheader,nounits | awk {$1$1};1) # 或更健壮的方案 MEM$(nvidia-smi --query-gpumemory.used --formatcsv,noheader,nounits | sed s/[^0-9]//g)4.3 HTTP探针超时但curl手动测试很快检查网络策略最隐蔽的问题K8s NetworkPolicy阻止了probe流量。Kubelet执行探针时使用Pod的network namespace但源IP是127.0.0.1。某些NetworkPolicy规则如from: {ipBlock: {cidr: 0.0.0.0/0}}会拒绝localhost流量。验证方法# 在Pod内执行 kubectl exec pod -- sh -c curl -v http://localhost:8000/ready 21 | head -20 # 如果返回Connection refused但手动telnet localhost 8000通则是NetworkPolicy问题修复方案在NetworkPolicy中显式允许localhost- from: - ipBlock: cidr: 127.0.0.1/324.4 Warmup时间波动大用CUDA_VISIBLE_DEVICES锁定GPU多GPU节点上nvidia-smi返回的GPU序号与CUDA_VISIBLE_DEVICES映射不一致导致L1探针检测错卡。例如# Pod内执行 $ echo $CUDA_VISIBLE_DEVICES 3,1 $ nvidia-smi -L GPU 0: A100-SXM4-40GB (UUID: GPU-xxx) GPU 1: A100-SXM4-40GB (UUID: GPU-yyy)此时nvidia-smi --id0实际操作的是物理GPU3。解决方案在探针脚本中动态映射# 获取CUDA_VISIBLE_DEVICES中索引0对应的物理GPU ID PHYSICAL_GPU_ID$(nvidia-smi -L | grep $(echo $CUDA_VISIBLE_DEVICES | cut -d, -f1) | cut -d -f2 | tr -d :) nvidia-smi --id$PHYSICAL_GPU_ID --query-gpumemory.used --formatcsv,noheader,nounits4.5 Prometheus指标探针返回503检查metrics-server权限当/health/metrics端点调用metrics.k8s.ioAPI失败时常见原因是RBAC权限不足。检查ServiceAccount绑定kubectl get clusterrolebinding | grep metrics # 应存在 system:metrics-server # 若缺失执行 kubectl create clusterrolebinding metrics-reader --clusterrolesystem:metrics-reader --serviceaccountnamespace:default5. 终极验证清单上线前必须跑通的7个测试用例5.1 冷启动压力测试# 模拟最恶劣场景连续启动10个Pod观察readiness收敛时间 for i in $(seq 1 10); do kubectl run llm-test-$i --imageyour-llm-image --restartNever --rm -it -- sh -c echo Starting pod $i; while ! curl -sf http://localhost:8000/ready; do sleep 1; done; echo Pod $i ready in $(($(date %s)-$(date -d now - 1 minute %s))) seconds; done合格标准所有Pod在initialDelaySeconds 2*periodSeconds内就绪。5.2 GPU故障注入测试# 在节点上模拟GPU故障 nvidia-smi --gpu-reset -i 0 # 重置GPU0 # 观察liveness探针是否在failureThreshold内杀死Pod kubectl get events --field-selector involvedObject.nameyour-pod | grep Killing5.3 网络分区测试# 在Pod内切断对外网络验证readiness是否及时摘除 kubectl exec pod -- sh -c iptables -A OUTPUT -d $(hostname -i) -j DROP # 30秒后检查Endpoints kubectl get endpoints service-name -o yaml | grep -A 5 addresses: # 应无该Pod IP5.4 显存泄漏测试# 连续发送1000次推理请求监控显存增长 for i in $(seq 1 1000); do curl -s http://localhost:8000/v1/chat/completions \ -H Content-Type: application/json \ -d {model:llama,messages:[{role:user,content:hello}]} done # 检查nvidia-smi显存是否线性增长 nvidia-smi --query-gpumemory.used --formatcsv,noheader,nounits5.5 HPA联动测试# 手动制造高负载验证HPA是否扩容 kubectl run -i --tty load-generator --rm --imagebusybox --restartNever -- \ sh -c while true; do wget -qO- http://your-service/ready; sleep 0.1; done # 观察 kubectl get hpa kubectl get pods -w5.6 节点重启测试# 模拟节点宕机后恢复 kubectl drain node --ignore-daemonsets --delete-emptydir-data # 等待Pod被驱逐到其他节点 kubectl get pods -o wide | grep node # 节点恢复后检查新Pod的readiness时间 kubectl uncordon node5.7 安全加固测试# 验证探针不泄露敏感信息 curl -v http://localhost:8000/ready 21 | grep -E (token|secret|key) # 应无任何敏感字段输出 # 检查探针进程权限 kubectl exec pod -- ps aux | grep health # health进程应以非root用户运行最后分享一个血泪教训我们在某次紧急上线时跳过了5.1冷启动测试结果新版本模型加载逻辑变更warmup时间从92秒增至135秒。由于failureThreshold8periodSeconds10Pod在130秒时被判定失败而initialDelaySeconds120导致所有Pod在就绪前就被摘除。线上流量瞬间归零。从此我们把“冷启动测试”写进CI/CD流水线作为merge前的必过门禁。真正的健康检查从来不是写完代码就结束而是用生产环境的每一秒压力去验证你写的每一行probe逻辑。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑