7.6 自愈、滚动更新与回滚

一句话

自愈是 “控制器把副本数拉回期望”,滚动更新是 “用新旧两个 ReplicaSet 换版本”,回滚是 “把旧 RS 扩回去”;三者都不保证服务不中断——不中断靠的是 readiness + PDB + 优雅下线这三件套。

三件套之一:三种探针,各管一件事

探针 失败时 该探什么 典型误用
liveness 重启容器 进程是否死锁/卡住(只看自身) 探下游依赖 → 下游抖动引发级联重启
readiness 摘出 Endpoint(不重启) 能否接流量(依赖、预热、限流开关) 只探 “端口通”,依赖没连上也说就绪
startup 重启容器,且在通过前屏蔽另外两个探针 慢启动应用(JVM/模型加载) 不配 startup,给 liveness 设 30 秒 → 启动期被杀

参数口径(默认值容易记错,写清更安全):

参数 默认 含义
initialDelaySeconds 0 首次探测前等待
periodSeconds 10 探测间隔
timeoutSeconds 1 单次超时
failureThreshold 3 连续失败几次判定失败
successThreshold 1 连续成功几次判定成功(liveness 只能为 1)
minReadySeconds 0 就绪后再观察多久才算 “可用”(影响滚动节奏)

三件套之二:滚动更新的五个旋钮

参数 默认 调大的后果 调小的后果
maxSurge 25% 资源峰值高,发布快 发布慢
maxUnavailable 25% 发布快,容量下降多 慢,但稳
progressDeadlineSeconds 600 卡住的发布要等更久才报 ProgressDeadlineExceeded 正常慢启动被误判失败
minReadySeconds 0 更保守(新 Pod 稳定后才继续) 新 Pod 刚就绪就被压流量,问题暴露延迟
revisionHistoryLimit 10 可回滚版本多,RS 占资源 可回滚版本少
apiVersion: apps/v1
kind: Deployment
metadata: {name: web}
spec:
  replicas: 6
  strategy:
    type: RollingUpdate
    rollingUpdate: {maxSurge: 1, maxUnavailable: 0}   # 先起后停:容量不降
  minReadySeconds: 10
  progressDeadlineSeconds: 300
  template:
    metadata: {labels: {app: web}}
    spec:
      terminationGracePeriodSeconds: 30
      containers:
        - name: app
          image: registry/web:1.4.2
          readinessProbe:
            httpGet: {path: /healthz, port: 8080}
            periodSeconds: 5
          livenessProbe:
            httpGet: {path: /livez, port: 8080}
            failureThreshold: 3
          startupProbe:
            httpGet: {path: /livez, port: 8080}
            failureThreshold: 30          # 30 × 2s = 最长等 60s 启动
            periodSeconds: 2
          lifecycle:
            preStop:
              exec: {command: ["sh", "-c", "sleep 8"]}   # 等 Endpoint 摘除传播完
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: {name: web}
spec:
  minAvailable: 4          # 6 副本至少留 4 个
  selector: {matchLabels: {app: web}}

maxUnavailable: 0 + maxSurge: 1 的语义是 “先起新的,再停旧的”:容量不降,代价是发布期间多占一份资源。

三件套之三:优雅下线的竞态

删除 Pod 时,两件事几乎同时发生:Endpoint 摘除 与 容器收到 SIGTERM。kube-proxy / kubelet 的规则更新有延迟,于是出现 “流量还在进来,进程已经在退出” 的 502 窗口。

措施 作用
preStop 里 sleep 5–10 秒 等 Endpoint 摘除传播到所有节点
terminationGracePeriodSeconds 足够大 给在途请求收尾时间(含 preStop 的时长)
应用收到 SIGTERM 后先停止接受新连接 把在途请求处理完再退出
配 readiness 的失败路径 摘除更早发生,窗口更小

自愈的边界:哪些能自愈,哪些不能

层级 机制 恢复动作 时间量级
进程卡死 liveness 探针 重启容器 秒级
容器崩溃 kubelet 按 restartPolicy 重启 秒级(含退避)
Pod 被删/节点不可用 ReplicaSet 在新节点重建 秒–分钟(含镜像拉取)
节点失联 node controller 约 40 秒标记 NotReady,约 5 分钟后开始驱逐 分钟级
应用逻辑错误 / 数据损坏 不会自愈 只能人工回滚或修数据 —
控制面故障 不会自愈 需要多控制面 + 备份 —

🔑 自愈只保证 “副本数回到期望值”,不保证 “回来的副本是健康的、数据是对的”。 数据一致性与业务正确性不在 K8s 的能力范围内。

回滚:回滚的是 RS,不是镜像

