背景

很多团队第一次把服务上到 Kubernetes 时,会觉得滚动发布已经帮我们解决了“不中断发布”的问题。

实际上,RollingUpdate 只是开始,不是答案。

线上真正导致发布抖动的,往往是这些细节:

  • 新 Pod 还没准备好就被加进流量
  • 老 Pod 收到 SIGTERM 后立刻退出
  • 长连接请求被中途切断
  • readiness 和 liveness 配置混乱
  • ingress、service、应用本身三层状态不同步

想做到真正意义上的零中断,得把整条链路串起来看。

先理解滚动发布到底做了什么

Deployment 默认使用 RollingUpdate 策略,大致过程是:

  1. 拉起新 Pod
  2. 等新 Pod Ready
  3. 逐步减少旧 Pod
  4. 直到新版本全部替换完成

一个常见配置如下:

strategy:
  type: RollingUpdate
  rollingUpdate:
    maxUnavailable: 0
    maxSurge: 1

这表示:

  • 发布期间不允许可用实例减少
  • 每次最多多起一个新 Pod

它能降低抖动概率,但不能保证应用层面的请求一定不受影响。

Readiness Probe 决定流量什么时候进来

如果只配了 liveness,没有配 readiness,基本等于告诉集群:

“只要进程还活着,就可以接流量。”

这在很多服务里是错的。

比如一个 API 服务启动后还要做这些事情:

  • 加载配置
  • 预热缓存
  • 建立数据库连接池
  • 初始化路由和依赖客户端

在这些动作完成之前,进程虽然活着,但根本不适合接请求。

readinessProbe:
  httpGet:
    path: /ready
    port: 8080
  initialDelaySeconds: 3
  periodSeconds: 5
  timeoutSeconds: 1
  failureThreshold: 3

livenessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 10

这两个探针不要混用:

  • /health 关注“进程是不是还活着”
  • /ready 关注“服务现在能不能接流量”

PreStop 和优雅停机必须配套

另一个常见误区是:Pod 删除时,Kubernetes 会帮你优雅处理一切。

实际上 Pod 被终止时,只会做标准动作:

  1. 发送 SIGTERM
  2. 等待宽限时间
  3. 超时后 SIGKILL

如果你的应用收到 SIGTERM 立刻退出,正在处理的请求照样会断。

可以在 Pod 里先加一个 preStop,给流量切走留一点时间窗口:

lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "sleep 5"]

terminationGracePeriodSeconds: 30

但只 sleep 还不够。应用本身也必须支持优雅停机:

  • 停止接收新请求
  • 继续处理当前请求
  • 关闭连接池和后台任务
  • 在超时前主动退出

否则 sleep 5 只是把问题往后推了 5 秒。

长连接场景更容易出问题

如果服务有下面这些能力,发布时要特别小心:

  • WebSocket
  • SSE
  • gRPC stream
  • 大文件上传下载

这类连接天然更长。只要下线动作太激进,就会直接把业务打断。

我的做法通常是:

  1. Readiness 先失败,停止新流量进入
  2. 服务进入 draining 状态
  3. 给已有连接一段收尾时间
  4. 到达上限再强制退出

这和普通短请求服务的发布策略不应该完全一样。

一个更稳的 Deployment 配置

apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1
  template:
    spec:
      terminationGracePeriodSeconds: 30
      containers:
        - name: app
          image: example/user-service:1.0.0
          ports:
            - containerPort: 8080
          readinessProbe:
            httpGet:
              path: /ready
              port: 8080
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /health
              port: 8080
            periodSeconds: 10
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 5"]

这份 YAML 不复杂,但它至少把几件关键事情补齐了:

  • 发布期间不减可用实例
  • 新 Pod Ready 前不接流量
  • 旧 Pod 下线前预留缓冲时间

监控发布过程,而不是“点完就走”

很多发布事故不是配置错,而是没人看发布过程。

建议至少盯这几类指标:

  • 发布期间 5xx 是否升高
  • p95/p99 延迟是否突增
  • readiness 失败次数
  • Pod 重启次数
  • Ingress / 网关层 upstream error

如果这些指标在发布窗口里波动明显,就说明你的“零中断”大概率只是看起来没挂。

常见坑点

1. 就绪接口依赖太重

/ready 最好只检查当前实例是否可接流量,不要把所有下游依赖都塞进去,否则一个依赖抖动就会把整批 Pod 标成 not ready。

2. terminationGracePeriodSeconds 配太短

应用要 20 秒才能把长请求收干净,你只给 5 秒,那最终还是硬杀。

3. maxUnavailable 配错

副本本来就不多的服务,如果发布时还允许实例减少,容量会马上变得很紧。

4. 只测功能,不测发布

很多服务功能测试都过了,真正出问题的是发布阶段。滚动升级、故障注入、优雅停机都应该单独验证。

总结

Kubernetes 想做到零中断发布,核心不只是 Deployment 策略,而是整条生命周期是否一致:

  • 新实例什么时候算 ready
  • 旧实例什么时候停止接流量
  • 现有请求如何收尾
  • 平台给了多长退出时间

这些环节有一个没对齐,发布就可能抖。

真正稳定的线上发布,不靠运气,靠的是应用和平台在细节上的配合。


发布系统最忌讳“平时不测,出事再猜”。零中断本身也应该被当作一个可验证的能力。