☰
k8s_PaaS 实战:Kubernetes 调度机制深度解析(资源模型、调度策略、优先级抢占与 CRI)
2026/10/12 14:45:32 网站建设 项目流程
  • 文档
  • 教程
  • 云原生
  • DevOps

【免费下载链接】k8s_PaaS

如何基于K8s(Kubernetes)部署成PaaS/DevOps(一套完整的软件研发和部署平台)--教程/学习(实战代码/架构设计/大量注释/操作配图),你将习得部署如:K8S(Kubernetes)、Dashboard、Harbor、Jenkins、本地Gitlab、Apollo框架、Promtheus、Grafana、Spinnaker等。

项目地址:https://gitcode.com/gh_mirrors/k8s/k8s_PaaS
点击查看免费下载

Kubernetes 调度机制决定了"一个 Pod 究竟该跑在哪台宿主机上",是所有 PaaS/DevOps 平台稳定运行的基石。本篇以 原理及源码解析/Kubernetes调度机制.md 为主体,结合本仓库(k8s_PaaS)中的集群部署实战文档,系统讲解资源模型(requests/limits 与 QoS)、默认调度策略(Predicates 过滤与 Priorities 打分)、优先级与抢占机制(PriorityClass),以及 kubelet 通过 CRI 调用容器运行时的完整链路。读完本文,你将能够正确为业务 Pod 声明资源配额、设计 QoS 级别,理解调度器选节点的打分逻辑,并能借助 PriorityClass 保障关键业务(如 DNS、监控)的优先调度。


一、Kubernetes 的资源模型与资源管理

所有跟调度和资源管理相关的属性,都属于 Pod 对象的字段,其中最重要的部分就是 Pod 的 CPU 和内存配置。如下所示:

apiVersion: v1 kind: Pod metadata: name: frontend spec: containers: - name: db image: mysql env: - name: MYSQL_ROOT_PASSWORD value: "password" resources: requests: memory: "64Mi" cpu: "250m" limits: memory: "128Mi" cpu: "500m" - name: wp image: wordpress resources: requests: memory: "64Mi" cpu: "250m" limits: memory: "128Mi" cpu: "500m"

这里有几个关键概念需要厘清:

  • CPU 是"可压缩资源":CPU 资源不足时,Pod 只会"饥饿"(被限流、排队等待),不会退出。
  • 内存是"不可压缩资源":内存不足时,Pod 会因为 OOM(Out-Of-Memory)被内核直接杀掉。
  • 资源限额是 Container 级别的:Pod 可以由多个 Container 组成,所以 CPU 和内存的限额要配置在每个 Container 的定义上,Pod 整体的资源配置由这些 Container 的配置值累加得到。

requests 与 limits 的区别

requests 和 limits 的区别很简单:在调度的时候,kube-scheduler 只会按照 requests 的值进行计算;而在真正设置 Cgroups 限制的时候,kubelet 则会按照 limits 的值来进行设置。

这一点对理解整个资源模型至关重要——它意味着:

  1. requests 决定"能不能调度":调度器只为 Pod 寻找 requests 资源能被满足的节点。
  2. limits 决定"能用多少":Pod 落到节点上之后,kubelet 根据 limits 值给容器设置 Cgroups 上限。

requests + limits 的做法,其实是 Borg 的思想:实际场景中,大多数作业使用到的资源其实远小于它所请求的资源限额。基于这种假设,用户可以声明一个相对较小的 requests 值供调度器使用,而 Kubernetes 真正设置给容器 Cgroups 的,则是相对较大的 limits 值。这样既提高了集群的资源装箱率,又给容器留出了足够的突发能力(burst)。

补充:容器底层资源隔离正是依赖 Cgroups 与 Namespace 这两项 Linux 内核能力。关于"容器是怎么隔离的"(Cgroups 约束资源、Namespace 修改进程视图),可参见仓库中的 原理及源码解析/Docker基础.md。

QoS 模型:Guaranteed / Burstable / BestEffort

Kubernetes 根据 Pod 中每个 Container 的 requests 与 limits 声明情况,把 Pod 划分为三个 QoS(服务质量)级别:

Guaranteed:Pod 里的每一个 Container 都同时设置了 requests 和 limits,并且 requests 和 limits 值相等时,这个 Pod 就属于 Guaranteed 类别。

apiVersion: v1 kind: Pod metadata: name: qos-demo namespace: qos-example spec: containers: - name: qos-demo-ctr image: nginx resources: limits: memory: "200Mi" cpu: "700m" requests: memory: "200Mi" cpu: "700m"

Burstable:当 Pod 不满足 Guaranteed 的条件,但至少有一个 Container 设置了 requests,那么这个 Pod 就会被划分到 Burstable 类别。

