AI时代的Kubernetes教程36招-第10招-Deployment、ReplicaSet和RollingUpdate怎么完成无损发布?

Kubernetes 无损发布实战:Deployment 控制链、RollingUpdate 与优雅退出回滚验证

无损发布难在退出,不难在启动

[读者] 用 Kubernetes 发布业务、被 502 或卡住的发布咬过的工程师
[痛点] 探针、preStop、maxSurge 都配了,发布仍不稳,回滚也从没验证过
[现在读] 默认参数与探针的真实边界,只在故障路径上才会暴露
[读完] 能独立设计一条可暂停、可验证、可回滚的发布链路
[旧方案] 改 image tag + kubectl apply + 默认 RollingUpdate
    |
    v
[新需求] 发布期间零 5xx、可中途暂停观察、失败快速暴露、回滚可验证
    |
    v
[冲突] 默认参数只调副本数,管不到端点摘除与进程退出之间的时间差
    |
    v
[后果] 偶发 502、失败要等 progressDeadlineSeconds 才发现、回滚语义靠猜

我是老李,做云原生平台和发布链路这块有些年头了,值班夜里被网关 502 叫醒的次数不算少。我们项目的部署描述集中在 deploy/base/gateway/deployment.yaml,网关是所有流量的总入口,它的 Deployment 既是最容易出事的链路,也最值得先治好。

最初的发布流程很朴素:改镜像 tag,kubectl apply,然后 kubectl get pod -w 盯着新 Pod 一列列起来。网关副本数不多,默认滚动参数下几乎每次发布都会出现一段副本缺口,客户端偶发 502,但每次都能自愈,所以一直没人深究。

后来有人补了 readinessProbe,也有人加了 preStop 睡眠,发布看起来稳了。但「看起来稳」不是结论:探针失败会怎样、错误镜像会怎样、undo 之后到底回到哪个 revision,这三件事谁都没验证过。

于是问题收敛成三个:maxSurge 与 maxUnavailable 调的到底是谁的副本数,为什么小副本场景下的实际表现和直觉相反?想让流量无损,是该信 readinessProbe,还是该靠 preStop 加应用优雅退出?如果错误镜像和 Readiness 失败一起出现,rollout 会以什么方式失败,undo 之后回到的又是哪一个 revision?

01、故事

我们这个网关跑在一个自建 Kubernetes 集群上,Deployment 声明在 deploy/base/gateway/deployment.yaml,副本数按容量估算给了 3,对外是 Service + 前置 Ingress。日常的发布动作就是改一个镜像 tag,然后 apply。

任务是把「一次镜像变更」做成一个可回滚、不掉流量的过程,而不是一次赌运气的重启。

当前方案里其实有三个默认值在悄悄起作用:strategy 不写等于 RollingUpdate;maxSurge 与 maxUnavailable 不写等于 25%;探针只写了 readinessProbe,preStop 是后来随手加的 5 秒睡眠,没人说得清它到底在挡什么。

需求变化来自一次线上事故:网关滚动发布期间,上游业务出现零星 502,监控上 ready 副本数几乎没掉,事后也没有留下任何能复现的证据。于是要求从「发布尽量不中断」升级成「发布必须可证明不中断,失败必须快速暴露,回滚必须可验证」。

冲突就出在这里:Deployment 的默认参数只负责调新旧 ReplicaSet 的副本数,它不负责「容器停止监听」与「数据面真正不再转发」之间的那段灰时间;而探针只负责回答「这个 Pod 什么时候算可用」。两件事都不在副本数这个维度上,所以调副本数永远调不出无损。

一纸镜像令
副本自成军
探针定生死
退出见真章

02、问题

旧方案失效的第一个点,是它把「发布成功」定义成了 rollout status 返回 0。可是滚动发布期间客户端的 502 与 rollout status 的成败没有因果关系,前者由数据面决定,后者由控制器状态决定。

业务影响是直接的:网关是总入口,它的 5xx 会原样透传到所有上游业务,故障半径最大。更糟的是这类 502 通常只有几秒、没有稳定复现路径,事后排查基本靠猜。

