K8s 中如何优雅地移除节点:从 Cordon 到 Delete 的完整实践

7次阅读
没有评论

共计 3491 个字符,预计需要花费 9 分钟才能阅读完成。

最近在整理我的 Homelab 集群,因为之前为了测试某个存储方案,临时加了几台配置参差不齐的旧机器进去。现在测试结束了,看着那些嗡嗡作响的旧风扇,我决定把它们从集群里退役掉,顺便给主集群做个"瘦身"。

这一过程其实让我回想起了很多年前刚接触 Kubernetes 的时候。那时候年轻气盛,觉得少个节点多大点事,直接 poweroff 或者在云控制台点击"删除实例",然后坐等 K8s 自己恢复。结果嘛,当然是遇到过 Pod 卡死、数据卷挂载不上去,甚至服务短暂不可用的情况。

这让我意识到,虽然 K8s 是为了容错设计的,但作为一个运维人员,我们不能把所有的容错压力都丢给系统。如何"优雅"地送走一个节点,其实是有一套标准流程的。今天就想和大家聊聊,在 K8s 中移除节点的正确姿势。

为什么要"优雅"地移除?

在深入操作之前,我们先聊聊背景。Kubernetes 的设计哲学里,节点(Node)确实被视为"牲畜"而非"宠物",它们是可以随时被替换的。但是,节点上运行的 Pod(容器组)以及 Pod 所挂载的存储,却可能承载着关键的业务状态。

如果直接暴力关机或断网:

  1. 服务中断:虽然 Deployment 会重建 Pod,但中间会有时间差。如果你的服务没有配置多副本或者反亲和性(Anti-Affinity),可能会导致短时间内服务不可用。
  2. 数据风险:如果 Pod 正在写入数据,强制终止可能导致数据损坏。
  3. 状态不一致: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 通常会报错。

常见的报错有两个:

  1. DaemonSet:节点上运行着 DaemonSet 管理的 Pod(比如 kube-proxy, calico-node 等)。DaemonSet 的特性就是每个节点都要有,你把它驱逐了,它又会试图回来,而且通常 DaemonSet 不受调度器限制。
  2. 本地数据(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,那又是另一个有趣的话题了。

希望这篇小记能帮你避开一些坑,让你在下一次维护集群时,能自信地按下回车键。

如果你在移除节点时遇到过什么奇葩问题,或者有更好的实践脚本,欢迎在评论区或者邮件告诉我,我们一起探讨。

正文完
 0
评论(没有评论)