资讯动态

Kubernetes Pod更新与迁移:调度器如何决定关联Pod的分布

发布时间:2026/9/10 3:38:59 来源:尧图企业网站定制
1. 先从“Pod更新”和“Pod迁移”的基本概念说起1.1 为什么说Pod不会自己“迁移”在Kubernetes里“Pod迁移”这个说法其实有点误导。严格意义上Pod从来不会从一台节点“走”到另一台节点它只存在三种命运被创建、被删除、被重新创建。所谓的迁移本质上就是“先删除旧Pod再在另一个节点上创建新Pod”中间那个瞬间应用是不可用的除非你有多个副本在同时工作。这个机制很多刚接触K8s的人会理解错以为Pod像虚拟机一样可以热迁移。实际上Kubelet只负责维护本节点上的Pod生命周期它没有能力把一个已经运行的容器从节点A搬到节点B。就算你用kubectl cordon加kubectl 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控制器会马上补一个新的出来由调度器重新安排位置。这个“重新安排位置”就是最典型的迁移动作。如果驱逐时PDBPodDisruptionBudget设置了maxUnavailable: 0驱逐还会被阻塞这也是迁移流程里很重要的一个环节。第三类是手动删除重建。比如你用kubectl delete pod删掉一个Pod或者直接改了Pod的标签导致控制器重建。这种情况下新Pod也要经过调度器。别小看这个场景很多关联Pod迁移的“诡异现象”就是从这里来的你删了一个Pod结果另一个Pod也被重建了究其原因往往是它们共享同一个控制器、同一个拓扑约束或者同一个污点容忍逻辑。2. 调度器在Pod更新中担当的角色2.1 滚动更新和调度过程的关系先看一个最常见的滚动更新流程。假设有一个Deployment三个副本分布在三个节点上你执行kubectl set image deployment/app appregistry.example.com/app:v2K8s会先创建一个新的ReplicaSet然后把新ReplicaSet的副本数从0往上加同时把旧ReplicaSet的副本数从3往下减。问题来了新Pod创建之后调度器不是“随机挑一个节点”而是要经过一整套打分和过滤机制。默认调度器会先执行预选Filtering把不满足条件的节点筛掉比如节点资源不够、标签不匹配、端口冲突、存储卷不匹配然后在剩下的节点里执行优选Scoring按资源利用率、Pod分布、亲和性等维度打分最后挑出得分最高的节点。我实际操作中遇到过一种情况新版本Pod对CPU的requests从0.5核调到了1核滚动更新发布后三个新Pod全部Pending一直等到我把集群里一台空闲节点加进来才恢复。原因就是所有现有节点在过滤阶段就被筛掉了没有任何一个节点能同时满足资源预留和拓扑分布约束。调度器本身没有错它只是忠实执行了规则真正的问题出在发布前没人评估资源增量。2.2 为什么新Pod总会先于旧Pod消失之前出现Deployment的滚动更新默认策略是RollingUpdate其中有个关键参数maxSurge和maxUnavailable。maxSurge控制更新期间允许超出期望副本数的Pod数量默认是25%maxUnavailable控制允许不可用的Pod数量默认也是25%。这意味着默认情况下K8s会先创建新Pod等新Pod进入Ready状态后才删一个旧Pod。所以你在滚动更新过程中会看到新旧Pod共存比如3副本的Deployment更新期间可能出现4个Pod。调度器在这种情况下要做的事情很明确给新Pod找到位置同时不能破坏现有的调度约束。但如果设置了maxUnavailable: 1、maxSurge: 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的副本数是3topologySpreadConstraints配置为按hostname打散。node-1故障后本该迁移到node-2但node-2上已经有一个相同topologyKey的Pod导致调度器直接判定node-2不满足分布约束新Pod只能等node-3上有空位。结果整个集群一度只有两个running副本。这个案例说明关联Pod迁移不是“被更新Pod单独决定”的拓扑分布约束会把它和同一组其他Pod的命运绑在一起。3.2 场景B更新触发的拓扑分布约束失效再讲一个我踩过几次坑的场景你的Deployment原本使用了podAntiAffinity或topologySpreadConstraints保证同一服务的多个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亲和性引发的级联调度还有一类容易被误判的问题是PodAffinityPod亲和性。它的含义是如果节点上已经存在某个满足标签选择器的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-servermaxSkew: 1的意思是任意两个节点之间的Pod数量差距最多为1。whenUnsatisfiable: DoNotSchedule的含义是如果无法满足分布就不调度宁可让Pod Pending也不要让它硬塞到已满的节点。另一个可选值是ScheduleAnyway它会优先满足但实在不行也会照常调度适合对分布软性要求的场景。我在配置时有几个心得第一labelSelector必须和你Pod的实际标签一致否则约束根本不会生效。这个东西用kubectl get pods --show-labels核对一下就能发现。第二多维度约束可以叠用比如既按节点打散又按可用区打散但要注意调度器会同时计算两个维度复杂度上升后问题排查会变困难。第三topologyKey的值不是随便写的可以用内置的kubernetes.io/hostname、topology.kubernetes.io/zone也可以用自定义节点标签。但自定义标签必须保证每个节点都有值否则调度器计算时会跳过这些节点。4.2 使用 podAntiAffinity 实现跨故障域除了topologySpreadConstraintspodAntiAffinity也是控制Pod调度分布的老牌手段。它们的区别在于topologySpreadConstraints是“尽量均分”而podAntiAffinity是“不要和某些Pod在一起”。下面这个配置的含义是不要把两个带有appapi-server标签的Pod调度到同一个节点上affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchLabels: app: api-server topologyKey: kubernetes.io/hostname我经常看到有人直接照抄requiredDuringSchedulingIgnoredDuringExecution也就是把它变成硬性要求。这种写法在节点数不够时会直接导致Pod Pending。所以我更建议在非关键场景先用preferred设定一个较高的权重让它尽量满足而不是一上来就用required。还有一点要特别注意podAntiAffinity的topologyKey和topologySpreadConstraints的topologyKey作用方式不同。前者表示的是“判断两个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个带appapi-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 --tail200有一次我就是靠这条命令发现是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_pods、scheduler_preemption_attempts_total用Prometheus把调度成功率长期记录下来这个数据对分析“哪次更新导致了Pod迁移异常”很有帮助。6. 我个人的一些落地经验写到最后分享几个我在生产环境里沉淀下来的通用经验。第一不要指望调度器自动做出“最优分布”。调度器只能按照你给的约束和打分函数做决策它不知道你的业务高峰期是什么、哪两台机器之间有专线、哪个存储盘更稳。如果你对Pod分布有明确要求一定要显式写出来。第二所有调度配置都要经过发布演练验证。尤其是带requiredDuringSchedulingIgnoredDuringExecution的硬性亲和性以及在节点数量有限的集群里配置doNotSchedule的拓扑分布约束一旦约束和副本数、节点数不匹配更新直接卡死是家常便饭。第三PDB和滚动更新策略必须配套设置。我习惯在Deployment上同时设置maxSurge、maxUnavailable并且为多副本应用单独建PDB。这样无论是主动发布还是被动驱逐应用可用性都有一个兜底。第四关注“强制迁移”和“平滑迁移”两种模式的区别。如果应用本身是无状态的可以接受旧Pod秒删秒建那maxUnavailable可以稍微激进如果应用有状态或者启动时间很长必须留足minReadySeconds和terminationGracePeriodSeconds否则每次更新都是一次可用性事故。调度器相关的知识初看只是K8s里的一块组件实际用起来会发现它牵涉到资源管理、应用架构、发布策略甚至容量规划。只要把“Pod更新为什么会触发关联Pod迁移”这个问题想透了很多集群调优的问题都会迎刃而解。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价