技术表现可以拆成三条。第一条,发布窗口内 ready 端点数量出现波动,容量出现缺口。第二条,被缩容的旧 Pod 在端点摘除真正生效之前就停止了监听,连接被转发到一个已经不响应的进程上。第三条,一旦镜像是错的或探针是错的,现象只有「卡住」,没有任何能区分两者的证据。

所以完成标准必须是可验证的,而不是感觉上的:

  1. 正常变更时 rollout status 成功返回,且期间 Service 的 ready 端点数量不低于 期望副本数 − maxUnavailable。
  2. 制造错误镜像时,rollout 在 progressDeadlineSeconds 内以非零退出,且 Deployment 的 AVAILABLE 不下降。
  3. 制造 Readiness 失败时,滚动停住但服务不中断,rollout history 能定位到对应 revision。
  4. 发布窗口内持续压请求,状态码分布中不出现 5xx。
  5. undo 之后 PodTemplate 回到目标 revision,且 history 中能查到这一次回滚。

旧规已不灵
新标尚未立
可用换进度
二者难两全

03、原理

先看控制链。Deployment 不直接创建 Pod,它只维护 ReplicaSet;ReplicaSet 才维护 Pod。每当 PodTemplate 发生变化,Deployment 控制器会按 pod-template-hash 计算出一个新的 ReplicaSet,旧 RS 被缩容但不删除。这条链决定了后面所有行为:滚动发布本质上是控制器在调整新旧两个 RS 的 spec.replicas。

再看 RollingUpdate 的算术。maxSurge 是相对期望副本数允许超出的最大值,maxUnavailable 是允许低于期望副本数的最大值,二者不能同时为 0。官方文档里有两条取整规则,方向是相反的:maxSurge 的百分比向上取整,maxUnavailable 的百分比向下取整。

这就是本篇第一个反直觉判断:3 副本、默认 25% 的组合,实际算出的是 surge = 1、unavailable = 0。也就是说小副本场景下默认策略并不会「先减后加」,而是「先加后减、期间不掉容量」。很多人以为默认会掉容量,于是在这里乱调参数,越调越糟。

第二个反直觉判断:readinessProbe 不是监控配置,而是发布控制器的输入。控制器只把 Ready=True 的 Pod 计入 availableReplicas,也只有 ready 的 Pod 才会进入 EndpointSlice 的 ready 集合。换句话说,改探针等价于改发布策略,它决定的是「何时算可用」,而不是「有没有告警」。

第三个要点是就绪与可用的三段传播。第一段,Pod 的 Ready condition 被探针置为 True;第二段,EndpointSlice 把该地址放进 ready 集合;第三段,kube-proxy 或 Ingress 规则更新、既有连接被自然耗尽。前两段有回执可查,第三段没有回执,你无法从 API 上读到「数据面已经不再转发」这个时刻。

这就直接解释了退出侧的问题。删除一个 Pod 的时序是:Deployment 缩容旧 RS → Pod 被设置 deletionTimestamp → EndpointSlice 摘除 → kubelet 执行 preStop → preStop 结束后向容器发 SIGTERM → 应用优雅退出或等到 terminationGracePeriodSeconds 到点 → SIGKILL。注意 preStop 是「先执行完、再发 TERM」,不是并行;它的唯一作用就是给没有回执的第三段买时间。

第四个要点是失败判定。Deployment 的 Progressing condition 有 progressDeadlineSeconds(默认 600s),到点仍未推进就置为 False、reason 为 ProgressDeadlineExceeded,rollout status 会报错退出。这意味着错误镜像的默认暴露时间可能长达十分钟,而服务本身在 maxUnavailable=0 时是不掉容量的——「卡住但没事」正是它最难被发现的原因。

最后是回滚语义。undo 不是把状态还原到旧编号,而是把旧的 PodTemplate 重新写回 Deployment,这会再生成一个新的 RS 和一个新的 revision,编号继续递增。--to-revision=0 表示上一个 revision,能回到哪个编号,取决于 revisionHistoryLimit(默认 10)是否还保留着对应的旧 RS。

探针非点缀
乃是发令官
取整方向异
小局最分明

04、架构

[输入] kubectl apply(新的 PodTemplate)
    |
    v
[模块] Deployment Controller(reconcile 新旧 ReplicaSet 的副本数)
    |
    v
