在Kubernetes节点维护窗口内实现Pod平滑驱逐,核心答案是提前设置PodDisruptionBudget、优雅终止时长和节点污点,配合滚动迁移策略,让Pod在维护窗口内有序撤离且服务不中断。
节点维护窗口内Pod平滑驱逐为什么容易出问题
很多运维团队遇到过这种场景:凌晨两点收到节点维护通知,窗口只有四小时,你以为执行kubectl drain就完事,结果节点上的Pod直接进入Terminating状态,部分请求超时报警,甚至有状态服务数据没来得及落盘。
问题根源在于默认驱逐逻辑太粗暴。kubectl drain会先对节点打上污点,然后强制删除Pod,但Pod的容器进程不一定能在几秒内优雅退出,比如Java应用启动时加载一堆类,关闭时还要释放连接池,十秒的terminationGracePeriodSeconds根本不够用,更麻烦的是,如果集群里没有配置PodDisruptionBudget,Controller Manager会同时把副本全部杀掉,服务瞬间雪崩。
行业共识认为,平滑驱逐不是一个命令能解决的,而是需要一套组合策略,你需要在维护窗口到来之前,就把节点的负载逐步降下来,让Pod自己找到新家。
配置PodDisruptionBudget是平滑驱逐的底线保护
先理解PDB的生效逻辑
PodDisruptionBudget不是阻止驱逐,而是限制自愿中断时不可用Pod的数量,比如你的应用有3个副本,PDB设置为minAvailable: 2,那么驱逐时最多只能有1个副本同时不可用,这里的“不可用”指Pod处于Pending、CrashLoopBackOff或者被删除后的空窗期。
实际操作时,PDB有两种写法:
minAvailable: 2或minAvailable: 80%表示最少可用副本数。maxUnavailable: 1表示最多允许几个副本同时不可用。
对于无状态应用,通常用maxUnavailable更灵活,有状态应用比如数据库,建议用minAvailable保证法定人数。
给PDB加上合理的保护范围
只给Deployment设置PDB不够,StatefulSet、ReplicaSet、甚至直接管理的Pod都需要覆盖,很多集群里裸奔的Pod没有被任何控制器管理,drain时只能硬删,数据丢失风险极高。
下面是一个实际可用的PDB清单示例:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: web-backend-pdb
namespace: production
spec:
minAvailable: 2
selector:
matchLabels:
app: web-backend
创建后可以用kubectl get pdb查看状态,注意ALLOWED DISRUPTIONS这一列,如果是0,说明当前副本数已经低于PDB要求,这时执行drain会被阻塞,直到有Pod恢复可用。
调整优雅终止时长和应用退出逻辑
terminationGracePeriodSeconds设置多少合适
默认值是30秒,但很多应用实际退出需要更长瞬间,建议根据应用情况分别设置:
- 轻量API服务:30秒足够。
- 消息消费者:需要保证当前消息处理完,建议120秒。
- 有状态数据库:可能需要在退出前同步数据,建议300秒甚至更长。
修改方式是在Deployment的Pod模板里加上:
spec:
terminationGracePeriodSeconds: 120
containers:
- name: app
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "curl -X POST http://localhost/health/drain && sleep 5"]
preStop钩子让应用自己先“停止接客”
不要让Kubernetes直接杀进程,而是通过preStop钩子主动通知应用进入维护模式,比如让应用先从负载均衡摘除、停止接收新请求、刷盘、关闭连接池,最后再退出,上面例子里的curl请求就是触发应用内部的drain接口。
注意,preStop钩子的执行时间算在terminationGracePeriodSeconds内,如果钩子卡住,整个Pod会被强制杀死,所以钩子命令一定要有超时控制,比如用timeout 10 curl ...。
节点维护窗口内的具体驱逐操作步骤
第一步:提前标记节点为不可调度
维护前15分钟执行:
kubectl cordon node-01
此时节点不会再接收新Pod,已有Pod继续运行,观察一下集群调度情况,确认没有新Pod调度到该节点。
第二步:分批驱逐并观察状态
不要一次性drain
所有节点,对维护窗口涉及的多节点集群,建议逐节点操作,执行:
kubectl drain node-01 --ignore-daemonsets --delete-emptydir-data
参数说明:
--ignore-daemonsets:DaemonSet控制的Pod(如日志采集器)不会被驱逐,因为其他节点也必须有。--delete-emptydir-data:允许删除使用emptyDir的Pod,这类临时数据本来就不该持久化。
驱逐过程中,观察Pod状态:
kubectl get pods -n production -o wide | grep node-01
如果PDB导致驱逐卡住,drain命令会一直等待,这时需要检查是不是PDB设置的minAvailable过高,或者当前副本数就低于阈值,允许的话,可以临时调低PDB,然后继续。
第三步:处理异常卡住的Pod
有几种情况会导致Pod卡在Terminating:
- 进程忽略SIGTERM信号,或者有不可中断的系统调用。
- 持久卷卸载失败。
- 容器运行时本身有问题。
针对第一类,可以这样处理:
kubectl delete pod <pod-name> --force --grace-period=0
但这是最后手段,用了强删务必确认数据已经落盘或复制到其他节点,对于卷卸载失败,检查该节点的kubelet日志,可能需要手动umount。
自动感知维护窗口的进阶方案
用节点污点提前把Pod“推”走
如果不想手工执行drain,可以在维护窗口开始前给节点加上自定义污点:
kubectl taint nodes node-01 maintenance=true:NoSchedule
已有Pod不受影响,但新Pod不会调度过来,配合tolerationSeconds或预算调度策略,可以在维护窗口内逐步驱逐,更彻底的方式是加NoExecute污点,让已有Pod在容忍时间到期后自动被驱逐。
与Cluster Autoscaler配合的节点池替换
很多云环境支持节点池升级,维护窗口内可以直接触发节点池滚动替换,新节点先就绪,旧节点再排空,这种方式对应用无感,但需要集群有冗余容量,行业共识是维持集群利用率在60%以下,这样替换节点时才有缓冲空间。
具体流程:
- 新增节点池,节点初始化完成后加入集群。
- 等待新节点Ready,并打上对应标签。
- 逐个
cordon并drain旧节点。 - 确认旧节点Pod全部迁移完毕,再释放旧节点池。
Q&A:节点维护窗口内Pod平滑驱逐常见问题
如果PDB设置过严,导致drain一直卡住怎么办?
先看kubectl get pdb的ALLOWED DISRUPTIONS是否为0,如果为0,要么等副本自动恢复,要么在确认业务允许的前提下临时编辑PDB,调低minAvailable或调高maxUnavailable,不建议直接删PDB,因为很容易忘记恢复原状,更稳妥的做法是分两步:先扩容副本数,再继续驱逐。
驱逐过程中Pod被调度到新节点,但新节点资源不够怎么办?
调度器会参考节点资源分配,如果集群整体资源不足,Pod会进入Pending状态,这时需要看是否启用了优先级类(PriorityClass),高优先级Pod会抢占低优先级Pod的节点,没有优先级类的话,只能临时扩容节点或者延后维护窗口,预防措施是在维护窗口前检查集群空闲资源,建议预留至少一个节点或30%的CPU内存余量。
有状态应用的Pod驱逐后数据会丢吗?
取决于存储类型,如果StatefulSet用的是云厂商持久卷(EBS、云盘),Pod迁移到新节点后卷可以直接挂载,数据不丢,但如果用的是emptyDir或hostPath,数据必然丢失,对于使用本地存储的应用,需要先做数据备份或复制,再允许驱逐,业内专家指出,对于数据库这类有状态服务,不建议直接走普通驱逐流程,最好结合Operator的备份恢复机制,或者提前做主从切换,让主节点迁移到新节点后再驱逐旧节点。
节点维护窗口内做Pod平滑驱逐,本质上是把“硬删”变成“软迁”,提前设置好PDB、调大优雅终止时间、用好preStop钩子,再按顺序执行cordon和drain,多数情况下都能在窗口内顺利完成,维护窗口结束前,记得用kubectl uncordon node-01恢复调度,并检查PDB状态是否需要回滚,这一套流程跑顺了,节点维护就不会再让人熬夜了。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/643888.html




