Kubernetes Pod更新与迁移:调度器如何决定关联Pod的分布
2026/9/10 3:38:53 网站建设 项目流程

1. 先从“Pod更新”和“Pod迁移”的基本概念说起

1.1 为什么说Pod不会自己“迁移”

在Kubernetes里,“Pod迁移”这个说法其实有点误导。严格意义上,Pod从来不会从一台节点“走”到另一台节点,它只存在三种命运:被创建、被删除、被重新创建。所谓的迁移,本质上就是“先删除旧Pod,再在另一个节点上创建新Pod”,中间那个瞬间应用是不可用的,除非你有多个副本在同时工作。

这个机制很多刚接触K8s的人会理解错,以为Pod像虚拟机一样可以热迁移。实际上,Kubelet只负责维护本节点上的Pod生命周期,它没有能力把一个已经运行的容器从节点A搬到节点B。就算你用kubectl cordonkubectl drain排空节点,底层发生的事也是:驱逐Pod -> 触发控制器重建 -> 调度器为新Pod选节点。整个过程对应用层来说就是一次“重启换机器”。

所以当我们说“Pod更新触发关联Pod迁移”时,真正要关心的不是某个进程能不能带着内存状态跑过去,而是:旧Pod消失后,新Pod会不会被调度器放到正确的位置,以及那些跟它有依赖关系的Pod会不会受到影响。这个问题搞清楚了,后面很多调度器相关的坑都能避开。

1.2 什么场景会触发Pod的“迁移”

从我的实践经验看,最常见的触发场景有三类:

第一类是控制器滚动更新。Deployment、StatefulSet、DaemonSet这些控制器在Pod模板更新之后,会按照各自的更新策略创建新Pod、删除旧Pod。这个过程中调度器需要为每一个新Pod选节点,如果新Pod因为资源不足、亲和性不满足、或者拓扑约束冲突而调度失败,滚动更新就会卡住。很多人看到更新卡住第一反应是拉镜像慢,其实调度器日志里往往早就写了原因。

第二类是节点驱逐。比如节点磁盘压力、内存压力、或者管理员主动执行kubectl drain,节点上的Pod会被逐出。被逐出的Pod如果属于Deployment这类受管Pod,控制器会马上补一个新的出来,由调度器重新安排位置。这个“重新安排位置”就是最典型的迁移动作。如果驱逐时PDB(PodDisruptionBudget)设置了maxUnavailable: 0,驱逐还会被阻塞,这也是迁移流程里很重要的一个环节。

第三类是手动删除重建。比如你用kubectl delete pod删掉一个Pod,或者直接改了Pod的标签导致控制器重建。这种情况下新Pod也要经过调度器。别小看这个场景,很多关联Pod迁移的“诡异现象”就是从这里来的:你删了一个Pod,结果另一个Pod也被重建了,究其原因往往是它们共享同一个控制器、同一个拓扑约束,或者同一个污点容忍逻辑。

2. 调度器在Pod更新中担当的角色

2.1 滚动更新和调度过程的关系

先看一个最常见的滚动更新流程。假设有一个Deployment,三个副本分布在三个节点上,你执行kubectl set image deployment/app app=registry.example.com/app:v2,K8s会先创建一个新的ReplicaSet,然后把新ReplicaSet的副本数从0往上加,同时把旧ReplicaSet的副本数从3往下减。

问题来了:新Pod创建之后,调度器不是“随机挑一个节点”,而是要经过一整套打分和过滤机制。默认调度器会先执行预选(Filtering),把不满足条件的节点筛掉,比如节点资源不够、标签不匹配、端口冲突、存储卷不匹配;然后在剩下的节点里执行优选(Scoring),按资源利用率、Pod分布、亲和性等维度打分,最后挑出得分最高的节点。

我实际操作中遇到过一种情况:新版本Pod对CPU的requests从0.5核调到了1核,滚动更新发布后三个新Pod全部Pending,一直等到我把集群里一台空闲节点加进来才恢复。原因就是所有现有节点在过滤阶段就被筛掉了,没有任何一个节点能同时满足资源预留和拓扑分布约束。调度器本身没有错,它只是忠实执行了规则,真正的问题出在发布前没人评估资源增量。

2.2 为什么新Pod总会先于旧Pod消失之前出现

Deployment的滚动更新默认策略是RollingUpdate,其中有个关键参数maxSurgemaxUnavailablemaxSurge控制更新期间允许超出期望副本数的Pod数量,默认是25%;maxUnavailable控制允许不可用的Pod数量,默认也是25%。

这意味着默认情况下,K8s会先创建新Pod,等新Pod进入Ready状态后,才删一个旧Pod。所以你在滚动更新过程中会看到新旧Pod共存,比如3副本的Deployment,更新期间可能出现4个Pod。调度器在这种情况下要做的事情很明确:给新Pod找到位置,同时不能破坏现有的调度约束。

但如果设置了maxUnavailable: 1maxSurge: 0,行为就完全相反:先删一个旧Pod,再创建新Pod。这种模式下,如果新Pod调度失败,整个集群副本数会掉到2,甚至更少,风险明显更高。我在生产环境里一般建议保留默认的maxSurge,除非有非常特殊的场景要求“不能同时存在两个版本”。

2.3 调度器决定“放哪里”之后还要考虑什么

很多人觉得调度器选完节点就完事了,其实后面还有一整套准入和绑定流程。调度器选好节点后,会把“Pod应该放在节点A”这个决定以Binding对象的方式写入etcd,然后节点上的Kubelet看到这个Pod被绑定到自己身上,才开始拉镜像、创建容器。

在这个过程中,还有一个容易被忽略的角色叫调度失败重试。Pod如果一直Pending,调度器会反复对它进行调度尝试,默认每次重试有指数退避。如果问题一直存在,调度队列里的Pod会越来越多,这时候你用kubectl describe pod能看到FailedScheduling事件,里面写着具体的失败原因。我排查问题第一步永远是先看事件,而不是看Pod日志,因为Pod还没启动,根本没有日志可看。

3. 从“更新”到“关联Pod迁移”的典型链路

3.1 场景A:节点维护触发整组Pod迁移

先讲一个非常常见的链路:节点要维护了,你执行kubectl drain node-1 --ignore-daemonsets。该节点上所有Pod被驱逐,如果这些Pod属于同一个StatefulSet,而且这个StatefulSet使用了拓扑分布约束,要求每个可用区最多一个副本,那么新Pod不一定能立刻调度到node-2,因为node-2可能已经有同名的Pod了。

我遇到过的情况是:三节点集群,StatefulSet的副本数是3,topologySpreadConstraints配置为按hostname打散。node-1故障后,本该迁移到node-2,但node-2上已经有一个相同topologyKey的Pod,导致调度器直接判定node-2不满足分布约束,新Pod只能等node-3上有空位。结果整个集群一度只有两个running副本。这个案例说明,关联Pod迁移不是“被更新Pod单独决定”的,拓扑分布约束会把它和同一组其他Pod的命运绑在一起。

3.2 场景B:更新触发的拓扑分布约束失效

再讲一个我踩过几次坑的场景:你的Deployment原本使用了podAntiAffinitytopologySpreadConstraints,保证同一服务的多个Pod分布在不同节点。更新Pod镜像时,K8s创建了新ReplicaSet的新Pod,调度器会尝试把新Pod也按同样的约束打散。

但问题在于,新Pod和旧Pod在这个瞬间是同时存在的,调度器计算分布时会把新旧Pod都算进去。如果约束是maxSkew: 1,那么当新Pod要放到node-2时,发现node-2上已经有了一个旧Pod,按skew计算可能会被拒绝,调度器只能把它放到node-3去。等旧Pod全部删除之后,新Pod的分布可能变得失衡,比如node-1上两个Pod、node-2上一个Pod、node-3上一个Pod,这个时候下一个滚动更新又会调整。

这种“更新导致分布临时不平衡”的现象其实是调度器的正常表现,但如果你没有给Pod设置足够的terminationGracePeriodSeconds,或者maxUnavailable设置得太激进,就可能出现大量Pod在更新期间集中在少数节点,给这些节点带来压力。

3.3 场景C:关联Pod亲和性引发的级联调度

还有一类容易被误判的问题是PodAffinity(Pod亲和性)。它的含义是:如果节点上已经存在某个满足标签选择器的Pod,那么新Pod愿意被调度到这些节点上。

举个例子,你有一个缓存服务和API服务,两个Deployment通过podAffinity绑定在同一个可用区或节点上,目的是让API访问缓存走内网并减少延迟。某天你单独更新API服务Pod,调度器为了保证亲和性,会优先把新API Pod放到缓存Pod所在的节点。如果该节点资源不够,新Pod会一直Pending,但不会主动去帮缓存Pod找新位置。这就是“关联Pod迁移”的另一个维度:不是关联Pod真的被迁移,而是新Pod的调度受关联Pod位置影响,反过来又可能拖累整体发布。

如果遇到这种问题,我的建议是优先检查PodAffinity和NodeAffinity的配置范围。把topologyKey定得太细(比如精确到hostname)会导致调度器选择面非常窄;定得太宽(比如到region),又失去了亲和的意义。一般按可用区来定是一个相对稳妥的折中。

4. 工程实践:用调度配置管理迁移

4.1 配置 topologySpreadConstraints 的要点

如果要让Pod更新时的分布可控,我最先推荐配置topologySpreadConstraints。这个字段的作用是让调度器尽量把同一控制器的Pod按照某个拓扑维度打散,避免把鸡蛋放在一个篮子里。

下面是一个我常用的配置示例,作用是把同一个Deployment的Pod按节点打散:

topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: app: api-server

maxSkew: 1的意思是,任意两个节点之间的Pod数量差距最多为1。whenUnsatisfiable: DoNotSchedule的含义是如果无法满足分布,就不调度,宁可让Pod Pending,也不要让它硬塞到已满的节点。另一个可选值是ScheduleAnyway,它会优先满足,但实在不行也会照常调度,适合对分布软性要求的场景。

我在配置时有几个心得:

第一,labelSelector必须和你Pod的实际标签一致,否则约束根本不会生效。这个东西用kubectl get pods --show-labels核对一下就能发现。

第二,多维度约束可以叠用,比如既按节点打散,又按可用区打散,但要注意调度器会同时计算两个维度,复杂度上升后问题排查会变困难。

第三,topologyKey的值不是随便写的,可以用内置的kubernetes.io/hostnametopology.kubernetes.io/zone,也可以用自定义节点标签。但自定义标签必须保证每个节点都有值,否则调度器计算时会跳过这些节点。

4.2 使用 podAntiAffinity 实现跨故障域

除了topologySpreadConstraintspodAntiAffinity也是控制Pod调度分布的老牌手段。它们的区别在于,topologySpreadConstraints是“尽量均分”,而podAntiAffinity是“不要和某些Pod在一起”。

下面这个配置的含义是:不要把两个带有app=api-server标签的Pod调度到同一个节点上:

affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchLabels: app: api-server topologyKey: kubernetes.io/hostname

我经常看到有人直接照抄requiredDuringSchedulingIgnoredDuringExecution,也就是把它变成硬性要求。这种写法在节点数不够时会直接导致Pod Pending。所以我更建议在非关键场景先用preferred,设定一个较高的权重让它尽量满足,而不是一上来就用required

还有一点要特别注意:podAntiAffinitytopologyKeytopologySpreadConstraintstopologyKey作用方式不同。前者表示的是“判断两个Pod是否在同一拓扑域”的粒度,后者表示的是“按哪个维度打散”。如果理解反了,很容易配置出自相矛盾的规则。

