K8s Lab 把当前仓库文档整理成一个可阅读的网页站点

Repository Reading Site

第十课:探针、滚动更新、优雅终止与 PDB 原理

前面几课我们已经把 Kubernetes 最关键的几条主链路拆开了: 但这还没有碰到线上最痛的那部分问题: 1. 为什么明明 Pod 已经 Running,业务还是没法接流量 2. 为什么有些 Pod 会不断被 kubelet 重启 3. 为什么发版时明明用了 Deployment,用户还是会抖一下 4. 为什么节点维护时有些 Pod 能赶走,有些赶不走 5

Markdown10-第十课-探针-滚动更新-优雅终止与PDB原理.md2026年4月10日 03:27

第十课:探针、滚动更新、优雅终止与 PDB 原理

为什么这一课必须接在 StatefulSet 后面

前面几课我们已经把 Kubernetes 最关键的几条主链路拆开了:

  • apply 如何进入控制面
  • 调度器如何选节点
  • 网络如何连通
  • 身份认证、授权、准入如何控权
  • 配置和 Secret 如何进入 Pod
  • 存储如何从 Pod 生命周期里独立出来
  • StatefulSet 如何给状态副本提供稳定身份

但这还没有碰到线上最痛的那部分问题:

  1. 为什么明明 Pod 已经 Running,业务还是没法接流量
  2. 为什么有些 Pod 会不断被 kubelet 重启
  3. 为什么发版时明明用了 Deployment,用户还是会抖一下
  4. 为什么节点维护时有些 Pod 能赶走,有些赶不走
  5. 为什么有些系统升级得很慢,却恰恰更安全

这些问题,背后连的是同一条主线:

Kubernetes 不只是“把容器跑起来”,它还要决定一个副本什么时候算健康、什么时候能接流量、什么时候该被替换、什么时候不能被随意驱逐。

这就是第十课的核心。


先建立一个最重要的心智模型:健康,不止一种

很多初学者把“健康”理解成一个布尔值。

但 Kubernetes 里至少有三层完全不同的健康语义:

  1. 进程是否还活着
  2. 应用是否已经启动完成
  3. 当前是否应该接收流量

这三件事看起来很像,但工程语义完全不同。


三种探针各自在回答什么问题

startupProbe

回答的是:

这个应用是不是还处在“启动期”,我现在先别用 liveness 去误杀它。

它解决的是:

  • 启动慢
  • 初始化时间长
  • 数据恢复耗时
  • JVM/数据库/大型服务冷启动慢

如果没有它,liveness 可能会在应用尚未启动完时就开始检查。

结果就是:

  • 应用还没准备好
  • kubelet 以为它挂了
  • 直接重启
  • 它又重新启动
  • 再次被误杀

这就形成:

  • CrashLoopBackOff

readinessProbe

回答的是:

这个 Pod 现在能不能接流量。

它不负责重启容器。

它的主要作用是:

  • 把不合适接流量的 Pod 从 Service Endpoints 里摘掉

所以 readiness 失败的典型表现是:

  • Pod 还活着
  • 容器不一定重启
  • 但 Service 不再转发请求给它

livenessProbe

回答的是:

这个进程是不是已经坏到需要 kubelet 强行重启它。

它的后果是最强的:

  • 检查失败到阈值
  • kubelet 杀掉容器
  • 再拉起

所以 liveness 不是“普通健康检查”。

它是:

自动重启开关

如果配得太激进,非常容易把慢启动、瞬时抖动、依赖短暂超时都误判成“进程死亡”。


你必须牢牢记住的区别

startupProbe 失败,不等于马上重启

它也是失败计数。

只有在:

  • failureThreshold * periodSeconds

这段窗口都耗尽之后,才会判定启动失败并重启。

在它还没成功前:

  • liveness 和 readiness 会被屏蔽

这就是它最重要的价值。

readinessProbe 失败,不等于容器坏了

它只是在告诉流量层:

  • 现在不要把请求发给我

所以你看到:

  • 0/1 Running

并不一定说明容器挂了。

很可能只是:

  • Pod 还在运行
  • 但 Endpoints 已摘流

livenessProbe 失败,才是 kubelet 要动刀

这时容器会被重启。

所以:

  • readiness 解决“流量是否进来”
  • liveness 解决“容器是否重启”

它们不能混用。


我们这次实验拿到的第一组关键证据

实验目录:

  • manifests/10-reliability