[数据/状态] RS(old, hash=A) / RS(new, hash=B) + revision 历史
    |
    v
[处理] RollingUpdate:按 maxSurge/maxUnavailable 加新减旧;探针决定 Ready
    |
    v
[输出] Ready Pod → EndpointSlice → 数据面 → 真实流量

边界:这条链路只管副本数与就绪判定,管不到数据面规则的分发延迟,也管不到容器内部是否有在途请求。凡是落到这两处的,都必须由 preStop 与应用退出逻辑补上。

收益:把「发布」从一个动作变成一组可观察的状态——期望副本、updated 副本、available 副本、revision 历史、Progressing condition,每一项都能在故障时当作证据。

代价:加上 maxUnavailable=0 与 preStop 睡眠之后,发布窗口会变长,峰值时刻会多占一份临时资源;preStop 睡眠本身就是拿时间换确定性。

适用条件:这套组合成立的前提是应用真的能优雅退出(收到 TERM 后停止接收新连接、把在途请求处理完)。如果应用是「收到 TERM 立刻断连」,preStop 只能兜住端点摘除的延迟,兜不住正在处理中的请求。

声明即目标
三层各守职
哈希辨新旧
副本作闸门

05、实战一次

先跑通 V1:一个能发布、能被观察、但还留着问题的版本。本节命令基于标准 Deployment API(apps/v1),版本相关行为需按当前官方文档核验。

准备命名空间与 V1 清单,写入 deploy/base/gateway/deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: gateway
  namespace: demo
  labels:
    app: gateway
spec:
  replicas: 3
  selector:
    matchLabels:
      app: gateway
  template:
    metadata:
      labels:
        app: gateway
    spec:
      containers:
        - name: gateway
          image: nginx:1.25-alpine
          ports:
            - name: http
              containerPort: 80
          readinessProbe:
            httpGet:
              path: /
              port: http
            initialDelaySeconds: 2
            periodSeconds: 3
            timeoutSeconds: 1
            failureThreshold: 3
---
apiVersion: v1
kind: Service
metadata:
  name: gateway
  namespace: demo
spec:
  selector:
    app: gateway
  ports:
    - name: http
      port: 80
      targetPort: http

启动与验证:

kubectl create namespace demo
kubectl -n demo apply -f deploy/base/gateway/deployment.yaml
kubectl -n demo rollout status deployment/gateway --timeout=120s

未在当前环境实测,以下为预期结果(示例输出):

deployment "gateway" successfully rolled out

查看控制链的三层状态:

kubectl -n demo get deployment gateway
kubectl -n demo get rs -l app=gateway
kubectl -n demo get pods -l app=gateway

示例输出:

NAME      READY   UP-TO-DATE   AVAILABLE   AGE
gateway   3/3     3            3           45s

NAME                 DESIRED   CURRENT   READY   AGE
gateway-6d8f9c7b4    3         3         3       45s

NAME                       READY   STATUS    RESTARTS   AGE
gateway-6d8f9c7b4-2xk9p    1/1     Running   0          45s
gateway-6d8f9c7b4-7lq2m    1/1     Running   0          45s
gateway-6d8f9c7b4-t4vzn    1/1     Running   0          45s

发一次变更,并观察 revision 历史:

kubectl -n demo set image deployment/gateway gateway=nginx:1.25-alpine
kubectl -n demo rollout status deployment/gateway --timeout=120s
kubectl -n demo rollout history deployment/gateway

示例输出:

deployment.apps/gateway image updated
deployment "gateway" successfully rolled out
deployment.apps/gateway
REVISION  CHANGE-CAUSE
1         <none>
2         <none>

到这里 V1 是通的:能发布、能看状态、能看 revision。但它留了三个洞:没有 preStop,容器收到 TERM 就断连;没有显式 RollingUpdate 参数,容量与进度完全交给默认值;错误镜像与探针失败这两条故障路径一次都没走过。下一节开始逐个走。

先通一条路
再留一处疑
版本须核对
示例即示例

06、排查

诊断链一:错误镜像导致发布停滞。

现象:执行 set image 到一个不存在的 tag 后,rollout status 长时间不返回。怀疑:是节点拉不动镜像,还是控制器根本没推进?检查:

