凌晨一点,手机嗡嗡震个不停。我迷迷糊糊摸起来一看,监控告警:node-a 磁盘空间使用率冲到 95%,Kubelet 开始疯狂驱逐Pod,一大片业务Pod变成了 Evicted 状态。那一瞬间的焦虑,凡是管过 Kubernetes 集群的人都懂——Pod 从 A 节点转移到 B 节点,这件事做得好不好,直接决定了线上服务是稳定过渡还是事故通报。
这期就专门聊聊这个高频操作:怎么把一个节点上的 Pod 安全、可控、不留坑地迁移到另一个节点。无论你是刚入门 K8s 的运维新人,还是已经背过几口锅的中年 SRE,这套流程都值得重新捋一遍。内容不绕弯子,从原理到命令再到常见坑,一次说透。
1. 为什么会有“把 Pod 从 A 节点搬到 B 节点”这个需求
很多人第一次接触 Pod 迁移,是在“节点坏了”的紧急情况下被逼上梁山的。但实际操作中,主动迁移的场景远比故障迁移更常见,提前搞清楚触发条件,你才能在事发时不慌。
1.1 节点维护是最大头的工作来源
服务器不可能永远不宕机,系统内核有 CVE 要修、内核要升级、磁盘要扩容、网卡要换固件,这些操作都需要重启节点。K8s 集群里重启一台节点,意味着上面所有 Pod 都会中断。如果你不做任何处理直接重启,Pod 确实会在其他节点被重新调度起来,但等待时间是随机的,调度器还要重新拉镜像、重新挂数据卷,对用户来说就是一段不可控的“服务抖动”。
所以正规做法是:先把节点标记为不可调度,把上面的 Pod 优雅驱逐到其他可用节点,等所有工作负载都跑顺了,再重启节点做维护。这就是典型的“Pod 从 A 节点转移到 B 节点”场景,也是日常操作频率最高的一个。
稳健的做法会让你在维护窗口内把流量提前切走,确保业务侧基本无感。这个思路对应到 K8s 操作,就是先把节点 cordon 掉,再逐个驱逐 Pod,等节点负载清空后再进行维护。
1.2 故障转移比想象中复杂
节点硬件故障、Kubelet 心跳超时、内核死锁、网络分区,这些都属于被动迁移的场景。被动迁移的问题在于:你没法优雅驱逐,因为节点可能已经失联了。
很多新手误以为节点宕机后 Pod 会自动转移到其他节点,其实要区分两种情况。如果 Pod 由 Deployment、StatefulSet 这类控制器管理,节点失联后,默认有 5 分钟(pod-eviction-timeout)的容忍期,超过时间控制器才会在其他节点重建 Pod。这 5 分钟对高可用要求高的服务来说已经很长了。更麻烦的是,如果节点只是网络分区而不是彻底宕机,可能出现“脑裂”:老节点上的 Pod 还在跑,新节点上又拉起了新 Pod,同一份数据两个实例在写。
所以故障场景下的“转移”,很多时候不是真正完成了优雅迁移,而是靠控制器重建实现的,这个过程有延迟、有状态不一致的风险。你真正要做的,是在日常就做好配置(探针、PDB、反亲和性等),让故障发生时系统能更快、更安全地完成这个转移。
1.3 资源均衡也需要主动迁
集群跑久了,会出现典型的“热点问题”:某些节点 CPU 打满,某些节点空闲到能跑 CI。原因是 Pod 创建时的调度只是基于当时的集群状态,后来业务增长、Pod 重建、节点配置更新,都会导致调度结果偏离初始状态。
这种场景做“转移”不是应急,而是日常工作。你可以手动把热点节点上的若干 Pod 驱逐到空闲节点,也可以用 Descheduler 这类工具定期扫描并自动迁移。下面第三部分会给出完整命令。总之,Pod 从一个节点到另一个节点,不是一句“kubectl delete pod”就完事那么简单,里面藏着调度器、驱逐策略、持久化存储的各种约束。
2. 动手前必做的三项检查
我见过太多人直接跑kubectl drain,然后卡在一堆 Pod 上进退两难,最后只能--force强杀,把线上业务带走一大片。迁移这件事,动手前花三分钟检查,比事后花三小时救火划算得多。
2.1 检查目标节点上到底有哪些 Pod
先搞清楚要迁移的对象,命令很简单:
kubectl get pods -A -o wide | grep <node-a>这条命令会把所有命名空间下跑在 node-a 上的 Pod 列出来。你要重点看三样东西:
- Pod 属于哪个控制器:是 Deployment、StatefulSet、DaemonSet 还是裸 Pod。前两种有控制器兜底,驱逐后会在其他节点重建;DaemonSet 比较特殊,下面单独说;裸 Pod 没有任何管理器,你驱逐了它就真的没了,必须自己重新创建。
- Pod 的 readiness 探针和优雅终止时间:
terminationGracePeriodSeconds如果设得很大,比如 120 秒,drain 的时候一个 Pod 可能要等两分钟才被强制干掉,几十个 Pod 排下来,维护窗口根本不够用。 - 是否有 PDB(PodDisruptionBudget):这是个非常关键的约束。PDB 限制了“自愿中断”(比如 drain 驱逐)时最多能挂几个副本。如果业务设置了
minAvailable: 3,但当前健康副本已经只剩 3 个,drain 就会一直等待,直到有其他副本变健康。
2.2 检查存储和网络的相关约束
如果你的 Pod 用了持久化存储,迁移前务必看清楚存储类型。云平台的块存储(阿里云云盘、AWS EBS)通常和可用区绑定,Pod 从 A 节点迁到 B 节点时,如果 B 节点不在同一个可用区,PVC 可能无法挂载,Pod 会卡在 ContainerCreating。这种情况要么选择同可用区的目标节点,要么提前迁移数据,要么换用共享存储(NAS、CephFS 等)。
网络方面主要看 Pod 是否用了 hostPort、固定 IP 或独立网卡(比如 CNI 的 IPAM 特性)。跨节点迁移后这些网络标识会变化,依赖固定 IP 的服务(比如白名单、DNS 记录)需要同步调整,最好提前确认业务侧有没有这种硬依赖。
2.3 检查多个目标节点的调度条件
最后别忘了确认 node-b 本身能不能接住这些 Pod。所谓“能不能”,包括:
kubectl describe node <node-b>重点看 Taints 和 Labels。如果 node-b 有污点,Pod 必须有对应容忍才能调过去;如果 Pod 有 nodeSelector 或 nodeAffinity 要求,node-b 的标签必须匹配;再看一下 node-b 剩余资源,内存和 CPU 是否充足,尤其是 GPU 这类特殊资源。
提示:很多时候 Pod 迁不过去不是调度器的问题,而是目标节点被各种条件卡住了。这一项检查能避开 90% 的“Pending 一整天”事故。
3. 标准迁移流程:cordon、drain、uncordon 三步走
做任何迁移之前,我的习惯是先确认目标节点资源充足、节点状态 Ready,同时检查所有工作负载是否都有控制器管理。确认没问题后,按照下面这套流程走。
3.1 第一步:cordon 标记节点为不可调度
kubectl cordon node-acordon 的中文意思就是“拉警戒线”。执行后节点状态变成SchedulingDisabled,调度器不会再往这个节点放置新 Pod,但节点上已有的 Pod 不受影响,会继续正常运行。没有这一步,你在 drain 驱逐 Pod 的同时,Deployment 的滚动更新或 HPA 扩容可能又调度了新 Pod 到这个节点上,造成边驱逐边新增的尴尬局面。
cordon 是幂等操作,执行前和执行后都不会影响已有工作负载,非常安全,可以放心大胆先执行。
3.2 第二步:drain 驱逐节点上的 Pod
cordon 完之后,正式驱逐:
kubectl drain node-a --ignore-daemonsets --delete-emptydir-data这条命令做的事,是把 node-a 上所有由控制器管理的 Pod 做eviction(驱逐),不是直接删除。驱逐过程中,控制器会很快在其他节点重建这些 Pod,从而实现“转移”。
我重点解释一下两个参数,这是最容易踩坑的地方:
--ignore-daemonsets:DaemonSet 的 Pod 是每台节点必须跑一个的(比如日志采集 fluentd、监控 exporter、网络插件 calico-node)。如果不用这个参数,drain 会一直等这些 Pod 结束,但它俩是“你驱逐了我,DaemonSet 控制器又会在同一台节点重建我”,死循环,drain 永远卡住。--delete-emptydir-data:如果 Pod 使用了 emptyDir 数据卷(临时目录),drain 默认拒绝驱逐,因为驱逐会丢失临时数据。加上这个参数表示“我知道数据会丢,我接受”。如果你的业务 Pod 用了 emptyDir 放缓存数据,需要确认数据丢失不影响业务,才加这个参数。
drain 还有个隐藏行为需要注意:它会等待 Pod 优雅终止。如果 Pod 一直不退出(比如有连接未关闭),drain 会一直卡着。此时可以加--grace-period调整秒数,或者--force强制删除。但--force是最后手段,会丢数据、断连接,一定要在确认业务可容忍后才用。
3.3 第三步:确认 Pod 跑到 node-b 上并验证状态
drain 执行完后,马上验证:
kubectl get pods -A -o wide | grep <node-b>重点检查新 Pod 是否 Running 且 Ready,查看 ReplicaSet 的副本数是否达到期望值。如果业务有接口,最好在窗口内做一次真实流量验证,而不只是看 Pod 状态。
如果需要跳过某几个不能迁移的特殊 Pod,可以使用--disable-eviction参数配合 Pod 注解,或者先单独处理这几个 Pod。总之确认清空状态,再用kubectl get nodes看看:如果显示 Ready、SchedulingDisabled,说明节点可以下线维护了。
3.4 维护完成后恢复节点调度
节点重启、打补丁、换硬件、清理磁盘等操作完成后,把它重新加入调度:
kubectl uncordon node-auncordon 只是把污点标记取消,让调度器可以再次分配 Pod。但注意,刚才迁走的 Pod 不会自动跑回来,它们已经在新家安顿好了。如果希望 Pod 尽量回到 node-a(比如访问量大、需要利用这台机器的本地缓存),可以多走一步:把 node-b cordon 掉,再把相关 Pod 驱逐回去,最后 uncordon node-b。这相当于做一次反向迁移。
实际操作中,我建议不要急着把 Pod 迁回去,除非有明确理由。频繁迁来迁去只会增加不稳定窗口,而且 K8s 的心跳机制和负载均衡器要重新适应。让 Pod 待在 node-b 多跑一段时间,稳定了再考虑回流。
4. 特殊场景下的 Pod 转移方法
第 3 部分讲的是标准流程,覆盖了 80% 的场景。但下面这几种情况,光靠drain搞不定,需要有额外手段。
4.1 裸 Pod 和“一次性任务”怎么迁
如果一个 Pod 不是由 Deployment、StatefulSet 等控制器管理的(比如用了kubectl run直接创建,或者手动 create 的 Pod),drain 对它无能为力。drain 会直接报错,说该 Pod 没有控制器,无法保证重建。
处理办法也很直接:先记录它的完整 YAML(kubectl get pod xxx -o yaml),然后手动删除,再修改nodeSelector或删除nodeName限制后重新创建。注意裸 Pod 不会自愈,整个过程必须手工介入,千万别指望系统帮你恢复。
一次性任务(Job 类型)更特殊:如果 Job 已经跑完了(Completed),直接删掉即可;如果还在执行,最好等它跑完或者主动删除 Job 让控制器决定是否重试。drain 遇到运行中的 Job 有时会卡住,因为它不确认 Job 是否允许重新调度。
4.2 直接修改 nodeName 实现“硬绑”
有些场景你确切知道 Pod 必须落在 node-b 上,不想让调度器参与决策。比如调试一个只有 node-b 才有的 GPU 驱动问题。这时可以修改 Pod 的spec.nodeName字段,把这个字段直接写成 node-b。
但注意:对于已有 Pod,nodeName字段是只读的,改不掉。你只能把 Pod 导出来,改掉 nodeName,再删掉旧 Pod,用新 YAML 创建。这算是一招“硬迁移”,绕过了调度器,Pod 在启动时不会再做调度判断,但副作用是:如果 node-b 后面挂了,没有控制器管理的裸 Pod(bare Pod)也不会自动转移到其他节点。而且对于正常控制器管理的 Pod,这样改 YAML 的方式与控制器期望可能存在冲突,容易让 ReplicaSet 计数混乱,我只建议在临时调试时使用,生产环境慎用。
4.3 用 Descheduler 做自动化均衡迁移
手动迁移适合应急和小规模场景,集群大了以后,每次都手动迁太累。Descheduler 是官方生态中专门做 Pod 重新均衡的工具,它能定期扫描集群,根据策略把运行中的 Pod 驱逐掉,让调度器重新分配。
常用策略有:
LowNodeUtilization:把高负载节点上的 Pod 驱逐到利用率低的节点,解决热点问题。RemoveDuplicates:把同一节点上多个相同 ReplicaSet 的 Pod 分散开,提升容灾能力。NodeAffinity:检测当前违反节点亲和的 Pod,驱逐后让调度器重新安排。
Descheduler 会先以“软性”的方式驱逐,同样受 PDB 限制,不会把业务打挂。集群内如果维护过几次迁移后,可以在 CICD 里周期性地把它跑一遍,省去大量人工操作。
5. 迁移过程中的常见问题与排查经验
下面这些坑,是我在真实环境中一个一个踩出来的,每一个都有线上事故的影子。
5.1 drain 卡住,一直不结束
这是最常遇到的问题。drain 卡住不外乎三种原因:
- PDB 限制太严格:比如
minAvailable: 3,当前就 3 个副本,驱逐任何一个都会让可用副本数低于阈值。解决方案:要么临时放宽 PDB,要么先扩容副本数再 drain。 - Pod 优雅终止时间太长:
terminationGracePeriodSeconds设得很大,Pod 收到 SIGTERM 后迟迟不退出。可以检查是否有进程忽略了 SIGTERM,或者临时加--grace-period=30缩短等待时间。这里要注意,这个参数是 drain 命令的,不是 Pod 的,它是给底层 eviction API 用的。 - 未受管 Pod 卡住:裸 Pod、Job、有本地存储(非 emptyDir 的 hostPath 等)的 Pod,都会让 drain 等待。检查方式:
kubectl get pods -n <ns> -o wide | grep <node-a>,看有没有 Pending、Terminating、Running 且无控制器的 Pod。
排查手段主要是看事件:
kubectl get events --all-namespaces --sort-by=.lastTimestamp | grep -i evict这里能看到驱逐请求是被接受还是被拒绝了,一般能看到“eviction failed”和原因。
5.2 Pod 被驱逐后,又回到原节点
有同学明明执行了 drain,Pod 也确实在新节点跑起来了,但过一会儿发现又回到老节点了。出现这种场景,首先检查是否执行了 uncordon。只要老节点还是 SchedulingDisabled,调度器不应该把 Pod 放回去,除非你手动改了 nodeName,或者节点上存在不受控的某种“自愈逻辑”。
更隐蔽的情况:如果老节点上有 local PV(节点绑定的持久卷),且 Pod 通过 PVC 引用这块盘,调度器为了满足卷的节点亲和性,会强行把 Pod 调度到老节点上。这正是为什么第一部分强调“检查存储类型”。处理方案:换用共享存储,或者删除 local PV 后重建(业务要接受数据丢失/迁移)。
5.3 有状态应用迁移后数据一致性怎么办
StatefulSet 迁移比 Deployment 复杂。因为 StatefulSet 的名称、网络标识(headless service 的 DNS)、PVC 都是固定的。如果你把 Pod 驱逐到新节点,但 PVC 还在老节点的云盘上(单可用区盘),新节点可能根本无法挂载。
经验法则是:有状态应用迁移前,先看 StorageClass 的 reclamationPolicy 和 volumeBindingMode。如果是WaitForFirstConsumer,PVC 会跟随 Pod 调度到对应的节点或可用区,情况会好一些;如果是Immediate,PVC 会在创建时绑定到某个可用区,Pod 迁移跨区就会失败。必要时,先把 StatefulSet 缩容到 0,修改 PVC 所用 StorageClass,或者用 velero 这类工具做备份恢复。总之,千万不要在没确认存储机制前直接 drain 有状态节点。
5.4 DaemonSet Pod 驱逐后立刻重建,正常吗
正常。drain 加--ignore-daemonsets的意思是“不驱逐”,而不是“驱逐了也拦不住”。如果不用这个参数,DaemonSet 的 Pod 会反复重建,导致卡死。所以看到 DaemonSet 的 Pod 还在节点上,是预期的。如果要更新或迁移 DaemonSet,应该改 DaemonSet 的配置(比如 nodeSelector),而不是用 drain 去逐出。
5.5 节点维护完,怎么让 Pod 均衡回流
这个问题没有标准答案。有的团队希望 Pod 回到老节点,有的团队觉得无所谓。
如果想尽量均衡回流,可以这样做:
- 先
kubectl cordon node-b,让 node-b 不再接收新 Pod。 - 再
kubectl drain node-b --ignore-daemonsets --delete-emptydir-data,把迁过来还没稳定的 Pod 重新逐出。 - 调度器看到 node-a 已经 Ready 可调度,并且镜像缓存还在,大概率会重新把 Pod 调度回 node-a。
- 最后
kubectl uncordon node-b。
听起来简单,但要考虑业务中断风险。每次驱逐都是一次重启,有状态服务你还要重新挂盘,如果 Pod 刚启动完又要赶走,线上可能会有几次 5xx。我个人更推荐:如果 node-a 维护周期短(比如只重启),不要急着回流;如果 node-a 是因为硬件更换导致未来可用性存疑,应该让 Pod 在新节点长期待着,老节点作为扩展资源待命。
6. 这次迁移过后,建议你留下这样一套操作规范
迁移这件事,最好提前写成流程文档,每次照着走,而不是靠脑子临时想。我自己常用的 checklist 长这样:
# 1. 前置检查 kubectl get nodes kubectl get pods -A -o wide | grep <node-a> kubectl get pdb -A kubectl get pvc -A | grep <namespace> # 2. 执行迁移 kubectl cordon <node-a> kubectl drain <node-a> --ignore-daemonsets --delete-emptydir-data # 3. 迁移确认 kubectl get pods -A -o wide | grep <node-b> kubectl get nodes | grep <node-a> # 4. 维护结束后恢复 kubectl uncordon <node-a>操作之外,还有三个软性建议:
- 提前建好 PDB 并验证它生效。没有 PDB,drain 可能会瞬间把关键服务的所有副本全部驱逐,等新 Pod 拉起来之前,线上全是 503。这项工作看起来不起眼,关键时刻救命。
- 低峰期执行迁移。迁移一定会带来 Pod 重启和连接波动,尽量避免双十一大促期间做这种事。如果必须做,先给业务方发值班通知,让人手待命。
- 保留迁移后的观察时间。不要 drain 完就急着维护节点,先观察 10-15 分钟,确认迁过去的 Pod 没有 CrashLoopBackOff 或者容量不足问题,再去碰物理节点或云主机。
我个人的习惯是:每次迁移前,先在非生产环境完整跑一遍kubectl drain,确认参数组合和 PDB 都正常,做到心里有数。生产操作时也会先把节点 cordon 掉,等一段时间再执行 drain。表面上看多花了几分钟,实际上把很多“万一”挡在了门外。这套方法这几年帮我避免了无数次服务抖动,也希望你以后在碰到 Pod 迁移时,不再手忙脚乱。