资讯详情

资讯详情

Kubernetes调度全解析:kube-scheduler、亲和性与污点实战

在Kubernetes里摸爬滚打久了你会发现一个特别典型的现象明明集群里几十个节点CPU和内存总量看着都够可你的Pod就是卡在Pending起不来。或者节点负载两极分化严重一台机器CPU被打满旁边一台闲到发慌。这种时候问题大概率不是资源不够而是调度策略没搞对。Kubernetes集群调度说白了就是回答一个问题一个新建的Pod到底该放到哪台Node上这个过程由kube-scheduler组件负责它决定了工作负载最终落在哪些机器上也决定了整个集群的资源利用率、稳定性和故障域分布。调度的好坏直接影响你在生产环境里能不能睡得着觉。这篇文章是Kubernetes系列第五篇我会从调度器的角色定位、一次完整调度的生命周期、预选与优选机制、亲和性和污点容忍度到源码层面的关键结构一路讲到生产环境里真正会踩到的坑。适合刚学完Pod和Deployment、开始接触集群资源管理的人也适合已经在用K8s但总觉得调度不太听话的运维同学。1. 调度器到底在干什么一个关于分配的精确问题先消除一个常见误解调度器不是负载均衡器。它不会因为你某个节点CPU使用率高就把新Pod塞过去也不会因为某个节点空闲就把Pod往那儿堆。它做的是过滤打分两件事像是招聘的HR先筛掉不符合硬性条件的候选人再给剩下的人按岗位匹配度排个序最后选综合得分最高的那个。1.1 调度器不是分蛋糕而是找最合适的坑你可能听过一句话Pod调度就是给Pod找个Node当成家。但这个最合适三个字才是关键所在。举个例子。你有三台节点Node A4核8G标签diskssd上面跑着大量CPU密集型业务Node B8核16G标签diskhdd刚加了新机器资源基本是空的Node C4核8G标签diskssd跑了几个内存型Pod这时候来了一个Pod声明需要2核4G并且要求必须落在SSD节点上通过nodeSelector指定diskssd。预选阶段直接就把Node B淘汰了哪怕它再空闲也不行因为硬性条件不满足。剩下的A和C再进入打分环节结合各类优先级策略综合比较才会得出最终选择。所以调度器的核心逻辑不是哪里空就往哪里去而是在满足硬性约束的前提下综合软性偏好挑一个最合理的落点。这个逻辑你要是没搞清楚后面所有调度策略都容易理解偏。1.2 一套调度三个参与方说到调度很多人以为只是kube-scheduler一个组件的事。实际整个流程是三个角色协作完成kube-apiserver所有调度信息的来源也是调度结果的落点。kube-scheduler负责决策算出这个Pod应该去哪个Node。kubelet调度决策的执行者收到绑定消息后真正把Pod跑起来。调度器并不直接把Pod推到节点上它只是往APIServer写一条Binding记录声明Pod X被分配到Node Y。剩下的活儿交给目标节点上的kubeletkubelet watch到这条绑定信息后才会去拉镜像、创建容器。这个设计初看绕了一圈但它是Kubernetes控制面声明式思想的核心体现所有状态都通过APIServer流转调度器、kubelet各自只负责自己的职责互不干扰。这也让调度器的故障不至于直接影响已经跑起来的业务——调度器挂了只是新Pod无法调度存量Pod照常运行。2. 一次调度的完整流程从Pod创建到绑定Node中间发生了什么我见过不少同学用kubectl describe pod看到Events里报FailedScheduling第一反应就是上节点上看资源。这没问题但要精准定位调度问题你得先把一次调度的生命周期完整过一遍。2.1 一条Pod从提交到运行的完整链路假设你执行了kubectl apply -f deployment.yaml从yaml生效到Pod真正跑起来调度层面其实经历了这些环节写入APIServerDeployment控制器创建出Pod对象写入etcd此时Pod的nodeName字段还是空的。调度器watchkube-scheduler通过List/Watch机制发现集群里出现了一个spec.nodeName为空的Pod。进入调度队列这个Pod被放进调度器的活跃队列activeQ等待被处理。调度周期调度器从队列里取出Pod依次执行预选和优选两个阶段。如果通过了它会计算出最优Node但此时还没真正绑定。绑定周期调度器将Pod和Node的绑定关系写入APIServer创建Binding资源这时Pod的spec.nodeName被赋值。kubelet执行目标节点上的kubelet watch到Pod被绑定到自己身上开始按照PodSpec干活——创建sandbox、拉镜像、启动容器。注意调度器在预选阶段如果找不到任何满足条件的NodePod会被放进不可调度队列unschedulableQ。一旦集群里有Node变更事件比如新节点加入、节点资源释放调度器会把这些Pod重新拿出来再试。2.2 调度周期 vs 绑定周期为什么要拆成两段你可能会问为什么不能一步到位直接绑定调度器把调度决策和绑定执行拆成两个阶段是为了给扩展留空间。Kubernetes 1.15之后引入的**调度框架Scheduling Framework**就是在这两个阶段里插了很多扩展点比如PreFilter、Filter、Score、Reserve、Permit、PreBind、Bind等。这些扩展点有什么用最典型的就是coscheduling场景。假设你有100个Pod想等它们全部通过预选之后再做决定——这种批量调度的需求如果调度和绑定不拆开根本没有插入策略的空间。拆开之后你可以在Permit扩展点实现等齐了一组Pod再放行也可以实现自己的Bind逻辑把绑定决策交给外部系统处理比如某些场景下把Pod绑定到特定资源池。调度框架出来以后自定义调度器的开发门槛低了很多。你不再需要fork整个kube-scheduler改代码只需要写一个二进制挂在扩展点上或者直接用kube-scheduler内置的framework插件机制。2.3 调度队列的三种状态ActiveQ、UnschedulableQ、BackoffQ源码里有个很关键的数据结构叫SchedulingQueue它维护着三种队列activeQ活跃队列存放等待调度的Pod按优先级和进入时间排序。backoffQ退避队列存放调度失败但可以重试的Pod。失败后先进入退避等退避时间到了再回到activeQ。unschedulableQ不可调度队列存放预选阶段找不到任何可行Node的Pod。这些Pod除非集群条件发生变化比如新节点加入、现有节点被删掉标签否则一直待着避免无效重试。这个设计说直白点就是调度器不会对一个Pod无限重试它会衡量这次失败是不是暂时性的。如果是资源不足导致的失败BackoffQ等一会儿再试如果是硬性条件永远不满足UnschedulableQ那就不试等外部条件变化触发重新入队。排查Pod卡在Pending的时候你可以看看它具体在哪个队列里。通过kubectl describe pod看到的Events里如果一直显示同样的错误比如0/3 nodes are available: 3 Insufficient cpu基本可以确定它在backoffQ里反复重试。如果错误信息五花八门那可能是unschedulableQ里待着等条件变化。3. 预选与优选调度器如何把能去的筛出来再在能去的里挑一个预选Predicates和优选Priorities是调度器最经典的两个阶段。虽然新版本里这两个名字被Filter和Score取代了但思想还是一脉相承。这部分是调度的核心值得仔细讲。3.1 预选的常见筛选规则预选的目的是过滤把不能满足Pod运行要求的节点剔除。内置的过滤规则很多挑几个最常用的说NodeResourcesFit检查Node的可用资源是否满足Pod的request。注意是request不是limit这个很多人搞混。调度只关心你申请的最低保障是多少不关心你limit设置多大。NodeName如果PodSpec直接指定了nodeName调度器会直接跳过该Pod因为不需要调度kubelet会直接收到绑定。NodeSelectorPod上定义了nodeSelectorNode必须包含对应标签才能通过。NodeAffinity节点亲和性的硬性规则requiredDuringScheduling...在这里检查。TaintToleration检查Node上的污点Pod如果没有对应的容忍度直接过滤掉。PodFitsHostPorts如果Pod要绑定宿主机端口Node上这个端口不能被占用。CheckVolumeBinding如果Pod使用了PVC检查PVC对应的PV是否能在该Node上挂载比如某些PV有节点亲和性限制。你可以通过kube-scheduler --v6开启详细日志看到每个Pod在每个Node上被哪条规则卡住了。生产环境排查调度失败这一步是最快的。3.2 优选的打分逻辑不止是看谁最闲通过预选的节点都符合硬性条件谁优谁劣就看打分。内置优先级策略里几个核心的LeastRequestedPriority越空闲的节点得分越高。它计算的是节点可分配总量 - 已分配request/可分配总量空闲比例越大分越高。这就是大多数人理解的调度器喜欢把Pod往空闲节点放。BalancedResourceAllocation看CPU和内存的利用率是否均衡。如果一台节点CPU快用满了但内存还很空得分就低。这个策略防止节点资源碎片化——宁可让两个维度都半满也别一个维度爆掉另一个空着。NodeAffinityPriority根据节点亲和性偏好preferred的匹配程度打分匹配项越多分越高。TaintTolerationPriorityPod容忍的污点越多、优先级越低因为容忍多了说明它没那么挑可以让给更挑的Pod。ImageLocalityPriorityNode上已经有所需镜像的得分高这个策略主要为了省去拉镜像的时间在离线环境或者大镜像场景下效果突出。最后总分 各策略得分的加权和谁高选谁。如果两个节点分数相同调度器会通过SelectHost随机选一个保证公平性。3.3 手动推演一次调度决策假设有个Podrequest CPU1, 内存1Gi没有nodeSelector没有亲和性只能容忍一个污点schedulemaintenance:NoSchedule。集群里两个可用NodeNode X可分配4C8G已分配2C4G无污点Node Y可分配4C8G已分配1C2G带污点schedulemaintenance:NoSchedule预选阶段两个Node都能通过Y的污点Pod能容忍。打分阶段按LeastRequestedPriority算Node XCPU空闲比例 (4-2)/40.5内存空闲 (8-4)/80.5Node YCPU空闲比例 (4-1)/40.75内存空闲 (8-2)/80.75单看LeastRequestedY得分更高。但TaintTolerationPriority里Y因为带污点且Pod容忍了它得分会相对更低。两个策略综合下来结果就不一定了。这就是为什么空闲不等于一定被选中——调度器考虑的是多维度权衡。4. 把Pod放到该去的地方亲和性、反亲和性与污点容忍度的实战用法调度策略里最容易被忽视、但在生产环境中最有用的就是亲和性和污点。它们是预选阶段的硬规则和打分阶段的软偏好的完美体现。4.1 nodeSelector、节点亲和性选什么样的机器nodeSelector最直接一句话Pod要求必须落在带指定标签的Node上。spec: nodeSelector: disk: ssd适合的场景你有专门的GPU节点池标签gputrue有专门的SSD节点池标签diskssd需要把不同业务指定到特定硬件上。简单粗暴必选必达。但nodeSelector只能做等于判断表达力有限。节点亲和性nodeAffinity则强得多spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: disk operator: In values: - ssd - nvme这里operator可以是In、NotIn、Exists、DoesNotExist等组合起来能表达必须落在ssd或nvme之一不能落在regioncn-bj-01上这类复杂逻辑。requiredDuringScheduling是硬性条件等价于高级版nodeSelectorpreferredDuringScheduling是软偏好只影响打分不满足也没关系。生产环境我建议把关键业务的必须性要求用required把尽量的需求用preferred别一刀切。4.2 Pod亲和性与反亲和性让Pod们挨着跑或分开跑节点亲和性管的是Pod和Node的关系。Pod亲和性管的是Pod和Pod的关系。典型场景你想让A服务和B服务尽量部署在同一台机器上比如它们之间有大量本地IPC通信跨节点走网络开销大→ 用Pod亲和性。你想让同一个应用的副本分散在不同节点上避免一台机器挂了全挂 → 用Pod反亲和性。反亲和性的配置长这样spec: affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchLabels: app: nginx topologyKey: kubernetes.io/hostname重点在topologyKey它决定分散的范围。kubernetes.io/hostname表示按节点分散topology.kubernetes.io/zone表示按可用区分散。这个字段理解透了反亲和性就掌握了一大半。提醒一句反亲和性在副本数大于节点数的时候硬性required反亲和会让部分Pod永远Pending。比如你有10个副本只有7个节点用required反亲和必然有3个Pod起不来。生产环境里这是很常见的调度事故务必在设required之前算好副本数和节点数的关系。4.3 污点与容忍度管理哪些Pod不能来与哪些Pod必须走如果说亲和性是Pod主动挑Node污点就是Node主动挑Pod。给节点打上污点默认情况下所有没有对应容忍度的Pod都无法调度上来kubectl taint nodes node1 schedulemaintenance:NoSchedule污点的effect有三种NoSchedule不调度新Pod已存在的Pod不受影响。PreferNoSchedule尽量不调度软性。NoExecute不仅不调度新Pod已经跑在上面的Pod还会被驱逐。NoExecute在节点故障场景下特别有用。比如节点失联kube-controller-manager会给它打上node.kubernetes.io/not-ready和node.kubernetes.io/unreachable污点默认情况下Pod会在一定时间后被驱逐这个时间由tolerationSeconds控制。你可以利用这个机制让某些关键Pod设置tolerationSeconds: 0拒绝被驱逐或者设置一个很大的值保证它死也不挪窝。配合污点Node可以分池管理普通节点无污点普通业务随便跑。关键业务节点打criticaltrue:NoSchedule只有容忍这个污点的Pod能上。专用节点打dedicatedingress:NoSchedule只跑ingress控制器其他Pod一律过滤。这套污点容忍度亲和性组合拳生产环境里比nodeSelector管用得多。5. 源码视角下的调度器从Scheduler Cache到Scheduling Framework关于调度器的源码如果你正准备看《深入理解kubernetes源码》这类书或者直接翻kube-scheduler的代码我建议先抓住三个核心结构Scheduler Cache、Scheduling Framework、以及调度器的list/watch机制。5.1 Scheduler Cache调度器自己的快照调度器不是每次调度都去查APIServer的实时数据它维护了一个本地缓存scheduler cache里面记录了每个Node的资源分配情况、Pod分布、PVC/PV绑定关系等。这个缓存通过Watch机制实时更新。为什么要搞缓存因为调度是一个高频操作如果每次决策都查一遍APIServer性能根本扛不住。有了缓存调度器可以在内存里完成所有判断几百毫秒内给一个Pod定好落点。但缓存也带来一个问题调度器拿到的节点资源数据是异步更新的可能存在延迟。尤其是Pod调度成功但还没实际运行起来binding刚写入、或者Pod被删除但kubelet还没回收资源的时候缓存里的已分配数据可能不是最准的。这也是为什么kubectl describe node看到的已分配资源和调度器实际认为的已分配资源偶尔对不上。官方给的兜底方案是节点状态上报和调度器的假设绑定机制调度器会先假设Pod已经绑定把资源扣掉再缓存平时没事但极端并发下可能出现短暂的资源超卖。5.2 Scheduling Framework是理解调度器的最佳入口如果你正在啃源码我强烈建议先看pkg/scheduler/framework这个目录。它定义了调度的扩展点接口把整个调度流程划分成十几个阶段。这些扩展点分布在调度周期和绑定周期里PreFilter/Filter预选阶段可以挂多个插件任何一个插件返回不可行Pod即被过滤。PreScore/Score/NormalizeScore优选阶段每个打分插件独立打分最后权重归一化。Reserve资源预留在绑定前把Pod占用的资源在缓存里扣除。Permit允许或延迟调度可用于实现等待一组Pod齐了再放行。PreBind/Bind/PostBind绑定前后的钩子PostBind里可以执行绑定成功后的异步动作。框架的好处是你不需要理解全部源码只要找准扩展点就能定制调度行为。官方内置插件nodeaffinity、noderesources、podtopologyspread等都实现了这套接口你在源码里看到的所有策略本质上都是一个个插件。我在实际调试自定义调度器时有个心得先在framework的扩展点里打日志比在调度主流程里打日志有用得多。因为主流程就是个循环代码真正干活的地方全在插件里。5.3 用日志和Metrics定位调度问题排查调度问题别一上来就翻源码。先看这几类信息kubectl describe pod看EventsFailedScheduling的错误信息里会写清楚是哪个插件拒绝的。比如0/3 nodes are available: 1 node(s) didnt match pod anti-affinity rules, 2 Insufficient memory。调度器日志把kube-scheduler日志级别调到--v5或--v6可以看到每个Pod在每个Node上的预选、打分明细。生产环境临时提高日志级别再回滚是定位调度故障最快的办法。调度Metricsscheduler_pending_pods、scheduler_preemption_attempts_total、scheduler_scheduling_attempt_duration_seconds这几个指标能反映调度器的整体健康度。如果你的Prometheus面板上有这几项挂到Grafana里长期观察比临时查日志靠谱得多。--v6日志会输出每个Node在优选阶段的具体得分例如Score for Node node-1: 378、Score for Node node-2: 421。看到分数差距你就能判断哪个策略在起作用。6. 生产环境里的调度实战预留、抢占与那些让人失眠的坑最后这部分聊聊我在生产环境里折腾调度器积累下来的一些经验和教训希望能帮你少走弯路。6.1 节点资源预留给看不见的资源留出空间你在kubectl describe node里看到的Allocatable并不等于节点物理资源总量。kubelet通过--system-reserved和--kube-reserved参数给系统进程和kubelet自己预留了一部分资源。这俩参数如果没设调度器会认为整个节点的资源都能用于Pod调度等到实际运行时系统进程和Pod抢CPU、抢内存节点状态就会变差甚至触发驱逐。我这里有个实际案例一台8C16G的节点没设置任何reserved调度器满打满算认为16G全可用结果kubelet和dockerd本身占掉1G多再加上systemd、sshd这些系统进程实际Pod可用内存不到14.5G。业务方写了request 15G的Pod调度成功跑起来直接OOM。后来我们把--kube-reserved和--system-reserved配好把Allocatable拉回真实水平这类问题就再没出现过。**这是调度里最容易被忽略的看不见的资源。**建议在部署集群的时候就把reserved配好别等出事再补。6.2 优先级与抢占别让你的Pod互相打架Kubernetes的PriorityClass允许高优先级Pod在资源不足时抢占低优先级Pod。看起来很美但生产环境里你一旦配置不当会引发连锁驱逐kubectl apply -f - EOF apiVersion: scheduling.k8s.io/v1 kind: PriorityClass metadata: name: high-priority value: 1000 globalDefault: false EOF假设你给线上核心服务设置了高优先级又没限制集群里的PriorityClass数量开发同学很可能会给自己所有应用也配一个高优先级。结果就是新版本发布时高优先级Pod互相抢占节点上的Pod被反复驱逐重启服务抖动得让人头皮发麻。我的建议是PriorityClass的value值域要有全局规划按业务重要性分层级比如critical10000、important5000、default1000。非必要不配置抢占如果配置了务必给低优先级服务设置合理的terminationGracePeriodSeconds和PDBPodDisruptionBudget避免驱逐的时候被一刀切。调度器支持的preemptionPolicy: Never可以禁用抢占只保留优先级排队功能这个在很多场景下更安全。6.3 常见调度故障的排查链路最后分享一个完整的排查思路。遇到Pod Pending按这个顺序走基本能覆盖90%的情况看Eventskubectl describe pod name看Events最后几行。调度器会在FailedScheduling里写明原因。看节点资源kubectl describe node node确认有没有节点满足request。注意看Allocated resources和Allocatable不是看总容量。看标签与污点检查Pod的nodeSelector/affinity要求和节点标签是否匹配检查节点污点与Pod容忍度。kubectl get nodes --show-labels一条命令全看清。看PVC绑定情况如果Pod用了PVC且处于ContainerCreating而不是Pending多半是存储的问题别在调度里死磕。看调度器日志前面说的把kube-scheduler日志开到--v6直接看哪个插件拒绝了。看队列状态如果Pod长时间Pending但Events里没有新记录可能是调度器积压队列里有大量Pod等调度或者Pod进了unschedulableQ但触发重新入队的事件一直没来。还有一个很容易被忽略的问题大集群调度性能。当集群节点超过500个Pod数量几千上万时调度器默认配置--kube-api-qps、--kube-api-burst可能会成为瓶颈。你可以用kubectl top nodes或者看scheduler_scheduling_attempt_duration_seconds这个指标如果P99超过1秒就该考虑调大调度器的QPS和Burst参数了。6.4 调度需求多副本时的风暴问题还有一个我踩过的坑属于调度风暴范畴。当你用一个Deployment一次性创建大量副本时比如滚动更新全量替换100个Pod一次性创建调度器会对这些Pod逐个打分、逐个绑定。如果你的节点数量不多每个节点的资源就那么多调度器一个接一个地绑前面的Pod刚绑上但还没真正运行资源还没实际占用后面的Pod也得分完了。这时候如果节点资源已经虚标用完就会出现Pod反复在backoffQ和activeQ之间切换调度效率下降。这类问题没有银弹常见的缓解手段是给Deployment配置maxSurge和maxUnavailable控制滚动更新的Pod个数避免一次性全量调度。用PodTopologySpreadConstraints把副本均匀打散到不同节点/可用区避免调度器把Pod全塞到前几个节点上。如果业务允许把replicas拆成多个Deployment分批发布。另外提醒一个容易混淆的点Kubernetes调度的最小单位是Pod不是Deployment副本。你写replicas: 10的DeploymentController会创建10个独立的Pod调度器对每个Pod做独立决策它不会统筹兼顾地打包调度这10个Pod。除非你用了Coscheduling这类扩展否则调度器天然不具备批量协同能力。Kubernetes集群调度这个东西初看是给Pod找地方实际深入下去它牵扯到资源建模、容量规划、故障域设计、业务可用性保障每一个决策背后都是一整套权衡。调度器默认策略能覆盖大多数场景但真正把集群玩转还是得理解它背后的过滤打分哲学并且根据你自己的业务特点去定制规则。最实用的建议就一句话先从kubectl describe pod的Events看起把预选和优选每个插件的日志看明白再去动那些亲和性、污点的高级配置你会少踩很多坑。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →