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

Repository Reading Site

本轮操作记录:探针、滚动更新、优雅终止与 PDB 实验

这一轮我不是只想讲几个 YAML 字段。 我要把下面这些机制做成可观察事实: 1. `startupProbe` 为什么能救慢启动应用 2. `readinessProbe` 和 `livenessProbe` 到底差在哪 3. `preStop + terminationGracePeriodSeconds` 怎样参与滚动更新 4. `Deployment

Markdown10-操作记录-探针-滚动更新-优雅终止与PDB实验.md2026年4月10日 03:27

本轮操作记录:探针、滚动更新、优雅终止与 PDB 实验

本轮目标

这一轮我不是只想讲几个 YAML 字段。

我要把下面这些机制做成可观察事实:

  1. startupProbe 为什么能救慢启动应用
  2. readinessProbelivenessProbe 到底差在哪
  3. preStop + terminationGracePeriodSeconds 怎样参与滚动更新
  4. Deployment 为什么能在升级时保持业务连续
  5. 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 这些东西,最怕学成“概念拼盘”。

我先确认这套集群里真实有哪些生产样本:

  • coredns
  • ingress-nginx-controller
  • harbor-core
  • gitea-postgresql

这样后面我的实验就不是纸上谈兵。

我确认到的关键结果

coredns

  • RollingUpdate
  • maxSurge = 25%
  • maxUnavailable = 1
  • readinessProbe = /ready
  • livenessProbe = /health
  • terminationGracePeriodSeconds = 30

ingress-nginx-controller

  • RollingUpdate
  • preStop = /wait-shutdown
  • terminationGracePeriodSeconds = 300
  • readinessProbe = /healthz
  • livenessProbe = /healthz

harbor-core

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

gitea-postgresql 的 PDB

  • maxUnavailable = 1
  • 当前 currentHealthy = 0
  • 当前 disruptionsAllowed = 0
  • expectedPods = 1

原理解释

这一步说明两件事:

  1. 这些不是“可选修饰项”,而是生产组件的核心稳定性配置
  2. PDB、探针、优雅终止都不是死参数,它们会随着当前健康状态动态生效

Step 2:设计本轮实验对象

我创建了哪些文件

路径:

  • manifests/10-reliability/00-namespace-reliability-lab.yaml
  • manifests/10-reliability/10-probe-bad-deployment.yaml
  • manifests/10-reliability/11-probe-good-deployment.yaml
  • manifests/10-reliability/20-web-service.yaml
  • manifests/10-reliability/21-web-deployment-v1.yaml
  • manifests/10-reliability/22-web-deployment-v2.yaml
  • manifests/10-reliability/30-web-pdb.yaml

为什么分成三组

这一轮我故意把实验拆成三条线。

线 1:探针对照组

  • probe-bad
  • probe-good

用来证明:

  • 没有 startupProbe 时,慢启动会被 liveness 误杀
  • startupProbe 后,慢启动可以安全通过

线 2:可控健康状态的 Web 服务

  • Service/web
  • Deployment/web

用来证明:

  • readiness 失败只摘流
  • liveness 失败会重启
  • 滚动更新怎样替换版本
  • preStop 怎样参与退出过程

线 3:PDB

  • PodDisruptionBudget/web

用来证明:

  • 驱逐预算是怎么动态变化的
  • 为什么一次驱逐能过,第二次立刻不让过

为什么我的 Web 镜像还是用 busybox

因为我要把重点放在:

  • Kubernetes 机制本身

而不是让一个复杂业务镜像掩盖本质。

我用 busybox httpd + 文件存在性 来模拟:

  • /ready
  • /live
  • preStop 摘流

这样更容易把现象和原理一一对应起来。


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-badprobe-good 都是 0/1 Running

但事件很快分化:

  • probe-bad 出现 Container app failed liveness probe, will be restarted
  • probe-good 出现 Startup probe failed

原理解释

这里要特别注意:

  • 两者都失败了“某个检查”
  • 但后果完全不同

probe-bad 没有 startupProbe,所以 liveness 直接介入。

probe-goodstartupProbe,所以启动期内先由 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-bad0/1,随后进入 CrashLoopBackOff
  • probe-good1/1 Running

后面再核对当前现场时,probe-bad 已经是:

  • CrashLoopBackOff
  • RESTARTS = 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/3
  • service/web 有 3 个后端 endpoint
  • pdb/web 显示:
    • minAvailable = 2
    • allowed 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 还在跑
  • 但请求打不过去

你以后看到这种场景,第一时间就要去看:

  • READY
  • endpoints
  • readinessProbe

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 failed
  • Container 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 1
  • revision 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 但服务打不过去

先看:

  • READY
  • endpoints
  • readinessProbe

如果滚动更新时服务抖动

重点检查:

  • maxUnavailable
  • readinessProbe
  • preStop
  • terminationGracePeriodSeconds

如果节点维护时 drain 卡住

重点检查:

  • PDB
  • 当前 healthy replica 数
  • disruptionsAllowed

本轮结论

这一轮我已经把第十课里最关键的 5 条机制都做成了现场证据:

  1. probe-bad 证明没有 startupProbe 时,慢启动会被 liveness 误杀。
  2. probe-good 证明 startupProbe 能保护启动窗口期。
  3. web 的 readiness 实验证明“摘流”和“重启”是两回事。
  4. web 的 v1 -> v2 更新证明滚动更新依赖 readiness / preStop / strategy 的协作。
  5. PDB 的 Eviction 实验证明它会在控制面层面真拦第二次驱逐。

这意味着你现在已经不只是会写 Deployment。

你开始真正掌握:

  • Kubernetes 的健康语义
  • 发布可用性语义
  • 优雅退出语义
  • 集群维护保护语义

这几条,是从“会用 k8s”走向“能带生产平台”的核心台阶。