拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Kubernetes调度全解析:从kube-scheduler原理到Pod调度策略实战

1. 从“挖坑”说起为什么每个k8s玩家迟早都要面对调度聊k8sKubernetes的人越来越多但真正能把“调度”这件事讲清楚的不多。很多人部署完集群天天忙着折腾网络插件、存储插件、Ingress等到Pod莫名其妙卡在Pending状态才第一次正视这个名字有点奇怪的组件——kube-scheduler。我见过不少运维和开发平时写YAML写得飞起一遇到Pod调度不过去就只会看events然后两眼一抹黑。这篇文章就用“在k8s调度的花园里面挖呀挖”这个梗把调度这个主题从里到外刨一遍。不管你是刚搭好集群的新手还是已经踩过无数调度坑的老手我希望你看完之后能搞明白这几件事k8s调度器到底在做什么、它是怎么做决策的、你能在哪些环节插手、以及最常见的调度问题到底怎么排查。我默认你会一点k8s基础操作至少知道Pod、Node、Deployment这几个概念。如果你还没接触过建议先花半小时跑一遍kubectl get nodes和kubectl get pods再回来读这篇文章体验会好很多。2. 调度的本质把Pod放到哪台机器上这是一道“筛选打分”题2.1 调度器不是随随便便把Pod扔给一台机器你在k8s里创建一个Deployment控制器Deployment Controller负责保证副本数但它不负责把Pod放到具体哪台节点上。这个“放哪儿”的决定就是调度器kube-scheduler的职责。Kubernetes集群里的节点Node是异构的有些机器CPU强、有些内存大、有些挂着GPU、有些在特定可用区。Pod对资源的需求也五花八门有的要求至少2核4G有的要绑定某块硬盘有的因为延迟敏感必须跑到离数据最近的机器上。调度器的任务就是在所有节点中找一个“最合适”的节点把Pending的Pod放上去。我习惯把调度比作选房子你手里有一堆房源节点你有一堆要求资源需求、位置需求、互斥需求中介调度器先帮你过滤掉不符合硬性条件的房子再按照你的偏好给每套房子打分最后挑出分数最高、性价比最好的那一套签约搬进去。这个过程在k8s里对应两个核心阶段过滤Filtering和打分Scoring。2.2 过滤阶段Predicates先把不合适的节点剔除过滤阶段也叫Predicates预选它的职责是判断“这个节点能不能跑这个Pod”。任何一个条件不满足节点直接淘汰。常见的过滤条件包括资源充足性节点剩余CPU、内存是否满足Pod的requests请求值。端口冲突Pod想用宿主机某个端口hostPort但该端口已被其他Pod占用。节点状态节点是否Ready、是否被标记为不可调度SchedulingDisabled。亲和性规则节点是否满足nodeSelector、NodeAffinity表达式。污点容忍节点带污点TaintPod是否声明了对应的容忍Toleration。卷和存储限制Pod引用的存储卷是否在节点上可用比如Local PV只绑定特定节点。拓扑约束比如Pod AntiAffinity要求同节点不能有另一个带特定标签的Pod。这个阶段快、狠、准能过滤的节点直接出局不会给任何商量余地。很多你写YAML时没注意的细节就是在这里把Pod卡死的。2.3 打分阶段Priorities在可选范围内选出“最优解”过了过滤阶段之后剩下的节点都是“能跑”的但“能跑”不等于“跑得最好”。打分阶段会把每个候选节点按一系列权重策略打分分值最高的节点胜出。常见的打分策略包括LeastRequestedPriority节点空闲资源越多分越高倾向于把Pod散开到资源富裕的机器上。BalancedResourceAllocationCPU和内存的使用率越均衡分越高避免出现CPU打满内存闲着或者反过来。ImageLocalityPriority节点上已经有所需镜像的加分省去拉镜像的时间。NodeAffinityPriority满足亲和性偏好preferred的节点加分。InterPodAffinityPriority满足Pod间亲和/反亲和的节点加减分。TaintTolerationPriority对污点容忍度好的节点加分尽量避免把Pod调度到有污点的机器上。这个阶段是“择优”没有绝对的对错不同策略之间权重不同最终结果也不同。社区经常说的“调度器版本差异导致同样的Pod被调度到不同节点”根源就在这里——打分策略在不同版本里可能被调整过。2.4 调度器不负责“运行”只负责“安排”这里有个新手容易混淆的点kube-scheduler只负责给Pod选好节点并把Pod和节点的绑定关系Binding写入etcd。真正把Pod拉到节点上并启动容器的是节点上的kubelet。所以你会看到这样的现象调度器明明给Pod选好了节点但Pod到了节点上启动失败比如镜像拉不下来、卷挂载失败。这类问题严格来说不属于调度问题而是kubelet层面的事情。排查时候要分清界线Pod卡在Pending才找调度器Pod已经Bound但ContainerCreating就得去看节点上的kubelet日志了。3. 动手实践从默认调度器到自定义调度策略3.1 先看默认调度器怎么工作在学习怎么干预调度之前最好先亲眼看一下默认调度器的运行过程。一个有用的方法是给调度器开日志或者直接看Pod的调度事件kubectl describe pod pod-name输出里有Events这一段会明确记录调度器何时开始调度选中的节点是哪台是否发生过抢占Preemption有没有成功绑定有一次我遇到一个Pod一直在Pendingdescribe之后发现事件里写着“0/5 nodes are available”下面列了每个节点失败的原因——有的节点是“Insufficient memory”有的是“node(s) had taint...”一目了然。这种排查方式效率极高比盲猜快得多。3.2 用nodeSelector做最简单的定向调度nodeSelector是最原始的调度干预方式说白了就是给节点打标签然后在Pod里指定要调度到带哪些标签的节点上。先给节点打标签kubectl label nodes node01 disktypessd然后Pod里这样写apiVersion: v1 kind: Pod metadata: name: test-pod spec: nodeSelector: disktype: ssd containers: - name: nginx image: nginx这样调度器在过滤阶段就会把不带disktypessd标签的节点全部排除。不过nodeSelector太“简单粗暴”了它只能做精确匹配。比如你想表达“优先调度到ssd节点但没有ssd节点时也可以放到普通节点”nodeSelector做不到。这种场景就要用NodeAffinity。3.3 NodeAffinity从“必须”到“最好”的灵活表达NodeAffinity有两大类型requiredDuringSchedulingIgnoredDuringExecution硬性要求相当于进阶版nodeSelector调度时不满足直接不调度。preferredDuringSchedulingIgnoredDuringExecution软性偏好满足加分不满足也不排除只是分低。写法示例spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: disktype operator: In values: - ssd preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 preference: matchExpressions: - key: zone operator: In values: - east这个例子里Pod必须落到disktypessd的节点上但如果同时满足zoneeast会被优先选择。权重weight可以理解为打分阶段的附加分数值越大影响力越大。这里要注意命名里那段很长的后缀IgnoredDuringExecution它的意思是调度阶段规则生效但如果节点在运行过程中标签变了导致规则不再满足k8s也不会把已经跑在上面的Pod赶走。理解这个语义很重要才不会在运维时被现象误导。3.4 Taint和Toleration节点怎么“拒绝”PodNodeAffinity是Pod主动选择节点Taint则反过来是节点主动拒绝Pod。节点上打了污点默认任何Pod都不能调度上去除非Pod声明了对应的容忍。给节点打污点kubectl taint nodes node02 gputrue:NoSchedulePod声明容忍spec: tolerations: - key: gpu operator: Equal value: true effect: NoSchedule这个机制最常见的应用场景给集群里的GPU节点打污点只有声明了GPU需求的Pod才能调度上去其他普通Pod全部绕开。污点的effect有三种NoSchedule不调度新Pod上去已有Pod不受影响。PreferNoSchedule尽量不调度但实在没地方放了也会放。NoExecute不仅不调度新Pod还会把节点上已有、且不匹配的Pod全部驱逐。我把三者比喻成物业的三种管理风格NoSchedule是“别再带新租客来了”PreferNoSchedule是“尽量别带新租客老租客不赶”NoExecute是“新租客不让进老租客也请搬走”。使用效果要区分清楚特别是在做节点维护、下线和故障隔离时很多人因为混淆了NoSchedule和NoExecute的差异导致节点上存量Pod在维护时被强制驱逐影响了业务。3.5 节点亲和性和污点容忍的组合拳实际生产环境里这几种策略往往配合使用而不是单独出现。比如一个常见的机器学习平台场景集群里有8台CPU机器、2台GPU机器。GPU机器打了污点gputrue:NoScheduleGPU任务Pod声明对应容忍并加nodeSelector强制落到gputrue的节点上。CPU通用服务则不需要容忍自然不会被调度到GPU机器上。这样一套组合下来资源隔离清晰、互不干扰。再比如数据库类的有状态应用你要求它和另一个特定应用尽量处于同一可用区减少跨可用区网络延迟可以用PodAffinity让它们“粘”在一起同时又要求它们不能部署在同一台物理机上避免单点故障属于典型的“既要亲和又要反亲和”的调度需求在k8s里这两件事可以同时表达调度器会综合考虑。3.6 自定义调度器当默认调度器不能满足你的时候Kubernetes可以运行多个调度器。默认的kube-scheduler处理大部分工作负载额外部署一个自定义调度器处理特殊需求互不干扰。Deployment里指定调度器spec: schedulerName: my-scheduler如果Pod里写了schedulerName: my-scheduler默认调度器会直接跳过它由同名自定义调度器接管。自定义调度器的写法可以是简单的Shell脚本加kubectl也可以是基于scheduler framework的扩展插件。我见过有人用几百行Python写一个满足内部需求的调度器逻辑就是轮询所有Pending Pod检查资源、亲和性然后调k8s API执行绑定。这种做法的价值在于当你对默认调度策略实在不够满意、又不想啃一堆Go代码时快速撸一个符合自己业务逻辑的调度器完全可行。不过这里我得泼一盆冷水自定义调度器等于你自己接管了“容器放哪里”的所有责任调度失败、资源碎片化、节点负载不均这些问题都要你自己扛。如果你只是需要一个调度策略上的微调优先考虑给默认调度器加扩展点Scheduler Extender或者直接用上面的Affinity/Taint原生能力不要一上来就自研调度器。3.7 PodTopologySpreadConstraints让Pod在拓扑间均匀分布还有一个比较现代的能力叫PodTopologySpreadConstraints它解决的是“Pod在节点/可用区之间分布不均”的问题。默认调度器打分会考虑资源但不会特意把同一应用的Pod均匀摊开。如果你有3个节点跑3个副本很可能3个Pod全被丢到同一台机器上——虽然资源都满足但一台挂了业务全挂。通过声明拓扑分布约束可以让调度器尽量把Pod分散开spec: topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: app: web这段配置的意思以节点主机名为拓扑维度让同一appweb的Pod在不同节点上的数量差最多不超过1maxSkew1。如果不满足就干脆不调度DoNotSchedule等有合适节点了再调度。把whenUnsatisfiable换成ScheduleAnyway则是“尽量均匀但别卡死”。这个功能特别适合多可用区部署的场景比如公有云上你希望Pod在3个可用区各放一个防止整个可用区故障导致服务不可用。这时候topologyKey用topology.kubernetes.io/zone比手动维护nodeSelector简单得多。4. 调度的“隐藏玩法”优先级、抢占与批处理4.1 优先级高的Pod会挤掉低优先级的吗Kubernetes里Pod有优先级概念PriorityClass定义了优先级级别。当高优先级Pod调度不进去时调度器会触发抢占Preemption把节点上低优先级的Pod挤掉腾出资源给高优先级Pod。PriorityClass示例apiVersion: scheduling.k8s.io/v1 kind: PriorityClass metadata: name: high-priority value: 1000000 globalDefault: false description: 用于核心业务PodPod里引用spec: priorityClassName: high-priority这里有几个容易踩坑的细节PriorityClass的value越大优先级越高。抢占只能发生在节点资源不足时如果高优先级Pod调度过去了但低优先级Pod还在Terminating过程中需要等被挤掉的Pod退出后新Pod才真正启动。这个中间态容易让人误以为“抢占不生效”。被抢占的Pod是优雅退出还是被强制kill受terminationGracePeriodSeconds影响。如果节点上所有低优先级Pod都被挤掉了还是不够放调度器会尝试在其他节点抢占如果都没有合适的节点新Pod继续保持Pending。我用这个机制做过一次线上故障紧急扩容核心接口的Pod因为节点资源不足一直调度不上去我给它们加了高优先级PriorityClass几十秒内调度器挤掉了一批批任务型Pod核心Pod成功跑起来。但那批批任务Pod被中断的代价也是真实的所以不要随便给所有Pod都设高优先级优先级膨胀会导致低优先级任务常年饿死。4.2 批处理场景把Pod塞满还是打散工作负载类型不同对调度的诉求可能完全相反。在线Web服务通常希望Pod分散开避免单点故障批处理任务比如跑模型训练、数据清洗反而希望把多个Pod尽量聚在一起利用本地数据、本地缓存提升性能。批处理场景最经典的需求是“把任务调度到已经缓存了数据的节点上”如果那台节点资源不够宁可等也不能去别的节点重新拉数据。这个诉求用nodeAffinity、taint/toleration、PodAffinity组合基本都能实现。还有一个痛点批处理任务通常短时、量大、创建销毁频繁默认调度器每个Pod独立评分、独立决策会产生资源碎片。社区有专门针对批处理的调度器如Volcano、Kueue我建议团队资源允许时考虑引入用它对Gang Scheduling的支持来保证“要么一堆Pod全部调度成功要么全部等待”这对跑分布式训练非常有用——试过的人都知道一半Pod在跑、一半Pod卡Pending是最让人崩溃的场景。4.3 云边协同下的调度新挑战随着边缘计算场景越来越多调度不再只是“集群内部选一个节点”的问题而是要跨云、跨边、跨网络层层协同。云边场景下中间节点网络不稳定、算力有限、离线自治需求突出传统调度器的一套模型很难直接搬过去。现在比较常见的做法是在云端部署中心控制面边缘侧通过分布式调度或缓存机制处理局部调度决策。数据面和控制面要分离边缘节点即使断网本地的任务调度、重启、拉起等操作也要能自主运行。这个方向正在快速迭代我先提个思路后面有机会单独写一篇展开。5. 调度体质的养生法资源申请、PodDisruptionBudget与调度失败排查5.1 requests和limits调度决策的基础数据调度器判断资源是否充足依据是Pod声明的requests而不是实际运行时消耗的资源。也就是说一个Pod运行时只用了0.1核CPU但如果它声明requests为4核调度器会按照它需要4核来预留资源。所以写YAML时requests一定要贴近真实需求写大了浪费集群资源写小了可能引发CPU节流throttling甚至OOM Kill。limits是上限约束超过之后会被杀死内存或限流CPU。我见过一个最典型的资源浪费例子某个服务真实内存用量只有200M但YAML里requests写的是2Gi整个集群几十个副本白白浪费了几十G内存调度容量。之后我把所有服务的requests按过去两周监控的P95用量重新校准集群容量立刻多出来一大截。这里再补充一个经验每次调整requests后观察一段时间Pod是否出现OOM或频繁驱逐如果没有说明调整方向正确。5.2 PodDisruptionBudget主动驱逐时的保命符当你要主动对节点做维护比如升级内核、更换硬件、调整OS参数通常需要把节点上的Pod驱走。如果不加任何保护k8s一次可能把所有副本全部驱逐造成服务不可用。PodDisruptionBudgetPDB就是干这个的apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: web-pdb spec: minAvailable: 2 selector: matchLabels: app: web这个配置表示主动驱逐时appweb的Pod至少要保持2个可用。或者你也可以用maxUnavailable: 1表示最多允许1个不可用。注意PDB只约束主动驱逐比如drain节点不约束节点宕机、抢占之类的情形。我多次在节点维护时遇到过类似状况节点驱逐Pod之后一直不完成查看PDB发现被卡住了因为另一台节点上的Pod也处于不健康状态可用数量达不到PDB阈值导致节点drain死等。这时候要么先把不健康的Pod修好要么临时把PDB调低让维护流程继续。5.3 调度失败排查思路一个标准化的五步走流程针对这类问题我总结了一个自己的排查顺序刚开始接触k8s调度的朋友可以直接照搬第一步看Pod状态和事件kubectl describe pod xxx永远是第一手信息Events字段基本能把调度器拒绝的原因写清楚。如果事件里没有调度器相关内容有可能是写错了schedulerName导致没有对应的调度器在处理。第二步看节点资源水位kubectl top nodes看一下各节点剩余资源是否真的满足Pod的requests。这里特别注意kubectl top看到的是实时使用量而调度器判断依据的是已声明的requests总量。有可能节点CPU实时用量才30%但所有Pod声明的requests已经把CPU给分配完了新Pod依然调度不进去这不是调度器坏了而是资源模型设计如此。第三步检查亲和性和污点用kubectl get nodes --show-labels看节点标签是否齐全用kubectl describe node看节点Taints。很多时候是节点维护时打了污点或者新增节点忘打标签导致Pod的亲和性要求不能被满足。第四步检查存储卷涉及PVC的Pod调度器会检查PV的节点可用性。如果用的是Local PVPod只能调度到PV所在节点其他节点全部过滤掉。如果那台节点资源不够Pod就会一直Pending。第五步看是否有优先级和抢占逻辑干扰高优先级Pod会打断正常的调度流程。遇到Pod Pending先查集群是否有高优先级Pod在等待它们可能正在触发抢占导致了调度器行为和你预期的不一致。5.4 常见调度问题速查表现象常见原因排查方向Pod一直Pending、无事件更新schedulerName写错、自定义调度器没部署describe看schedulerName查调度器Pod状态事件提示节点资源不足requests声明过大、节点配置过小kubectl describe node看Allocated resources事件提示节点有污点节点维护、GPU专用、专有节点kubectl get nodes -o json | jq .items[].spec.taints部分节点被过滤但原因不明NodeAffinity写错、标签不匹配对比Pod亲和语法和节点标签高优先级Pod抢占后仍在Pending被抢占Pod尚未完全退出、抢占目标节点不够观察系统事件、查看被抢占Pod的Terminating状态Pod调度到节点后启动缓慢镜像拉取量大、存储卷挂载慢节点上查看kubelet日志和容器运行时日志应用副本数正常但负载不均没有TopologySpreadConstraints、打分策略天然分布检查Pod分布考虑引入topologySpreadConstraints这张表建议大家截图收藏遇到问题先对着表看一遍90%的调度异常都能对号入座。5.5 集群规模大时的性能问题调度器在大型集群里也会遇到瓶颈。一个数千节点、数万Pod的集群里调度器每次调度都要对所有节点做过滤和打分计算量不小。早期版本出现过调度性能下降、调度时延变长的问题新版本针对性地优化了调度队列和缓存机制但架构上依然要求我们尽量控制集群规模。实践经验是把集群拆成多个小集群按业务域隔离每个集群几百到一千节点性能和运维复杂度都在可控范围内。如果你确实需要在单集群里跑超大规模建议认真给调度器做参数调优设置合理的percentageOfNodesToScore值限制打分节点比例避免每次调度都遍历全部节点。6. 我的一点体会折腾k8s调度这几年最大的感受是调度器本身并不复杂复杂的是你对自己的业务和底层资源到底有多少理解。你知道每个Pod需要多少资源、能容忍什么故障、必须靠近什么数据调度策略自然清晰反过来什么都不清楚就指望调度器“自动优化好一切”大概率只能得到一堆无序散落的Pod。Taint、Affinity、TopologySpreadConstraints、PriorityClass、PDB这些能力每一样单独看都不难难的是组合运用。我建议你现在就打开自己集群里的Pod定义看一遍每个字段有没有把资源预留、打散策略、故障域隔离这套组合拳打完整。补齐了这些你再回头看那句“在k8s调度的花园里面挖呀挖”会发现花园里能挖的东西确实还有很多。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门