凌晨六点,城市智慧物流中心的传送带开始嗡嗡作响。成千上万的包裹如同潮水般涌进分拣大厅——这是这座城市一天中最繁忙的时刻。大厅经理王师傅站在中央控制台前,面前是不断跳动的大屏:待分拣包裹数量、各条传送带负载、司机排班状态……他的工作看似简单:决定每个包裹走哪条传送带、上哪辆车、送到哪个区域。但稍有不慎,某个片区的快递就会堆积如山,而另一片区却空车待发。 王师傅干这行十五年,练就了一双火眼金睛:他先扫一眼包裹面单上的配送区域,剔除那些还没开通的路线;再根据包裹重量和体积挑选合适的传送带;如果某条线路上有特殊要求(比如冷链、危化品),还得看哪个司机能接这单;最后,加急件永远插在最前面。这个看似朴素的决策过程,其实就是Kubernetes调度器的生动写照——只不过,K8s调度的“包裹”叫Pod,“传送带”叫节点,“司机”叫kubelet,“配送区域规则”则叫调度算法。接下来,让我们一起走进K8s调度器这个现代云原生的“智慧分拣大厅”。

岗位关联

学习价值:掌握K8s调度器原理,是平台工程师和SRE优化集群资源利用率、排查Pod调度异常、设计高可用架构的核心基本功。无论是面试大厂云原生岗位还是落地生产环境,调度机制都是绕不开的硬核知识。

目录
  1. 可视化演示
  2. 调度器核心流程
  3. 节点打分算法
  4. 节点亲和性
  5. 污点与容忍
  6. 优先级抢占
  7. 资源配额管理
  8. 限制范围控制
  9. 调度器实战策略
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示K8s调度器Filter→Score→Bind三阶段流水线、节点资源分配与亲和性污点调度机制。

本节配有交互动效(进入视口后自动加载,约 3KB)。

调度三阶段:Filter→Score→Bind Filter阶段通过Predicates剔除资源不足或污点不匹配的节点 Score阶段对候选节点打分排序,Bind阶段将Pod绑定到最优节点 Schedule=Filter(Nodes)→Score(Filtered)→Bind(Best) 三阶段流水线实现Pod到节点的智能放置

调度器核心流程

智慧物流中心的王师傅每天开工第一件事,就是看一眼大屏上的“待分拣队列”。这个队列可不是随便排的——加急件永远排在最前面,普通件按到达时间依次排列。如果某个包裹面单信息不全,会被先剔除到“待人工复核区”。这套流程,和K8s调度器处理 Pod 的路径惊人地一致。

在 Kubernetes 中,调度器(kube-scheduler)是集群的“王师傅”。它通过 API Server 持续监听集群中所有未被调度的 Pod(即 spec.nodeName 为空的 Pod),将它们放入一个称为 调度队列 的内部结构中。这个队列支持优先级排序:高优先级的 Pod 会排在前面优先处理。调度器对一个 Pod 的处理分为两个大阶段:过滤(Filtering) 和 打分(Scoring)。

过滤阶段又叫“预选”(Predicates),它的任务是硬性排除那些不满足条件的节点——就像王师傅先剔除配送范围之外的路线。比如节点资源不足、端口冲突、不满足节点选择器等条件,都会被直接淘汰。打分阶段又叫“优选”(Priorities),在过滤后剩余的候选节点中,根据一系列策略算出每个节点的得分,选出最优解。最终,调度器将 Pod 与节点的绑定关系(Binding)写入 API Server,由目标节点上的 kubelet 完成真正的容器启动。

回到物流中心:王师傅每天下班前会复盘当天的分拣效率——哪个传送带利用率低?哪条路线总是积压?K8s 调度器也一样,它的每一次调度决策都在追求“全局最优”,而不是“局部够用”。理解了这个流程,你就掌握了 K8s 调度器的骨架。

调度结果=Filter(Queue(Pods),Nodes)→Score最优 Node\text{调度结果} = \text{Filter}( \text{Queue}(Pods), Nodes) \xrightarrow{\text{Score}} \text{最优 Node}
公式透视:Pod 先从队列取出,经过过滤剔除不合法节点,再对合法节点打分,得分最高者赢得本次调度。
  1. 调度队列入队新创建的 Pod 会被调度器 watch 到并入队,队列按优先级排序,等待被处理。
    Queue←Unscheduled Pods (sorted by priority)Queue \leftarrow \text{Unscheduled Pods (sorted by priority)}
  2. 过滤阶段调度器遍历所有节点,依次检查资源、端口、亲和性等硬性条件,过滤后得到候选节点集合。
    Candidates={n∈Nodes∣Predicates(n,pod)=true}Candidates = \{ n \in Nodes \mid \text{Predicates}(n, pod) = true \}
  3. 打分阶段对每个候选节点运行多个打分函数,加权求和得到最终得分。
    Score(n)=∑iwi×PriorityFunci(n,pod)Score(n) = \sum_{i} w_i \times \text{PriorityFunc}_i(n, pod)