其中第一组对照对象是:

  • probe-bad
  • probe-good

这两个 Deployment 的应用逻辑几乎一样:

  • 都要先 sleep 25
  • 然后才真正监听 8080

区别只有一个:

  • probe-good 配了 startupProbe
  • probe-bad 没配

实际结果

我观察到:

  • probe-bad 最终进入 CrashLoopBackOff
  • probe-good 最终变成 1/1 Running

事件里非常清楚:

  • probe-bad 的 liveness 在应用监听前就开始检查
  • 因为连接被拒绝,kubelet 判定失败并重启容器
  • probe-good 虽然 startup probe 也失败过,但没有被误杀
  • 等应用真正启动起来后,它就通过 startupProbe,再进入正常状态

这说明什么

说明 startupProbe 的本质不是:

  • 多做一个探针

而是:

  • 在启动窗口期内,暂时接管健康判断,避免 liveness 误杀慢启动应用

这是一个非常重要的设计点。


真实集群里,谁在用这些机制

这套集群里已经有非常典型的生产样本。

1. kube-system/coredns

我确认到它的关键配置是:

  • strategy.type = RollingUpdate
  • maxSurge = 25%
  • maxUnavailable = 1
  • readinessProbe.path = /ready
  • livenessProbe.path = /health
  • terminationGracePeriodSeconds = 30

这很符合基础设施服务的需求:

  • 先确认 DNS 副本 ready 再接流量
  • 活着和可服务分开看
  • 升级时不能一下全切掉

2. ingress-nginx/ingress-nginx-controller

它的关键点更值得你背:

  • lifecycle.preStop.exec.command = /wait-shutdown
  • terminationGracePeriodSeconds = 300
  • readinessProbe.path = /healthz
  • livenessProbe.path = /healthz

这说明 ingress 这种流量入口非常强调:

  • 优雅停机
  • 连接排空
  • 给已有请求足够的处理时间

为什么它的优雅终止时间有 300 秒?

因为流量入口不是普通 stateless worker。

它需要处理:

  • 正在进行中的连接
  • 下游转发
  • 可能还没结束的 HTTP 请求

3. harbor/harbor-core

它有一个特别好的 startupProbe 样本:

  • startupProbe.path = /api/v2.0/ping
  • failureThreshold = 360
  • periodSeconds = 10
  • terminationGracePeriodSeconds = 120

这意味着它愿意给启动阶段最多:

  • 360 * 10 = 3600 秒

也就是:

  • 1 小时

这听起来很夸张,但对某些重型系统、初始化复杂的组件来说,这恰恰是“别误杀”的工程体现。

4. gitea/gitea-postgresqlPDB

我核对到它有:

  • maxUnavailable = 1

而且当前状态里:

  • expectedPods = 1
  • currentHealthy = 0
  • disruptionsAllowed = 0

这说明什么?

说明 PDB 是动态计算的。

不是你写了 maxUnavailable: 1,就永远都能驱逐一个。

如果当前健康副本都不够,它一样会阻止驱逐。


Readiness 失败和 Liveness 失败,到底差在哪

这一块你以后必须讲得非常利索。

我们的 web 实验对象

第二组实验对象是:

  • Service/web
  • Deployment/web
  • PodDisruptionBudget/web

它的容器里会持续生成:

  • /www/index.html
  • /www/ready
  • /www/live

而探针分别检查:

  • /ready
  • /live

我还预留了两个控制文件:

  • /tmp/fail-readiness
  • /tmp/fail-liveness

通过它们,我们可以精确地让 Pod 进入不同类型的故障。

Readiness 失败实验

我在一个副本里创建了:

  • /tmp/fail-readiness

随后观察到:

  • 这个 Pod 变成 0/1 Running
  • restartCount 仍然是 0
  • Service/web 的 Endpoints 里,这个 Pod 的地址消失了

然后我把这个控制文件删掉。

很快看到:

  • Pod 恢复 1/1
  • Endpoints 又重新包含了它

这一步的本质

说明 readiness 的职责是:

  • 控制流量接入

不是:

  • 重启容器

这是排障时非常关键的分界线。

如果你看到:

  • Running
  • READY=0/1

第一反应应该是:

  • 它可能被摘流了

而不是立刻说:

  • 应用挂了

Liveness 失败实验

我又对另一个 Pod 创建了:

  • /tmp/fail-liveness

随后事件里出现:

  • Liveness probe failed
  • Container web failed liveness probe, will be restarted