kubectl -n demo get pods -l app=gateway
kubectl -n demo describe pod -l app=gateway | sed -n '/Events/,$p'
kubectl -n demo get deployment gateway -o jsonpath='{range .status.conditions[*]}{.type}{"\t"}{.status}{"\t"}{.reason}{"\n"}{end}'

证据(示例输出):

NAME                       READY   STATUS             RESTARTS   AGE
gateway-7c9d4b6f5-8jq7r    0/1     ImagePullBackOff   0          3m11s
gateway-6d8f9c7b4-2xk9p    1/1     Running            0          12m

Warning  Failed  Failed to pull image "nginx:1.25-alpin": ... not found

Progressing  False  ProgressDeadlineExceeded

根因:镜像 tag 拼写错误,容器根本起不来,新 RS 的 availableReplicas 始终是 0,控制器认定没有推进,直到 progressDeadlineSeconds 默认 600s 到点才把 Progressing 置为 False。修复:换成正确 tag(生产上更稳的做法是锁定 digest),并把 progressDeadlineSeconds 调小,让失败提前暴露。

诊断链二:Readiness 失败导致滚动停滞但服务不掉容量。

现象:新 Pod 是 Running,但 READY 一直是 0/1,旧 Pod 一个都没被删。怀疑:探针路径或端口不对,还是应用真的没起来?检查:

kubectl -n demo describe pod -l app=gateway | grep -i 'readiness probe failed'
kubectl -n demo get endpointslices -l kubernetes.io/service-name=gateway -o yaml

证据(示例输出):

Warning  Unhealthy  Readiness probe failed: HTTP probe failed with statuscode: 404

endpoints:
- addresses: [10.244.1.7, 10.244.2.3, 10.244.3.9]
  conditions:
    ready: true

根因:探针 path 写成了镜像里不存在的路径,返回 404,容器进程本身是健康的;因为 3 副本 × 25% 中 maxUnavailable 向下取整为 0,控制器不会先删旧 Pod,于是表现成「发布卡住但容量不降」。修复:探针指向真实存在的路径与端口,必要时拆成 startupProbe + readinessProbe 两段。

错误尝试: 第一反应是把 maxSurge 从 25% 调到 100%,想让发布「快一点冲过去」。这是错的:发布停滞的根因是新 Pod 永远不 Ready,不是并发度不够。调大 maxSurge 只会让更多不 Ready 的 Pod 占着资源,而控制器计算新 RS 的可用副本时依然得 0,推进条件仍然不满足,发布还是卡着,节点余量却先被挤掉。真正该动的是探针定义,不是副本参数。

诊断链三:发布窗口内的零星 502。

现象:发布过程本身成功,但回看监控有一段 502。怀疑:是探针不够灵敏,还是退出时序有问题?检查:

kubectl -n demo get pod -l app=gateway -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.deletionTimestamp}{"\n"}{end}'
kubectl -n demo get events --sort-by=.lastTimestamp | tail -20

证据(示例输出):

gateway-6d8f9c7b4-2xk9p  2024-06-11T03:12:41Z

Normal  Killing  Stopping container gateway

502 的时间点集中落在 Pod 进入 Terminating 之后的几秒内。根因:容器收到 SIGTERM 后立即停止监听并退出,而 EndpointSlice 的变更还没在数据面全部生效,仍有连接被转发到这个已经不响应的进程;探针在这里帮不上忙,因为 Pod 已被摘除,探针不再参与判定。修复:加 preStop,让 kubelet 先等一段时间再发 TERM,同时应用侧实现优雅退出。

现象先落纸
假设后开口
证据不落地
加码皆徒劳

07、优化

V2 的每一处改动都必须对应第 06 节里的一条证据,否则就是凭空加防御。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: gateway
  namespace: demo
  labels:
    app: gateway
spec:
  replicas: 3
  revisionHistoryLimit: 10
  progressDeadlineSeconds: 120
  minReadySeconds: 5
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  selector:
    matchLabels:
      app: gateway
  template:
    metadata:
      labels:
        app: gateway
    spec:
      terminationGracePeriodSeconds: 30
      containers:
        - name: gateway
          image: nginx:1.25-alpine
          ports:
            - name: http
              containerPort: 80
          startupProbe:
            httpGet:
              path: /
              port: http
            periodSeconds: 2
            failureThreshold: 30
          readinessProbe:
            httpGet:
              path: /
              port: http
            periodSeconds: 3
            timeoutSeconds: 1
            failureThreshold: 3
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 5"]

