ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Kubernetes 集群调度全解析:kube-scheduler 原理与 Pending 故障排查实战

Kubernetes 集群调度全解析:kube-scheduler 原理与 Pending 故障排查实战 做 Kubernetes 的人最烦躁的时刻之一就是Deployment 写好了kubectl apply 下去Pod 卡在 Pendingdescribe 一看 “0/3 nodes available”一头雾水。这个卡住你的东西就是集群调度。调度是 Kubernetes 里最“隐形”又最关键的机制之一kube-scheduler 在控制面里扮演的角色就像工地上排工的调度员每天有大量 Pod 入场它要决定谁去哪台机器、按什么顺序去、依据什么规则去。这篇文章是 kubernetes 系列的第五篇我把集群调度从头到尾梳理一遍包括调度器的核心运行逻辑、过滤与打分两阶段算法、影响调度结果的核心字段nodeSelector、亲和性、污点容忍、怎么通过部署 Nginx 完整验证调度链路以及我平时排查 Pending 故障的一套排查模板。内容尽量兼顾新手和有经验的同学新手可以从 0 理解调度老手可以直接跳到第 5 章看排障速查表。1. 调度的本质Pod 是怎么被“安排”的1.1 kube-scheduler 在集群里的角色Kubernetes 所有组件各司其职kube-apiserver 是所有操作的入口etcd 负责存数据kubelet 管单机容器。而 kube-scheduler 只做一件事——决定 Pod 与 Node 的绑定关系。它不是一个“分配资源”的组件真正把容器跑起来的是 kubelet调度器只负责回答一个问题这个待调度的 Pod放到集群里哪台 Node 上最合理。这个定位很多人一开始会搞混。我刚用 Kubernetes 的时候以为 scheduler 会直接把容器拉起来后来才发现它只是把 Pod 的 spec.nodeName 字段填上剩下的活全部交给目标节点上的 kubelet。这个机制非常重要理解了它你排查问题的思路就会清晰很多Pod 没起来先看它有没有绑定节点如果一直没绑定那大概率是调度环节出了问题而不是容器或镜像的问题。1.2 一次调度的完整链路一个普通的 Deployment 创建出 ReplicaSetReplicaSet 再创建出 Pod。新 Pod 写入 etcd 之后kube-scheduler 通过 watch 机制监听到这个“待调度”的 Pod随后执行两阶段计算过滤和打分。过滤是把不满足条件的 Node 直接排除掉打分是在剩下符合条件的 Node 里排优先级得分最高的 Node 胜出。调度器将结果写回 apiserverPod 的 spec.nodeName 被填上目标节点名这一步叫绑定Binding。绑定完成后目标节点上的 kubelet 看到属于自己的新 Pod才开始拉镜像、创建容器、运行探针。这条链路非常快毫秒到秒级就能完成。但如果集群规模变大、调度策略变复杂调度耗时也会上升这就是为什么生产环境要控制单集群的 Pod 数量也要关注调度器的 QPS 和并发参数。很多大规模集群的性能问题最终都出在调度链路被拖慢上。1.3 调度不是“随便找一台机器”有些同学会问Pod 那么多Node 配置都一样调度器随便放一台不就行了其实不行。一个节点是否适合运行某个 Pod要考虑的因素非常多剩余 CPU 和内存够不够、端口有没有冲突、是否满足 Pod 的亲和性要求、节点有没有污点、Pod 是否容忍这个污点、数据盘所在可用区是否匹配、机架或可用区的分布是否符合高可用要求等等。kube-scheduler 把能想到的因素都抽象成规则每一轮调度都把这些规则过一遍最终给出一个相对最优解。我习惯把调度器想象成一个非常刻板的 HR它先筛简历凡是不满足硬性条件的直接淘汰然后对剩下的人做综合评分从学历、经验、匹配度等维度加权求和取最高分。Kubernetes 把这套机制做成了插件化的调度框架目的就是让不同业务场景能定制自己的“招聘标准”。理解了这个比喻后面看过滤和打分就顺了。2. 调度算法的两段论过滤与打分2.1 过滤阶段先把不合适的节点淘汰调度器首先执行过滤Filter这个阶段也叫预选。过滤是刚性条件任何一个不满足就直接出局。常见的过滤插件有NodeName如果 Pod 指定了 spec.nodeName那就只考虑这一台节点。NodeResourcesFit节点剩余资源是否满足 Pod 的 requests。注意这里按 request 算不按 limit 算很多人在这踩坑。NodePortsPod 声明了 hostPort 时检查端口是否冲突。NodeAffinity节点标签是否满足 Pod 的 nodeAffinity 硬性要求。TaintToleration节点上的污点 Pod 是否能容忍。PodTopologySpread跨拓扑打散的约束是否满足。VolumeBindingPod 用到的 PVC 是否能在该节点上挂载比如云盘有可用区属性。过滤阶段的特点是“一票否决”。我见过不少同学以为节点资源够就能跑结果 Pod 一直 Pendingdescribe 看到 failedScheduling最后排查发现是 PVC 绑定的云盘只在某一个可用区而那个可用区的节点已经被打满其他可用区节点虽然资源充足但因为挂不上盘全部被过滤掉了。这类问题从事件里其实看得很清楚只是很多人没往存储拓扑方向想。2.2 打分阶段从合格者里选最优过滤完之后如果只剩一台节点那就直接选它如果有多台就进入打分Score环节。打分插件会各自给节点打一个分然后按权重加权求和。常见的打分项有ImageLocality节点上是否已经有了 Pod 要用的镜像。镜像已存在会加分因为省掉了拉镜像的时间。NodeResourcesBalancedAllocation节点的 CPU 和内存使用是否均衡倾向于均衡利用。NodeResourcesFit资源越充足的节点得分越高。NodeAffinity满足 nodeAffinity 的 preferred 规则会加分。InterPodAffinityPod 亲和/反亲和的加分或减分。PodTopologySpread满足拓扑打散分布的话得分更高。这里有一个很重要的经验调度器打分选出的“最优”节点不一定是长期最优因为打分只反映调度那一刻的状态。比如某节点的镜像 cache 很全连续多个 Pod 都被调度过去很快资源就打满了形成热点。生产环境里一般用 descheduler 这类组件定期重新平衡或者靠拓扑分布约束来避免热点。理解“打分是瞬时最优”这一点对排查节点负载不均衡问题非常有帮助。2.3 调度策略的配置方式老版本的 kube-scheduler 支持通过 --policy-config-file 传入 Predicates 和 Priorities 列表这也是网上很多老教程里能看到的 SchedulePolicy.json。但从 1.19 开始Kubernetes 主推 ComponentConfig 方式通过 kube-scheduler 的配置文件来定义 profile、插件启用与权重。配置文件大致长这样apiVersion: kubescheduler.config.k8s.io/v1 kind: KubeSchedulerConfiguration profiles: - schedulerName: default-scheduler plugins: score: disabled: - name: NodeResourcesLeastAllocated enabled: - name: NodeResourcesBalancedAllocation weight: 2如果你只是想控制单个应用的调度行为一般不直接改调度器配置而是通过 Pod 上的字段nodeSelector、亲和性、容忍度、拓扑分布约束来影响调度结果。改调度器配置属于集群级操作需要谨慎评估改了之后要关注是否会造成调度热点、是否影响已有的工作负载并且在测试环境充分验证后再上生产。3. 实战控制调度nodeSelector、亲和性与污点容忍3.1 nodeSelector最朴素的定点投放nodeSelector 是 Pod.spec 里的一个字段要求目标节点必须带有指定标签。用法非常简单spec: nodeSelector: disktype: ssd语义是只调度到带 disktypessd 标签的节点上。nodeSelector 的优点就是简单直观任何人看一眼就懂缺点是表达能力有限只有精确匹配没有“优先”“至少”“排除”这类逻辑。如果你的需求只是“把数据库 Pod 固定到 SSD 节点”nodeSelector 完全够用不需要引入更复杂的机制。3.2 nodeAffinity更灵活的节点偏好nodeAffinity 不是简单替代 nodeSelector它提供了更丰富的匹配表达。比如 Pod 可以声明“必须跑在 amd64 节点上但优先选 zone-a”。需求被拆成硬性和软性两部分spec: affinity: nodeAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 80 preference: matchExpressions: - key: topology.kubernetes.io/zone operator: In values: - zone-a requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/arch operator: In values: - amd64其中 requiredDuringSchedulingIgnoredDuringExecution 是硬性条件不满足就直接不调度preferredDuringSchedulingIgnoredDuringExecution 是软性偏好只在打分阶段加分。两个机制可以同时使用这也是生产环境最常用的组合拳硬性条件圈定范围软性偏好优化选择。我自己写业务 YAML 的习惯是必须满足的条件放 required带有“希望”“尽量”语义的放 preferred不用含糊的中间态。3.3 Pod 亲和与反亲和让 Pod 扎堆或散开nodeAffinity 解决的是 Pod 和节点的关系Pod 亲和/反亲和解决的则是 Pod 和 Pod 之间的关系。podAffinity 让某些 Pod 尽量跑到同一拓扑域同一节点、同一可用区适合缓存类应用和需要本地通信的服务podAntiAffinity 让某些 Pod 尽量分散适合高可用应用避免所有副本落在同一个故障域。spec: affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchLabels: app: nginx topologyKey: kubernetes.io/hostname上面这段配置的含义是集群里每一台节点最多只能运行一个 appnginx 的 Pod。如果副本数是 3而集群只有 2 台节点第三副本就会一直 Pending。这个行为很多人第一次遇到会以为是 bug其实它是在按你写的反亲和规则严格执行。所以写 podAntiAffinity 的时候一定要先算一下节点数量和拓扑分布别给自己挖坑。3.4 污点与容忍让节点主动“排斥” Pod前面的机制都是 Pod 在选节点污点Taint则是节点在挑 Pod。节点打上污点后不带对应容忍Toleration的 Pod 就不会被调度过来。污点有三种效果NoSchedule不调度新 Pod已运行的 Pod 不受影响。PreferNoSchedule尽量不调度实在没别的节点也能调度过来。NoExecute不仅不调度新 Pod已经在节点上运行的 Pod 也会被驱逐除非带容忍且容忍里有 tolerationSeconds。污点最常见的用途是节点维护。比如要对 node-1 做内核升级可以先给它打一个污点kubectl taint nodes node-1 maintenancetrue:NoSchedule这样新的工作负载不会上去而已经在跑的业务 Pod 会随着伸缩慢慢减少等节点负载降下来再执行 drain 操作影响面会小很多。维护结束后删除污点kubectl taint nodes node-1 maintenancetrue:NoSchedule-注意末尾的减号就是“删除污点”的意思。常见的坑是给节点打了污点之后忘了删导致后续新 Pod 全部挤到其他节点业务侧“莫名其妙”出现热点查半天才发现是污点残留。4. 把调度玩明白用 Deployment 部署 Nginx 验证整个链路4.1 准备一个带标签的多节点集群这一节我用一个可以照着复现的演练把前面的概念串起来。假设有一个 3 节点集群node-1、node-2、node-3先给节点打上标签kubectl label nodes node-1 disktypessd kubectl label nodes node-2 disktypesata kubectl label nodes node-3 disktypesata这里模拟的是生产环境里很常见的“某类 Pod 固定跑在某批机器”的场景。打标签之前建议先看一遍现有标签避免重复或者覆盖kubectl get nodes --show-labels4.2 用 nodeSelector 把 Nginx 固定到指定节点然后创建一个 DeploymentapiVersion: apps/v1 kind: Deployment metadata: name: nginx-sched spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: nodeSelector: disktype: ssd containers: - name: nginx image: nginx:1.27 resources: requests: cpu: 100m memory: 128Miapply 之后观察 Pod 分布kubectl get pods -o wide你会看到 3 个副本全部落在 node-1 上因为只有 node-1 带 disktypessd 标签其他节点在过滤阶段就被淘汰了。这个结果直观验证了 nodeSelector 的硬性约束也顺便演示了“节点数量不足时副本只能堆在同一台机器上”的现象。如果你想提高可用性要么增加带该标签的节点要么调整调度约束。4.3 用反亲和把 Nginx 副本打散前面 3 个副本全挤在一台节点高可用上肯定不合格。如果希望 3 个副本分散到 3 台机器给模板加上 podAntiAffinityspec: affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchLabels: app: nginx topologyKey: kubernetes.io/hostname重新 apply 后3 个副本会分别落在 3 台节点上。注意修改 Deployment 的 template 会触发滚动更新调度器会严格按照反亲和规则让新副本尽量落到还没有该应用副本的节点上。如果此时集群里只有 2 台节点那第 3 副本会一直 Pending这也是为什么我在前面强调要先数节点数量。4.4 用污点把节点从调度池里剔除继续模拟一个维护场景。假设 node-2 要临时下线先打 NoSchedule 污点kubectl taint nodes node-2 maintenancetrue:NoSchedule此时如果删除 node-2 上已有的 Nginx Pod你会发现新副本不会调度到 node-2因为 Pod 没有容忍这个污点。要让某个 Pod 能调度上去就得在模板里加容忍spec: tolerations: - key: maintenance value: true operator: Equal effect: NoSchedule加了容忍的 Pod 才被允许调度到打了对应污点的节点。生产环境里“GPU 节点只跑 GPU 任务”“裸金属节点只跑网络密集型 Pod”这类隔离诉求基本都是靠污点加容忍实现的比纯靠 nodeSelector 干净得多因为节点可以主动声明自己的“准入条件”。4.5 观察调度事件与结果调试调度问题最核心的技能是看事件。一个 Pod 刚创建时执行kubectl describe pod nginx-sched-xxxxx在 Events 字段能看到调度的完整轨迹Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 12s default-scheduler Successfully assigned default/nginx-sched-xxxxx to node-1如果调度失败Reason 是 FailedSchedulingMessage 会列出不满足的条件。看到 “0/3 nodes available” 不要慌展开 events 看后面的括号内容里面把每个节点被过滤的原因都写得清清楚楚比你自己瞎猜快得多。5. 调度问题排查指南与调度框架扩展5.1 Pending 问题的标准排查流程Pod 一直 Pending 是 Kubernetes 里最常见的故障之一按下面的顺序排查基本不会漏看事件kubectl describe pod 优先看 Events 里的 FailedScheduling。看节点资源kubectl top nodes确认节点是不是真的没有足够资源。看污点kubectl describe nodes 看 Taints 字段再对比 Pod 的 Tolerations。看亲和性确认 Pod 的 nodeSelector、nodeAffinity、podAffinity 是否与节点标签匹配。看 PVC如果使用持久化存储确认 PVC 是否 Bound、PV 所在可用区是否与节点匹配。看配额检查是否达到 ResourceQuota 或 LimitRange 的限制。我平时最顺手的命令组合是kubectl get pods -o wide -A | grep Pending kubectl describe pod namespace/pod | tail -20 kubectl get events --sort-by.metadata.creationTimestamp | tail -30先全局找到 Pending 的 Pod再 describe 单个 Pod 看调度拒绝理由最后看集群级事件找共性。三步走下来90% 的调度问题都能定位到原因。5.2 常见调度失败原因速查表这是我平时积累的一张速查表遇到 Pending 直接对着查现象常见原因快速验证方式0/3 nodes available: insufficient cpu/memory节点资源不足kubectl top nodes / kubectl describe node0/3 nodes available: didnt match nodeSelector节点标签不匹配kubectl get nodes --show-labels0/3 nodes available: node(s) had taintsPod 没有对应容忍kubectl describe node 看 Taints0/2 nodes available: didnt match pod anti-affinity rules反亲和规则限制数副本数和节点数是否匹配pod has unbound immediate PersistentVolumeClaimsPVC 没有绑定kubectl get pvc / pvnode(s) didnt find available persistent volumes to bind存储拓扑不匹配看 PV 的 zone 与节点 zone节点很多但 Pod 总堆在同一台打分权重或镜像本地性导致热点调整打分插件或加拓扑分布约束这张表我建议保存下来。遇到 Pending 问题先按表格对照事件信息里的关键词通常几分钟就能定位方向而不是反复 delete 重建 Pod 碰运气。5.3 调度框架与自定义调度器如果你对调度源码感兴趣从 1.15 开始引入的调度框架值得了解一下。它把调度过程拆成了多个扩展点PreFilter、Filter、PreScore、Score、Reserve、Permit、PreBind、Bind、PostBind每个扩展点都可以用插件注入自定义逻辑。社区里有很多人基于这些扩展点做自定义调度器处理特殊诉求比如根据 GPU 显存、网络延迟或者业务自定义指标来决定调度。实现自定义调度器通常有两种方式一是写一个独立调度器二进制监听 Pod 更新并执行自己的调度算法最后通过 POST binding 写绑定二是用调度框架的插件模式在默认调度器里挂自己的扩展点。第一种适合完全自定义的场景第二种适合在默认调度基础上做增强。老实说绝大多数团队用不到自定义调度器把 nodeSelector、亲和性、污点容忍和拓扑分布约束用熟练已经能覆盖 95% 以上的场景。真有特殊需求时我更推荐先看看能不能用现成的控制器解决比如 descheduler 处理资源碎片化而不是一上来就写调度器。6. 聊聊我在调度上踩过的坑6.1 request 和 limit 的误区调度只看 requests不看 limits这个规则非常反直觉但非常关键。如果一个节点上所有 Pod 的 requests 总和已经接近 100%即便实际负载很低新 Pod 也会因为资源不足而无法调度。反过来说Pod 的 limit 设置很高但 request 很低节点照样会因为 requests 不足而拒绝调度。很多人给容器设 limits 时习惯往大了写以为这是“防止资源超卖”结果集群资源被 requests 占死实际利用率却很低。所以我一直强调生产环境一定要重视 requests 的设置这不仅是调度问题更是容量规划问题。6.2 标签混乱带来的隐性故障标签是调度的基础但标签管理不当会带来很多隐性故障。有人把节点标签当成临时备注用调完就忘有人复制 YAML 时搞混 labelSelector 和 nodeSelector导致调度行为完全偏离预期。我踩过一次很深的坑某次把节点标签改了导致一批带 nodeSelector 的 DaemonSet 之外的工作负载全部 Pending排查了大半天才发现是标签被误删。从那之后我的习惯是重要标签统一命名规范、记录在文档里每次变更前先 kubectl get nodes --show-labels 检查一遍再执行修改。6.3 如何深入调度器源码如果你喜欢刨根问底可以看看 scheduler 的源码入口在 kubernetes 仓库的 pkg/scheduler 目录。核心数据结构是 scheduleOne它完整走一遍调度主流程然后调用 framework 的 RunFilterPlugins 和 RunScorePlugins 把插件串起来。社区里口碑很好的《深入理解 Kubernetes 源码》可以作为参考书不过书只能帮你搭骨架真正理解调度还是得动手改一两个示例插件或者在测试环境把调度器日志开到 V5看它每一步到底在干什么。这里分享一个很多人没用过的技巧给 kube-scheduler 加上 -v5 参数日志会变得非常详细每个插件对每个节点打了多少分、为什么加分为什么减分都看得清清楚楚。排查打分类问题、或者想验证自定义调度策略是否生效的时候这个级别的日志比任何文档都管用。我个人在实际操作中的体会是调度器不难理解难的是你愿不愿意打开日志、沉下心把那几个关键插件的行为看透。看透了之后你会发现所谓“调度很玄学”的说法其实只是因为没去看底层数据。
返回列表