课程目录(第 14 章 / 共 33 章)
课程/走向生产

调度入门:把 Pod 放到合适的节点上

14 章 / 共 33·16 分钟·入门+调度节点亲和性kubectl

从调度器的过滤与打分讲起,用 nodeSelector、亲和性、污点容忍与 cordon/drain,把 Pod 放到(或赶下)你指定的节点。

学完这一章,你将能够

  • 说清调度器「过滤 → 打分 → 绑定」三步分别在做什么
  • 用 nodeSelector 与 nodeAffinity 把 Pod 绑定到指定节点
  • 用污点与容忍解释为什么 Pod 不会跑到控制面节点上
  • 会用 cordon / drain / uncordon 安全地腾空一台节点

调度器到底在做什么

前面几章里,你只写了「我要 3 个副本」,从来没说过它们该跑在哪台机器上。做出这个决定的是 kube-scheduler:它盯着所有 spec.nodeName 为空的 Pod,替它们挑一个节点。挑选分三步,你写的每一条约束都挂在其中某一步上

text
你提交 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: ...,冒号后面列出每个节点被排除的原因。这是排查调度问题最重要的一句话。

你想表达的意思用哪个字段在哪一步生效
只允许落在带某标签的节点nodeSelectornodeAffinity 的 required过滤
尽量落在带某标签的节点nodeAffinity 的 preferred打分
必须和我同类的 Pod 待在一起podAffinity过滤(required)或打分(preferred)
必须和某类 Pod 分开(高可用)podAntiAffinity过滤(required)或打分(preferred)
这个节点只给特定团队用节点污点 + Pod 容忍过滤
副本均匀分散到各节点topologySpreadConstraints过滤或打分

调度是一次性决定

Pod 一旦被绑定到某台节点,除非它被删除重建、被驱逐(eviction)或节点消失,否则不会自动搬家。节点的标签后来变了,也不会把已经在跑的 Pod 挪走——这就是字段名里 IgnoredDuringExecution 的含义。

最简单的指定方式:nodeSelector 与 nodeName

nodeSelector 是最容易上手的写法:给节点打标签,Pod 里写上同名标签,两边完全相等才会被调度过去。

bash
# 1. 看一眼节点名字,后面所有命令都用这个变量
NODE=$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}')

# 1. 给节点打标签并确认
kubectl label node "$NODE" disktype=ssd
kubectl get node "$NODE" --show-labels
yaml
apiVersion: v1
kind: Pod
metadata:
  name: web-pinned
spec:
  nodeSelector:
    disktype: ssd
  containers:
    - name: web
      image: nginx:1.27
bash
kubectl 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 归你了」,完全不经过调度器

yaml
apiVersion: v1
kind: Pod
metadata:
  name: web-forced
spec:
  nodeName: kind-control-plane   # 直接指定节点,跳过调度
  containers:
    - name: web
      image: nginx:1.27

nodeName 只用来调试

nodeName 意味着跳过资源检查、污点检查、亲和性检查:节点内存爆了也照样往上塞,节点名写错了 Pod 就一直 Pending,事件里还没有调度器留下的理由。生产环境不要用。

亲和性:nodeAffinity、podAffinity 与拓扑分布

节点亲和性:required 与 preferred

nodeSelector 只能表达「等于」,而且只有硬性要求。nodeAffinity 补上了两个能力:更丰富的匹配表达式,以及「尽量满足」的软约束。

写法语义不满足时
requiredDuringSchedulingIgnoredDuringExecution必须满足Pod 一直 Pending
preferredDuringSchedulingIgnoredDuringExecution尽量满足,带 weight 权重照样调度到别的节点
yaml
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

常用的 operatorInNotInExistsDoesNotExistGtLt。注意 nodeSelectorTerms 之间是「或」,matchExpressions 之间是「与」,写多个条件时很容易记反。

Pod 亲和与反亲和

podAffinity 关心的是「别的 Pod 在哪」,而不是节点标签。最常用的场景是反亲和:让同一个 Deployment 的多个副本不要挤在同一台机器上,一台机器挂了不至于全灭。

yaml
spec:
  affinity:
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 100
          podAffinityTerm:
            labelSelector:
              matchLabels:
                app: web
            topologyKey: kubernetes.io/hostname

topologyKey 是「按什么维度分开」:kubernetes.io/hostname 表示按节点分,也可以写 topology.kubernetes.io/zone 表示按可用区分。

反亲和应用软约束

把上面的 preferred 换成 required,语义就变成「绝对不能在同一节点」。如果只有 1 台节点、却有 2 个副本,第二个副本会永远 Pending。单节点实验集群里请一律用 preferred

拓扑分布约束

topologySpreadConstraints 一句话:按 topologyKey 把副本均匀撒到多个故障域,用 maxSkew 控制允许的最大偏差,whenUnsatisfiable: ScheduleAnyway 是软约束、DoNotSchedule 是硬约束。副本数多、要求打散时,它比反亲和更好用——反亲和只能表达「不在一起」,它还能表达「每台最多差 1 个」。

