4.2 灰度发布与生产化:滚动更新、会话亲和、回滚与监控告警


文档摘要

4.2 灰度发布与生产化:滚动更新、会话亲和、回滚与监控告警 压测帮你算清了「能扛多少」,但真正上线后最吓人的往往不是容量,而是「改一发而动全身」——一次配置更新、一次权重切换,可能让整个推理服务抖动甚至雪崩。这一节讲的是把 GLM-5.2 推理服务做成一个可灰度、可回滚、可观测的生产级系统,而不是一个「跑起来就别碰」的黑盒。 我踩过最痛的坑:第一次升级 vLLM 版本时直接 replace 了 Deployment,旧 Pod 立刻被删、新 Pod 启动要重新加载权重(冷启动十几秒),这十几秒内所有请求 5xx。业务方以为服务挂了。从那以后我定下铁律:任何变更都必须灰度,任何变更都必须能 30 秒内回滚。

4.2 灰度发布与生产化:滚动更新、会话亲和、回滚与监控告警

压测帮你算清了「能扛多少」,但真正上线后最吓人的往往不是容量,而是「改一发而动全身」——一次配置更新、一次权重切换,可能让整个推理服务抖动甚至雪崩。这一节讲的是把 GLM-5.2 推理服务做成一个可灰度、可回滚、可观测的生产级系统,而不是一个「跑起来就别碰」的黑盒。

我踩过最痛的坑:第一次升级 vLLM 版本时直接 replace 了 Deployment,旧 Pod 立刻被删、新 Pod 启动要重新加载权重(冷启动十几秒),这十几秒内所有请求 5xx。业务方以为服务挂了。从那以后我定下铁律:任何变更都必须灰度,任何变更都必须能 30 秒内回滚

```mermaid flowchart LR A[新版本 Pod 就绪] --> B[切 5% 流量灰度] B --> C{指标正常?} C -->|是| D[逐步 25%→50%→100%] C -->|否| E[立即回滚旧版本] D --> F[旧版本下线] E --> F ```

滚动更新:别用 replace,用策略化的 RollingUpdate

K8s Deployment 默认的更新策略就是 RollingUpdate,但默认参数对大模型服务极不友好:

  • maxSurge 默认 25%:意味着更新时最多多起 25% 的 Pod。对 GPU 服务,这等于更新期间要多占一份显存——如果节点显存本就吃紧,新 Pod 会 Pending 卡住,更新永远完成不了。我的建议:把 maxSurge 设为 0 或 1,maxUnavailable 设为 1,做到「先下后上、逐台替换」,避免显存峰值叠加。
  • minReadySeconds:Pod 变成 Ready 后还要等几秒才算稳定。对 vLLM,Ready 只是容器启动,权重加载完才算真能服务。务必配合就绪探针(readinessProbe)——探针在权重加载完成、能响应推理请求后才返回成功,否则 Pod 不会进 Service endpoints,就不会接流量。
  • terminationGracePeriodSeconds:旧 Pod 收到终止信号后,要留够时间处理完在途请求(尤其是长生成),否则会被强制杀掉、前端收到断流。建议设 60–120 秒,并在 preStop 钩子里做优雅退出。

优雅退出:为什么大模型服务最怕"硬杀"

上面提到 preStop,这里必须展开讲,因为这是长文本生成服务区别于普通无状态 Web 服务的关键点。普通 Web 请求几十毫秒就结束,Pod 被杀基本无感;但 GLM-5.2 的一次长生成可能持续几十秒甚至上分钟,如果 Pod 收到 SIGTERM 就立刻退出,正在流式输出的用户会突然断流、拿到半截答案,体验极差。

K8s 删除 Pod 的完整流程是这样的:控制面先把 Pod 从 Service 的 endpoints 摘除(新请求不再进来),同时向容器发 SIGTERM,然后等待 terminationGracePeriodSeconds,超时才发 SIGKILL 强杀。但这里有个经典竞态:摘除 endpoints 和发 SIGTERM 几乎同时发生,而 kube-proxy/Ingress 更新转发规则有延迟,导致 SIGTERM 已经到了、新请求却还在往这个 Pod 打。解决办法是加一个 preStop 睡眠钩子(如 sleep 15),让 Pod 先"装死"十几秒,等转发规则彻底收敛后再真正开始退出。