逐条对应关系如下。

根因一:失败要等 600s 才暴露。修改:progressDeadlineSeconds 从默认 600 改为 120。原因:默认值是通用折中,对网关这种高频发布的服务太长。新行为:错误镜像在两分钟内以非零退出。验证方式:制造错误 tag,确认 rollout status 在 120s 左右报错,且 Deployment 的 AVAILABLE 不下降。

根因二:探针路径与端口写错时新 Pod 永不 Ready。修改:拆出 startupProbe 并复用同一 httpGet 定义。原因:启动慢与运行中不健康是两件事,混在一个探针里会让 failureThreshold 变得两头不讨好。新行为:启动阶段允许更长的失败容忍,运行阶段用更短的阈值快速摘除。验证方式:临时把探针端口改错,确认发布停滞且端点数量不下降,这是保护而非故障。

根因三:容器收到 TERM 立即退出,端点摘除尚未生效。修改:加 preStop 的 sleep 5,并把 terminationGracePeriodSeconds 显式写成 30。原因:这段睡眠是给没有回执的数据面传播买的缓冲;宽限期必须大于 preStop 耗时 + 应用排空耗时,否则 kubelet 会在排空完成前 SIGKILL。新行为:删除旧 Pod 时会先等一段再发 TERM,应用有时间把在途请求处理完。验证方式:发布窗口内持续发请求,观察状态码分布中是否还有 5xx。

根因四:滚动进度与资源占用需要显式表达。修改:maxSurge 固定为 1,maxUnavailable 固定为 0,minReadySeconds 设为 5。原因:百分比在小副本数下受取整方向影响,行为不直观;绝对值让「最多多起一个、期间不掉容量」变成可读的意图。新行为:发布期间 ready 端点数量不下降,代价是发布窗口变长、峰值多占一份资源。验证方式:发布时轮询端点数量,确认始终不低于期望副本数。

需要说明的是:这一节只给出验证方案与判定标准,不提供任何实测数字。上面的每一条都必须在你自己的集群里跑一遍才算数。另外,nginx 对 SIGTERM 是快速关闭,preStop 睡眠是它唯一的缓冲手段;真正要在途请求不丢,得靠应用自己实现优雅退出,例如 Go 的 http.Server.Shutdown 或 Java 的 server.shutdown=graceful,preStop 只负责兜住端点摘除那一段。

根因已落定
改动方有据
新行须复验
验证不可编

08、演进

[同一输入]
    |
    +--[V1] 默认 RollingUpdate + 单个 readinessProbe
    |        行为:先加后减小副本下不掉容量,但退出即断连
    |        代价:偶发 502,错误镜像要等 600s 才暴露
    |
    +--[V2] 显式 maxSurge/maxUnavailable + startup/readiness
    |        + preStop sleep + 应用优雅退出 + 更短的 progressDeadline
    |        行为:容量不缺口,退出不丢在途请求,失败两分钟内暴露
    |        代价:发布窗口变长,峰值多占一份资源,需要应用配合
    |
[Trade-off]
{得到:可预测、可暂停、可回滚的发布;失去:一部分发布速度与峰值资源;适用边界:应用必须真的支持优雅退出}

正确性上,V1 与 V2 对同一个 PodTemplate 的最终收敛结果是一致的,差别在于过程中是否出现容量缺口与连接丢失。V1 的正确性依赖运气,V2 的正确性依赖时序设计。

稳定性上,V2 把两类失败从「静默」变成「有证据」:错误镜像在 progressDeadlineSeconds 内失败,探针错误以停滞而非中断的形式表现。V1 的两类失败都会退化成「发布卡住,原因不明」。

复杂度上,V2 多出三个需要维护的旋钮:preStop 睡眠长度、terminationGracePeriodSeconds、progressDeadlineSeconds。它们之间还必须满足「宽限期 > preStop + 排空时间」的不等式,配置错了反而会引入 SIGKILL 截断。

