调度控制
概述#
如果说资源管理是"Pod 该拿多少资源",那么调度控制就是"Pod 该跑到哪个节点上"。Kubernetes 默认调度器已经很智能——它会综合考虑节点的资源余量、亲和性规则、污点容忍等因素。但在生产环境中,我们往往需要更精细的控制:把 GPU 任务调度到 GPU 节点、让同一个服务副本分散部署、给运维节点加"污染"防止业务 Pod 误入。这一章覆盖从简单的 nodeSelector 到复杂的亲和性/反亲和性规则,再到弹性伸缩,帮你掌握 Pod 调度的全部手段。
NodeSelector:最简单的节点选择#
基本原理#
NodeSelector 是 Kubernetes 最早的调度控制手段。核心逻辑是:给节点打标签 → 在 Pod 中指定 nodeSelector → Pod 只调度到匹配标签的节点。
它的优点是简单直接,缺点是只能做精确匹配(AND 逻辑),不支持"优先选择"这类软约束。
使用步骤#
第一步:给节点打标签
# 给 node1 打上磁盘类型标签
kubectl label node node1 disktype=ssd
# 查看节点标签
kubectl get nodes --show-labels | grep disktype
# 删除标签
kubectl label node node1 disktype-第二步:在 Pod 中指定 nodeSelector
apiVersion: v1
kind: Pod
metadata:
name: nginx-ssd
spec:
containers:
- name: nginx
image: nginx:1.25
nodeSelector:
disktype: ssd # 只调度到有 disktype=ssd 标签的节点如果集群中没有任何节点带有 disktype=ssd 标签,这个 Pod 会一直处于 Pending 状态,调度器找不到合适节点。
适用场景#
- 简单区分节点用途(如
role: workervsrole: gpu) - 确保 IO 密集型服务调度到 SSD 节点
- 小型集群、规则简单的场景
Affinity / Anti-Affinity:灵活的亲和性#
当 nodeSelector 不够用的时候,就需要 Affinity。它提供了更丰富的匹配语法(In、NotIn、Exists、Gt、Lt 等),以及软约束(尽量满足,不满足也可以)。
硬亲和 vs 软亲和#
| 类型 | YAML 字段 | 含义 |
|---|---|---|
| 硬亲和 | requiredDuringSchedulingIgnoredDuringExecution | 必须满足条件才调度,否则 Pod 保持 Pending |
| 软亲和 | preferredDuringSchedulingIgnoredDuringExecution | 尽量满足条件,满足不了也调度(配合 weight 权重) |
命名里有两个 DuringScheduling / DuringExecution:
DuringScheduling:调度时起作用IgnoredDuringExecution:运行后不管(即使标签被改了,Pod 也不会被驱逐)
NodeAffinity:让 Pod 跑到特定节点#
比 nodeSelector 更强大,支持条件表达式:
apiVersion: v1
kind: Pod
metadata:
name: gpu-job
spec:
affinity:
nodeAffinity:
# 硬要求:必须是 GPU 节点,且在指定区域
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-type
operator: In
values:
- gpu
- key: topology.kubernetes.io/zone
operator: In
values:
- us-west-2a
- us-west-2b
# 软偏好:尽量选 SSD 节点,权重 80
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80
preference:
matchExpressions:
- key: disktype
operator: In
values:
- ssd
containers:
- name: training
image: pytorch/pytorch:2.1PodAffinity:让 Pod 聚在一起#
想让某个服务的 Pod 和数据缓存 Pod 部署在同一台机器上(降低网络延迟):
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: redis-cache
topologyKey: kubernetes.io/hostname # 必须在同一台宿主机topologyKey 定义"在一起"的范围:用 kubernetes.io/hostname 表示同一节点;用 topology.kubernetes.io/zone 表示同一可用区。
PodAntiAffinity:让 Pod 分散部署#
这是生产环境最常用的亲和性规则——确保同一服务的多个副本不在同一个节点上:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: my-api
topologyKey: kubernetes.io/hostname
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: my-api
topologyKey: topology.kubernetes.io/zone这段配置的效果:
- 硬要求:同一应用的两个 Pod 绝不在同一台宿主机(避免单点故障)
- 软偏好:尽量不在同一可用区(避免整个 AZ 故障影响全部副本)
Taint 与 Toleration:节点"污点"机制#
NodeSelector 和 Affinity 是 Pod 选择节点,Taint/Toleration 是节点拒绝 Pod。
概念对比#
| 概念 | 作用方 | 含义 |
|---|---|---|
| Taint(污点) | 打在节点上 | "这节点有污点,不想被普通 Pod 调度上来" |
| Toleration(容忍) | 写在 Pod 上 | "我能忍受这个污点,可以调度到有污点的节点" |
三种效果#
| 效果 | 含义 |
|---|---|
NoSchedule | 新 Pod 不会调度到此节点,但已运行的 Pod 不受影响 |
PreferNoSchedule | 尽量不调度到此节点,但没有绝对保证 |
NoExecute | 新 Pod 不调度,已运行的 Pod 也会被驱逐(除非有对应 Toleration) |
实战:给运维节点加污点#
# 给运维专用节点加污点,防止业务 Pod 被调度上来
kubectl taint node ops-node1 role=ops:NoSchedule
# 查看污点
kubectl describe node ops-node1 | grep Taints
# 输出:Taints: role=ops:NoSchedule
# 删除污点
kubectl taint node ops-node1 role=ops:NoSchedule-此时,除非 Pod 写了对应 Toleration,否则调度器会跳过这个节点。
Pod Toleration 配置#
apiVersion: v1
kind: Pod
metadata:
name: ops-agent
spec:
tolerations:
- key: "role"
operator: "Equal"
value: "ops"
effect: "NoSchedule"
# 容忍所有污点(不推荐在生产环境用)
# - operator: "Exists"
containers:
- name: agent
image: ops-agent:1.0Kubernetes 的 Master 节点(或 control-plane 节点)默认带有 node-role.kubernetes.io/control-plane:NoSchedule 污点,这就是为什么普通 Pod 不会被调度到 Master 节点的原因。
PodTopologySpread:跨拓扑域打散#
这是 Kubernetes 1.19 之后 GA 的高级调度特性,一句话概括:让 Pod 在各个拓扑域中均匀分布。
topologySpreadConstraints:
- maxSkew: 1 # 各拓扑域之间 Pod 最多差几个
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule # 或 ScheduleAnyway
labelSelector:
matchLabels:
app: my-api这段配置的意思是:my-api 的 Pod 在各个可用区之间的数量差异不超过 1。如果 AZ-a 有 3 个 Pod,AZ-b 有 2 个 Pod,maxSkew=1 意味着可以(差 1);但如果是 3 和 1,差 2,新 Pod 会被强制调度到 AZ-b。
它和 PodAntiAffinity 有重叠,但 TopologySpread 更关注"均匀分布",而 AntiAffinity 更关注"不和某些 Pod 在一起"。生产环境中两者常配合使用。
HPA 与 VPA:弹性伸缩#
资源管理和调度控制之后,还有一个终极问题:副本数应该是多少?手动设定的副本数在流量波动时要么不够,要么浪费。HPA 和 VPA 就是解决这个问题。
HPA:水平弹性伸缩#
HPA(Horizontal Pod Autoscaler)根据 CPU/内存或自定义指标自动调整 Pod 副本数。
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: my-api-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-api
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # 所有 Pod 的平均 CPU 利用率目标 70%
behavior:
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100 # 每次最多翻倍
periodSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300 # 缩容前等待 5 分钟,防抖动
policies:
- type: Percent
value: 10
periodSeconds: 60HPA 的核心原则:
- 扩容要快(
stabilizationWindowSeconds: 0),缩容要慢(stabilizationWindowSeconds: 300) - CPU 指标最适合做自动伸缩,内存指标慎用(Java/Go 的内存释放慢,不适合做缩容依据)
- HPA 依赖 metrics-server 提供指标数据——必须确保 metrics-server 正常运行
# 查看 HPA 状态
kubectl get hpa -n production
# 查看 HPA 事件(为什么不扩/不缩)
kubectl describe hpa my-api-hpa -n productionVPA:垂直弹性伸缩(简介)#
VPA(Vertical Pod Autoscaler)做的事情不同——它不是在水平方向加减 Pod 数量,而是调整每个 Pod 的 Request/Limit 建议值。
VPA 有三种模式:
| 模式 | 行为 | 适用场景 |
|---|---|---|
| Off | 只计算推荐值,不修改 Pod | 生产环境获取 requests 建议 |
| Initial | 只在 Pod 创建时设置 | 新上线的服务 |
| Auto | 自动调整并重启 Pod | 非关键服务、开发环境 |
VPA 当前仍处于 beta 阶段,生产环境谨慎使用 Auto 模式。推荐用 Off 模式来获取资源建议,然后人工调整。
重要提示:HPA 和 VPA 不要用相同指标同时启用,会因为两者都尝试调整而互相冲突。如果共用,让 VPA 管内存、HPA 管 CPU 是一条可行的策略。
实战要点#
- 从 nodeSelector 开始:如果简单的标签匹配够用,就别上复杂的 Affinity。
- 生产必备 PodAntiAffinity:同一服务多个副本应分散到不同节点(
hostname),避免单点故障。 - 污点保护关键节点:GPU 节点、运维节点、有特殊硬件的节点都应加 Taint。
- HPA 缩容要保守:缩容太快会导致流量打到剩余 Pod 上引发新的扩容,形成振荡。
stabilizationWindowSeconds至少设 300 秒。 - 监控 HPA 指标:
kubectl describe hpa的 Events 是排障第一入口。
小结#
调度控制从简单到复杂分为几个层次:NodeSelector(标签匹配)→ Affinity/Anti-Affinity(软硬亲和、灵活表达式)→ Taint/Toleration(节点排斥)→ TopologySpread(均匀分布)。从另一个维度看,HPA/VPA 解决的是"副本数量该多少"的动态调度问题。记住调度设计的核心原则:把关键服务的副本打散,把特殊用途的节点保护起来,让弹性伸缩根据真实负载自动调整。