我的实战配置口径是:preStop 睡 10–15 秒消化转发收敛,terminationGracePeriodSeconds 给到 120 秒覆盖最长的一次生成,vLLM 进程本身要能响应 SIGTERM 停止接新请求、但把已在跑的序列生成完。三者配合,才能做到「滚动更新时用户完全无感」。少了任何一环,都会在高并发下露出断流的马脚。

```mermaid sequenceDiagram participant K as K8s控制面 participant E as Service endpoints participant C as 容器(vLLM) K->>E: 摘除该Pod K->>C: 发送 SIGTERM C->>C: preStop sleep 15s(等转发收敛) C->>C: 停止接新请求,跑完在途生成 K->>C: 超时后 SIGKILL(兜底) ```
```mermaid sequenceDiagram participant C as 控制器 participant O as 旧 Pod(GPU) participant N as 新 Pod(GPU) participant P as readinessProbe C->>O: 标记终止 O->>O: 处理完在途请求 C->>N: 启动新 Pod N->>P: 权重加载中... P-->>N: 就绪(可服务) C->>N: 加入 Service endpoints C->>O: 彻底删除 ```

会话亲和:多副本下别让用户"丢了上下文"

如果你的业务是「多轮对话」,且 vLLM 没开跨请求前缀缓存,那么把同一用户的请求固定到同一个 Pod 就很重要——否则用户第二轮对话打到另一个 Pod,那个 Pod 没有上一轮的 KV Cache,要么重新 prefill(慢),要么直接丢失历史(错)。

两种做法:

  • Service 层会话亲和:给 K8s Service 设 sessionAffinity: ClientIP,按客户端 IP 哈希。简单,但缺点明显:客户端 IP 往往在大网 NAT 后趋同(多个用户共享一个 IP),且 Pod 扩缩容会打破哈希、导致大批量会话漂移。我只在对一致性要求不高的场景用。
  • 应用层会话绑定(推荐):在网关/接入层(如你自己的代理或 Ingress)维护 session_id → Pod 的映射,把同一 session 的请求路由到固定 Pod。这比 ClientIP 更精准,也不受 Pod 扩缩影响。代价是要自己写一点路由逻辑——但对「多轮对话」业务,这笔投入值得。

我的主张:多轮对话业务,会话亲和不是可选项,是必选项。否则用户会体验到「上一句还记得,下一句就失忆」的诡异行为,这种 bug 比延迟高更伤信任。

```mermaid graph TD U[用户请求 session_A] --> G[网关路由层] G -->|查 session 表| M[session_A → Pod2] M --> P2[Pod2(持有该会话上下文)] U2[用户请求 session_A 第二轮] --> G G -->|同一 session 命中| P2 note[若打到 Pod3 则上下文丢失/重算] ```

回滚:30 秒内救场的底气

K8s 的 kubectl rollout undo 能把 Deployment 回滚到上一个 ReplicaSet,这是你的第一道保险。但大模型场景要注意:

  • 回滚也要走灰度吗? 紧急回滚我建议直接全量回滚(救命优先),但确认恢复后立刻补一次压测验证,防止「回滚后的版本」也有隐藏问题。
  • 镜像与权重要可回退:回滚不仅仅是 Deployment 配置,还包括镜像 tag。务必给镜像打不可变 tag(如带 commit hash),别用 latest——否则你以为回滚了,结果拉到的还是新镜像。权重同理,PVC 里的权重路径要版本化。
  • 灰度期间保留旧 ReplicaSet:默认 K8s 保留历史 ReplicaSet,但有的集群会清理。确认 revisionHistoryLimit 足够(如 5),保证你能回退到足够早的版本。

我给团队的 SOP:变更前先 kubectl rollout history 确认上一份版本在;变更后用 kubectl rollout status 盯进度;异常就 kubectl rollout undo。这套命令要烂熟于心,因为出事时你没时间翻文档。