成本上,maxUnavailable=0 意味着发布期间至少多占一份资源,滚动时间也相应拉长,三次滚动里多出来的 Pod 是实打实的调度开销。

适用范围上,这套组合适合无状态、可水平扩展、能在有限时间内排空的服务;对长连接或长事务服务,preStop 睡眠的秒数要按连接生命周期重新推,不能照抄 5 秒。

遗留问题有两个。第一,preStop 睡眠长度没有普适答案,它取决于 kube-proxy、Ingress 或云负载均衡的规则传播时间,需按当前官方文档核验并结合自己集群实测。第二,本节没有做金丝雀分流,pause/resume 只用来冻结与恢复滚动,真正的按比例引流还需要额外的流量治理能力。

同入不同出
得到亦失去
边界写明处
取舍自安然

09、洞见

9.1 反直觉判断:readinessProbe 是发布策略的一部分,不是监控配置

它不产生告警价值,它产生的是控制信号。控制器只把 Ready 的 Pod 计入可用副本,端点集合也只接纳 Ready 的地址。所以改探针的失败阈值,等价于改了「发布时允许多少个 Pod 不被算作可用」。把探针当监控配的人,实际上是在不知不觉中修改发布语义。

9.2 反直觉判断:决定「无损」的关键路径在退出侧,不在启动侧

所有人第一反应都是优化启动:加探针、缩短启动时间、调大 maxSurge。但第 06 节的证据显示,502 出现在 Pod 进入 Terminating 之后的几秒内,与启动毫不相干。数据面传播没有回执,你无法从任何 API 上读到「已经不再转发」,唯一能做的就是退出侧买时间:preStop 兜住传播,应用自己兜住在途请求。

9.3 maxUnavailable=0 不是零风险,它把风险从可用性换成了时长与资源

默认 25% 在 3 副本下向下取整就是 0,所以很多人其实一直在享受这个保护。代价是发布推进变慢、峰值多占资源,而且一旦新 Pod 永远不 Ready,表现是「发布卡住、服务正常」,这种安静反而更难被发现。这是一个明确的风险置换,不是免费的保险。

9.4 revision 是声明历史的编号,不是镜像 tag 的别名

undo 的语义是重新应用旧的 PodTemplate,它会再生成一个新的 RS 和新的 revision,编号继续增长。所以「回滚到 revision 2」在执行后可能表现为「revision 5 的模板等于 revision 2 的模板」。能不能回去,取决于 revisionHistoryLimit 是否还留着对应的旧 RS;默认 10 这个数字,是回滚能力的实际上限。

直觉常出错
关节在细节
判断有出处
洞见自成篇

10、系统落地

原来有什么:一条 deploy/base/gateway/deployment.yaml,默认 RollingUpdate,一个 readinessProbe,一次靠盯屏幕完成的发布流程。

本篇新增了什么:显式的 maxSurge/maxUnavailable 与 minReadySeconds;startupProbe 与 readinessProbe 的两段式判定;preStop 与 terminationGracePeriodSeconds 组成的退出时序;更短的 progressDeadlineSeconds;以及三条可以随时复跑的故障路径(错误镜像、探针失败、退出期 502)。

现在能做什么:一次 apply 之后,可以用 rollout status 判断成败,用 rollout history 判断 revision,用 pause/resume 在中途冻结发布观察状态,用 undo 执行确定性回滚;发布期间的端点数量与请求状态码分布都可观测。

还缺什么:preStop 睡眠秒数尚未按真实数据面传播时间校准;应用侧的优雅退出没有在网关进程里验证;没有做按比例的金丝雀引流;回滚的触发仍靠人判断,没有和 SLO 打通。

下一步如何演进:先把 preStop 长度压到实测能覆盖传播时间的最小值,避免无谓地拉长发布窗口;再把「发布期间 5xx 比例」接入发布门禁,让 undo 从人工判断变成基于指标的动作;最后在这条链路上加一层按比例引流,把 pause/resume 手工冻结升级成自动化金丝雀。

原有已能跑
今增三件套
能做什么事
缺什么待补

11、小结

Q1 → maxSurge/maxUnavailable 调的是新旧 ReplicaSet 的副本数,
     百分比场景下 surge 向上取整、unavailable 向下取整,小副本时表现为不掉容量
