共计 3491 个字符,预计需要花费 9 分钟才能阅读完成。
最近在整理我的 Homelab 集群,因为之前为了测试某个存储方案,临时加了几台配置参差不齐的旧机器进去。现在测试结束了,看着那些嗡嗡作响的旧风扇,我决定把它们从集群里退役掉,顺便给主集群做个"瘦身"。
这一过程其实让我回想起了很多年前刚接触 Kubernetes 的时候。那时候年轻气盛,觉得少个节点多大点事,直接 poweroff 或者在云控制台点击"删除实例",然后坐等 K8s 自己恢复。结果嘛,当然是遇到过 Pod 卡死、数据卷挂载不上去,甚至服务短暂不可用的情况。
这让我意识到,虽然 K8s 是为了容错设计的,但作为一个运维人员,我们不能把所有的容错压力都丢给系统。如何"优雅"地送走一个节点,其实是有一套标准流程的。今天就想和大家聊聊,在 K8s 中移除节点的正确姿势。
为什么要"优雅"地移除?
在深入操作之前,我们先聊聊背景。Kubernetes 的设计哲学里,节点(Node)确实被视为"牲畜"而非"宠物",它们是可以随时被替换的。但是,节点上运行的 Pod(容器组)以及 Pod 所挂载的存储,却可能承载着关键的业务状态。
如果直接暴力关机或断网:
- 服务中断:虽然 Deployment 会重建 Pod,但中间会有时间差。如果你的服务没有配置多副本或者反亲和性(Anti-Affinity),可能会导致短时间内服务不可用。
- 数据风险:如果 Pod 正在写入数据,强制终止可能导致数据损坏。
- 状态不一致:Controller Manager 需要一定时间(默认是 5 分钟的
pod-eviction-timeout)才能确认节点失联并开始迁移 Pod,这期间流量可能还会被分发到死掉的节点上。
所以,我们需要一套机制,告诉集群:"嘿,这个节点我要拿走了,你先把上面的活儿安排给别人,处理完了我再关机。"
这套机制,在 K8s 里主要由三个命令组成:cordon(警戒)、drain(驱逐)和 delete(删除)。
深度解析:核心逻辑
在实际操作中,我把这个过程看作是一个"交接工作"的离职流程。
Cordon:停止派单
kubectl cordon <node-name>
这个单词的意思是"警戒线"。当你给一个节点打上 Cordon 标记时,K8s 调度器(Scheduler)就会把这个节点标记为 Unschedulable(不可调度)。
这就像是告诉公司 HR,这个员工提交了离职申请,不要再给他安排新的任务了。但是,他手头上现有的工作(正在运行的 Pod)还是会继续做,不会受到影响。
Drain:交接工作
kubectl drain <node-name>
kubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data
这是最关键的一步,也是最容易出问题的一步。Drain 会驱逐节点上的 Pod,让它们在其他节点上重新调度。
这个过程非常智能,它会遵循 Pod 的生命周期,发送 SIGTERM 信号给容器,让容器有机会进行清理工作(比如保存状态、关闭连接),实现平滑迁移。这也是为什么我们在写应用的时候,处理好优雅退出(Graceful Shutdown)那么重要。
Delete:正式除名
kubectl delete node <node-name>
当节点被掏空之后,它就真的是一个空壳了。这时候我们在 API Server 中删除这个对象,彻底断绝关系。
我的实践经验与踩坑记录
好了,理论说完了,来看看我在这次"瘦身"行动中的具体操作。
确认节点状态
首先,我习惯先看一眼现在的集群状态。
kubectl get nodes
输出大概是这样:
NAME STATUS ROLES AGE VERSION
master-01 Ready control-plane 300d v1.28.2
worker-01 Ready <none> 300d v1.28.2
worker-02 Ready <none> 300d v1.28.2
worker-old Ready <none> 10d v1.28.2
我要移除的是 worker-old 这个节点。
禁止调度(Cordon)
执行命令:
kubectl cordon worker-old
再次查看状态,你会发现它变成了 SchedulingDisabled:
NAME STATUS ROLES AGE VERSION
worker-old Ready,SchedulingDisabled <none> 10d v1.28.2
这时候,如果我有新的 Deployment 扩容,Pod 绝对不会落在这个节点上。
驱逐 Pod(Drain)—— 重头戏
这里有个坑。直接运行 kubectl drain worker-old 通常会报错。
常见的报错有两个:
- DaemonSet:节点上运行着 DaemonSet 管理的 Pod(比如 kube-proxy, calico-node 等)。DaemonSet 的特性就是每个节点都要有,你把它驱逐了,它又会试图回来,而且通常 DaemonSet 不受调度器限制。
- 本地数据(Local Data):如果 Pod使用了
emptyDir这种本地临时存储,驱逐意味着这些数据会丢失,K8s 会发出警告。
所以我通常使用的完整命令是:
kubectl drain worker-old --ignore-daemonsets --delete-emptydir-data --force
--ignore-daemonsets:忽略 DaemonSet 管理的 Pod,不驱逐它们(反正节点删了它们也就没了)。--delete-emptydir-data:明确同意删除使用本地临时存储的 Pod。--force:如果有一些裸 Pod(不是由 Deployment/ReplicaSet 管理的),也强制驱逐。
执行后,你会看到类似这样的日志流:
node/worker-old cordoned
WARNING: ignoring DaemonSet-managed Pods: kube-system/kube-proxy-xyz...
evicting pod default/nginx-deployment-abc...
evicting pod monitoring/node-exporter-def...
pod/nginx-deployment-abc evicted
...
node/worker-old drained
看着一个个 Pod 被 evicted,感觉就像是在看着搬家公司把家具一件件搬走。
特别注意 PDB(PodDisruptionBudget):
如果你在生产环境,可能会遇到 drain 卡住的情况。这通常是因为配置了 PDB。比如你有一个应用,最少需要 2 个副本在线,而你一共只有 2 个副本,其中一个就在你要移除的节点上。这时候 drain 会一直重试,直到新的副本在其他节点 Ready 之后,才会继续驱逐。
这是一个保护机制,防止维护操作搞挂服务。遇到这种情况,千万别急着 Ctrl+C,去检查一下其他节点的资源够不够,能不能把新 Pod 跑起来。
从集群删除节点
确认节点已经 drained(除了 DaemonSet 啥也没了),就可以在控制平面执行删除操作了:
kubectl delete node worker-old
这时候,worker-old 这个名字就会从 kubectl get nodes 的列表中消失。
物理关机
如果是物理机,现在可以放心地去按电源键了。如果是云服务器,现在可以去控制台释放实例了。
我个人的习惯是,在 delete node 之后,还会登录到那台机器上,运行一下 kubeadm reset(如果是用 kubeadm 部署的),清理一下 iptables 规则和 CNI 留下的垃圾文件,虽然要重装系统的话这步可以省略,但对于强迫症来说,清理干净总是舒服的。
最后
其实,移除节点这个操作,本质上是在处理状态的转移。
K8s 的强大之处在于它帮我们自动化了大部分状态转移的过程,但作为操作者,我们需要理解这背后的约束。
- Cordon 是为了止损,防止新流量进入。
- Drain 是为了平滑,保证旧业务的善后。
- Delete 才是最后的清理。
我越来越觉得,运维 Kubernetes 就像是在玩一个复杂的策略游戏。每一个命令背后都有它的代价和收益。以前我觉得只要业务不挂就行,现在我更追求操作的"优雅"和"可控"。
另外,如果你的集群开启了 Cluster Autoscaler,其实这一步大部分时候是自动完成的。当节点空闲率过高时,Autoscaler 会自动触发 drain 和 delete,那又是另一个有趣的话题了。
希望这篇小记能帮你避开一些坑,让你在下一次维护集群时,能自信地按下回车键。
如果你在移除节点时遇到过什么奇葩问题,或者有更好的实践脚本,欢迎在评论区或者邮件告诉我,我们一起探讨。