监控告警:Prometheus + Grafana 接什么

没有监控的生产服务等于盲飞。vLLM 本身暴露了 metrics 端点(如 /metrics),配合 K8s 生态的 Prometheus + Grafana 就能搭起可观测性。重点监控四类信号:

  • GPU 指标:用 dcgm-exporter 采集显存占用、SM 利用率、显存带宽、GPU 温度。设告警:显存持续 > 90% 持续 5 分钟 → 预警(容量将触顶);温度 > 85℃ → 严重(可能降频/掉卡)。
  • 推理服务指标:vLLM 的 metrics 含队列长度、运行中请求数、TTFT、ITL、生成token数等。设告警:队列长度持续 > --max-num-seqs 的 80% → 容量预警;P99 延迟超过 SLA 两倍 → 严重。
  • K8s 健康:Pod 重启次数(OOMKilled 信号)、 readiness 失败、Pending 状态。Pod 频繁重启基本就是显存 OOM,要立刻查。
  • 业务层:请求成功率、错误码分布(5xx 占比)。
```mermaid flowchart TD A[vLLM /metrics] --> P[Prometheus 抓取] B[dcgm-exporter] --> P C[K8s kube-state-metrics] --> P P --> G[Grafana 看板] P --> A2[Alertmanager] A2 --> N[告警: 企业微信/钉钉/邮件] ```

告警阈值怎么定:别一上来就轰炸

新手最容易把告警配成「任何波动都通知」,结果三天后所有人默默静音了告警——这是最危险的。我的原则:

  • 预警(warning):指标越过「舒适区边缘」但服务还能用,比如显存 85%、P99 是 SLA 的 1.5 倍。通知到值班群,但不打电话。
  • 严重(critical):指标越过「红线」,比如显存 95%、连续 5xx、Pod 重启。立刻打电话/企微机器人 @负责人。
  • 避免抖动误报:所有阈值加「持续 N 分钟」条件(如持续 5 分钟),过滤瞬时抖动。

一个实用技巧:把你压测得到的容量数字直接写进告警阈值。比如压测得出舒适并发 16、拐点 32,那就可以设「运行中请求数 > 25 持续 5 分钟」为预警——这正是拐点前的缓冲区,提醒你该扩容了,而不是等崩了才知。

容量规划与 HPA:要不要自动扩缩

GLM-5.2 推理服务要不要上 HPA(Horizontal Pod Autoscaler)?我的判断:谨慎。原因:

  • GPU Pod 冷启动慢(加载权重十几秒),HPA 扩容反应太慢,等它扩出来流量早过了峰值。
  • GPU 节点资源昂贵且常紧缺,基于 CPU 利用率的 HPA 对 GPU 服务几乎无效(GPU 服务 CPU 往往不忙)。
  • 更合理的是基于队列长度的自定义指标 HPA定时扩缩(预判业务高峰提前扩)。

我的建议:生产环境先用「固定副本数 + 预留余量」稳跑,HPA 只作为应对可预期峰值的辅助手段,且必须基于 vLLM 队列长度这类真实负载指标,而非 CPU。不要为了「自动化」而自动化,把一个本就紧张的 GPU 服务交给反应迟钝的 HPA,往往适得其反。

```mermaid graph LR subgraph 推荐[生产推荐] R1[固定副本+余量] R2[队列长度自定义HPA] R3[定时扩缩应对高峰] end subgraph 谨慎[谨慎] W1[基于CPU的HPA] W2[纯响应式自动扩] end ```

Ingress 金丝雀:真正把「5% 流量」落到实处

前面流程图里那句「切 5% 流量灰度」听起来简单,但很多人到这一步就卡住了:靠 Deployment 自己是做不到按百分比分流的——它只能控制新旧 Pod 的数量比例,不能控制流量比例。想真正做到「只让 5% 的请求打到新版本」,需要在接入层动手。

以 nginx-ingress 为例,它支持 canary 注解:给新版本建一个独立 Service 和一份带 nginx.ingress.kubernetes.io/canary: "true"canary-weight: "5" 的 Ingress,它就会把 5% 的流量导到新版本,其余 95% 还走旧版。确认新版指标正常,就把 weight 逐步改成 25、50、100,最后把旧版 Ingress 下线。这才是真正可控的流量灰度。