4.3 PodDisruptionBudget 对迁移的影响

提到Pod更新和迁移,绝对不能绕过 PDB。PDB的作用是限制自愿中断(voluntary disruptions)发生时同时不可用的副本数量。节点排水、主动删除Pod、滚动更新都算自愿中断,但如果节点故障导致Pod被强制驱逐,PDB就能发挥作用了。

一个常见的PDB配置如下:

apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: api-server-pdb spec: minAvailable: 2 selector: matchLabels: app: api-server

意思是:任何时候至少要有2个带app=api-server标签的Pod可用。如果你对3副本的Deployment执行节点排水,PDB会限制驱逐动作,防止一次性把3个Pod全干掉。

实际使用中,我会同时设置PDB和maxUnavailable,因为它们一个保护“自愿驱逐”,一个保护“滚动更新”。很多人只配其中一个,结果在节点维护时发现Deployment副本数掉到了1甚至0。

不过PDB也不是越多越好。如果集群里所有工作负载都配了PDB且要求非常严格,节点维护时会因为PDB不满足而卡住,连drain都执行不下去。这时候要么临时调整PDB,要么先扩副本,再把节点排空。我先后做过一个小脚本,根据PDB状态自动判断是否可以安全drain,比手动看省心很多。

4.4 设置合适的优先级(PriorityClass)与抢占

调度器还有一个容易被误解的能力:抢占(Preemption)。当一个高优先级Pod因为资源不足而Pending时,调度器可能会驱逐节点上的低优先级Pod,为高优先级Pod腾位置。这个过程其实也算一种“关联Pod迁移”,只不过触发源不是更新,而是资源竞争。

看看PriorityClass的配置:

apiVersion: scheduling.k8s.io/v1 kind: PriorityClass metadata: name: high-priority value: 1000000 globalDefault: false description: "用于关键在线业务"

我见过有团队把所有业务Pod都设置成同一个高优先级,结果调度器抢占功能完全失效,因为大家优先级一样,谁也不能抢占谁。正确的做法是分出梯队:在线业务高优、离线任务低优,这样集群资源紧张时,低优Pod会被挤走,高优Pod才能快速调度起来。

还有一点:即使配置了抢占,也不一定保证高优Pod一定能调度成功,因为抢占发生在过滤阶段之后,抢占Pod腾出的节点未必满足其他约束。比如拓扑分布约束依然可能拦住它。所以优先级和抢占只是一个“兜底”机制,不应该作为主要的调度保障来依赖。

5. 故障排查实录

5.1 更新后新Pod一直Pending

这个问题绝对排在Kubernetes故障排查榜前三位。kubectl get pods里出现一堆Pending,第一件事不是去看Deployment,而是执行:

kubectl describe pod <pod-name> -n <namespace>

重点看Events段,里面通常会有FailedScheduling事件。常见原因无非这几类:节点资源不足、节点亲和性/反亲和性不满足、持久卷调度不匹配、污点未容忍、拓扑分布约束不满足。

我有一个习惯,排查时把事件里的调度器日志也拉出来看一下,尤其是当事件信息比较笼统时。调度器Pod一般在kube-system命名空间下,它的日志会输出详细的过滤原因。比如:

kubectl logs -n kube-system kube-scheduler-<node-name> --tail=200

有一次我就是靠这条命令发现是PVC的nodeAffinity和一个节点标签不匹配导致的,前端describe里只显示“0/4 nodes available”,根本看不出真实问题。

5.2 关联Pod被误调度到同一节点

有时候你会看到两个本来应该打散的Pod跑到了同一个节点上,比如一边是数据库,一边是API,明明设置了反亲和却还是共处一室。出现这个问题,我第一个怀疑的是labelSelector写错了。

调度器判断Pod亲和性时,用的是Pod上的标签,而不是Deployment名字。如果你在Deployment的template里改了标签,但Affinity里的labelSelector还维持着旧标签,那约束自然就失效了。我建议修改模板时同时检查这两个地方。