污点与容忍

前面都是 Pod 挑节点,污点反过来:节点主动拒绝 Pod。节点上打污点,只有声明了对应「容忍」的 Pod 才能落上去。控制面节点就是这么保护自己的。

bash
# 给节点打污点: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 侧的容忍长这样:

yaml
spec:
  tolerations:
    - key: dedicated
      operator: Equal
      value: kube101
      effect: NoSchedule

operatorExists 时可以省略 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

要给一台节点打补丁、换内核或下线维护时,顺序是这样的:

bash
# 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 观察调度器

bash
NODE=$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}')
kubectl label node "$NODE" disktype=ssd --overwrite

实验一:软约束照样能跑。 把「节点亲和性」的示例存成 web-preferred.yaml,把 preferred 里的 values 改成不存在的 nvmekubectl apply -f web-preferred.yaml 后期望 kubectl get pod web-preferred -o wideRunning,只是拿不到偏好分。

实验二:硬约束会 Pending。preferredDuringSchedulingIgnoredDuringExecution 改成 requiredDuringSchedulingIgnoredDuringExecution,删掉 weightpreference 这一层再 apply,然后 kubectl describe pod web-preferred | grep -A5 Events,期望 FailedScheduling0/1 nodes are available: ... didn't match Pod's node affinity

实验三:污点与 cordon 都会挡住新 Pod。

bash
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,忽略即可):

bash
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 只说明「还没被调度成功」,原因有五六类。按下面的顺序逐条排除,每条都对应上面「过滤」阶段的一个检查点:

bash
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
  1. 先读那句话0/N nodes are available: ...,冒号后面按节点列出原因,通常直接写出 Insufficient cpuhad untolerated taintdidn't match Pod's node affinitydidn't find available persistent volumes,对应下面某一条。
  2. 资源不足Insufficient cpu / Insufficient memory。看 describe nodeAllocated resources,要么调小 requests,要么加节点。
  3. 亲和性与选择器didn't match Pod's node affinity/selectordidn't match pod affinity rules。用 kubectl get node "$NODE" --show-labels 核对标签,注意 nodeSelectorTerms 是「或」、matchExpressions 是「与」。
  4. 污点没容忍had untolerated taint {node-role.kubernetes.io/control-plane: NoSchedule},给 Pod 补对应的 tolerations
  5. PVC 没绑上pod has unbound immediate PersistentVolumeClaimskubectl get pvc -n demoSTATUS 不是 Bound。细节见 第 10 章 存储卷与 PV/PVC
  6. 配额不够exceeded quota,或 LimitRange 强制了最小 requests 而你没写。kubectl describe resourcequota -n demo 看用量。
  7. 节点不可调度node(s) were unschedulable(被 cordon 过的节点)、node(s) had volume node affinity conflict、节点 NotReady。用 kubectl get nodes 确认状态。

常见坑与速查表

调度类问题几乎都能在 Events 里找到答案

  • required 写太严导致 PendingnodeAffinity 的 required 或 podAntiAffinity 的 required,只要没有任何节点同时满足就永远 Pending。先看 describe pod0/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 affinitynodeSelector 或 required 亲和性的键值对不上kubectl get node <node> --show-labels
Pending,unbound immediate PersistentVolumeClaimsPVC 没绑定成功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 就变成「打分」阶段的扣分项:没有更好的选择时,仍然允许挤在一起。所以单节点实验集群里,副本分散一律用软约束,或者干脆用 topologySpreadConstraintsScheduleAnyway

自测:为什么用 `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 / podAntiAffinitytopologyKey 决定 Pod 之间靠近还是分开,副本分散优先用 preferredtopologySpreadConstraints
  • 污点写在节点上、容忍写在 Pod 上;NoSchedule 只挡新 Pod,NoExecute 还会驱逐已有 Pod。
  • 维护节点的标准动作是 cordondrainuncordon,drain 记得带上 --ignore-daemonsets

相关章节:第 11 章 探针与资源管理requests 怎么影响调度,第 15 章 排障手册 把这里的事件分析放进了完整的排查流程。

练习

  1. 给节点打上 gpu=true 标签,写一个 required 的 nodeAffinity,再故意把值改成 false,观察 Events 里调度器的原话。
  2. topologySpreadConstraints 把一个 3 副本的 Deployment 分散到多节点(如果没有多节点,先用 maxSkew: 1 + ScheduleAnyway 跑通语法)。
  3. 思考题:如果一台节点磁盘写满被 kubelet 打上 node.kubernetes.io/disk-pressure:NoSchedule,已经在上面跑的 Pod 会被赶走吗?为什么?

调度只决定 Pod 落在哪,落下去之后出问题就是另一套功夫了。下一章我们整理一份排障手册,把「先看什么、再看什么」固定成流程。