一文读懂K8s调度:原理+实操精简例子
# 前言
在K8s(Kubernetes)集群中,我们每天部署Pod、运行服务,却很少深究一个核心问题:Pod是怎么被分配到具体节点(Node)上运行的?
其实这背后全靠「调度器」在默默工作——kube-scheduler,作为K8s Master节点的核心组件,它的职责只有一个:为每个待调度的Pod,找到最适合它的Node。今天就用通俗的语言+精简实操例子,把K8s调度的过程、原理讲透,新手也能轻松理解。
# 一、先搞懂:K8s调度的核心逻辑(一句话总结)
调度的本质:筛选合格节点 → 给合格节点打分 → 选最高分节点绑定Pod。
类比生活场景:你要找一间办公室(Pod),先筛选出“面积够大、有空调、在市中心”的合格写字楼(过滤),再给这些写字楼打分(交通便利度、租金、环境),最后选分数最高的入驻(绑定)。kube-scheduler就是这个“找办公室的人”。
核心前提:未被调度的Pod,其spec.nodeName字段为空;调度完成后,kube-scheduler会将选中的Node名称写入该字段,Node上的kubelet看到后,就会拉取镜像、启动容器。
# 二、K8s调度完整过程(分3步,附精简例子)
整个调度流程分为「过滤(Predicate)→ 打分(Priority)→ 绑定(Bind)」三步,每一步都有明确的逻辑和实操场景,我们结合例子逐个拆解。
# 第一步:过滤(Predicate)—— 淘汰“不合格”节点
过滤的核心是“排除法”:遍历集群中所有Node,根据预设规则,删掉所有不能运行当前Pod的Node,剩下的就是“候选节点”。
常见过滤规则(结合精简例子,直接套用):
# 1. 资源足够(最基础)
Pod声明的CPU、内存等资源,不能超过Node的剩余可分配资源。
例子:创建一个Pod,声明请求CPU 1核、内存1Gi,集群中有3个Node:
Node1:剩余CPU 0.8核 → 资源不足,被过滤
Node2:剩余CPU 1.2核、内存1.5Gi → 合格
Node3:剩余CPU 2核、内存2Gi → 合格
过滤后,候选节点为Node2、Node3。
# 2. 端口不冲突(避免端口占用)
如果Pod使用了hostPort(直接占用Node的端口),则该Node上的这个端口不能被其他Pod或服务占用。
例子:Pod声明hostPort: 8080,Node2上已有一个Pod占用8080端口 → Node2被过滤,仅剩Node3为候选节点。
# 3. 污点与容忍(Taint/Toleration)—— 节点“拒绝”不合格Pod
Node可以设置“污点”(Taint),表示“我不接受某些Pod”;Pod必须设置“容忍”(Toleration),才能被调度到有对应污点的Node上。
精简例子(实操可直接执行):
给Node3设置污点:
kubectl taint nodes node3 key=value:NoSchedule(NoSchedule表示“不调度新Pod到该节点,已有Pod不受影响”)若Pod未设置容忍,则Node3被过滤,无候选节点(Pod会处于Pending状态);
给Pod添加容忍,即可调度到Node3:
apiVersion: v1
kind: Pod
metadata:
name: test-pod
spec:
tolerations: # 容忍Node3的污点
- key: "key"
operator: "Equal"
value: "value"
effect: "NoSchedule"
containers:
- name: nginx
image: nginx:alpine
resources:
requests:
cpu: "1"
memory: "1Gi"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 4. 节点亲和性(NodeAffinity)—— 主动“选择”符合条件的Node
通过标签(Label)控制Pod调度到指定类型的Node上,比nodeSelector更灵活。
精简例子:
给Node2、Node3打标签:
kubectl label nodes node2 node3 env=prod(标记为生产环境节点)创建Pod时,设置亲和性,只调度到env=prod的Node上:
apiVersion: v1
kind: Pod
metadata:
name: prod-pod
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution: # 调度时必须满足
nodeSelectorTerms:
- matchExpressions:
- key: env
operator: In
values:
- prod # 只选标签为env=prod的Node
containers:
- name: nginx
image: nginx:alpine
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
此时,若集群中还有Node4(标签env=test),会被过滤掉,仅Node2、Node3为候选。
补充:其他常见过滤规则还包括「节点健康(未NotReady)」「未被封锁(cordon)」「PVC存储匹配」等,只要有一条不满足,Node就会被淘汰。
# 第二步:打分(Priority)—— 给候选节点“排先后”
过滤后得到候选节点,接下来调度器会给每个候选节点打分(0~100分),分数越高,越适合运行当前Pod。打分的核心逻辑是「资源利用率均匀、负载最低」,避免单节点压力过大。
常见打分策略(结合例子理解):
# 1. LeastRequested(默认策略)—— 负载最低优先
计算节点的资源剩余比例,剩余越多,分数越高。公式简化:分数 = (1 - 已用资源/总资源)× 100
例子:候选节点Node2、Node3,总CPU均为2核、总内存均为2Gi:
Node2:已用CPU 0.5核、内存0.5Gi → 剩余比例高 → 打分80分
Node3:已用CPU 1核、内存1Gi → 剩余比例低 → 打分60分
此时Node2分数更高,成为首选。
# 2. BalancedResourceAllocation —— 资源使用均匀优先
优先选择CPU和内存利用率最均衡的节点,避免“单种资源耗尽、另一种资源闲置”。
例子:Node2(CPU利用率30%、内存利用率70%),Node3(CPU利用率50%、内存利用率50%) → Node3更均衡,打分更高。
# 3. NodeAffinity —— 亲和性加分
若Pod设置了亲和性,满足亲和性条件的节点会额外加分(比如加10分),进一步提升优先级。
# 第三步:绑定(Bind)—— 确定最终节点并执行
打分完成后,调度器选择分数最高的Node,将Node名称写入Pod的spec.nodeName字段,这个过程就是「绑定」。
绑定完成后,该Node上的kubelet会实时监听自身节点的Pod列表,发现新绑定的Pod后,立即拉取镜像、启动容器,Pod状态从Pending变为Running,调度完成。
# 三、调度核心补充(实操中必遇)
# 1. 调度是“一次性”的(默认不重调度)
Pod一旦被调度到某个Node,除非Pod重建(比如删除重启、滚动更新),否则不会被重新调度。即使Node负载变高,调度器也不会主动将Pod迁移到其他节点(需借助第三方工具,如Descheduler)。
# 2. 抢占机制(Preemption)—— 高优先级Pod“挤走”低优先级
若所有Node都没有足够资源,高优先级的Pod会“抢占”低优先级Pod的资源:删除低优先级Pod,释放资源后,将高优先级Pod调度到该Node。
例子:Node2上有一个低优先级Pod(优先级10),此时调度一个高优先级Pod(优先级100),且Node2是唯一候选节点 → 低优先级Pod被删除,高优先级Pod被调度到Node2。
# 3. 常见调度异常及解决(精简版)
Pod一直Pending,事件显示“No nodes available to schedule pods” → 检查过滤规则(资源不足、污点无容忍、亲和性不匹配);
Pod调度到错误节点 → 检查Node标签、亲和性配置,或是否有污点未处理;
高优先级Pod无法调度 → 检查是否开启抢占机制,或低优先级Pod是否可被删除。
# 四、总结(一句话串起所有逻辑)
用户创建Pod(spec.nodeName为空)→ 进入待调度队列 → kube-scheduler过滤掉不合格Node → 给候选Node打分 → 绑定最高分Node → kubelet启动容器 → 调度完成。
K8s调度的核心,本质是“按需分配、负载均衡”,通过过滤和打分,既保证Pod能正常运行,又能让集群资源利用率最大化。结合上面的精简例子,大家可以动手实操一遍,就能快速掌握调度的核心逻辑,遇到调度问题也能快速定位原因。