再过几秒看 Pod:

  • 还是同一个 Pod 名
  • RESTARTS = 1

这一步的本质

说明 liveness 的语义是:

  • 进程已经坏到需要 kubelet 重启

注意:

  • Pod 名不一定变
  • 容器 ID 会变
  • restartCount 会增加

这和 Deployment 创建新 Pod,不是一回事。


优雅终止不是“慢一点删 Pod”,而是一整条退出链路

很多人把优雅终止理解成:

Kubernetes 杀 Pod 前会等一会儿。

这太浅了。

真正的退出链路应该是:

控制器决定删除旧 Pod
        |
        v
Pod 开始终止流程
        |
        v
preStop 执行
        |
        v
readiness 变坏 / 端点摘除
        |
        v
Service 不再给它导流
        |
        v
容器处理剩余请求
        |
        v
到 grace period 结束后真正退出

在我们的实验里,web 容器配置了:

  • preStop 先删 /www/ready
  • sleep 20
  • terminationGracePeriodSeconds = 30

这意味着什么?

意味着它在退出前会先主动变成“不接流量”,然后留出 20 秒做排空。


滚动更新到底是怎么保证可用性的

Deployment 的滚动更新,不是“杀一个旧 Pod,再起一个新 Pod”这么简单。

它真正的控制旋钮主要有:

  • maxSurge
  • maxUnavailable
  • minReadySeconds
  • progressDeadlineSeconds

maxSurge

表示:

更新期间,最多允许额外多出来多少个新 Pod。

我们在实验里配置的是:

  • maxSurge = 1

也就是:

  • 目标副本 3 个
  • 更新过程中最多先起到 4 个

maxUnavailable

表示:

更新期间,最多允许多少个副本不可用。

我们配置的是:

  • maxUnavailable = 1

这意味着至少要保住:

  • 2 个可用副本

minReadySeconds

表示:

新 Pod 不是一 Ready 就立刻算稳定,而是至少要连续 Ready 一段时间。

这能避免:

  • 刚 ready 一下就抖掉
  • 控制器过早缩旧副本

我们在滚动更新实验里看到了什么

我先让 web 运行:

  • APP_VERSION = v1

随后把 Deployment 更新成:

  • APP_VERSION = v2

同时从另一个 Pod 持续访问:

  • http://web.reliability-lab.svc.cluster.local:8080

实际现象

请求序列里先看到:

  • 多次返回 version=v1

接着开始出现:

  • version=v2

而且中间没有出现:

  • FAIL

也就是说,服务在更新窗口里经历了:

  • 旧版和新版同时存在
  • Service 在后端副本间切换
  • 但请求没有中断

控制器侧证据

更新完成后,我看到:

  • rollout history 里有 revision 1revision 2
  • 旧 ReplicaSet web-6689b6645b 缩到 0
  • 新 ReplicaSet web-866ccc4db7 变成 3

更有意思的是,在 rollout status 已完成时,我仍然看到:

  • 旧 Pod 还处于 Terminating

而这时 Endpoints/web 已经只包含新版 Pod 地址。

这说明什么

说明“发布完成”不是指:

  • 旧进程已经物理退出

而是指:

  • 新副本已经足够稳定
  • 旧副本已经被摘流
  • 业务入口已经可以完全由新副本承接

这正是:

  • preStop
  • readiness
  • rollingUpdate

三者协同的结果。


PDB 到底保护的是什么

PDBPodDisruptionBudget

它保护的不是:

  • 所有类型的故障

它保护的是:

  • voluntary disruption

也就是:

  • 节点维护
  • kubectl drain
  • Eviction API
  • 集群管理员主动进行的可控驱逐

它不保护:

  • 机器突然宕机
  • 内核 panic
  • 节点断电
  • 应用自己崩溃
  • kubelet 直接因为 liveness 重启容器
  • 你手工 kubectl delete pod --force

这点必须非常清楚。


我们的 PDB 实验到底证明了什么

实验里我给 web 配了:

  • minAvailable: 2

在 3 副本全健康时,kubectl get pdb 显示:

  • ALLOWED DISRUPTIONS = 1

这说明:

  • 现在最多允许一个 voluntary disruption

第一次 Eviction

我对第一个 Pod 调用了 Eviction API。

返回结果是:

  • Status Success

随后 PDB 变成:

  • ALLOWED DISRUPTIONS = 0

同时控制器拉起了一个新 Pod 顶上。