节点打分算法

假设王师傅要从三条传送带中选一条来送这批包裹。第一条传送带已经装了 80% 的货物,第二条装了 30%,第三条装了 50%。他当然选择第二条——因为剩余空间最大,能容纳更多后续包裹。这就是 LeastRequested(最少请求)策略的朴素思想。

在 K8s 打分阶段,LeastRequested 是一个核心打分函数。它的计算方式是:节点空闲资源越多,得分越高。计算公式为:

除了关注“剩余多不多”,还要关注“资源配比均不均”。想象两条传送带:一条剩余 2 个 CPU 但只剩 1GB 内存,另一条剩余 1 个 CPU 但有 4GB 内存。如果下一批包裹是内存密集型,第二条显然更合适。BalancedResource 策略正是为此而生——它评估节点上 CPU 和内存的使用率是否均衡,差距越小得分越高。调度器还会结合用户自定义的 weight 权重,让不同策略按比例综合生效。

这些打分策略共同构成了 K8s 调度器“精明分拣员”的形象。在实际生产环境中,默认的调度器会综合多种策略加权计分。理解这些算法,对你后续调优调度策略或排查“节点资源明明很多但 Pod 调度不过去”之类的问题,至关重要。

Score=(capacity−requested)×10capacity\text{Score} = \frac{(\text{capacity} - \text{requested}) \times 10}{\text{capacity}}
BalancedScore=10−∣cpuUsage−memUsage∣2×10\text{BalancedScore} = 10 - \frac{|\text{cpuUsage} - \text{memUsage}|}{2} \times 10
公式透视:CPU 与内存使用率越接近,差值越小,得分越接近 10 分满分。
  1. LeastRequested 剩余量计算从节点总容量减去已分配的 requests,得到剩余可分配资源。
    remaining=capacity−requestedremaining = capacity - requested
  2. 归一化评分剩余量占比越高,分数越高(0-10 分区间)。
    score=remainingcapacity×10score = \frac{remaining}{capacity} \times 10
  3. BalancedResource 差值计算计算 CPU 与内存使用率的绝对差,表征资源失衡程度。
    diff=∣cpuUsage−memUsage∣diff = |\text{cpuUsage} - \text{memUsage}|
  4. 反向映射得分差值越大得分越低,鼓励均衡利用。
    score=10−diff×10score = 10 - diff \times 10

节点亲和性

王师傅接到一个大客户的长期合作需求:他们每天有 200 件冷链包裹,必须送到有冷藏设备的东区配送站。王师傅把东区配送站标记上“冷藏”标签,之后所有冷链包裹自动分到东区。这就是 节点亲和性(Node Affinity) 的实践——通过给节点打标签,让 Pod 表达“我想去哪种节点”的意愿。

节点亲和性有两种类型。第一种是 硬亲和(requiredDuringSchedulingIgnoredDuringExecution),像冷链包裹必须送东区一样,如果集群中没有任何带匹配标签的节点,Pod 将一直处于 Pending 状态。第二种是 软亲和(preferredDuringSchedulingIgnoredDuringExecution),它更像“优先选择”——如果能找到带标签的节点就用,找不到就退而求其次。此外还有 反亲和性(podAntiAffinity),用于确保两个 Pod 不会落在同一个节点上——比如两个互相竞争的资源密集型应用,或者高可用架构中的多个副本应该分散部署。

回到物流中心:亲和性就像是包裹面单上写着“请送往具备冷藏条件的东区站”,调度器看到这个面单就会自动匹配标签。但注意,标签是动态维护的——如果运维同学给节点打了新标签,或者删除了旧标签,已调度的 Pod 不会因此被重新调度,这就是 IgnoredDuringExecution 的含义。

yaml15 行
affinity:  nodeAffinity:    requiredDuringSchedulingIgnoredDuringExecution:      nodeSelectorTerms:      - matchExpressions:        - key: disk-type          operator: In          values: ["ssd"]    preferredDuringSchedulingIgnoredDuringExecution:    - weight: 100      preference:        matchExpressions:        - key: zone          operator: In          values: ["east-1"]

污点与容忍

东区配送站虽然配备了冷藏设备,但王师傅并不想让所有包裹都涌过去。他给东区站设了一道门禁:只有持有“冷链资质”的司机才能进入。其他包裹到了门口,会自动被门卫拦下,重新分拣到别的站点。在 K8s 里,这道门禁就是 污点(Taint),司机的资质就是 容忍(Toleration)。

