Repository Reading Site
第十课:探针、滚动更新、优雅终止与 PDB 原理
前面几课我们已经把 Kubernetes 最关键的几条主链路拆开了: 但这还没有碰到线上最痛的那部分问题: 1. 为什么明明 Pod 已经 Running,业务还是没法接流量 2. 为什么有些 Pod 会不断被 kubelet 重启 3. 为什么发版时明明用了 Deployment,用户还是会抖一下 4. 为什么节点维护时有些 Pod 能赶走,有些赶不走 5
第十课:探针、滚动更新、优雅终止与 PDB 原理
为什么这一课必须接在 StatefulSet 后面
前面几课我们已经把 Kubernetes 最关键的几条主链路拆开了:
apply如何进入控制面- 调度器如何选节点
- 网络如何连通
- 身份认证、授权、准入如何控权
- 配置和 Secret 如何进入 Pod
- 存储如何从 Pod 生命周期里独立出来
- StatefulSet 如何给状态副本提供稳定身份
但这还没有碰到线上最痛的那部分问题:
- 为什么明明 Pod 已经 Running,业务还是没法接流量
- 为什么有些 Pod 会不断被 kubelet 重启
- 为什么发版时明明用了 Deployment,用户还是会抖一下
- 为什么节点维护时有些 Pod 能赶走,有些赶不走
- 为什么有些系统升级得很慢,却恰恰更安全
这些问题,背后连的是同一条主线:
Kubernetes 不只是“把容器跑起来”,它还要决定一个副本什么时候算健康、什么时候能接流量、什么时候该被替换、什么时候不能被随意驱逐。
这就是第十课的核心。
先建立一个最重要的心智模型:健康,不止一种
很多初学者把“健康”理解成一个布尔值。
但 Kubernetes 里至少有三层完全不同的健康语义:
- 进程是否还活着
- 应用是否已经启动完成
- 当前是否应该接收流量
这三件事看起来很像,但工程语义完全不同。
三种探针各自在回答什么问题
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-badprobe-good
这两个 Deployment 的应用逻辑几乎一样:
- 都要先
sleep 25 - 然后才真正监听
8080
区别只有一个:
probe-good配了startupProbeprobe-bad没配
实际结果
我观察到:
probe-bad最终进入CrashLoopBackOffprobe-good最终变成1/1 Running
事件里非常清楚:
probe-bad的 liveness 在应用监听前就开始检查- 因为连接被拒绝,kubelet 判定失败并重启容器
probe-good虽然 startup probe 也失败过,但没有被误杀- 等应用真正启动起来后,它就通过 startupProbe,再进入正常状态
这说明什么
说明 startupProbe 的本质不是:
- 多做一个探针
而是:
- 在启动窗口期内,暂时接管健康判断,避免 liveness 误杀慢启动应用
这是一个非常重要的设计点。
真实集群里,谁在用这些机制
这套集群里已经有非常典型的生产样本。
1. kube-system/coredns
我确认到它的关键配置是:
strategy.type = RollingUpdatemaxSurge = 25%maxUnavailable = 1readinessProbe.path = /readylivenessProbe.path = /healthterminationGracePeriodSeconds = 30
这很符合基础设施服务的需求:
- 先确认 DNS 副本 ready 再接流量
- 活着和可服务分开看
- 升级时不能一下全切掉
2. ingress-nginx/ingress-nginx-controller
它的关键点更值得你背:
lifecycle.preStop.exec.command = /wait-shutdownterminationGracePeriodSeconds = 300readinessProbe.path = /healthzlivenessProbe.path = /healthz
这说明 ingress 这种流量入口非常强调:
- 优雅停机
- 连接排空
- 给已有请求足够的处理时间
为什么它的优雅终止时间有 300 秒?
因为流量入口不是普通 stateless worker。
它需要处理:
- 正在进行中的连接
- 下游转发
- 可能还没结束的 HTTP 请求
3. harbor/harbor-core
它有一个特别好的 startupProbe 样本:
startupProbe.path = /api/v2.0/pingfailureThreshold = 360periodSeconds = 10terminationGracePeriodSeconds = 120
这意味着它愿意给启动阶段最多:
360 * 10 = 3600 秒
也就是:
- 1 小时
这听起来很夸张,但对某些重型系统、初始化复杂的组件来说,这恰恰是“别误杀”的工程体现。
4. gitea/gitea-postgresql 的 PDB
我核对到它有:
maxUnavailable = 1
而且当前状态里:
expectedPods = 1currentHealthy = 0disruptionsAllowed = 0
这说明什么?
说明 PDB 是动态计算的。
不是你写了 maxUnavailable: 1,就永远都能驱逐一个。
如果当前健康副本都不够,它一样会阻止驱逐。
Readiness 失败和 Liveness 失败,到底差在哪
这一块你以后必须讲得非常利索。
我们的 web 实验对象
第二组实验对象是:
Service/webDeployment/webPodDisruptionBudget/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仍然是0Service/web的 Endpoints 里,这个 Pod 的地址消失了
然后我把这个控制文件删掉。
很快看到:
- Pod 恢复
1/1 - Endpoints 又重新包含了它
这一步的本质
说明 readiness 的职责是:
- 控制流量接入
不是:
- 重启容器
这是排障时非常关键的分界线。
如果你看到:
Running- 但
READY=0/1
第一反应应该是:
- 它可能被摘流了
而不是立刻说:
- 应用挂了
Liveness 失败实验
我又对另一个 Pod 创建了:
/tmp/fail-liveness
随后事件里出现:
Liveness probe failedContainer 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”这么简单。
它真正的控制旋钮主要有:
maxSurgemaxUnavailableminReadySecondsprogressDeadlineSeconds
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 1和revision 2- 旧 ReplicaSet
web-6689b6645b缩到 0 - 新 ReplicaSet
web-866ccc4db7变成 3
更有意思的是,在 rollout status 已完成时,我仍然看到:
- 旧 Pod 还处于
Terminating
而这时 Endpoints/web 已经只包含新版 Pod 地址。
这说明什么
说明“发布完成”不是指:
- 旧进程已经物理退出
而是指:
- 新副本已经足够稳定
- 旧副本已经被摘流
- 业务入口已经可以完全由新副本承接
这正是:
preStopreadinessrollingUpdate
三者协同的结果。
PDB 到底保护的是什么
PDB 是 PodDisruptionBudget。
它保护的不是:
- 所有类型的故障
它保护的是:
- 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 pod 和 evict 的本质区别
这是很多人学了一年都没真正分清的地方。
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 / 架构师,你要能讲出来的完整链路
以后团队里有人问你:
我们怎样才能上线不抖、维护不停、坏副本自动恢复?
你应该能把下面这条链一口气讲出来:
- 用
readinessProbe决定什么时候接流量。 - 用
livenessProbe只处理“进程确实坏了”的场景。 - 对慢启动应用加
startupProbe,避免 liveness 误杀。 - 用
preStop + terminationGracePeriodSeconds给副本排空时间。 - 用
Deployment rollingUpdate控制新旧版本替换节奏。 - 用
PDB保证节点维护和驱逐时不会把可用副本一下赶没。
如果这 6 条你能讲清楚,而且能拿真实样本和真实实验说明白,你就已经不只是会“部署应用”了。
你是在开始掌握:
- 平台稳定性设计
- 发布可靠性设计
- 控制面与流量层协作机制
本课你必须真正背下来的结论
startupProbe的核心作用是保护启动窗口期,不让 liveness 误杀慢启动应用。readinessProbe失败会摘流,不一定重启容器。livenessProbe失败才会触发 kubelet 重启容器。preStop的关键不是“多等几秒”,而是先摘流再退出。- 滚动更新的可用性来自
maxSurge + maxUnavailable + readiness + preStop的组合。 PDB只保护 voluntary disruption,不保护机器宕机或直接 delete。Eviction和delete pod不是一回事,前者才真正会受 PDB 约束。
给你的专家化训练题
- 为什么
startupProbe更适合解决慢启动问题,而不是简单增大 liveness 的initialDelaySeconds? - 为什么
readiness失败时 Pod 还可以继续 Running? - 为什么滚动更新完成时,旧 Pod 仍然可能处于
Terminating? - 为什么
PDB minAvailable: 2在 3 副本时允许一次驱逐,但不允许立即第二次驱逐? - 为什么 ingress 这种流量入口往往需要更长的
terminationGracePeriodSeconds?
你把这五题讲透,这一课才算真正学会。