drain 已经跑了三十分钟还没结束
目标
在 drain 被卡住时直接调用驱逐 API,读出是什么在阻止;用集群计算出的数字确认百分比预算的向上取整规则、选择器重叠预算所造成的死锁,以及未就绪 Pod 的驱逐策略;然后在整个集群中找出没有预算的工作负载并整理成清单。
为什么重要
kubectl drain 并不会删除 Pod。它会对每个 Pod 调用驱逐(Eviction)子资源,而这个请求由 PodDisruptionBudget 来评估。预算不够时,返回的不是删除,而是 HTTP 429,drain 会一直重试到成功为止。所以被卡住的 drain 不会以失败告终,而是会永远持续下去。不了解这个结构,就只能看着画面上的 error when evicting pods 而找不到原因。预算不是用一个数字,而是用 currentHealthy、desiredHealthy、disruptionsAllowed 三个数字来说明的,懂得读这些数字,被卡住的原因通常就能缩小到三种之一——预算为 0、选择器重叠,或者未就绪的 Pod 已经把预算打破了。
步骤
- 创建命名空间
ops-pdb,并在/root/ops-disruption/api.yaml中写入 Deploymentapi——副本数 4,标签app: api,镜像nginx:1.27.3,nodeSelector为kubernetes.io/hostname: lab-node-0。应用它,并等待 4 个 Pod 全部在 lab-node-0 上变为 Running。 - 在
/root/ops-disruption/api-pdb.yaml中写入 PodDisruptionBudgetapi-pdb——命名空间ops-pdb,minAvailable: 4,选择器为app: api。应用之后,等控制器计算出状态,再把它保存为一行到/root/ops-disruption/pdb-status.tsv——api-pdb制表符<currentHealthy>制表符<desiredHealthy>制表符<disruptionsAllowed>制表符<expectedPods>。 - 任选一个
apiPod,在/root/ops-disruption/eviction.json中写入 Eviction 对象——apiVersion为policy/v1,kind为Eviction,metadata.name为该 Pod 的名称,metadata.namespace为ops-pdb。用这个文件执行kubectl create --raw /api/v1/namespaces/ops-pdb/pods/<파드이름>/eviction -f /root/ops-disruption/eviction.json(占位符为 Pod 名称),并把错误输出保存到/root/ops-disruption/eviction-denied.txt(必须同时包含标准错误)。 - 执行
kubectl drain lab-node-0 --ignore-daemonsets --delete-emptydir-data --force --timeout=20s,并把输出保存到/root/ops-disruption/drain-blocked.txt(包含标准错误)。drain 会先把节点改为禁止调度的状态,所以命令结束后,务必用kubectl uncordon lab-node-0恢复原状。 - 在
/root/ops-disruption/api-pdb-fixed.yaml中重新写入同名的 PDBapi-pdb,但用maxUnavailable: 1取代minAvailable,并应用它(两个字段不能同时使用,所以新文件中不能有minAvailable)。等到允许中断变为 1 之后,以与第 2 步相同的五列保存到/root/ops-disruption/pdb-after.tsv,并把第 3 步的请求加上?dryRun=All重新发送,把其输出保存到/root/ops-disruption/eviction-allowed.txt。 - 在
/root/ops-disruption/batch.yaml中写入 Deploymentbatch——命名空间ops-pdb,副本数 7,标签app: batch,镜像nginx:1.27.3。并在/root/ops-disruption/batch-pdb.yaml中写入 PDBbatch-pdb——minAvailable: 50%,选择器为app: batch。两者都应用,状态计算出来之后,保存为一行到/root/ops-disruption/rounding.tsv——batch-pdb制表符7制表符50%制表符<desiredHealthy>制表符<disruptionsAllowed>。 - 在
/root/ops-disruption/batch-extra.yaml中再写入一个 PDBbatch-extra——命名空间ops-pdb,maxUnavailable: 1,选择器与batch-pdb完全相同,为app: batch。应用之后,针对某个batchPod 发送加上?dryRun=All的驱逐请求,把其输出保存到/root/ops-disruption/overlap.txt(请求体放在/root/ops-disruption/overlap-eviction.json中)。 - 在
/root/ops-disruption/flaky.yaml中写入 Deploymentflaky(副本数 3,标签app: flaky,镜像nginx:1.27.3),在/root/ops-disruption/flaky-pdb.yaml中写入 PDBflaky-pdb(minAvailable: 3,选择器app: flaky),并应用。然后给某个flakyPod 的status.conditions打补丁,把Ready改为False(kubectl -n ops-pdb patch pod <이름> --subresource=status --type=merge -p ...,占位符为 Pod 名称)。针对该 Pod 发送?dryRun=All驱逐,把拒绝消息保存到/root/ops-disruption/unhealthy-denied.txt,然后把flaky-pdb的spec.unhealthyPodEvictionPolicy改为AlwaysAllow,再发送同样的请求,保存到/root/ops-disruption/unhealthy-allowed.txt。 - 在
/root/ops-disruption/orphan.yaml中写入 Deploymentorphan——命名空间ops-pdb,副本数 3,标签app: orphan,镜像nginx:1.27.3,不创建 PDB。然后创建/root/ops-disruption/pdb-audit.sh——在所有命名空间的 Deployment 中,把spec.replicas在 2 以上、却没有被同一命名空间中任何 PDB 覆盖的,按<네임스페이스>制表符<이름>(占位符依次为命名空间与名称)仅输出到标准输出,并按名称排序。如果 PDB 的spec.selector.matchLabels是 Deployment 的spec.template.metadata.labels的子集,就视为被覆盖。把该输出保存到/root/ops-disruption/pdb-audit.txt。
参考
- 驱逐子资源可以用
kubectl create --raw /api/v1/namespaces/<ns>/pods/<pod>/eviction -f <파일>(占位符为文件)直接调用。 - 加上
?dryRun=All,就可以在不删除 Pod 的情况下只获取判定。 - PDB 的状态全都在
kubectl get pdb <이름> -o json(占位符为 PDB 名称)的.status中。 - 常见错误:保存输出时把
2>&1写在前面,导致标准错误没有进入文件。 - 常见错误:drain 被卡住之后没有 uncordon 节点,导致容量悄悄减少后一直保持这种状态。
- 参考:https://kubernetes.io/docs/concepts/workloads/pods/disruptions/
- 参考:https://kubernetes.io/docs/tasks/run-application/configure-pdb/
集中在一个节点上的工作负载
创建命名空间 ops-pdb,并在 /root/ops-disruption/api.yaml 中写入 Deployment api——副本数 4,标签 app: api,镜像 nginx:1.27.3,nodeSelector 为 kubernetes.io/hostname: lab-node-0。应用它,并等待 4 个 Pod 全部在 lab-node-0 上变为 Running。
drain 是以节点为单位的操作,所以目标节点必须固定才能重现。把它们集中在一个节点上,后面要清空该节点时,是什么在阻止就会变得清楚。新命名空间的 default ServiceAccount 需要过一会儿才会生成。
制造出“允许中断为 0”这个数字
在 /root/ops-disruption/api-pdb.yaml 中写入 PodDisruptionBudget api-pdb——命名空间 ops-pdb,minAvailable: 4,选择器为 app: api。应用之后,等控制器计算出状态,再把它保存为一行到 /root/ops-disruption/pdb-status.tsv——api-pdb 制表符 <currentHealthy> 制表符 <desiredHealthy> 制表符 <disruptionsAllowed> 制表符 <expectedPods>。
刚创建 PDB 时,status 可能是空的。请等中断控制器统计出匹配选择器的 Pod 并填好 status——当 expectedPods 不是 0 时,计算就完成了。既然要求把四个全部保住,请先预测允许中断会是多少。
直接向驱逐 API 询问
任选一个 api Pod,在 /root/ops-disruption/eviction.json 中写入 Eviction 对象——apiVersion 为 policy/v1,kind 为 Eviction,metadata.name 为该 Pod 的名称,metadata.namespace 为 ops-pdb。用这个文件执行 kubectl create --raw /api/v1/namespaces/ops-pdb/pods/<파드이름>/eviction -f /root/ops-disruption/eviction.json(占位符为 Pod 名称),并把错误输出保存到 /root/ops-disruption/eviction-denied.txt(必须同时包含标准错误)。
drain 不是删除 Pod,而是调用驱逐子资源。所以 PDB 一旦阻止,返回的不是删除,而是 HTTP 429——响应正文里写着为什么被阻止。要把输出留在文件里,顺序必须是 > 파일 2>&1,而不是 2>&1 > 파일(占位符均为文件名)。
亲眼看到 drain 无法结束
执行 kubectl drain lab-node-0 --ignore-daemonsets --delete-emptydir-data --force --timeout=20s,并把输出保存到 /root/ops-disruption/drain-blocked.txt(包含标准错误)。drain 会先把节点改为禁止调度的状态,所以命令结束后,务必用 kubectl uncordon lab-node-0 恢复原状。
drain 命令做两件事——在节点上留下禁止调度的标记,并通过驱逐 API 把其上的 Pod 一个个赶出去。前一件事成功而后一件事被卡住,节点就会一直处于被禁止的状态。这就是现场中容量悄悄减少的常见路径。
修改预算后,同样的请求就能通过
在 /root/ops-disruption/api-pdb-fixed.yaml 中重新写入同名的 PDB api-pdb,但用 maxUnavailable: 1 取代 minAvailable,并应用它(两个字段不能同时使用,所以新文件中不能有 minAvailable)。等到允许中断变为 1 之后,以与第 2 步相同的五列保存到 /root/ops-disruption/pdb-after.tsv,并把第 3 步的请求加上 ?dryRun=All 重新发送,把其输出保存到 /root/ops-disruption/eviction-allowed.txt。
驱逐子资源支持 dryRun——不真正删除 Pod,只询问现在能否把这个 Pod 赶出去。这是在生产中确定维护窗口之前进行确认的方法,而在这里是为了不删除后面步骤要用的 Pod。成功时会返回包含 code 201 的 Status。
百分比向哪一侧取整
在 /root/ops-disruption/batch.yaml 中写入 Deployment batch——命名空间 ops-pdb,副本数 7,标签 app: batch,镜像 nginx:1.27.3。并在 /root/ops-disruption/batch-pdb.yaml 中写入 PDB batch-pdb——minAvailable: 50%,选择器为 app: batch。两者都应用,状态计算出来之后,保存为一行到 /root/ops-disruption/rounding.tsv——batch-pdb 制表符 7 制表符 50% 制表符 <desiredHealthy> 制表符 <disruptionsAllowed>。
7 的 50% 是 3.5。请先预测 Kubernetes 会往哪一侧取整,再与集群计算出的数字核对。如果预测错了,请想一想为什么那个方向是更安全的一侧。在 YAML 中,把百分比用引号括起来更稳妥。
选择器重叠的两个预算会造成死锁
在 /root/ops-disruption/batch-extra.yaml 中再写入一个 PDB batch-extra——命名空间 ops-pdb,maxUnavailable: 1,选择器与 batch-pdb 完全相同,为 app: batch。应用之后,针对某个 batch Pod 发送加上 ?dryRun=All 的驱逐请求,把其输出保存到 /root/ops-disruption/overlap.txt(请求体放在 /root/ops-disruption/overlap-eviction.json 中)。
两个预算各自都有余量,请求却被阻止。因为驱逐子资源本身就不支持一个 Pod 匹配两个预算的情况。请原样读一下响应的句子——看过一次这条消息的人,以后会立刻怀疑选择器重叠。
未就绪的 Pod 拖住了 drain
在 /root/ops-disruption/flaky.yaml 中写入 Deployment flaky(副本数 3,标签 app: flaky,镜像 nginx:1.27.3),在 /root/ops-disruption/flaky-pdb.yaml 中写入 PDB flaky-pdb(minAvailable: 3,选择器 app: flaky),并应用。然后给某个 flaky Pod 的 status.conditions 打补丁,把 Ready 改为 False(kubectl -n ops-pdb patch pod <이름> --subresource=status --type=merge -p ...,占位符为 Pod 名称)。针对该 Pod 发送 ?dryRun=All 驱逐,把拒绝消息保存到 /root/ops-disruption/unhealthy-denied.txt,然后把 flaky-pdb 的 spec.unhealthyPodEvictionPolicy 改为 AlwaysAllow,再发送同样的请求,保存到 /root/ops-disruption/unhealthy-allowed.txt。
PDB 所统计的健康 Pod,是 Ready 状况为 True 的 Pod。要求保住三个,却有一个不是 Ready,预算就已经被打破了,在默认策略下,连这个坏掉的 Pod 也无法赶出去——这是 drain 永远无法结束的常见原因。改变策略之后,同样的请求就能通过。
在整个集群中找出没有预算的工作负载
在 /root/ops-disruption/orphan.yaml 中写入 Deployment orphan——命名空间 ops-pdb,副本数 3,标签 app: orphan,镜像 nginx:1.27.3,不创建 PDB。然后创建 /root/ops-disruption/pdb-audit.sh——在所有命名空间的 Deployment 中,把 spec.replicas 在 2 以上、却没有被同一命名空间中任何 PDB 覆盖的,按 <네임스페이스> 制表符 <이름>(占位符依次为命名空间与名称)仅输出到标准输出,并按名称排序。如果 PDB 的 spec.selector.matchLabels 是 Deployment 的 spec.template.metadata.labels 的子集,就视为被覆盖。把该输出保存到 /root/ops-disruption/pdb-audit.txt。
这份清单是设计维护窗口时最先需要的资料——没有预算的工作负载,会在 drain 时毫无阻力地整体下线。子集的判定,用 jq 统计 PDB 选择器的每一项是否在 Pod 标签中以相同的值存在即可。如果脚本直接写文件,评分器再次运行时会覆盖学员的产出,所以请只通过标准输出输出。