污点附着在节点上,格式为 key=value:effect。effect 有三种:NoSchedule(不调度新 Pod)、PreferNoSchedule(尽量不调度)和 NoExecute(既不让新 Pod 上来,还会驱逐已有的不满足条件的 Pod)。与之对应,Pod 通过 tolerations 字段声明自己容忍哪些污点,匹配后即可正常调度。

污点最常见的应用场景是资源隔离。例如给 GPU 节点打上污点,只有声明了容忍的 GPU 任务 Pod 才能调度上去,防止普通 Pod 抢占 GPU 资源。另一个场景是故障隔离:当某个节点出现异常时,运维可以给它打上 NoExecute 污点,将该节点上已有的 Pod 全部驱逐,快速将流量切换到健康节点。

王师傅后来发现,门禁机制特别适合处理“机房维护”场景——给要维护的节点打上 NoExecute 污点,机器上的 Pod 会自动搬走,维护完成后删掉污点,节点重新接入。这种优雅的资源腾挪,正是污点与容忍机制的价值所在。

Pod 能否调度到该节点={False若 Taint 存在且 Toleration 不匹配True若 Taint 不存在或 Toleration 完全匹配\text{Pod 能否调度到该节点} =\begin{cases}\text{False} & \text{若 Taint 存在且 Toleration 不匹配} \\\text{True} & \text{若 Taint 不存在或 Toleration 完全匹配}\end{cases}
公式透视:污点与容忍是“钥匙-锁”的关系,只有匹配的钥匙才能开对应的锁。
  1. Taint 判断扫描节点上的所有污点,逐一检查 Pod 是否声明了对应容忍。
    ∀t∈Taints(node),∃tol∈Tolerations(pod),tol matches t\forall t \in \text{Taints}(node), \exists \text{tol} \in \text{Tolerations}(pod), \text{tol matches } t
  2. Effect 匹配容忍的 effect 必须与污点的 effect 完全一致才生效。
    tol.effect=t.effect (or tol.effect=Empty)\text{tol.effect} = t.effect \text{ (or } \text{tol.effect} = \text{Empty)}
  3. NoExecute 驱逐判定对已运行的 Pod,如果容忍不匹配且 tolerationSeconds 已过期,则被驱逐。
    evict if t.effect=NoExecute and not matched\text{evict if } t.effect = \text{NoExecute} \text{ and not matched}

优先级抢占

物流中心每年双十一都会遇到一个棘手问题:普通包裹已经占满了传送带,突然来了一批标注“限时配送”的加急件。王师傅的手法是:把加急件插到最前面,如果实在没位置,就把传送带上优先级最低的普通包裹先挪到暂存区,等传送带空了再放回去。K8s 的 优先级抢占(Preemption) 机制,和这套手法如出一辙。

K8s 通过 PriorityClass 定义优先级等级,每个 Pod 可以关联一个 PriorityClass,从而获得对应优先级数值。当高优先级 Pod 无法调度(所有节点资源都不足)时,调度器会尝试抢占(Preempt):找到能让高优 Pod 落脚的节点,挑选该节点上一个或多个低优先级 Pod,将它们驱逐(Eviction)或删除(Delete),然后在腾出的资源上调度这个高优 Pod。被抢占的 Pod 如果受 Deployment 等控制器管理,通常会在其他节点被重新创建——所以整个过程体现为“高优任务挤掉低优任务”。

在真实场景中,抢占机制需要谨慎使用。“抢地盘”虽然高效,但如果滥用会导致高优 Pod 频繁挤掉低优 Pod,造成集群不稳定。像王师傅只在双十一这种时刻才动用“挪包裹”的权力——K8s 也建议只在关键业务场景(如在线交易链路)使用高优先级,让抢占成为“应急重器”而非“日常操作”。

Preemption 触发条件=Priority(pod)>Priority(victim)∧资源不足无法调度\text{Preemption 触发条件} = \text{Priority}(pod) > \text{Priority}(victim) \wedge \text{资源不足无法调度}
公式透视:抢占不是随意的——只有“我优先级比你高”才允许抢你的资源。
  1. 高优 Pod 入队失败调度器在过滤阶段发现所有节点都无法满足高优 Pod 的资源需求。
    ∃node:feasible(pod,node)=False,∀node\exists \text{node}: \text{feasible}(pod, node) = \text{False}, \forall node
  2. 寻找牺牲者调度器在各节点上挑选优先级最低且删除后能释放足够资源的 Pod 作为被抢占对象。
    victim=arg⁡min⁡p∈pods(node)Priority(p),且释放资源足够victim = \arg\min_{p \in \text{pods(node)}} \text{Priority}(p), \text{且释放资源足够}
  3. 执行抢占与调度删除低优 Pod,将高优 Pod 绑定到该节点。
    delete(victim)→bind(pod,node)\text{delete}(victim) \rightarrow \text{bind}(pod, node)

资源配额管理

物流中心的每个分拣区域都有自己的容量上限:东区最多存 500 件,南区最多 300 件。如果某个区域已经满了,新来的包裹只能拒收。王师傅的这套“区域限额”制度,在 K8s 里对应的就是 ResourceQuota(资源配额)。

ResourceQuota 是命名空间级别的资源管控机制。集群管理员可以为每个命名空间设置资源总量上限,包括 CPU、内存等计算资源,以及 Pod 数量、PVC 数量等对象数量。一旦该命名空间内已创建的资源总和达到配额上限,后续的创建请求将被 API Server 直接拒绝。这意味着从源头杜绝了“某个团队无限创建 Pod 导致集群资源耗尽”的问题。

配额机制是云原生平台“多租户治理”的基石。就像物流中心必须保证每个区域都有独立的空间和通道,K8s 集群中不同团队(不同命名空间)需要资源隔离,ResourceQuota 就是那道“隔墙”。配合 LimitRange(下一节)使用,配额可以设置总量上限,而 LimitRange 则约束单个 Pod 的资源范围,两者合力构建完整的资源治理体系。

yaml12 行
apiVersion: v1kind: ResourceQuotametadata:  name: dev-quota  namespace: devspec:  hard:    requests.cpu: "10"    requests.memory: 20Gi    limits.cpu: "20"    limits.memory: 40Gi    pods: "50"

限制范围控制

王师傅的徒弟小李问:“师傅,我们只规定每个区域最多存多少件,但万一有个包裹特别大,一个人就占了半个区怎么办?”王师傅点点头——这个问题,需要 LimitRange 来解决。

LimitRange 也是命名空间级别的策略,但它管的是“单个 Pod 的资源大小”。管理员可以设置每个 Pod 的 CPU/内存的最小值、最大值和默认值。例如,在某个命名空间内,一个 Pod 最少申请 1 核,最多申请 8 核;如果 Pod 没有显式声明资源 requests/limits,LimitRange 会自动给它填上默认值。这样既防止了“巨无霸 Pod 独占集群”,也避免了“所有 Pod 都不写资源声明导致调度失控”。

在生产实践中,LimitRange 通常与 ResourceQuota 组合出拳:ResourceQuota 控制总量,LimitRange 控制单量。“总量 + 单量”的双重约束,让集群资源分配既有宏观边界,又有微观规则。就像物流中心既有限制总包裹量的区域墙,也有控制单件包裹大小的卡尺,两者缺一不可。

有效资源={min⁡≤request(pod)≤max⁡若 Pod 显式声明default若 Pod 未声明\text{有效资源} =\begin{cases}\min \le \text{request}(pod) \le \max & \text{若 Pod 显式声明} \\\text{default} & \text{若 Pod 未声明}\end{cases}
公式透视:LimitRange 像一把“卡尺”——每个 Pod 的资源声明必须落在尺子范围内,没写声明的直接套用默认值。
  1. 校验显式声明对于声明了 resources 的 Pod,逐一校验是否落在 [min, max] 区间内,越界即拒绝。受此约束的字段包括 requests.cpu、requests.memory、limits.cpu、limits.memory。
    min⁡cpu≤requests.cpu≤max⁡cpu\min_{cpu} \le \text{requests.cpu} \le \max_{cpu}
  2. 填充默认值Pod 容器未声明资源时,LimitRange 根据 default 字段自动为 limits 赋值,根据 defaultRequest 为 requests 赋值。
    limits=default,requests=defaultRequest\text{limits} = \text{default}, \quad \text{requests} = \text{defaultRequest}

调度器实战策略

王师傅在物流中心干了这么多年,有一套自己的“实战心法”:每天早上先看大屏数据,再检查各片区异常,最后根据未来一周的包裹量预测调整排班。K8s 调度策略的实战运维,也需要类似的“监控—诊断—调优”三部曲。

第一步:调度失败排查。 Pod 一直 Pending,最常见的原因有:节点资源不足、节点有污点且 Pod 不容忍、节点亲和性无法满足、ResourceQuota 已超限。排查时通过 kubectl describe pod 查看 Events 事件,通常会给出明确的失败原因。例如 0/4 nodes are available: 4 Insufficient cpu 说明 CPU 资源不足。

第二步:性能压测。 如果需要评估调度器的吞吐能力(每秒能调度多少个 Pod),可以使用 clusterloader2 或自建压测脚本,在集群中批量创建 Pod,观察调度延迟和队列深度。这有助于提前发现调度器性能瓶颈。

第三步:策略调优。 根据业务特征调整调度策略:为关键业务配置高优先级类、为不同可用区设置亲和性、为 GPU 节点打污点。同时关注资源利用率指标,通过计算平台(如 Prometheus + Grafana)观测 kube_scheduler 指标,持续迭代调度配置。

回到王师傅的智慧物流中心——他深知调度不是一成不变的水管,而是需要随需应变的智能网络。K8s 调度器的实战功力,也正是建立在对队列、过滤、打分、亲和性、污点、配额和限制这些机制烂熟于心之上的。把这套方法论装进你的工具箱,无论面对什么样的集群调度挑战,你都能像王师傅一样,胸有成竹、从容应对。

调度成功率=成功绑定的 Pod 数请求调度的 Pod 总数×100%\text{调度成功率} = \frac{\text{成功绑定的 Pod 数}}{\text{请求调度的 Pod 总数}} \times 100\%
公式透视:调度成功率是衡量调度器健康度的核心指标。通过区分“因资源不足失败”和“因策略被拒失败”,可以针对性优化。
  1. 调度失败原因分类通过 kubectl describe pod 查看事件,将所有失败分为资源型、策略型、配额型三类,分别定位到对应机制。
    Failuretype=查 Events 中的 Reason 字段\text{Failure}_{type} = \text{查 Events 中的 Reason 字段}
  2. 压测吞吐计算在单位时间内批量创建 Pod,统计从创建到绑定的平均耗时与调度成功率,公式如下。
    Throughput=Total ScheduledTime(s)\text{Throughput} = \frac{\text{Total Scheduled}}{\text{Time}(s)}

AI 应用场景

场景一:大规模微服务集群的智能调度优化

阶段:模型部署与资源调度

解决的问题:某互联网公司有 2000 多个微服务运行在 K8s 集群中,原来所有 Pod 平权调度,导致高流量服务与低流量服务混部,资源利用率仅 35%,且频繁出现资源争抢。通过合理配置 PriorityClass、ResourceQuota 和 LimitRange,实现分级调度与资源隔离。

具体案例:核心交易链路的服务配置为高优先级类,批处理任务配置为低优先级类并设置较低配额。运维团队在 12 个命名空间分别设定资源配额,配合 LimitRange 强制所有 Pod 声明资源请求。优化后集群整体利用率提升至 55%,核心链路 P99 延迟下降 40%。

场景二:机器学习训练任务的 GPU 资源管控

阶段:训练资源编排

解决的问题:GPU 资源昂贵且稀缺,如何保证只有真正的训练任务才能使用 GPU 节点,同时避免单个训练任务独占全部 GPU?利用 Taint 标记 GPU 节点 + Toleration 授权训练 Pod + ResourceQuota 限制每团队 GPU 数量,实现精细化的 GPU 资源治理。

具体案例:数据科学团队在 GPU 节点上打上 gpu=training:NoSchedule 污点,所有训练 Pod 声明对应容忍。同时为每个算法团队设置 nvidia.com/gpu 配额上限为 8 块,确保团队间公平共享。模型训练任务的 GPU 利用率从 45% 提升到 78%。

场景三:电商大促高峰的弹性调度应对

阶段:容量规划与弹性伸缩

解决的问题:双十一大促期间流量突增 10 倍,常规调度策略无法快速扩容。结合 Pod 优先级抢占、节点亲和性和调度器压测,提前规划大促容量,保障核心链路持久稳定。

具体案例:提前使用 clusterloader2 对调度器进行压测,确定单集群 Pod 调度吞吐上限。大促期间核心购物车服务配置高优先级类,在大促前 30 分钟通过 HPA 扩容。调度器成功在 2 分钟内完成 5000 个 Pod 的调度,且核心服务零抢占。

Python 演示

python89 行
# -*- coding: utf-8 -*-"""一个简化的 K8s 调度器模拟器:基于过滤打分实现 Pod 到节点的绑定决策。""" class Scheduler:    def __init__(self, nodes):        self.nodes = nodes        self.records = []        self.stats = {"scheduled": 0, "pending": 0}     def filter_nodes(self, pod):        """过滤阶段:只保留资源足够且满足亲和性的节点。"""        candidates = []        for node in self.nodes:            if node.available_cpu < pod.request_cpu:                continue            if node.available_mem < pod.request_mem:                continue            if pod.node_selector and not node.match_labels(pod.node_selector):                continue            candidates.append(node)        return candidates     def score_nodes(self, candidates, pod):        """打分阶段:LeastRequested 与 BalancedResource 加权综合。"""        scored = []        for node in candidates:            cpu_usage = pod.request_cpu / node.total_cpu            mem_usage = pod.request_mem / node.total_mem            least = (1 - cpu_usage) * 50 + (1 - mem_usage) * 50            balance = 10 - abs(cpu_usage - mem_usage) * 100            scored.append((node, least + balance))        return sorted(scored, key=lambda x: x[1], reverse=True)     def schedule(self, pod):        """执行调度:过滤 -> 打分 -> 绑定。"""        candidates = self.filter_nodes(pod)        if not candidates:            self.stats["pending"] += 1            self.records.append((pod.name, "pending"))            return None        best_node, _ = self.score_nodes(candidates, pod)[0]        best_node.available_cpu -= pod.request_cpu        best_node.available_mem -= pod.request_mem        self.stats["scheduled"] += 1        self.records.append((pod.name, best_node.name))        return best_node  class Node:    def __init__(self, name, total_cpu, total_mem, labels=None):        self.name = name        self.total_cpu = total_cpu        self.total_mem = total_mem        self.available_cpu = total_cpu        self.available_mem = total_mem        self.labels = labels or {}     def match_labels(self, selector):        return all(k in self.labels and self.labels[k] == v                   for k, v in selector.items())  class Pod:    def __init__(self, name, request_cpu, request_mem, node_selector=None):        self.name = name        self.request_cpu = request_cpu        self.request_mem = request_mem        self.node_selector = node_selector or {}  if __name__ == "__main__":    nodes = [        Node("node-a", 8, 32, {"disk": "ssd", "zone": "east-1"}),        Node("node-b", 4, 16, {"disk": "hdd", "zone": "east-2"}),        Node("node-c", 4, 16, {"disk": "ssd", "zone": "west-1"}),    ]    pods = [        Pod("pod-1", 2, 8, {"disk": "ssd"}),        Pod("pod-2", 4, 16),        Pod("pod-3", 2, 4, {"zone": "west-1"}),    ]    scheduler = Scheduler(nodes)    for pod in pods:        target = scheduler.schedule(pod)        if target:            print(f"{pod.name} 绑定到 {target.name}")        else:            print(f"{pod.name} 无法调度,保持 Pending")    print("调度统计:", scheduler.stats)
目的:这是一个极简调度器模拟器,用 Python 实现 K8s 调度器的核心流程(过滤 → 打分 → 绑定),帮助理解调度算法和资源更新的工作机制。
  1. class Scheduler:定义调度器主类,持有节点列表和调度统计信息。
  2. def filter_nodes(self, pod):过滤阶段的核心,检查每个节点的可用资源是否满足 Pod 请求。
  3. pod.node_selector and not node.match_labels(...)模拟节点亲和性:如果 Pod 声明了节点选择器,节点标签必须完全匹配。
  4. least = (1 - cpu_usage) * 50 + (1 - mem_usage) * 50LeastRequested 策略:资源剩余越多得分越高。
  5. balance = 10 - abs(cpu_usage - mem_usage) * 100BalancedResource 策略:CPU 和内存利用率越均衡得分越高。
  6. best_node, _ = self.score_nodes(candidates, pod)[0]从打分排序结果中取出最高分节点。
  7. best_node.available_cpu -= pod.request_cpu绑定后扣减节点可用资源,模拟资源预留。

常见误区

误区我一直以为调度器把 Pod 分配到节点后,Pod 就自动开始运行了。毕竟“调度”听起来就是把任务安排下去执行的意思。

正解实际上调度器只做“绑定”决策——它把 Pod 的 nodeName 字段写入 API Server,类似物流经理把包裹贴上“送往东区站”的标签。真正的容器启动是由节点上的 kubelet 监听到绑定信息后,调用容器运行时(如 containerd)完成的。调度器和 kubelet 是两个独立的组件:调度器负责“决策”,kubelet 负责“执行”。

为何常见:学习 K8s 时通常先接触“Pod 被调度到节点”这个抽象描述,容易把调度和执行合并理解。在排查 Pod 长时间处于 Pending 问题时尤其要注意:Pending 并不总是调度器没决策,也可能是 kubelet 启动容器失败。

误区我觉得给节点打上污点就像给节点上了把锁,所有 Pod 都不能调上去了,这是个绝对隔离机制。

正解污点是“默认拒绝,有证放行”——只要 Pod 声明了匹配的容忍(Toleration),它依然可以调度到带污点的节点上。污点与容忍是配对的:污点声明“这个节点有特殊资源”,容忍声明“我知道这个特殊资源,我愿意来”。更关键的是,NoExecute 污点不仅阻止新 Pod 调度,还会将节点上不满足容忍条件的已有 Pod 全部驱逐。污点的效果由作用等级决定,不是简单的“一刀切”。

为何常见:很多人从“污点”的字面含义和“系统维护时给节点打污点”的案例中,形成了“打上污点节点就封闭”的认知。实际上污点只是设了个门槛,带通行证的人(有容忍的 Pod)依然畅通无阻。

误区我给集群设置了一个 ResourceQuota,以为从此所有命名空间的 Pod 都要遵守这个资源上限。

正解ResourceQuota 是命名空间级别的隔离机制。每个 ResourceQuota 必须绑定到指定的 namespace(写在 metadata.namespace 中),它的 spec.hard 字段只限制该命名空间内的资源总和。其他命名空间完全不受影响。如果要做全局治理,需要在每个命名空间分别创建配额对象,或者借助策略引擎(如 Kyverno)统一生成。

为何常见:使用 kubectl apply 创建 ResourceQuota 时如果没指定 namespace,会默认创建在 default 命名空间。实际操作中很容易忽略 namespace 维度,误以为配额是全局生效的“总阀门”。

误区节点亲和性让 Pod 主动选节点,污点让节点拒绝 Pod,这不都是控制 Pod 调度位置的两个表面吗?我觉得它们本质上是一回事。

正解两者的作用方向和语义完全不同。节点亲和性是 Pod 对节点的“请求” ——Pod 说“我想去带这些标签的节点”,它表达的是“偏好”或“需求”。污点与容忍是 节点对 Pod 的“门槛” ——节点说“我有特殊属性,你得有证才能进来”。两者是互补关系而不是重叠关系:亲和性帮 Pod 找合适节点,污点帮节点筛选合适 Pod。同一个 Pod 可能同时使用亲和性(主动选 GPU 节点)和污点容忍(被动获得 GPU 节点入场券),它们可以协同工作。

为何常见:从“最终效果影响 Pod 放在哪里”这个结果看,两者确实有相似之处。但深入理解调度机制后会发现,它们在调度流程中处于不同阶段、由不同对象声明,是两种独立的表达方式。

误区我配置了高优先级类后,以为高优 Pod 在任何情况下都能抢到资源,是集群里的“特权阶级”。

正解优先级抢占有严格的限制条件。首先,只有当高优 Pod 在所有节点都无法被调度(资源全员不足)时才会触发抢占;其次,调度器会挑选“牺牲最少”的受害者——优先驱逐优先级最低的 Pod,而且 Pod 的 PDB(PodDisruptionBudget)策略可能禁止某些 Pod 被驱逐;最后,抢占也不是瞬时完成的,需要先删除受害者,kubelet 完成资源释放后高优 Pod 才能绑定成功。此外,如果受害者受控制器管理,被驱逐后它会在其他节点重新创建,集群压力只是被转移了而非被消除。

为何常见:从“高优先级”这个命名上容易产生“全能特权”的直觉,忽略了抢占机制的触发条件、约束边界和实际执行过程。这也是面试中常考查的细节题。

面试题

Q1. K8s 调度器对 Pod 的调度过程,正确的顺序是?

单选题 · mid

  • A.打分 → 过滤 → 绑定
  • B.过滤 → 打分 → 绑定← 正确答案
  • C.绑定 → 过滤 → 打分
  • D.打分 → 绑定 → 过滤

答案:B。调度流程像王师傅分拣包裹:先剔除不可达的配送路线(过滤),再对剩下的路线评估优劣(打分),最后贴上目的地标签(绑定)。先过滤保证可行性,再打分追求最优性。选项B正确。

Q2. 关于 LeastRequested 打分策略,以下说法正确的是?

单选题 · mid

  • A.节点使用率越高,得分越高
  • B.节点剩余资源越多,得分越高← 正确答案
  • C.节点性能越强,得分越高
  • D.节点上 Pod 数量越少,得分越高

答案:B。LeastRequested 策略的核心思想是“负载均衡”——就像传送带剩余空间越大越容易被选中,保证新 Pod 尽量落在资源空闲的节点上。得分公式为 (capacity - requested) / capacity × 10,剩余越多得分越高,选 B。

Q3. 关于硬亲和(requiredDuringScheduling)与软亲和(preferredDuringScheduling)的区别,正确的是?

单选题 · mid

  • A.硬亲和只在调度时生效,软亲和运行期也生效
  • B.硬亲和找不到匹配节点则调度失败,软亲和找不到可以退而求其次← 正确答案
  • C.硬亲和用于 Pod 间调度,软亲和用于节点间调度
  • D.两者没有区别

答案:B。硬亲和是“必须满足的条件”——像冷链包裹必须送到有冷藏设备的站点,找不到匹配节点 Pod 就一直 Pending。软亲和是“尽量满足的偏好”——实在找不到匹配节点也可以调度到其他节点。两者只在调度阶段生效,运行期都不重新评估。选 B。

Q4. 节点上有污点 gpu=true:NoExecute,一个不匹配该容忍的 Pod 已经运行在这台节点上,会发生什么?

单选题 · hard

  • A.Pod 继续正常运行,不受影响
  • B.Pod 被驱逐,迁移到其他节点← 正确答案
  • C.Pod 被立即删除且不会重新创建
  • D.节点上的所有 Pod 都会被驱逐

答案:B。NoExecute 污点的语义是“既不调度新 Pod,也驱逐不匹配的已有 Pod”。就像配送站突然宣布“只有有证的人才能待在这里”,没证的人必须离开。Pod 被驱逐后会由控制器(如 Deployment)在其他节点重新创建。选项 B 正确,C 错在“不会重新创建”,D 错在“所有 Pod”——有容忍的 Pod 不会受影响。

Q5. 关于 ResourceQuota 和 LimitRange 的关系,最准确的说法是?

单选题 · easy

  • A.两者功能相同,可以互相替代
  • B.ResourceQuota 管单 Pod 资源量,LimitRange 管命名空间总量
  • C.ResourceQuota 管命名空间资源总量,LimitRange 管单个 Pod 的资源范围← 正确答案
  • D.ResourceQuota 只限制 CPU,LimitRange 只限制内存

答案:C。ResourceQuota 像物流中心的“区域总量墙”——限制某个命名空间的资源总和上限;LimitRange 像“单件包裹卡尺”——限制单个 Pod 的资源声明范围并填充默认值。两者组合实现“总量 + 单量”的双层治理。选 C。

Q6. 关于 K8s 优先级抢占机制,以下描述正确的是?

多选题 · hard

  • A.高优 Pod 可以在任何资源不足的情况下抢占低优 Pod
  • B.抢占发生时,调度器会优先选择删除优先级最低的 Pod 作为受害者← 正确答案
  • C.被抢占的 Pod 如果由 Deployment 管理,会在其他节点被重新创建← 正确答案
  • D.抢占可以违反 PDB(PodDisruptionBudget)策略强行驱逐

答案:BC。抢占不是无条件的——只有当高优 Pod 在所有节点都调度失败时才触发(A 错)。选择受害者时优先挑优先级最低的 Pod(B 对)。被抢占的 Pod 受控制器管理时会被重新创建,集群压力只是转移(C 对)。PDB 是保护伞,调度器必须尊重 PDB 策略,不能强行驱逐(D 错)。

Q7. 以下哪些是 Pod 调度失败时 Pending 状态的可能原因?

多选题 · mid

  • A.集群中所有节点资源不满足 Pod 请求← 正确答案
  • B.节点存在污点且 Pod 没有对应容忍← 正确答案
  • C.Pod 的硬亲和条件无法被任何节点满足← 正确答案
  • D.命名空间 ResourceQuota 限制的 Pod 数量超出上限← 正确答案

答案:ABCD。四种情况都会导致 Pod 无法被调度而保持 Pending。资源不足被过滤、污点不匹配被过滤、硬亲和找不到节点被过滤、配额超限在 API Server 层就被拒绝。排查时可用 kubectl describe pod 查看 Events 定位具体原因。

Q8. 节点亲和性 requiredDuringSchedulingIgnoredDuringExecution 中后半段 IgnoredDuringExecution 的含义是?

单选题 · mid

  • A.调度完成后,即使节点标签变化,已调度的 Pod 也不会被重新调度← 正确答案
  • B.调度完成后,节点标签变化会触发 Pod 重新调度
  • C.调度过程中忽略节点标签的作用
  • D.Pod 运行期间亲和性规则自动失效

答案:A。亲和性规则只在“调度时”被评估。Pod 绑定节点后,即使节点的标签发生变化(比如被打上/删除了匹配标签),已运行的 Pod 也不会因此被重新调度或驱逐。这个设计保证了系统稳定性——标签是动态的,不能让运行中的应用频繁搬家,选 A。

Q9. 在 K8s 中,Pod 的调度决策最终写到哪里?

单选题 · easy

  • A.写入党节点上的本地磁盘
  • B.通过 API Server 写入 etcd,更新 Pod 的 nodeName 字段← 正确答案
  • C.写入调度器的内存缓存
  • D.写入 kubelet 的配置文件

答案:B。调度器通过 API Server 将 Pod 的 spec.nodeName 字段更新为选中的节点名,这个状态最终持久化在 etcd 中。节点上的 kubelet watch 到该绑定信息后才会真正创建容器。这体现了“调度器只做决策,kubelet 负责执行”的组件分工。

Q10. 关于 LimitRange 的功能,以下说法正确的有?

多选题 · mid

  • A.可以设置命名空间内单个 Pod 的 CPU 最小值和最大值← 正确答案
  • B.可以为未声明资源请求的 Pod 自动填充默认值← 正确答案
  • C.可以限制命名空间内所有 Pod 的总内存用量
  • D.可以单独设置 requests 和 limits 的默认值← 正确答案

答案:ABD。LimitRange 管“单量”——设置单个 Pod 的资源最小/最大值(A 对),还可以通过 default/defaultRequest 字段为未声明资源的 Pod 填充默认值(B、D 对)。限制命名空间资源总量是 ResourceQuota 的职责(C 错),两者定位不同。