还有一个隐蔽原因:你给Deployment配置了自定义标签,但控制器默认还会加一个pod-template-hash。如果你在标签选择器里用了这个hash,滚动更新之后hash变了,反亲和性也就匹配不上了。所以不要轻易把pod-template-hash写进亲和性选择器。

5.3 迁移过程大量节点资源紧张

节点排水或批量更新时,多组Pod同时迁移到少数节点,极易引发节点资源紧张,甚至出现驱逐风暴。这种场景我见过不止一次,特征就是:一个节点上的Pod数量暴涨,CPU和内存使用率飙升,然后Kubelet开始触发驱逐。

排查这种问题的思路是反向的:先看节点资源监控,确认哪些节点超载;再看这些超载节点上集中了哪些Deployment的Pod;最后看这些Deployment是否都使用了相同的调度规则。

我之前遇到一个比较典型的情况:三个Deployment分别设置了podAntiAffinity,但它们只对自己的副本生效,不会关心其他Deployment的Pod。所以在更新节点失败后,三个Deployment的新Pod都被调度到了同一个节点,因为该节点上没有任何一个Deployment的旧Pod,不违反各自的亲和性。这个问题单纯靠调度配置很难完全解决,只能从部署结构上尽量错开更新窗口,或使用topologySpreadConstraints把所有相关Deployment的Pod纳入同一套分布约束中。

5.4 快速排查建议与避坑小技巧

如果要我给出一套快速排查流程,大概是这样的:

首先,看kubectl get events -A,抓全局异常;其次,对Pending的Pod执行describe,确认是否有FailedScheduling;第三,去调度器日志里找过滤原因;第四,检查相关节点资源、污点、标签;最后,检查PDB和关联Pod的分布约束。

这里有几个避坑技巧,都是我实际踩出来的:

  • 不要在调度问题上靠猜测,一定要看事件和日志,信息越全越好。
  • kubectl describe node里的“非终止Pod数”不能直接等于资源占用,要看requests和limits,特别是CPU和内存的requests。
  • 修改调度相关配置后,建议在测试环境先跑一轮滚动更新,观察新Pod调度分布和旧Pod回收顺序,不要直接上生产。
  • 如果集群规模大,可以开启调度器指标,比如scheduler_pending_podsscheduler_preemption_attempts_total,用Prometheus把调度成功率长期记录下来,这个数据对分析“哪次更新导致了Pod迁移异常”很有帮助。

6. 我个人的一些落地经验

写到最后,分享几个我在生产环境里沉淀下来的通用经验。

第一,不要指望调度器自动做出“最优分布”。调度器只能按照你给的约束和打分函数做决策,它不知道你的业务高峰期是什么、哪两台机器之间有专线、哪个存储盘更稳。如果你对Pod分布有明确要求,一定要显式写出来。

第二,所有调度配置都要经过发布演练验证。尤其是带requiredDuringSchedulingIgnoredDuringExecution的硬性亲和性,以及在节点数量有限的集群里配置doNotSchedule的拓扑分布约束,一旦约束和副本数、节点数不匹配,更新直接卡死是家常便饭。

第三,PDB和滚动更新策略必须配套设置。我习惯在Deployment上同时设置maxSurgemaxUnavailable,并且为多副本应用单独建PDB。这样无论是主动发布还是被动驱逐,应用可用性都有一个兜底。

第四,关注“强制迁移”和“平滑迁移”两种模式的区别。如果应用本身是无状态的,可以接受旧Pod秒删秒建,那maxUnavailable可以稍微激进;如果应用有状态,或者启动时间很长,必须留足minReadySecondsterminationGracePeriodSeconds,否则每次更新都是一次可用性事故。

调度器相关的知识,初看只是K8s里的一块组件,实际用起来会发现它牵涉到资源管理、应用架构、发布策略,甚至容量规划。只要把“Pod更新为什么会触发关联Pod迁移”这个问题想透了,很多集群调优的问题都会迎刃而解。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询