我的建议是:对推理服务,灰度不要只看「没报错」,要重点对比新旧版的 P99 延迟、首 token 延迟(TTFT)、显存占用、生成质量。因为很多回归不是 5xx——新版 vLLM 参数调校不当导致延迟悄声变高、或量化配置变化导致输出质量下降,这些都不会报错,只能用指标和抽样看输出才能发现。灰度期多盯 15–30 分钟,别急着放量。

一个最小可行的安全生产闭环

把上面所有点串起来,一个最小可行的闭环是:

  1. 变更前kubectl rollout history 确认可回退版本;记下当前压测基线。
  2. 变更中:RollingUpdate(maxSurge=1, maxUnavailable=1)+ readinessProbe 守卫;先灰度 5% 流量。
  3. 变更后:盯 Grafana 看板 30 分钟,确认延迟/显存/成功率无劣化,再逐步放量到 100%。
  4. 异常时kubectl rollout undo 回退,Alertmanager 已自动通知。
  5. 日常:Prometheus 持续采集,阈值基于压测数字,预警在缓冲区内就提醒扩容。
```mermaid flowchart TD S[变更] --> H[确认可回退] H --> R[灰度 5%] R --> M[盯监控 30min] M -->|正常| F[放量 100%] M -->|异常| U[rollout undo 回滚] F --> D[日常 Prometheus 观测] ```

常见问题 FAQ

Q1:滚动更新时新 Pod 一直 Pending,更新卡死怎么办?
基本就是显存/GPU 不够。maxSurge 大于 0 时会先拉起新 Pod,但节点没多余 GPU,新 Pod 就排不上队。把 maxSurge 设 0、maxUnavailable 设 1,改成「先下后上」即可。

Q2:sessionAffinity: ClientIP 为什么会失效?
两个常见原因:一是客户端在 NAT 后 IP 趋同,哈希到同一 Pod;二是 Pod 扩缩容打破哈希。多轮对话一致性要求高时,改用应用层 session_id 绑定。

Q3:回滚后发现拉到的还是新镜像?
你用了 latest 或可变 tag。镜像必须打不可变 tag(带 commit hash),权重路径也要版本化,否则回滚只是自欺欺人。

Q4:告警频繁误报导致大家静音了怎么办?
所有阈值加「持续 N 分钟」条件过滤瞬时抖动,分级为 warning/critical,只有 critical 才打电话。阈值直接用压测数字校准,别拍脑袋。

本节收尾:你带走了什么

补充一个很多人忽略的细节:灰度不只是发布新版时才用,排查线上问题时它同样是利器。比如怀疑某个参数(如 --max-num-seqs--gpu-memory-utilization)调整能不能提升吞吐,不要直接全量改,而是起一个小比例 canary 用真实流量跑 A/B,对比两边的 P99 和显存曲线再决定。把生产环境当成「可控实验室」而非「不敢碰的黑盒」,是资深运维与新手的分水岭。

还有一点心态上的提醒:生产化不是一次性工程,而是持续迭代的习惯。第一次上线你可能只做到了 RollingUpdate 和基本监控,会话亲和、Ingress 金丝雀、告警分级可以在后续迭代中逐步补齐。关键是每次事故复盘后都把教训固化成一条 SOP 或一个告警规则,让系统随时间越跑越稳。

读完这一节,你应该能搭出一个「不会一改就崩」的生产级 GLM-5.2 服务:RollingUpdate 配 maxSurge=1 避免显存叠加、readinessProbe 守门冷启动、会话亲和保住多轮上下文、kubectl rollout undo 作 30 秒回血、Prometheus+Grafana 把压测结论变成告警阈值。一句话——压测给你『能扛多少』,这一节给你『怎么安全地改动和兜底』。到本章结束,你已经从『能跑』走到了『敢上线』。


作者与出处
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 不智能的AI的小龙虾 转发
评论区 (0)
U