kubectl rollout history deploy/web
kubectl rollout undo deploy/web --to-revision=3
kubectl rollout status deploy/web            # 等收敛,别只看命令返回
kubectl get rs -l app=web                    # 看到旧 RS 被扩容、新 RS 缩容
问题 答案
回滚会改 YAML 文件吗? 不会。集群状态回到旧模板,git 里的清单要另行处理(通常再提交一次)
回滚能回滚数据库变更吗? 不能。所以要先做兼容性变更(expand-contract),见 55.4 数据库变更管理 (Expand-Contract)
--to-revision 能回到多久以前? 只到 revisionHistoryLimit 保留的范围(默认 10)
回滚失败怎么办? 看 rollout status 与 Pod 事件;常见原因是旧镜像已被删除或旧配置已被改

回滚策略的完整设计(含数据侧)见 55.3 回滚设计。

反模式

反模式 后果
liveness 探下游依赖 下游一抖,全量 Pod 级联重启(雪崩)
不配 readiness Endpoint 收进未就绪 Pod,滚动更新必炸
maxUnavailable: 100% 发布期间容量归零,等同于停机
无 PDB 就做节点维护 批量驱逐,副本数瞬间不足
preStop 不设、terminationGracePeriodSeconds 用默认 30 秒却跑长任务 强杀在途请求
认为 “回滚了就万事大吉” 数据库已改,旧代码读不了新数据
用 kubectl delete pod 当成 “重启服务” 的常规手段 绕过控制器语义,短时容量下降且无记录

三条铁律

🔑 liveness 只探自己,依赖放 readiness;否则下游故障会变成级联重启。

🔑 先起后停(maxUnavailable: 0)+ PDB + preStop,三者合起来才叫 “不中断发布”。

🔑 回滚只回代码,不回数据;涉及数据变更时先做双向兼容。

自检

Q1 滚动更新卡在 ProgressDeadlineExceeded,怎么定位?

▸ 答案:先看新 RS 的 Pod 事件与探针:① 镜像拉取失败(凭证/网络);② readiness 一直不通过(依赖没就绪、initialDelaySeconds 太短、路径写错);③ 资源不足导致新 Pod Pending;④ minReadySeconds 与 progressDeadlineSeconds 配比不合理(慢启动应用被误判)。定位顺序是 “Pod 事件 → 探针日志 → 资源与配额”,而不是先回滚。

Q2 为什么 liveness 探针里不能检查数据库连接?

▸ 答案:因为 liveness 失败会重启容器。数据库抖动(1 秒超时)会让所有 Pod 同时探测失败并集体重启,重启又加剧依赖拥塞 → 雪崩。数据库连通性属于 “能不能接流量” 的问题,应放在 readiness(失败只摘流量、不重启),并配合重试与超时预算。

Q3 PDB 能防住节点故障导致的副本下降吗?

▸ 答案:不能。PDB 只约束自愿中断(kubectl drain、驱逐 API、集群升级),节点失联属于非自愿中断,node controller 会直接驱逐,PDB 不参与。所以 “有 PDB 就不怕节点挂” 是误解;节点级容灾要靠多副本 + 反亲和 + 容量冗余。

Q4 回滚到上一版后业务仍然报错,最可能的原因是什么?

▸ 答案:数据或外部状态已经不兼容。典型情形:新版本做了不可逆的数据库迁移(删列、改类型)、发了新版消息格式、改了缓存 key 结构,回滚后的旧代码读不了新数据。修法不是 “再回滚一次”,而是按 expand-contract 的顺序发布(先加兼容字段、再迁数据、最后删旧字段),把 “代码可回滚” 守住。

Q5 发布期间 “零 502” 需要哪些条件同时成立?

▸ 答案:① readiness 正确(新 Pod 真就绪才进 Endpoint);② maxUnavailable: 0(先起后停,容量不降);③ preStop + grace period(等 Endpoint 摘除传播、处理完在途请求);④ 应用优雅退出(收到 SIGTERM 先停新连接);⑤ 入口层重试/超时合理(覆盖残余窗口)。缺任何一条,都会留下几十秒的零星 502。

跨篇相关:控制器如何把副本数拉回期望见 7.1 声明式 API 与控制器模式;RS 与 Deployment 的套娃关系见 7.2 K8s 对象模型 (Pod / ReplicaSet / Deployment / StatefulSet / DaemonSet / Job);Endpoint 与就绪的关系见 7.3 Service 与 Ingress;回滚的数据侧设计见 55.3 回滚设计、55.4 数据库变更管理 (Expand-Contract);灰度与流量染色见 55.6 灰度与流量染色;发布策略的取舍见 55.1 发布策略 (滚动 / 蓝绿 / 金丝雀)。