Repository Reading Site
本轮操作记录:探针、滚动更新、优雅终止与 PDB 实验
这一轮我不是只想讲几个 YAML 字段。 我要把下面这些机制做成可观察事实: 1. `startupProbe` 为什么能救慢启动应用 2. `readinessProbe` 和 `livenessProbe` 到底差在哪 3. `preStop + terminationGracePeriodSeconds` 怎样参与滚动更新 4. `Deployment
本轮操作记录:探针、滚动更新、优雅终止与 PDB 实验
本轮目标
这一轮我不是只想讲几个 YAML 字段。
我要把下面这些机制做成可观察事实:
startupProbe为什么能救慢启动应用readinessProbe和livenessProbe到底差在哪preStop + terminationGracePeriodSeconds怎样参与滚动更新Deployment为什么能在升级时保持业务连续PDB到底拦什么,不拦什么
Step 1:先看真实集群里哪些组件已经在用这些机制
实际命令
KUBECONFIG=~/.kube/config-k8s-lab kubectl get pdb -A
KUBECONFIG=~/.kube/config-k8s-lab \
kubectl get deploy,statefulset -A -o wide | sed -n '1,120p'
KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n kube-system get deploy coredns -o yaml | sed -n '1,260p'
KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n ingress-nginx get deploy ingress-nginx-controller -o yaml | sed -n '1,320p'
KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n harbor get deploy harbor-core -o yaml | sed -n '1,260p'
KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n gitea get pdb gitea-postgresql -o yaml | sed -n '1,220p'
为什么先看真实组件
因为探针、滚动更新、优雅终止、PDB 这些东西,最怕学成“概念拼盘”。
我先确认这套集群里真实有哪些生产样本:
corednsingress-nginx-controllerharbor-coregitea-postgresql
这样后面我的实验就不是纸上谈兵。
我确认到的关键结果
coredns
RollingUpdatemaxSurge = 25%maxUnavailable = 1readinessProbe = /readylivenessProbe = /healthterminationGracePeriodSeconds = 30
ingress-nginx-controller
RollingUpdatepreStop = /wait-shutdownterminationGracePeriodSeconds = 300readinessProbe = /healthzlivenessProbe = /healthz
harbor-core
startupProbe = /api/v2.0/pingfailureThreshold = 360periodSeconds = 10terminationGracePeriodSeconds = 120
gitea-postgresql 的 PDB
maxUnavailable = 1- 当前
currentHealthy = 0 - 当前
disruptionsAllowed = 0 expectedPods = 1
原理解释
这一步说明两件事:
- 这些不是“可选修饰项”,而是生产组件的核心稳定性配置
PDB、探针、优雅终止都不是死参数,它们会随着当前健康状态动态生效
Step 2:设计本轮实验对象
我创建了哪些文件
路径:
manifests/10-reliability/00-namespace-reliability-lab.yamlmanifests/10-reliability/10-probe-bad-deployment.yamlmanifests/10-reliability/11-probe-good-deployment.yamlmanifests/10-reliability/20-web-service.yamlmanifests/10-reliability/21-web-deployment-v1.yamlmanifests/10-reliability/22-web-deployment-v2.yamlmanifests/10-reliability/30-web-pdb.yaml
为什么分成三组
这一轮我故意把实验拆成三条线。
线 1:探针对照组
probe-badprobe-good
用来证明:
- 没有
startupProbe时,慢启动会被 liveness 误杀 - 有
startupProbe后,慢启动可以安全通过
线 2:可控健康状态的 Web 服务
Service/webDeployment/web
用来证明:
- readiness 失败只摘流
- liveness 失败会重启
- 滚动更新怎样替换版本
- preStop 怎样参与退出过程
线 3:PDB
PodDisruptionBudget/web
用来证明:
- 驱逐预算是怎么动态变化的
- 为什么一次驱逐能过,第二次立刻不让过
为什么我的 Web 镜像还是用 busybox
因为我要把重点放在:
- Kubernetes 机制本身
而不是让一个复杂业务镜像掩盖本质。
我用 busybox httpd + 文件存在性 来模拟:
/ready/livepreStop摘流
这样更容易把现象和原理一一对应起来。
Step 3:先做本地清单校验,避免把错误 YAML 打进集群
实际命令
find manifests/10-reliability -maxdepth 1 -type f | sort
KUBECONFIG=~/.kube/config-k8s-lab kubectl apply --dry-run=client -f manifests/10-reliability
为什么要先 dry-run
因为我要先确认:
- 语法没问题
- 资源种类和字段能被 API 识别
这和直接 apply 的区别在于:
- dry-run 只做客户端对象校验,不真的落集群
我看到的结果
所有对象都通过了:
- namespace
- deployment
- service
- pdb
原理解释
这一步是非常值得养成的习惯:
- 改一批 YAML 前先校验
- 比 apply 之后再回头抓语法错,更省成本
Step 4:先应用探针对照组
实际命令
KUBECONFIG=~/.kube/config-k8s-lab kubectl apply \
-f manifests/10-reliability/00-namespace-reliability-lab.yaml \
-f manifests/10-reliability/10-probe-bad-deployment.yaml \
-f manifests/10-reliability/11-probe-good-deployment.yaml
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n reliability-lab get deploy,pod -o wide
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n reliability-lab get events --sort-by=.lastTimestamp | tail -n 60
为什么先只上探针实验
因为我想先把:
- 启动期误杀
- 启动期保护
这件事单独看清楚。
如果一开始就把滚动更新和 PDB 也掺进去,初学者很容易看乱。
我最先看到的现象
刚创建出来时:
probe-bad和probe-good都是0/1 Running
但事件很快分化:
probe-bad出现Container app failed liveness probe, will be restartedprobe-good出现Startup probe failed
原理解释
这里要特别注意:
- 两者都失败了“某个检查”
- 但后果完全不同
probe-bad 没有 startupProbe,所以 liveness 直接介入。
probe-good 有 startupProbe,所以启动期内先由 startupProbe 接管健康判断,liveness 暂时不会误杀。
Step 5:等启动窗口过去,验证两者最终走向
实际命令
sleep 28
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n reliability-lab get deploy,pod -o wide
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n reliability-lab get events --sort-by=.lastTimestamp | tail -n 80
我看到的结果
最终状态是:
probe-bad:0/1,随后进入CrashLoopBackOffprobe-good:1/1 Running
后面再核对当前现场时,probe-bad 已经是:
CrashLoopBackOffRESTARTS = 6
原理解释
这一步把 startupProbe 的真正价值坐实了:
- 同样是慢启动 25 秒
- 没
startupProbe的 Pod 被 liveness 提前误判并持续重启 - 有
startupProbe的 Pod 可以平稳跨过启动窗口
如果你以后遇到:
- 应用明明只是冷启动慢
- 却被 kubelet 持续重启
第一反应就该是:
- 是不是缺
startupProbe
而不是先去怪镜像、怪调度、怪 CNI。
Step 6:部署稳定发布实验对象
实际命令
KUBECONFIG=~/.kube/config-k8s-lab kubectl apply \
-f manifests/10-reliability/20-web-service.yaml \
-f manifests/10-reliability/21-web-deployment-v1.yaml \
-f manifests/10-reliability/30-web-pdb.yaml
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n reliability-lab rollout status deployment/web --timeout=180s
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n reliability-lab get deploy,pod,svc,endpoints,pdb,rs -o wide
为什么这组资源要同时创建
因为这里的关键不是单个 Pod。
我要看的是完整协作链:
- Pod 的健康状态
- Service 的流量转发
- Deployment 的副本编排
- PDB 的驱逐预算
我看到的结果
创建成功后:
deployment/web = 3/3service/web有 3 个后端 endpointpdb/web显示:minAvailable = 2allowed disruptions = 1
原理解释
这说明现在系统状态是:
- 3 个副本都健康
- 至少要保住 2 个
- 所以当前最多允许 1 次 voluntary disruption
Step 7:故意制造 readiness 失败,验证“摘流但不重启”
实际命令
POD=$(KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n reliability-lab get pod -l app=web -o jsonpath='{.items[0].metadata.name}')
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n reliability-lab exec "$POD" -- touch /tmp/fail-readiness
sleep 6
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n reliability-lab get pod "$POD" -o wide
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n reliability-lab get endpoints web
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n reliability-lab exec "$POD" -- rm -f /tmp/fail-readiness
sleep 6
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n reliability-lab get pod "$POD" -o wide
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n reliability-lab get endpoints web
命令解释
jsonpath
用来从 kubectl get pod 的返回里直接拿出一个 Pod 名。
kubectl exec ... touch /tmp/fail-readiness
这是我预先在容器逻辑里埋的故障开关。
当这个文件存在时,容器脚本会删掉:
/www/ready
导致 readiness probe 返回 404。
我看到的结果
故障注入后:
- Pod 变成
0/1 Running RESTARTS = 0- Endpoints 从 3 个变成 2 个
恢复后:
- Pod 回到
1/1 - Endpoints 又恢复成 3 个
原理解释
这一步说明得非常直白:
- readiness 失败只是摘流
- 并不触发容器重启
这也是为什么很多线上问题表现成:
- Pod 还在跑
- 但请求打不过去
你以后看到这种场景,第一时间就要去看:
READYendpointsreadinessProbe
Step 8:故意制造 liveness 失败,验证“kubelet 重启容器”
实际命令
POD=$(KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n reliability-lab get pod -l app=web -o jsonpath='{.items[1].metadata.name}')
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n reliability-lab exec "$POD" -- touch /tmp/fail-liveness
sleep 12
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n reliability-lab get pod "$POD" -o wide
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n reliability-lab describe pod "$POD" | sed -n '1,220p'
我看到的关键事件
describe 里出现:
Liveness probe failedContainer web failed liveness probe, will be restarted
之后再看 Pod:
RESTARTS = 1
为什么我还补了一次二次确认
因为第一次 get pod 的快照和 describe 的事件之间,存在真实的时间差。
当时我先看到:
- 事件已经说要重启
但那一瞬间的 get pod 还没来得及反映出新的 restartCount。
我又补了一次:
sleep 8
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n reliability-lab get pod <pod> -o wide
最终确认:
RESTARTS = 1
原理解释
这一步非常像真实生产现场。
你不能把:
- 事件流
- 状态快照
当成同一个时间点。
kubectl describe 里的事件经常比你那一瞬间的 get pod 更早暴露出变化趋势。
Step 9:开始滚动更新前,先通过 Service 连续访问旧版本
实际命令
KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n reliability-lab exec deploy/probe-good -- \
sh -c 'for i in $(seq 1 18); do echo "--- request-$i ---"; wget -qO- http://web.reliability-lab.svc.cluster.local:8080 || echo FAIL; echo; sleep 2; done'
为什么要从另一个 Pod 发请求
因为我想验证的是:
- Service 作为集群内入口
- 在滚动更新过程中是否持续可用
不是只看 Deployment 状态栏。
原理解释
这一招非常实战:
- 状态面告诉你“控制器认为成功”
- 数据面告诉你“用户请求是不是真的没断”
两者都要看。
Step 10:把 web 从 v1 升到 v2,观察滚动更新
实际命令
KUBECONFIG=~/.kube/config-k8s-lab kubectl apply -f manifests/10-reliability/22-web-deployment-v2.yaml
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n reliability-lab rollout status deployment/web --timeout=240s
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n reliability-lab rollout history deployment/web
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n reliability-lab get rs,pod -l app=web -o wide
为什么这里用第二份 manifest,而不是临时改 env
因为我希望把:
- v1
- v2
都固化成可复现实验材料。
以后你回头复盘、重跑,都能一键复现。
我看到的关键结果
请求流里先是:
version=v1
然后逐渐变成:
version=v2
中间没有出现:
FAIL
控制器层面看到:
revision 1revision 2
ReplicaSet 层面看到:
- 旧 RS
web-6689b6645b - 新 RS
web-866ccc4db7
原理解释
这一步证明:
- Service 会在滚动过程中同时接到旧版和新版 Pod
- 只要 readiness 没问题,用户视角就可以保持连续服务
这比“Deployment 3/3 Ready”更有说服力。
Step 11:专门观察 preStop 和 endpoints 的配合
实际命令
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n reliability-lab get endpoints web
sleep 22
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n reliability-lab get pod -l app=web -o wide
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n reliability-lab get endpoints web
我看到的现象
在 rollout status 已经完成时,我一度还能看到:
- 旧 Pod 还处在
Terminating
但与此同时:
endpoints/web已经只包含新版本 Pod 的地址
等 20 多秒后:
- 旧 Pod 完全消失
- endpoints 仍然只有新版本
原理解释
这非常漂亮地说明:
- preStop 期间旧 Pod 还活着
- 但它已经被摘流
也就是说:
- “Terminating” 不等于还在接业务流量
如果团队里有人看到 Pod 还没退出就慌了,这个实验就是最好的解释材料。
Step 12:开始做 PDB 驱逐实验
第一次尝试,我故意踩到了一个命令坑
我一开始尝试了这种写法:
kubectl create -f eviction.yaml
或者把 Eviction 对象直接从标准输入喂给 kubectl create -f -。
我得到的报错
no matches for kind "Eviction" in version "policy/v1"
为什么会这样
因为 Eviction 不是普通顶级资源。
它是:
- Pod 的一个子资源
也就是说你不能把它当普通对象那样走 create -f。
原理解释
这反而是一个很重要的知识点:
PDB真正生效的位置是在 eviction 流程里- 所以我们应该直接调用 Eviction API
Step 13:改用 Eviction API,验证 PDB 真的会拦截
实际命令
set -- $(KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n reliability-lab get pod -l app=web -o jsonpath='{range .items[*]}{.metadata.name}{" "}{end}')
P1=$1
P2=$2
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n reliability-lab get pdb web
cat <<EOF | KUBECONFIG=~/.kube/config-k8s-lab kubectl create --raw "/api/v1/namespaces/reliability-lab/pods/${P1}/eviction" -f -
{"apiVersion":"policy/v1","kind":"Eviction","metadata":{"name":"${P1}","namespace":"reliability-lab"}}
EOF
sleep 2
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n reliability-lab get pdb web
cat <<EOF | KUBECONFIG=~/.kube/config-k8s-lab kubectl create --raw "/api/v1/namespaces/reliability-lab/pods/${P2}/eviction" -f -
{"apiVersion":"policy/v1","kind":"Eviction","metadata":{"name":"${P2}","namespace":"reliability-lab"}}
EOF
sleep 2
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n reliability-lab get pdb web
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n reliability-lab get pod -l app=web -o wide
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n reliability-lab get endpoints web
命令解释
set -- $(...)
把当前 app=web 的 Pod 名列表直接塞进 shell 位置参数里。
这里我这么写,是为了兼容当前 zsh 环境,避免前面那种数组下标细节干扰实验。
kubectl create --raw
直接调用 Kubernetes API 路径:
/api/v1/namespaces/<ns>/pods/<pod>/eviction
这才是 Eviction 的正确入口。
我看到的结果
第一次驱逐:
- 返回
Status Success
随后 PDB 变成:
ALLOWED DISRUPTIONS = 0
第二次驱逐:
- 返回
Error from server (TooManyRequests): Cannot evict pod as it would violate the pod's disruption budget.
与此同时,Deployment 迅速创建了 replacement Pod:
- 新 Pod
web-866ccc4db7-sjvnj被拉起
Endpoints 也一直保持 3 个后端。
原理解释
这一步是全课最硬的一条证据链之一。
因为它证明:
- PDB 不是文档概念
- 它真的在控制面层面阻止第二次驱逐
而且触发条件是:
- 第一次驱逐消耗了 disruption budget
- replacement 还没让预算恢复
所以第二次直接被拒绝。
Step 14:再等一会儿,验证预算会恢复
实际命令
sleep 18
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n reliability-lab get pdb web
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n reliability-lab get pod -l app=web -o wide
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n reliability-lab get events --sort-by=.lastTimestamp | tail -n 60
我看到的结果
replacement Pod ready 后,PDB 又恢复成:
ALLOWED DISRUPTIONS = 1
原理解释
这说明 PDB 预算不是一次性消耗品。
它是:
- 根据当前健康副本数动态重新计算
这也是为什么我们前面说:
PDB是实时约束- 不是静态注释
Step 15:本轮最重要的排障和设计结论
如果 Pod 一直起不来
先问自己:
- 是不是慢启动被 liveness 误杀了
- 是否应该有
startupProbe
如果 Pod 还在 Running 但服务打不过去
先看:
READYendpointsreadinessProbe
如果滚动更新时服务抖动
重点检查:
maxUnavailablereadinessProbepreStopterminationGracePeriodSeconds
如果节点维护时 drain 卡住
重点检查:
PDB- 当前 healthy replica 数
disruptionsAllowed
本轮结论
这一轮我已经把第十课里最关键的 5 条机制都做成了现场证据:
probe-bad证明没有startupProbe时,慢启动会被 liveness 误杀。probe-good证明startupProbe能保护启动窗口期。web的 readiness 实验证明“摘流”和“重启”是两回事。web的 v1 -> v2 更新证明滚动更新依赖 readiness / preStop / strategy 的协作。PDB的 Eviction 实验证明它会在控制面层面真拦第二次驱逐。
这意味着你现在已经不只是会写 Deployment。
你开始真正掌握:
- Kubernetes 的健康语义
- 发布可用性语义
- 优雅退出语义
- 集群维护保护语义
这几条,是从“会用 k8s”走向“能带生产平台”的核心台阶。