Q2 → 两者都要,但「无损」的关键路径由 preStop + 应用优雅退出决定,
     探针只决定一个 Pod 何时被算作可用
Q3 → 错误镜像会让 rollout 在 progressDeadlineSeconds 后以非零退出,
     Readiness 失败则表现为发布停滞但容量不降;undo 会生成新的 revision
状态 → 一条可暂停、可验证、可回滚的发布链路(deploy/base/gateway/deployment.yaml)

三个问题的答案指向同一件事:发布不是一次动作,而是一组状态的收敛过程。副本数只是这组状态里最容易被看见的那一个,真正决定成败的是就绪判定与退出时序。

三问皆有答
一态可交付
未证不言果
有据方称实

12、作业

12.1 理解题:为什么 3 副本 + 默认 25% 的组合,实际 maxUnavailable 是 0?

参考答案:maxSurge 的百分比向上取整,3 × 25% = 0.75 向上得到 1;maxUnavailable 的百分比向下取整,0.75 向下得到 0。因此默认策略在小副本场景下表现为「先加后减、期间不掉容量」。这也解释了为什么很多团队觉得「没配什么,发布也挺稳」,一旦把副本数放大到 8,取整结果变化,行为就会跟着变。

12.2 实战题:写一段流程,验证一次发布确实没有造成流量中断。

参考答案:先记录变更前后的端点数量,确保发布期间不低于期望副本数减去 maxUnavailable;再在一个终端持续发请求并按状态码聚合;同时另开窗口执行 kubectl rollout status 与 rollout history,确认发布成功且 revision 递增。发布结束后核对状态码分布中不存在 5xx,并用 undo 走一次回滚,确认回滚同样不产生 5xx、且 history 中新增了一条记录。注意所有判定都必须来自实际观测,不能凭预期填写。

12.3 排障题:新 Pod 一直是 0/1 Running,旧 Pod 一个都没被删,怎么定位?

参考答案:先 describe Pod 看 Events 里的 Readiness probe failed 记录,确认是探针问题还是应用问题;再用 get endpointslices 看 ready 地址数量是否维持在原水平,确认服务确实没掉容量。如果确认是探针路径或端口错误,修正探针;不要通过调大 maxSurge 或拉长 progressDeadlineSeconds 来「绕过」,因为根因在新 Pod 永远不可用,调副本参数只会浪费资源且不推进发布。

12.4 架构判断题:既然有 readinessProbe,能不能省掉 preStop 和优雅退出?

参考答案:不能。探针解决的是「什么时候把一个 Pod 算作可用」,它在 Pod 被标记删除之后就不再参与判定;而 502 出现在删除之后的端点摘除传播窗口,是数据面问题。省掉 preStop 与优雅退出,等于放弃退出侧唯一的缓冲与排空手段。反过来,只加 preStop 而不改探针,则在发布停滞时会失去「新 Pod 算不算可用」这一关键判据。两者解决的是不同阶段的问题,不可互相替代。

四题各一关
理解先于做
排障重在证
架构问边界

13、思考

这一篇的核心冲突只有一个:Deployment 的默认参数和探针,管的都是副本数与就绪状态,而「无损」这件事发生在另一个维度——端点摘除与进程退出之间的那段没有回执的时间差。只要还站在副本数维度调参数,这个冲突就永远解不开。

可复用的工程判断有三条。第一,凡是「没有回执」的环节,只能用时间兜住,并且必须把这段时间显式写进配置,而不是靠默认值撞运气。第二,失败的暴露时间本身就是一项需要设计的指标,600 秒和 120 秒的差别,是「无人发现」和「发布流水线拦截」的差别。第三,回滚不是时间旅行,它是把旧模板重新声明一遍,所以回滚能力的上限由 revision 保留数量决定,必须提前验证而不是出事时才第一次用。

把这些落到日常动作上,就是每次改发布参数之前先问一句:我改的是副本数、是就绪判定,还是退出时序?如果答案是第一项,那它多半不是解决这次问题的钥匙。

发布非终点
退出是起点
控制链一稳
无损自然来

posted @ 2026-10-12 10:23  李福春  阅读(3)  评论(0)    收藏  举报