课程目录(第 14 章 / 共 33 章)
调度入门:把 Pod 放到合适的节点上
从调度器的过滤与打分讲起,用 nodeSelector、亲和性、污点容忍与 cordon/drain,把 Pod 放到(或赶下)你指定的节点。
学完这一章,你将能够
- ✓说清调度器「过滤 → 打分 → 绑定」三步分别在做什么
- ✓用 nodeSelector 与 nodeAffinity 把 Pod 绑定到指定节点
- ✓用污点与容忍解释为什么 Pod 不会跑到控制面节点上
- ✓会用 cordon / drain / uncordon 安全地腾空一台节点
调度器到底在做什么
前面几章里,你只写了「我要 3 个副本」,从来没说过它们该跑在哪台机器上。做出这个决定的是 kube-scheduler:它盯着所有 spec.nodeName 为空的 Pod,替它们挑一个节点。挑选分三步,你写的每一条约束都挂在其中某一步上:
你提交 Pod(spec.nodeName 为空)
│
▼
① 过滤 Filter:把不合格的节点全部排除,活下来的才能进入下一步
· 资源够不够(requests 里的 CPU / 内存、端口冲突)
· nodeSelector / nodeAffinity 的 required ← 硬约束在这里
· 节点污点 taints vs Pod tolerations ← 污点在这里
· podAffinity 的 required、卷拓扑、节点状态
│ 活下来的节点(0 个 → Pod 停在 Pending,原因写进事件)
▼
② 打分 Score:给候选节点排序 0~100
· ImageLocality:镜像是不是已经在这台机器上
· LeastAllocated:谁更空闲
· nodeAffinity 的 preferred、podAffinity、
topologySpreadConstraints ← 软约束在这里加分
│ 取最高分(并列时随机挑一个)
▼
③ 绑定 Bind:写入 spec.nodeName,该节点 kubelet 开始拉镜像、起容器如果没有节点通过「过滤」这一关,Pod 就停在 Pending,事件里会写 0/N nodes are available: ...,冒号后面列出每个节点被排除的原因。这是排查调度问题最重要的一句话。
| 你想表达的意思 | 用哪个字段 | 在哪一步生效 |
|---|---|---|
| 只允许落在带某标签的节点 | nodeSelector 或 nodeAffinity 的 required | 过滤 |
| 尽量落在带某标签的节点 | nodeAffinity 的 preferred | 打分 |
| 必须和我同类的 Pod 待在一起 | podAffinity | 过滤(required)或打分(preferred) |
| 必须和某类 Pod 分开(高可用) | podAntiAffinity | 过滤(required)或打分(preferred) |
| 这个节点只给特定团队用 | 节点污点 + Pod 容忍 | 过滤 |
| 副本均匀分散到各节点 | topologySpreadConstraints | 过滤或打分 |
调度是一次性决定
Pod 一旦被绑定到某台节点,除非它被删除重建、被驱逐(eviction)或节点消失,否则不会自动搬家。节点的标签后来变了,也不会把已经在跑的 Pod 挪走——这就是字段名里 IgnoredDuringExecution 的含义。
最简单的指定方式:nodeSelector 与 nodeName
nodeSelector 是最容易上手的写法:给节点打标签,Pod 里写上同名标签,两边完全相等才会被调度过去。
# 1. 看一眼节点名字,后面所有命令都用这个变量
NODE=$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}')
# 1. 给节点打标签并确认
kubectl label node "$NODE" disktype=ssd
kubectl get node "$NODE" --show-labelsapiVersion: v1
kind: Pod
metadata:
name: web-pinned
spec:
nodeSelector:
disktype: ssd
containers:
- name: web
image: nginx:1.27kubectl apply -f web-pinned.yaml
kubectl get pod web-pinned -o wide-o wide 会多出一列 NODE,确认它就是你打了标签的那台。kubectl describe pod web-pinned 的 Events 里应该有一行 Successfully assigned default/web-pinned to <节点名>。
nodeName 则是最粗暴的写法:直接告诉 kubelet「这个 Pod 归你了」,完全不经过调度器。
apiVersion: v1
kind: Pod
metadata:
name: web-forced
spec:
nodeName: kind-control-plane # 直接指定节点,跳过调度
containers:
- name: web
image: nginx:1.27nodeName 只用来调试
用 nodeName 意味着跳过资源检查、污点检查、亲和性检查:节点内存爆了也照样往上塞,节点名写错了 Pod 就一直 Pending,事件里还没有调度器留下的理由。生产环境不要用。
亲和性:nodeAffinity、podAffinity 与拓扑分布
节点亲和性:required 与 preferred
nodeSelector 只能表达「等于」,而且只有硬性要求。nodeAffinity 补上了两个能力:更丰富的匹配表达式,以及「尽量满足」的软约束。
| 写法 | 语义 | 不满足时 |
|---|---|---|
requiredDuringSchedulingIgnoredDuringExecution | 必须满足 | Pod 一直 Pending |
preferredDuringSchedulingIgnoredDuringExecution | 尽量满足,带 weight 权重 | 照样调度到别的节点 |
apiVersion: v1
kind: Pod
metadata:
name: web-preferred
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/os
operator: In
values:
- linux
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: disktype
operator: In
values:
- ssd
containers:
- name: web
image: nginx:1.27常用的 operator 有 In、NotIn、Exists、DoesNotExist、Gt、Lt。注意 nodeSelectorTerms 之间是「或」,matchExpressions 之间是「与」,写多个条件时很容易记反。
Pod 亲和与反亲和
podAffinity 关心的是「别的 Pod 在哪」,而不是节点标签。最常用的场景是反亲和:让同一个 Deployment 的多个副本不要挤在同一台机器上,一台机器挂了不至于全灭。
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: web
topologyKey: kubernetes.io/hostnametopologyKey 是「按什么维度分开」:kubernetes.io/hostname 表示按节点分,也可以写 topology.kubernetes.io/zone 表示按可用区分。
反亲和应用软约束
把上面的 preferred 换成 required,语义就变成「绝对不能在同一节点」。如果只有 1 台节点、却有 2 个副本,第二个副本会永远 Pending。单节点实验集群里请一律用 preferred。
拓扑分布约束
topologySpreadConstraints 一句话:按 topologyKey 把副本均匀撒到多个故障域,用 maxSkew 控制允许的最大偏差,whenUnsatisfiable: ScheduleAnyway 是软约束、DoNotSchedule 是硬约束。副本数多、要求打散时,它比反亲和更好用——反亲和只能表达「不在一起」,它还能表达「每台最多差 1 个」。
污点与容忍
前面都是 Pod 挑节点,污点反过来:节点主动拒绝 Pod。节点上打污点,只有声明了对应「容忍」的 Pod 才能落上去。控制面节点就是这么保护自己的。
# 给节点打污点:key=value:效果
kubectl taint nodes "$NODE" dedicated=kube101:NoSchedule
# 查看污点(Taints 一行)
kubectl describe node "$NODE" | grep -i taints
# 删除污点:末尾加一个减号
kubectl taint nodes "$NODE" dedicated=kube101:NoSchedule-三种效果的区别:
| 效果 | 对已经在跑的 Pod | 对新的 Pod |
|---|---|---|
PreferNoSchedule | 不处理 | 尽量不调度过来(软约束) |
NoSchedule | 不处理 | 不忍就调度不上去 |
NoExecute | 不容忍就被驱逐 | 不忍就调度不上去 |
Pod 侧的容忍长这样:
spec:
tolerations:
- key: dedicated
operator: Equal
value: kube101
effect: NoScheduleoperator 用 Exists 时可以省略 value,表示「只要键在就行」;key 也留空的话就是容忍一切污点,等于放弃保护,慎用。NoExecute 还支持 tolerationSeconds,表示「忍 N 秒再走」,常用于节点失联后的宽限期。
控制面节点默认带的污点是 node-role.kubernetes.io/control-plane:NoSchedule(较早的版本里键名是 master)。kubeadm 装出来的集群因此不允许普通业务 Pod 落在控制面;kind 这类单节点实验集群通常已经把它去掉了,用 kubectl describe node "$NODE" | grep -i taints 看一眼就知道。
维护节点:cordon / drain / uncordon
要给一台节点打补丁、换内核或下线维护时,顺序是这样的:
# 1. 标记为不可调度:新 Pod 不再上来,已经在跑的不动
kubectl cordon "$NODE"
kubectl get nodes # STATUS 变成 Ready,SchedulingDisabled
# 2. 驱逐节点上的 Pod(Deployment 管的会被调度到别的节点)
kubectl drain "$NODE" --ignore-daemonsets --delete-emptydir-data
# 3. 维护完成后恢复可调度
kubectl uncordon "$NODE"--ignore-daemonsets 是必须的:DaemonSet 的 Pod 本来就是要跑在每个节点上的,drain 无法驱逐它们,不加这个参数会直接报错退出。--delete-emptydir-data 是明确承认「这些用 emptyDir 的 Pod 数据会丢」。
单节点集群别随便 drain
只有一个节点时,drain 会把所有 Pod 赶下来,而它们无处可去,整个集群的应用都会变成 Pending。想演示「不可调度」的效果,用 cordon 就够了。
动手练习:在单节点集群里验证
用标签、污点和 cordon 观察调度器
NODE=$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}')
kubectl label node "$NODE" disktype=ssd --overwrite实验一:软约束照样能跑。 把「节点亲和性」的示例存成 web-preferred.yaml,把 preferred 里的 values 改成不存在的 nvme,kubectl apply -f web-preferred.yaml 后期望 kubectl get pod web-preferred -o wide 是 Running,只是拿不到偏好分。
实验二:硬约束会 Pending。 把 preferredDuringSchedulingIgnoredDuringExecution 改成 requiredDuringSchedulingIgnoredDuringExecution,删掉 weight 与 preference 这一层再 apply,然后 kubectl describe pod web-preferred | grep -A5 Events,期望 FailedScheduling 加 0/1 nodes are available: ... didn't match Pod's node affinity。
实验三:污点与 cordon 都会挡住新 Pod。
kubectl taint nodes "$NODE" dedicated=kube101:NoSchedule
kubectl run tainted-test --image=nginx:1.27 # 期望 Pending
kubectl taint nodes "$NODE" dedicated=kube101:NoSchedule-
kubectl cordon "$NODE"
kubectl run cordon-test --image=nginx:1.27 # 期望 Pending
kubectl uncordon "$NODE"
kubectl get pod cordon-test -o wide # 期望很快变成 Running收尾清理(污点若已删除,最后一条会报 taint not found,忽略即可):
kubectl delete pod web-pinned web-preferred tainted-test cordon-test --ignore-not-found
kubectl label node "$NODE" disktype-
kubectl taint nodes "$NODE" dedicated=kube101:NoSchedule-Pod 一直 Pending 的排查顺序
Pending 只说明「还没被调度成功」,原因有五六类。按下面的顺序逐条排除,每条都对应上面「过滤」阶段的一个检查点:
kubectl describe pod web-pinned | grep -A5 Events # 先读 0/N nodes are available 那一行
kubectl describe node "$NODE" | grep -E 'Taints|Allocatable|Allocated'
kubectl get pvc -n demo
kubectl describe resourcequota -n demo- 先读那句话:
0/N nodes are available: ...,冒号后面按节点列出原因,通常直接写出Insufficient cpu、had untolerated taint、didn't match Pod's node affinity或didn't find available persistent volumes,对应下面某一条。 - 资源不足:
Insufficient cpu/Insufficient memory。看describe node的Allocated resources,要么调小requests,要么加节点。 - 亲和性与选择器:
didn't match Pod's node affinity/selector、didn't match pod affinity rules。用kubectl get node "$NODE" --show-labels核对标签,注意nodeSelectorTerms是「或」、matchExpressions是「与」。 - 污点没容忍:
had untolerated taint {node-role.kubernetes.io/control-plane: NoSchedule},给 Pod 补对应的tolerations。 - PVC 没绑上:
pod has unbound immediate PersistentVolumeClaims,kubectl get pvc -n demo的STATUS不是Bound。细节见 第 10 章 存储卷与 PV/PVC。 - 配额不够:
exceeded quota,或LimitRange强制了最小 requests 而你没写。kubectl describe resourcequota -n demo看用量。 - 节点不可调度:
node(s) were unschedulable(被cordon过的节点)、node(s) had volume node affinity conflict、节点NotReady。用kubectl get nodes确认状态。
常见坑与速查表
调度类问题几乎都能在 Events 里找到答案
- required 写太严导致 Pending:
nodeAffinity的 required 或podAntiAffinity的 required,只要没有任何节点同时满足就永远 Pending。先看describe pod的0/N nodes are available那一行。 - 改了标签忘了 `--overwrite`:修改已有键必须加
--overwrite,否则直接报错。 - drain 卡住:报
cannot delete Pods with local storage就补--delete-emptydir-data;报 DaemonSet 相关错误就补--ignore-daemonsets。
| 现象 | 原因 | 怎么确认 |
|---|---|---|
Pending,0/N nodes are available: Insufficient cpu | 所有节点剩余可分配资源都不够 | kubectl describe node <node> 看 Allocated resources |
Pending,had untolerated taint | 节点有 NoSchedule / NoExecute 污点,Pod 没有对应容忍 | kubectl describe node <node> 的 Taints 行 |
Pending,didn't match Pod's node affinity | nodeSelector 或 required 亲和性的键值对不上 | kubectl get node <node> --show-labels |
Pending,unbound immediate PersistentVolumeClaims | PVC 没绑定成功 | kubectl get pvc -n <ns> 的 STATUS 列 |
Pending,node(s) were unschedulable | 节点被 cordon 过或处于维护状态 | kubectl get nodes 的 STATUS 列 |
事件里完全没有 FailedScheduling | 用了 nodeName,跳过了调度器 | kubectl get pod <pod> -o jsonpath='{.spec.nodeName}' |
自测题
自测:为什么给节点新打的标签不会把已经跑着的 Pod 挪走?(点击展开答案)
因为调度只在「Pod 还没有节点」时发生一次,绑定之后这个 Pod 就归那台节点的 kubelet 管了。nodeAffinity 的字段名里那半截 IgnoredDuringExecution(执行期间忽略)说的正是这件事:亲和性只在调度那一刻参与判断,Pod 跑起来之后节点标签再变也不影响它。这是刻意设计——如果标签一变就搬家,运维改一次标签就会引发全集群的 Pod 重启雪崩。想让 Pod 换节点,得删掉它让控制器重建,或者对节点执行 drain 主动驱逐。
自测:为什么只有一台节点时,`podAntiAffinity` 用 required 会让副本一直 Pending?(点击展开答案)
required 在「过滤」阶段生效,要求「不能和带 app: web 标签的 Pod 落在同一个 topologyKey 域内」。只有一台节点时,第一个副本占掉这台机器,第二个副本能选的所有节点都被过滤掉了,候选集合变成空集,Pod 就只能停在 Pending。把同一个约束改成 preferred 就变成「打分」阶段的扣分项:没有更好的选择时,仍然允许挤在一起。所以单节点实验集群里,副本分散一律用软约束,或者干脆用 topologySpreadConstraints 的 ScheduleAnyway。
自测:为什么用 `nodeName` 时事件里看不到调度失败的原因?(点击展开答案)
因为 nodeName 是直接写给 Pod 的字段,等于跳过了 kube-scheduler。调度器压根没参与,自然不会产生 FailedScheduling 事件;Pod 只是卡在 Pending 等着那台(可能根本不存在的)节点上的 kubelet 来接管。这也解释了为什么 nodeName 写错时最难查——没有事件、没有理由,只能靠 kubectl get pod -o jsonpath='{.spec.nodeName}' 自己核对节点名。用 nodeSelector 或亲和性时,过滤阶段的每一条排除理由都会写进事件,排查成本低得多。
小结
- 调度器的工作流是过滤 → 打分 → 绑定;没有节点通过过滤时 Pod 停在
Pending,原因写在describe的 Events 里。 nodeSelector表达「等于」,nodeAffinity表达更复杂的规则;required是硬约束(会 Pending),preferred是软约束。podAffinity/podAntiAffinity按topologyKey决定 Pod 之间靠近还是分开,副本分散优先用preferred或topologySpreadConstraints。- 污点写在节点上、容忍写在 Pod 上;
NoSchedule只挡新 Pod,NoExecute还会驱逐已有 Pod。 - 维护节点的标准动作是
cordon→drain→uncordon,drain 记得带上--ignore-daemonsets。
相关章节:第 11 章 探针与资源管理 讲 requests 怎么影响调度,第 15 章 排障手册 把这里的事件分析放进了完整的排查流程。
练习
- 给节点打上
gpu=true标签,写一个 required 的nodeAffinity,再故意把值改成false,观察 Events 里调度器的原话。 - 用
topologySpreadConstraints把一个 3 副本的 Deployment 分散到多节点(如果没有多节点,先用maxSkew: 1+ScheduleAnyway跑通语法)。 - 思考题:如果一台节点磁盘写满被 kubelet 打上
node.kubernetes.io/disk-pressure:NoSchedule,已经在上面跑的 Pod 会被赶走吗?为什么?
调度只决定 Pod 落在哪,落下去之后出问题就是另一套功夫了。下一章我们整理一份排障手册,把「先看什么、再看什么」固定成流程。