apiVersion: v1 kind: Pod metadata: name: qos-demo-2 namespace: qos-example spec: containers: - name: qos-demo-2-ctr image: nginx resources: limits: memory: "200Mi" requests: memory: "100Mi"

BestEffort:如果 Pod 既没有设置 requests,也没有设置 limits,那么它的 QoS 类别就是 BestEffort。

apiVersion: v1 kind: Pod metadata: name: qos-demo-3 namespace: qos-example spec: containers: - name: qos-demo-3-ctr image: nginx

说明:若按原文档严格表述,Burstable 是"Pod 不满足 Guaranteed 条件,但至少有一个 Container 设置了 requests";这里给出的 YAML 已补全为合法格式(limits 与 requests 均可选,若只设 requests 也可归入 Burstable)。

QoS 划分的主要应用场景,是当宿主机资源紧张时,kubelet 对 Pod 进行 Eviction(资源回收)时的驱逐顺序。当 Eviction 发生时,kubelet 挑选 Pod 进行删除的优先级如下:

BestEffort < Burstable < Guaranteed

也就是说,资源紧张时会最先回收 BestEffort 类 Pod,最后才考虑 Guaranteed 类 Pod。并且 Kubernetes 会保证:只有当 Guaranteed 类别的 Pod 资源使用量超过了其 limits 的限制,或者宿主机本身正处于 Memory Pressure(内存压力)状态时,Guaranteed 的 Pod 才可能被选中进行 Eviction 操作。这为关键业务 Pod 提供了强有力的保护。

cpuset:把容器绑定到独占 CPU 核

cpuset 是一个实际生产中非常有用的特性。使用容器时,可以通过设置 cpuset 把容器绑定到某个 CPU 的核上,而不是像 CPU Share 那样共享 CPU 的计算能力。这样 CPU 之间进行上下文切换的次数大大减少,容器里应用的性能会得到大幅提升。

实现方法需要同时满足两个条件:

  1. Pod 必须是Guaranteed的 QoS 类型;
  2. 将 Pod 的 CPU 资源的 requests 和 limits 设置为同一个相等的整数值。

如下例子:

spec: containers: - name: nginx image: nginx resources: limits: memory: "200Mi" cpu: "2" requests: memory: "200Mi" cpu: "2"

这样,该 Pod 就会绑定在 2 个独占的 CPU 核上,具体是哪两个 CPU,由 kubelet 分配。

基于上面的情况,建议将 DaemonSet(或类似的常驻型工作负载)的 Pod 都设置为 Guaranteed 的 QoS 类型,否则一旦因资源紧张被回收,又立即会在宿主机上重建出来,这样资源回收的动作就没有意义了——这也是本仓库在部署集群关键组件时的重要实践依据。


二、Kubernetes 默认的调度策略

调度器(kube-scheduler)的工作可以拆解为两个阶段:Predicates(过滤)与Priorities(打分)。上文的调度机制工作原理示意图直观展示了这一流程。

Predicates:四类过滤规则

第一种类型,GeneralPredicates:这一组过滤规则负责最基础的调度策略,计算的是宿主机的 CPU 和内存资源等是否够用——也就是根据 Pod 的 requests 值,判断节点是否满足最基本的资源需求。

第二种类型,与 Volume 相关的过滤规则:这一组过滤规则负责与容器持久化 Volume 相关的调度策略,例如检查 Pod 声明的 PV 是否能够挂载到候选节点上(访问模式、地域等约束)。

第三种类型,宿主机相关的过滤规则:这一组规则主要考察待调度 Pod 是否满足 Node 本身的某些条件。比如PodToleratesNodeTaints,负责检查的就是我们经常用到的 Node 的"污点"(Taint)机制——只有带对应容忍(Toleration)的 Pod 才能调度到被打上污点的节点上。

第四种类型,Pod 相关的过滤规则:这一组规则与 GeneralPredicates 大多数是重合的。比较特殊的是PodAffinityPredicate,这个规则的作用是检查待调度 Pod 与 Node 上已有 Pod 之间的亲密(affinity)和反亲密(anti-affinity)关系。

在具体执行的时候,当开始调度一个 Pod 时,Kubernetes 调度器会同时启动 16 个 Goroutine,并发地为集群里的所有 Node 计算 Predicates,最后返回可以运行这个 Pod 的宿主机列表。

Goroutine 是 Go 语言中的"协程/轻量级线程",比传统线程对资源的占用更合理,这也是调度器能够高并发完成全集群节点过滤的底层保障。

Priorities:为候选节点打分

在 Predicates 阶段完成了节点的"过滤"之后,Priorities 阶段的工作就是为这些节点打分。打分范围是 0-10 分,得分最高的节点就是最后被 Pod 绑定的最佳节点。

Priorities 里最常用到的一个打分规则是LeastRequestedPriority(最少请求优先)。它的计算方法可以总结为如下公式:

score = (cpu((capacity-sum(requested))10/capacity) + memory((capacity-sum(requested))10/capacity))/2

这个算法实际上就是在选择空闲资源(CPU 和 Memory)最多的宿主机——即"空闲资源越多,得分越高"。

与 LeastRequestedPriority 一起发挥作用的,还有BalancedResourceAllocation(资源均衡分配)。它的计算公式如下:

score = 10 - variance(cpuFraction,memoryFraction,volumeFraction)*10

每种资源的 Fraction 的定义是"Pod 请求的资源 / 节点上的可用资源"。而 variance 算法的作用,是计算每两种资源 Fraction 之间的"距离"。最后选择的,是资源 Fraction 差距最小的节点。

也就是说,调度完成后,会优先选中所有节点里各种资源分配最均衡的那个节点,从而避免一个节点上 CPU 被大量分配、而 Memory 大量剩余的情况——这对于提升集群整体的资源利用率和稳定性都非常重要。

此外,还有NodeAffinityPriority、TaintTolerationPriority和InterPodAffinityPriority这三种 Priority。除了默认的调度策略,还有很多默认不会开启的策略,可以通过为 kube-scheduler 指定一个配置文件,或者创建一个 ConfigMap,来配置哪些规则需要开启、哪些规则需要关闭;并且还可以通过为 Priorities 设置权重,来控制调度器的调度行为。

仓库佐证:在 第二章——企业部署实战_K8S.md 中可以看到本仓库实际部署 kube-scheduler 的方式——通过 supervisord 守护启动kube-scheduler.sh,核心启动参数包括--leader-elect(多主高可用选主)、--master http://127.0.0.1:8080(连接 API Server)与--v 2(日志级别),并将日志输出到/data/logs/kubernetes/kube-scheduler,这是调度器在真实 PaaS 环境中的落地形态。


三、调度器的优先级与抢占机制

实际工作中,我们也需要一些 Pod 拥有优先级和抢占机制。比如 Pod 调度失败后,会被"搁置",直到 Pod 被更新或集群状态发生变化,调度器才会对 Pod 进行重新调度。但我们希望高优先级的 Pod 调度失败后不被搁置,而是能够"挤走"某些低优先级的 Pod。

该特性在 Kubernetes 1.11 版本后已经是 Beta,用法如下。

第一步:创建 PriorityClass

先在 Kubernetes 里提交一个 PriorityClass,如下:

apiVersion: scheduling.k8s.io/v1beta1 kind: PriorityClass metadata: name: high-priority value: 1000000 globalDefault: false description: "This priority class should be used for high priority service pods only."

这个 YAML 文件名为 high-priority。

value:值越大代表优先级越高(最大 1000000000/10 亿)。

globalDefault:声明使用该 PriorityClass 的 Pod 拥有值为 1000000 的优先级,没有声明则为 0。如果为 true,则意味着这个 PriorityClass 的值会成为系统的默认值。

第二步:在 Pod 中声明使用

然后 Pod 就可以声明使用该优先级:

apiVersion: v1 kind: Pod metadata: name: nginx labels: env: test spec: containers: - name: nginx image: nginx imagePullPolicy: IfNotPresent priorityClassName: high-priority

提交后,Kubernetes 的PriorityAdmissionController就会自动将这个 Pod 的spec.priority字段设置为 1000000。

优先级与抢占如何发生作用

  • 优先级:调度器里维护着一个调度队列,高优先级的 Pod 就可能会比低优先级的 Pod 提前出队,从而尽早完成调度过程。这个过程,就是"优先级"这个概念在 Kubernetes 里的主要体现。
  • 抢占:当一个高优先级的 Pod 调度失败的时候,调度器的抢占能力就会被触发。这时,调度器就会试图从当前集群里寻找一个节点,使得当这个节点上的一个或多个低优先级 Pod 被删除后,待调度的高优先级 Pod 就可以被调度到这个节点上。这个过程,就是"抢占"这个概念在 Kubernetes 里的主要体现。

仓库佐证:在 第三章——k8s集群.md 部署 CoreDNS 的 Deployment 中,就为 DNS 服务声明了priorityClassName: system-cluster-critical——这是 Kubernetes 内置的系统级关键优先级类,保证 DNS 这类集群基础设施 Pod 在资源紧张时优先调度、不被低优先级负载抢占,属于优先级与抢占机制最典型的生产实践。


四、kubelet 与 CRI(容器运行时接口)

kubelet 为什么不直接调用 Docker API

kubelet 调用下层容器运行时的执行过程,并不会直接调用 Docker 的 API,而是通过一组叫作 CRI(Container Runtime Interface,容器运行时接口)的 gRPC 接口来间接执行。之所以由 CRI 来间接执行,是关乎历史原因的:Kubernetes 需要支持多种容器运行时(Docker、containerd、CRI-O 等),如果 kubelet 直接依赖 Docker API,架构就会失去通用性。