第二次 Eviction

在 replacement 还没把预算恢复前,我立即对第二个 Pod 再次调用 Eviction API。

返回结果是:

  • Error from server (TooManyRequests): Cannot evict pod as it would violate the pod's disruption budget.

这说明什么

说明 PDB 真正干的事是:

  • 在控制面层面阻止“你主动把太多可用副本赶走”

也就是说它不是提醒。

而是真拦。


你还要分清 kubectl delete podevict 的本质区别

这是很多人学了一年都没真正分清的地方。

kubectl delete pod

更像是:

  • 直接删对象

通常不会走 PDB 的保护语义。

Eviction API

更像是:

  • 我请求系统安全地把这个 Pod 迁走

它会考虑:

  • PDB 是否允许

所以:

  • 节点排空 drain
  • 集群维护驱逐

本质上更接近:

  • eviction

不是:

  • 直接 delete

为什么很多系统会把探针、preStop、PDB 一起配

因为它们分别控制的是不同层次:

探针

负责:

  • 这个副本什么时候可用
  • 这个副本什么时候该被重启

preStop + terminationGracePeriodSeconds

负责:

  • 这个副本退出时如何优雅下线

Deployment rollingUpdate

负责:

  • 发布时新旧副本如何交替

PDB

负责:

  • 维护或驱逐时,最少保住多少个副本

它们叠加在一起,才构成真正的:

  • 稳定发布
  • 安全维护
  • 有边界的自愈

单独只配一个,往往都不够。


你以后设计探针时最常犯的错误

错误 1:把 readiness 和 liveness 指向同一个“脆弱”依赖

例如:

  • 一旦数据库慢了
  • readiness 失败可以理解
  • 但如果 liveness 也跟着失败

就会把暂时外部依赖问题放大成:

  • 整个 Pod 被不断重启

错误 2:没有 startupProbe 却把 liveness 设得很激进

这是最常见的慢启动误杀。

错误 3:preStop 只 sleep,不先摘流

如果你只是:

  • sleep 20

但没先让 readiness 失败,那么这 20 秒里流量还可能继续打到它。

错误 4:把 PDB 当成高可用本身

PDB 只能拦“主动驱逐”。

它不能替你做:

  • 多副本架构
  • 跨节点部署
  • 应用复制
  • 备份恢复

作为 CTO / 架构师,你要能讲出来的完整链路

以后团队里有人问你:

我们怎样才能上线不抖、维护不停、坏副本自动恢复?

你应该能把下面这条链一口气讲出来:

  1. readinessProbe 决定什么时候接流量。
  2. livenessProbe 只处理“进程确实坏了”的场景。
  3. 对慢启动应用加 startupProbe,避免 liveness 误杀。
  4. preStop + terminationGracePeriodSeconds 给副本排空时间。
  5. Deployment rollingUpdate 控制新旧版本替换节奏。
  6. PDB 保证节点维护和驱逐时不会把可用副本一下赶没。

如果这 6 条你能讲清楚,而且能拿真实样本和真实实验说明白,你就已经不只是会“部署应用”了。

你是在开始掌握:

  • 平台稳定性设计
  • 发布可靠性设计
  • 控制面与流量层协作机制

本课你必须真正背下来的结论

  1. startupProbe 的核心作用是保护启动窗口期,不让 liveness 误杀慢启动应用。
  2. readinessProbe 失败会摘流,不一定重启容器。
  3. livenessProbe 失败才会触发 kubelet 重启容器。
  4. preStop 的关键不是“多等几秒”,而是先摘流再退出。
  5. 滚动更新的可用性来自 maxSurge + maxUnavailable + readiness + preStop 的组合。
  6. PDB 只保护 voluntary disruption,不保护机器宕机或直接 delete。
  7. Evictiondelete pod 不是一回事,前者才真正会受 PDB 约束。

给你的专家化训练题

  1. 为什么 startupProbe 更适合解决慢启动问题,而不是简单增大 liveness 的 initialDelaySeconds
  2. 为什么 readiness 失败时 Pod 还可以继续 Running?
  3. 为什么滚动更新完成时,旧 Pod 仍然可能处于 Terminating
  4. 为什么 PDB minAvailable: 2 在 3 副本时允许一次驱逐,但不允许立即第二次驱逐?
  5. 为什么 ingress 这种流量入口往往需要更长的 terminationGracePeriodSeconds

你把这五题讲透,这一课才算真正学会。