ElIauk

K8s 生产实践:滚动更新与回滚

滚动更新不是“点一下发布” kubectl set image 之后,Deployment 控制器会新建一个 ReplicaSet,按 maxSurge 和 maxUnavailable 的节奏扩新缩旧。整个过程没有任何“事务”保证:新 Pod 可能一直起不来,旧 Pod 可能已经被缩掉一半,流量此

评论
K8s 生产实践:滚动更新与回滚

滚动更新不是“点一下发布”

kubectl set image 之后,Deployment 控制器会新建一个 ReplicaSet,按 maxSurge 和 maxUnavailable 的节奏扩新缩旧。整个过程没有任何“事务”保证:新 Pod 可能一直起不来,旧 Pod 可能已经被缩掉一半,流量此时落在哪些 Pod 上完全取决于就绪探针。

所以滚动更新的本质是:用探针把“能不能接流量”的决定权交给应用自己。探针写错,发布就变成一次赌博。

maxSurge 和 maxUnavailable 怎么选

两个参数共同决定发布速度与可用容量:

配置 含义 适用场景
maxSurge: 25% / maxUnavailable: 25% 默认值,先扩后缩 大多数服务
maxSurge: 1 / maxUnavailable: 0 严格不降容 容量紧张、不能少实例的核心服务
maxSurge: 0 / maxUnavailable: 1 不加资源,先杀后起 节点资源固定的边缘服务,可用性会短暂下降

maxUnavailable: 0 听起来最安全,但它要求集群有额外资源放下 surge 出来的 Pod。节点资源刚好卡满时,新 Pod 会 Pending,发布就此挂住,而此时旧 Pod 一个都没少——服务是可用的,只是永远发不完。

探针的分工比参数更重要

三类探针的职责必须分清,混用会造成非常诡异的故障:

  • startupProbe 只管“启动完了没有”,成功一次之后就不再执行;
  • readinessProbe 管“能不能接流量”,失败只是从 Service Endpoints 里摘掉,不重启;
  • livenessProbe 管“是不是死了”,失败会重启容器。

最常见的事故是把 readiness 和 liveness 都指向 /actuator/health,而 health 里包含了数据库和 Redis 的连通性。数据库抖动一下,liveness 失败,全部 Pod 同时重启,本来只是慢,被探针放成了雪崩。

正确做法是分组:/actuator/health/liveness 只反映进程自身和死锁,/actuator/health/readiness 才包含外部依赖。Spring Boot 需要显式打开:

management:
  endpoint:
    health:
      probes:
        enabled: true
  health:
    livenessstate:
      enabled: true
    readinessstate:
      enabled: true

再加上 minReadySeconds: 10,让新 Pod 就绪后再观察一小段时间才计入可用,避免“刚就绪就报错”的实例被立刻当成健康容量。

发布与回滚命令

# 发布并等待,--timeout 让 CI 能明确失败而不是一直挂着
kubectl set image deployment/order-api app=registry.example.com/shop/order-api:2026.02.1 -n shop-prod
kubectl rollout status deployment/order-api -n shop-prod --timeout=180s

# 看历史与当前 revision
kubectl rollout history deployment/order-api -n shop-prod
kubectl rollout history deployment/order-api -n shop-prod --revision=8

# 出问题时立刻回滚到上一个版本
kubectl rollout undo deployment/order-api -n shop-prod

# 回到指定 revision(注意不是“重放”,是重建)
kubectl rollout undo deployment/order-api -n shop-prod --to-revision=7

# 想先发一个实例观察,可以暂停
kubectl rollout pause deployment/order-api -n shop-prod
kubectl rollout resume deployment/order-api -n shop-prod

revisionHistoryLimit 默认只保留 10 个 ReplicaSet,超过之后最早的 revision 会被清掉,--to-revision 就找不到目标了。生产上建议调到 20 以上,并且发布记录要能对上镜像 tag。

回滚不是万能的

镜像回滚只回滚了代码,没有回滚数据库。如果这一版带了 ALTER TABLE 或者改了消息格式,回滚后老代码读到新字段很可能直接报错。所以发布纪律是:数据库变更必须向前兼容,先加字段、再双写、再切读、最后删旧字段,每个版本都能独立运行。

配置也有同样的问题。ConfigMap 或 Secret 被一起改了、但没有一起回滚,回滚后应用拿到的是新配置配旧代码,问题反而更隐蔽。

排查步骤

发布卡住时,按“卡在哪一层”排查,比反复重试有效:

# 1. 谁没就绪
kubectl get pods -n shop-prod -l app=order-api -o wide

# 2. 卡住的 Pod 到底在干什么
kubectl describe pod -n shop-prod <pod-name>
kubectl logs -n shop-prod <pod-name> --previous --tail=100

# 3. 事件里通常直接写着原因
kubectl get events -n shop-prod --sort-by=.lastTimestamp | tail -30

# 4. 确认是不是调度不上(Pending 时看这个)
kubectl get pod -n shop-prod <pod-name> -o jsonpath='{.status.conditions}{"\n"}'

常见原因只有几类:镜像拉取失败(私有仓库凭据过期)、startupProbe 太短导致 JVM 还没起来就被判死、资源不足导致 Pending、readiness 依赖的下游没通、以及最隐蔽的一种——旧 Pod 的 terminationGracePeriodSeconds 太短,滚动过程中把正在处理的请求直接截断,监控上表现为发布期间错误率上升。

常见坑

以为 PodDisruptionBudget 能限制发布速度。 PDB 只对 kubectl drain、节点升级这类主动驱逐生效,Deployment 的滚动更新是直接删 Pod,不经过 eviction API。发布期间的保护要靠 maxUnavailable,PDB 保护不了。

没有 preStop 就开优雅停机。 容器收到 SIGTERM 时,Service 端点摘除和进程停止几乎同时发生,仍有请求在途。加一个 preStop: sleep 5,让摘除先传播完。

回滚只看 Deployment 状态。 rollout undo 成功不代表服务恢复,还要核对配置版本、数据库兼容性、以及对下游的调用契约有没有变。

同一时间多个发布叠加。 两个人先后改镜像,第二个 set image 会把第一个的 ReplicaSet 直接顶掉,rollout undo 回到的版本可能谁都没预期过。

参考

评论

推荐阅读

订阅邮件

订阅更新,及时收到最新文章。

作者