搭配 CRI 之后,Kubernetes 以及 kubelet 本身的架构可以描述为:

  • Kubernetes 通过编排能力创建了一个 Pod 之后,调度器会为这个 Pod 选择一个具体的节点来运行。
  • 此时,kubelet 实际上会调用一个叫作GenericRuntime的通用组件,来发起创建 Pod 的 CRI 请求。
  • 如果使用的容器项目是 Docker,那么负责响应这个请求的就是一个叫作dockershim的组件,它会把 CRI 请求里的内容拿出来,然后组装成 Docker API 请求发给 Docker Daemon。

CRI 接口的两组定义

CRI 这个接口的定义如上图所示,主要由两组 gRPC 服务组成:

  • 第一组,RuntimeService:提供的接口主要与容器操作相关。比如创建和启动容器、删除容器、执行 exec 命令等等。
  • 第二组,ImageService:提供的接口主要与容器镜像操作相关。比如拉取镜像、删除镜像等等。

CRI 设计的一个重要原则:只关注容器,不关注 Pod

为什么 CRI 里没有直接创建 Pod 或启动 Pod 的接口?文档给出了两层原因:

第一,Pod 是 Kubernetes 的编排概念,而不是容器运行时的概念。所以我们不能假设所有下层容器项目,都能够暴露出可以直接映射为 Pod 的 API。

第二,如果 CRI 里引入了关于 Pod 的概念,那么接下来只要 Pod API 对象的字段发生变化,CRI 就很有可能需要变更。而在 Kubernetes 开发的前期,Pod 对象的变化还是比较频繁的,但对于 CRI 这样的标准接口来说,这个变更频率就有点麻烦了。

所以,在 CRI 的设计里,并没有一个直接创建 Pod 或者启动 Pod 的接口——Pod 被拆解为"一组共享某些 Namespace 的容器",由 kubelet 层面负责 Pod 语义的组装,运行时层面只关心单个容器的生命周期与镜像管理。

仓库佐证:在 第二章——企业部署实战_K8S.md 中可以看到本仓库实际部署 kubelet 的启动脚本(kubelet.sh),关键参数包括--cgroup-driver systemd(与宿主机的 Cgroups 驱动保持一致)、--cluster-dns 192.168.0.2与--cluster-domain cluster.local(注入 DNS 配置)、--kubeconfig ./conf/kubelet.kubeconfig(连接 API Server)、--pod-infra-container-image(指定 pause 基础容器镜像)等,这些都是 kubelet 在真实 PaaS 集群中与 CRI 运行时协同工作所必需的配置。


总结

从资源模型到调度策略,再到运行时调用,Kubernetes 的调度机制是一条完整链路:

  1. 资源声明是起点:在 Container 上配置 requests 与 limits,决定 Pod 的 QoS 级别(Guaranteed / Burstable / BestEffort),进而决定资源紧张时的驱逐顺序,以及能否使用 cpuset 获得独占 CPU。
  2. 调度器是决策中枢:Predicates 阶段以 requests 为依据过滤节点(16 个 Goroutine 并发执行),Priorities 阶段按 LeastRequestedPriority、BalancedResourceAllocation 等规则打分(0-10 分),选出最合适的节点;通过 kube-scheduler 配置文件或 ConfigMap 可以灵活开关策略、调整权重。
  3. 优先级与抢占是关键保障:PriorityClass + priorityClassName 让关键业务 Pod 优先出队、失败时可抢占低优先级 Pod,适合 DNS、监控等系统级组件(如仓库中 CoreDNS 使用system-cluster-critical)。
  4. kubelet + CRI 是落地执行:调度完成后,kubelet 通过 CRI(RuntimeService + ImageService)间接调用下层运行时(如 dockershim → Docker Daemon),实现"只关注容器、不关注 Pod"的标准化运行时抽象。

结合本仓库 原理及源码解析/ 与 第二章——企业部署实战_K8S.md、第三章——k8s集群.md 等章节,你可以进一步把上述机制落到真实集群的部署与调优实践中。

  • 文档
  • 教程
  • 云原生
  • DevOps

【免费下载链接】k8s_PaaS

如何基于K8s(Kubernetes)部署成PaaS/DevOps(一套完整的软件研发和部署平台)--教程/学习(实战代码/架构设计/大量注释/操作配图),你将习得部署如:K8S(Kubernetes)、Dashboard、Harbor、Jenkins、本地Gitlab、Apollo框架、Promtheus、Grafana、Spinnaker等。

项目地址:https://gitcode.com/gh_mirrors/k8s/k8s_PaaS
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询