236|Kubernetes 调度与资源管理:调度器原理、亲和性、污点容忍、QoS 与自动伸缩

DevOps、容器与云原生约 24 分钟阅读13,998 字符
知识库文档 237 / 251

考点 236|阅读时间约 45 分钟 涵盖:调度器原理、亲和性、污点容忍、QoS 与自动伸缩


一、引言:为什么调度与资源管理是 K8s 的核心

Kubernetes 的本质是一个分布式资源编排系统。它把成百上千台物理机/虚拟机抽象为一个统一的资源池,然后回答两个核心问题:

  1. Pod 应该跑在哪台机器上?(调度)
  2. 每个 Pod 能用多少资源?不够了怎么办?(资源管理)

这两个问题看似简单,但在一套承载数千个服务、数十万容器的生产集群中,调度决策的质量直接影响集群的资源利用率、服务稳定性、故障恢复能力。一个糟糕的调度策略可能在流量高峰时将关键服务集中到少数节点,导致雪崩;一个不合理的资源限制可能让 JVM 应用被 OOM Killer 反复杀死。

本文从调度器内部原理出发,逐步覆盖亲和性、污点容忍、QoS 模型和自动伸缩,帮你建立完整的知识体系。


二、调度器原理:从 Pod 创建到节点绑定

2.1 调度器在 K8s 架构中的位置

Pod 的创建流程中,调度器是承上启下的关键环节:

code
用户/Controller 创建 Pod → API Server 写入 etcd
    → Scheduler Watch 到未调度的 Pod(nodeName 为空)
        → 执行调度算法,选出最优节点
            → 向 API Server 写入 Binding(设置 nodeName)
                → 目标节点 kubelet 感知到 Binding,拉取镜像启动容器

调度器不直接操作 Pod,而是通过 API Server 的 Watch 机制异步工作。整个过程是声明式 + 异步的,这也是 K8s 控制循环(Control Loop)的核心范式。

2.2 调度流程:预选 → 优选 → 绑定

文章配图
文章配图

kube-scheduler 的默认调度过程分为三个阶段:

阶段一:预选(Filtering / Predicates)

预选阶段的目的是一票否决:把明显不满足 Pod 需求的节点全部过滤掉。常见的预选规则包括:

预选规则 作用
PodFitsResources 节点剩余 CPU/内存是否满足 Pod 的 requests
PodFitsHostPorts Pod 声明的 HostPort 是否被占用
PodMatchNodeSelector 节点标签是否匹配 Pod 的 nodeSelector
CheckNodeCondition 节点是否处于 Ready 状态
NoDiskConflict 声明的 GCE PD/AWS EBS 卷是否冲突
PodToleratesNodeTaints Pod 是否能容忍节点的所有污点

预选是并发执行的,各规则独立判断,任一规则不通过则节点被淘汰,不进入后续阶段。

阶段二:优选(Scoring / Priorities)

预选后剩余的节点进入优选阶段,每个节点被若干打分函数评分,最后加权求和得到总分。默认的打分函数包括:

打分函数 逻辑
LeastRequestedPriority 已用资源越少,分越高(倾向于打散)
MostRequestedPriority 已用资源越多,分越高(倾向于装箱,未默认启用)
BalancedResourceAllocation CPU 和内存使用比例越均衡,分越高
NodeAffinityPriority 匹配 Pod 亲和性偏好时加分
TaintTolerationPriority 匹配容忍时加分
ImageLocalityPriority 节点已有镜像时加分(减少拉取时间)
SelectorSpreadPriority 将同一 Service/Controller 的 Pod 分散到不同节点

每个打分函数返回 0-10 分,最终分数 = Σ(权重 × 打分函数得分)。调度器选择总分最高的节点。

阶段三:绑定(Binding)

选定最优节点后,调度器向 API Server 提交一个 Binding 对象。这一步是乐观并发的——如果这期间节点资源被其他 Pod 占用导致不满足条件,kubelet 会执行 Admission 检查并拒绝,调度器重新调度。

2.3 调度框架(Scheduling Framework)

文章配图
文章配图

从 Kubernetes 1.15 开始,调度器被重构为插件化架构——调度框架(Scheduling Framework)。一个 Pod 的调度过程经过一系列扩展点,每个扩展点可以注册多个插件:

code
Sort → PreFilter → Filter → PostFilter → PreScore → Score → NormalizeScore → Reserve → Permit → PreBind → Bind → PostBind
扩展点 作用 典型插件
Sort 对等待调度的 Pod 排序 PrioritySort
PreFilter 预处理 Pod 信息,计算 Filter 所需的共享数据 NodeResourcesFit
Filter 等同于预选,过滤不合规节点 NodeUnschedulable, NodePorts, TaintToleration
PostFilter Filter 后无可用节点时介入(如抢占) DefaultPreemption
PreScore 打分前预处理 InterPodAffinity
Score 对节点打分 NodeResourcesFit, ImageLocality
NormalizeScore 分数归一化 —
Reserve 预留资源,防止资源被其他 Pod 占用 VolumeBinding
Permit 允许/拒绝/等待调度 —
PreBind 绑定前准备工作(如挂载卷) VolumeBinding
Bind 执行绑定 DefaultBinder
PostBind 绑定后通知 —

这种设计让调度器高度可扩展。你可以用 Go 编写自定义插件并编译进调度器,也可以通过 Scheduler Extender(HTTP 回调)实现外部扩展。

2.4 多调度器与自定义调度器

一个集群可以运行多个调度器。Pod 通过 spec.schedulerName 指定使用哪个调度器:

yaml
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  schedulerName: my-custom-scheduler  # 默认是 default-scheduler
  containers:
  - name: app
    image: nginx

自定义调度器的典型场景:

  • GPU 调度:根据 GPU 型号、拓扑亲和性做精细化调度(如 Volcano)
  • 批处理调度:支持 Gang Scheduling(一批任务同时启动,否则都不启动)
  • 成本优化:优先调度到竞价实例,失败再回退到按量实例

三、亲和性:精确控制 Pod 的"邻居"关系

Pod 默认可以被调度到任何满足资源需求的节点。但实际生产环境中,我们经常需要更精细的控制。亲和性(Affinity)就是为此设计的。

3.1 节点亲和性(Node Affinity)

文章配图
文章配图

节点亲和性让 Pod 表达"我想调度到(或不想调度到)具有特定标签的节点"。

两种策略:

  • requiredDuringSchedulingIgnoredDuringExecution:硬约束,必须满足,否则 Pod 无法调度
  • preferredDuringSchedulingIgnoredDuringExecution:软偏好,尽量满足,不满足也可以调度

注意名称中的 IgnoredDuringExecution:亲和性只在调度时生效,Pod 运行后如果节点标签变化,不会驱逐 Pod。

yaml
apiVersion: v1
kind: Pod
metadata:
  name: gpu-pod
spec:
  affinity:
    nodeAffinity:
      # 硬约束:节点必须有 gpu-type=nvidia-a100 标签
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: gpu-type
            operator: In
            values:
            - nvidia-a100
            - nvidia-h100
      # 软偏好:优先选择 ssd=true 的节点
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 80
        preference:
          matchExpressions:
          - key: disk-type
            operator: In
            values:
            - ssd
      - weight: 20
        preference:
          matchExpressions:
          - key: zone
            operator: In
            values:
            - zone-a
  containers:
  - name: training
    image: pytorch/pytorch:latest

关键点:

  • matchExpressions 支持 In、NotIn、Exists、DoesNotExist、Gt、Lt 等操作符
  • 软偏好中的 weight 是 1-100 的权重,多个偏好加权求和
  • nodeSelectorTerms 之间是 OR 关系,内部的 matchExpressions 是 AND 关系

3.2 Pod 亲和性与反亲和性(Pod Affinity / Anti-Affinity)

Pod 亲和性控制的是 Pod 与 Pod 之间的关系——不是"Pod 想去什么节点",而是"Pod 想和哪些 Pod 做邻居"。

两种类型:

  • podAffinity:与指定 Pod 调度到同一拓扑域(如同一节点、同一可用区)
  • podAntiAffinity:与指定 Pod 调度到不同拓扑域

关键概念:拓扑域(topologyKey)

topologyKey 定义了"邻居"的范围——是一个 Kubernetes 节点标签。常见值:

topologyKey 含义
kubernetes.io/hostname 同一节点(主机名)
topology.kubernetes.io/zone 同一可用区
topology.kubernetes.io/region 同一地域
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis
  labels:
    app: redis
spec:
  replicas: 3
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      affinity:
        # 反亲和性:不要把同一个 Redis 的多个副本调度到同一节点
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - redis
            topologyKey: kubernetes.io/hostname
      containers:
      - name: redis
        image: redis:7

这个配置确保 Redis 的 3 个副本分布在 3 个不同节点上(如果集群满足条件),避免单节点故障导致所有副本同时不可用。

Pod 亲和性的典型应用:

yaml
# 将 Web 应用调度到与 Redis 同一节点,减少网络延迟
affinity:
  podAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
    - labelSelector:
        matchExpressions:
        - key: app
          operator: In
          values:
          - redis
      topologyKey: kubernetes.io/hostname

性能注意事项:

  • Pod 亲和性/反亲和性在大规模集群中开销较大,因为调度器需要扫描所有符合标签的 Pod
  • 如果节点数很少但 requiredDuringScheduling 的反亲和性要求很高,会导致 Pod 永远无法调度
  • 尽量使用 preferredDuringScheduling 而不是 required,给调度器更多灵活性

3.3 nodeSelector vs Node Affinity

nodeSelector 是最简单的节点选择方式,但功能有限——只支持精确的 key=value 匹配(AND 逻辑)。Node Affinity 是 nodeSelector 的超集,支持:

  • 表达式匹配(In、NotIn、Exists、Gt、Lt)
  • 软偏好(preferred)+ 硬约束(required)
  • 多个 nodeSelectorTerms 间的 OR 逻辑

实际生产环境中,优先使用 Node Affinity 替代 nodeSelector。


四、污点与容忍:反向控制节点准入

亲和性是 Pod "选择"节点的机制,而**污点(Taint)和容忍(Toleration)**是节点"拒绝"Pod 的机制——两者是同一个硬币的两面。

4.1 核心概念

  • Taint(污点):打在节点上,表示"这个节点有某种限制"
  • Toleration(容忍):写在 Pod 上,表示"这个 Pod 能接受某种限制"

一个节点可以有多个 Taint,一个 Pod 可以有多个 Toleration。Pod 能被调度到某个节点,当且仅当它容忍了该节点的所有 Taint。

4.2 Taint 的组成

bash
kubectl taint nodes node1 key1=value1:NoSchedule
#                    │     │      │      └── Effect(效果)
#                    │     │      └── Value(值)
#                    │     └── Key(键)
#                    └── 节点名

一个 Taint 由三部分组成:key=value:effect。value 可以省略(key:effect)。

三种 Effect:

Effect 行为
NoSchedule 不调度新 Pod 到此节点(已有 Pod 不受影响)
PreferNoSchedule 尽量不调度新 Pod(软约束)
NoExecute 不调度新 Pod,且驱逐已有不匹配容忍的 Pod

NoExecute 是最强力的——它不仅阻止新 Pod,还会驱逐已在运行的 Pod。可以配合 tolerationSeconds 给 Pod 一个"宽限期":

yaml
tolerations:
- key: node.kubernetes.io/not-ready
  operator: Exists
  effect: NoExecute
  tolerationSeconds: 300   # 节点 NotReady 后 300 秒才被驱逐

4.3 Toleration 匹配规则

文章配图
文章配图

yaml
apiVersion: v1
kind: Pod
metadata:
  name: special-pod
spec:
  tolerations:
  # 容忍所有带有 key=special 且 effect=NoSchedule 的污点
  - key: special
    operator: Equal
    value: "true"
    effect: NoSchedule
  # 容忍所有带有 key=gpu 的污点,不管 value 和 effect
  - key: gpu
    operator: Exists
  containers:
  - name: app
    image: nginx

匹配规则:

  • operator: Equal:key、value、effect 三者完全匹配
  • operator: Exists:key 匹配即可,value 和 effect 无所谓
  • 空的 key + operator: Exists 匹配所有(很少用)
  • 空的 effect 匹配所有 effect

4.4 内置污点:K8s 的自动保护机制

Kubernetes 的节点控制器(Node Controller)会自动给异常节点打上污点,保护集群稳定性:

内置污点 触发条件
node.kubernetes.io/not-ready:NoExecute 节点 NotReady
node.kubernetes.io/unreachable:NoExecute 节点不可达
node.kubernetes.io/out-of-disk:NoSchedule 磁盘空间不足
node.kubernetes.io/memory-pressure:NoSchedule 内存压力
node.kubernetes.io/disk-pressure:NoSchedule 磁盘压力
node.kubernetes.io/pid-pressure:NoSchedule PID 压力
node.kubernetes.io/network-unavailable:NoSchedule 网络不可用
node.kubernetes.io/unschedulable:NoSchedule 节点被 cordon

这些内置污点与 tolerationSeconds 配合,构成了 K8s 的自动故障转移机制。一个没有显式容忍这些污点的普通 Pod,实际上被 K8s 自动添加了默认容忍:

yaml
# K8s 自动为 Pod 添加的默认容忍
tolerations:
- key: node.kubernetes.io/not-ready
  operator: Exists
  effect: NoExecute
  tolerationSeconds: 300
- key: node.kubernetes.io/unreachable
  operator: Exists
  effect: NoExecute
  tolerationSeconds: 300

这意味着节点 NotReady 后,Pod 有 300 秒的容忍时间,之后才会被驱逐到其他节点。

4.5 实战场景:专用节点池

一个典型的生产集群资源隔离方案,结合 Node Affinity + Taint/Toleration:

bash
# 给 GPU 节点打上标签和污点
kubectl label node gpu-node-1 node-role=gpu
kubectl label node gpu-node-2 node-role=gpu
kubectl taint node gpu-node-1 gpu=true:NoSchedule
kubectl taint node gpu-node-2 gpu=true:NoSchedule

# 给高内存节点打标签
kubectl label node mem-node-1 node-role=memory-intensive
kubectl label node mem-node-2 node-role=memory-intensive
yaml
# GPU 任务的 Pod 配置
spec:
  tolerations:
  - key: gpu
    operator: Equal
    value: "true"
    effect: NoSchedule
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: node-role
            operator: In
            values:
            - gpu

这样 GPU 节点只接受专门配置了容忍的 GPU Pod,普通 Pod 不会被调度到 GPU 节点上——同时 GPU Pod 也只调度到 GPU 节点。

设计原则:

  • Taint/Toleration 控制准入(能不能来)
  • Node Affinity 控制倾向(想去哪)
  • 两者配合做独占式节点池:Taint 排斥其他 Pod,Node Affinity 确保 Pod 只去目标节点

五、QoS 模型:资源不足时,先杀谁?

Kubernetes 通过 QoS(Quality of Service) 模型对 Pod 进行分级。当节点资源(尤其是内存)紧张时,kubelet 按照 QoS 等级从低到高驱逐 Pod。

5.1 三种 QoS 等级

QoS 等级由 Pod 中每个容器的 requests 和 limits 决定,具体规则:

QoS 等级 判定条件
Guaranteed 每个容器都设置了 CPU 和 Memory 的 requests 和 limits,且 requests == limits
Burstable 至少一个容器设置了 requests 或 limits,但不满足 Guaranteed 条件
BestEffort 没有任何容器设置 requests 或 limits
yaml
# Guaranteed:requests == limits
spec:
  containers:
  - name: app
    image: myapp:latest
    resources:
      requests:
        cpu: "500m"
        memory: "512Mi"
      limits:
        cpu: "500m"
        memory: "512Mi"

# Burstable:requests < limits
spec:
  containers:
  - name: app
    image: myapp:latest
    resources:
      requests:
        cpu: "250m"
        memory: "256Mi"
      limits:
        cpu: "1000m"
        memory: "1Gi"

# BestEffort:没有 resources 配置
spec:
  containers:
  - name: app
    image: myapp:latest

5.2 驱逐顺序与 OOM Score

当节点内存不足时,kubelet 的驱逐顺序:

code
BestEffort → Burstable(使用量超过 requests 的) → Burstable(使用量不超过 requests 的) → Guaranteed

底层实现依赖于 Linux 的 OOM Score。kubelet 根据 QoS 等级计算 OOM Score:

  • Guaranteed:OOM Score = -998(几乎不会被 OOM Killer 杀死)
  • BestEffort:OOM Score = 1000(最先被杀死)
  • Burstable:OOM Score = 基于内存使用量计算,越接近 limit 分数越高

OOM Score 范围是 -1000 到 1000,分数越高越容易被杀死。可以通过以下命令查看:

bash
# 进入容器查看 OOM Score
cat /proc/1/oom_score
cat /proc/1/oom_score_adj

5.3 资源驱逐机制

除了 Node 级别的 OOM,kubelet 还有一套软驱逐和硬驱逐机制:

硬驱逐(Hard Eviction):资源使用量达到阈值,立即驱逐 Pod。

yaml
# kubelet 配置
evictionHard:
  memory.available: "200Mi"       # 可用内存 < 200Mi 时驱逐
  nodefs.available: "10%"         # 磁盘可用 < 10% 时驱逐
  nodefs.inodesFree: "5%"         # inode 可用 < 5% 时驱逐
  imagefs.available: "15%"        # 镜像文件系统可用 < 15% 时驱逐

软驱逐(Soft Eviction):资源使用量超过阈值并持续一段时间后驱逐。

yaml
evictionSoft:
  memory.available: "500Mi"
evictionSoftGracePeriod:
  memory.available: "1m30s"       # 超过阈值 90 秒后才驱逐

5.4 生产实践建议

文章配图
文章配图

  1. 关键服务使用 Guaranteed QoS:确保核心服务在内存压力下不会被驱逐
  2. 为所有容器设置 requests:这是调度的基础依据,缺失 requests 会导致调度器无法做出正确的资源判断
  3. limits 不应过高:过高的 limits 导致节点过度承诺(overcommit),增加 OOM 风险
  4. CPU 是可压缩资源:CPU 不够时 Pod 会被 throttle(限速),但不会被杀死;内存是不可压缩资源,不够就会 OOM Kill
  5. 监控 Pod 驱逐事件:频繁的驱逐表明集群资源不足或 Pod 配置不合理

六、自动伸缩:让集群随负载呼吸

Kubernetes 提供了多维度的自动伸缩能力,覆盖 Pod 层面和节点层面。

6.1 HPA:水平 Pod 自动伸缩

HPA(Horizontal Pod Autoscaler)根据 CPU/内存使用率或自定义指标,自动调整 Deployment、StatefulSet 的副本数。

核心原理:

code
HPA 控制器每隔 15 秒(可通过 --horizontal-pod-autoscaler-sync-period 调整)
    → 从 Metrics API 获取当前指标值
        → 计算 desiredReplicas = ceil(currentReplicas × (currentMetric / targetMetric))
            → 调整 Deployment 的 replicas 字段

计算公式:

code
desiredReplicas = ceil(currentReplicas × (currentMetricValue / desiredMetricValue))

例如:当前 3 个副本,CPU 使用率 80%,目标 50% → desiredReplicas = ceil(3 × 80/50) = 5

yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: myapp-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: myapp
  minReplicas: 2
  maxReplicas: 10
  metrics:
  # CPU 指标
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 60
  # 内存指标
  - type: Resource
    resource:
      name: memory
      target:
        type: AverageValue
        averageValue: 512Mi
  # 自定义指标(需要 Prometheus Adapter 等)
  - type: Pods
    pods:
      metric:
        name: http_requests_per_second
      target:
        type: AverageValue
        averageValue: "1000"
  behavior:
    # 扩容行为
    scaleUp:
      stabilizationWindowSeconds: 0    # 扩容不等待,立即响应
      policies:
      - type: Percent
        value: 100
        periodSeconds: 15              # 每 15 秒最多翻倍
      - type: Pods
        value: 4
        periodSeconds: 15              # 每 15 秒最多增加 4 个 Pod
      selectPolicy: Max
    # 缩容行为
    scaleDown:
      stabilizationWindowSeconds: 300  # 缩容前等待 5 分钟,防止抖动
      policies:
      - type: Pods
        value: 1
        periodSeconds: 60              # 每分钟最多减少 1 个 Pod

HPA 的局限:

  • 依赖 Metrics Server 或自定义 Metrics API
  • 基于 CPU/内存的 HPA 响应较慢(需要积累足够的数据点)
  • 无法处理"冷启动"场景(从 0 副本开始)

6.2 VPA:垂直 Pod 自动伸缩

VPA(Vertical Pod Autoscaler)自动调整 Pod 的 requests 和 limits,而不是副本数。适用于"请求量稳定但资源配置不合理"的场景。

VPA 有三种运行模式:

模式 行为
Off 只提供建议,不自动调整
Initial 只在 Pod 创建时设置 resources
Auto 自动调整(会驱逐 Pod 重建)
Recreate 自动调整,驱逐重建 Pod
yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: myapp-vpa
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: myapp
  updatePolicy:
    updateMode: Auto
  resourcePolicy:
    containerPolicies:
    - containerName: '*'
      minAllowed:
        cpu: "100m"
        memory: "128Mi"
      maxAllowed:
        cpu: "2"
        memory: "4Gi"
      controlledResources: ["cpu", "memory"]

注意:VPA 和 HPA 的冲突

VPA 修改 requests 会改变 HPA 的 CPU 利用率计算基准,两者同时使用可能导致行为异常。如果需要同时使用,设置 VPA 为 Off 模式(仅建议),或使用 controlledResources 限制 VPA 只调整内存。

6.3 Cluster Autoscaler:节点级自动伸缩

Cluster Autoscaler(CA)在集群层面自动增减节点:

  • 扩容:当有 Pod 因资源不足无法调度(Pending 状态)时,CA 向云厂商申请新节点
  • 缩容:当节点利用率持续低于阈值,CA 将 Pod 重新调度到其他节点后,回收空闲节点
code
CA 工作流程:
1. 每 10 秒检查是否有 Pending Pod
2. 如果有 Pending Pod 且现有节点无法满足:
   → 判断节点组(Node Group)是否在最大节点数限制内
   → 调用云厂商 API 增加节点
3. 每 10 秒检查是否有低利用率节点:
   → 节点上 Pod 的 requests 总和 < 节点容量的 50%(默认)
   → 节点上的 Pod 可以安全迁移到其他节点
   → 调用云厂商 API 删除节点

CA 与 HPA 的配合:

code
流量增加 → HPA 扩容 Pod → Pod Pending(资源不足) → CA 增加节点 → Pod 调度成功
流量减少 → HPA 缩容 Pod → 节点空闲 → CA 回收节点

6.4 KEDA:事件驱动的自动伸缩

文章配图
文章配图

KEDA(Kubernetes Event-driven Autoscaling)扩展了 HPA,支持从 0 到 1 的伸缩,以及基于各种事件源(Kafka、RabbitMQ、Redis、Prometheus 等)的伸缩。

yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: kafka-consumer-scaler
spec:
  scaleTargetRef:
    name: kafka-consumer
  minReplicaCount: 0    # 支持缩容到 0
  maxReplicaCount: 30
  triggers:
  - type: kafka
    metadata:
      bootstrapServers: kafka:9092
      consumerGroup: my-group
      topic: orders
      lagThreshold: "100"    # 积压超过 100 条就扩容

KEDA 的核心价值:

  • 缩容到 0:没有消息时完全停止消费者,节省资源
  • 事件驱动:基于消息积压、队列长度等业务指标伸缩,比 CPU 指标更精准
  • 与 HPA 兼容:KEDA 底层管理 HPA 对象,不引入新的 API

七、总结:调度与资源管理的全景图

code
┌─────────────────────────────────────────────────────────────────┐
│                     K8s 调度与资源管理全景                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐                  │
│  │ 亲和性    │    │ 污点容忍  │    │ 拓扑分布  │                  │
│  │ Affinity │    │ Taint/   │    │ Topology │                  │
│  │ Pod 选择  │    │ Toleration│    │ Spread   │                  │
│  │ 节点      │    │ 节点拒绝  │    │ 跨域均衡  │                  │
│  └────┬─────┘    └────┬─────┘    └────┬─────┘                  │
│       │               │               │                         │
│       └───────────────┼───────────────┘                         │
│                       ▼                                         │
│              ┌────────────────┐                                 │
│              │   调度器        │                                 │
│              │  Filter→Score  │                                 │
│              │  →Bind         │                                 │
│              └───────┬────────┘                                 │
│                      │                                          │
│       ┌──────────────┼──────────────┐                           │
│       ▼              ▼              ▼                           │
│  ┌──────────┐ ┌──────────┐ ┌──────────────┐                    │
│  │  QoS     │ │ 资源配额  │ │ 自动伸缩      │                    │
│  │ G/B/BE   │ │ RQ/LQ    │ │ HPA/VPA/CA   │                    │
│  │ 驱逐保护  │ │ 隔离限制  │ │ 弹性伸缩      │                    │
│  └──────────┘ └──────────┘ └──────────────┘                    │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘


八、面试提问与深度解析

以下问题从面试官视角出发,考察候选人对 K8s 调度与资源管理的深层理解——不是背概念,而是理解为什么这样设计和生产环境如何落地。


Q1:调度器的预选和优选阶段分别做什么?如果一个 Pod 同时设置了 nodeSelector 和 PodAntiAffinity,两者分别在哪个阶段处理?

回答:

预选(Filtering)是一票否决式的硬过滤——每个节点都要通过所有预选规则,任一规则不通过则直接淘汰。优选(Scoring)是对通过预选的节点打分排序,选最高分。

  • nodeSelector 在预选阶段由 PodMatchNodeSelector 规则处理。它检查节点标签是否包含 Pod 声明的所有 key=value 对,本质是简单的 AND 逻辑匹配。
  • PodAntiAffinity 分两种情况:
    • requiredDuringScheduling 在预选阶段由 InterPodAffinity 插件的 Filter 扩展点处理,不满足的节点直接淘汰。
    • preferredDuringScheduling 在优选阶段由 InterPodAffinity 插件的 Score 扩展点处理,匹配越多得分越高。

追问: 如果集群有 3 个节点,但 Pod 设置了 requiredDuringScheduling 的 PodAntiAffinity 要求与另外 3 个已运行的 Pod 不在同一节点(topologyKey=hostname),且每个节点恰好各运行了 1 个这样的 Pod,结果会怎样?→ Pod 永远 Pending,因为预选后没有可用节点。这就是为什么生产环境要谨慎使用 required 反亲和性。


Q2:什么时候用 Node Affinity,什么时候用 Taint/Toleration?两者可以互换吗?

回答:

两者不可互换,解决的是不同方向的问题:

维度 Node Affinity Taint / Toleration
控制方向 Pod 主动选择节点 节点主动排斥 Pod
典型场景 "我的服务需要 GPU" "GPU 节点不给普通服务用"
默认行为 所有节点都可选 无 Toleration 的 Pod 被排斥
粒度 按 Pod 配置 按节点配置

关键区别: 如果只用 Node Affinity 把 GPU Pod 调度到 GPU 节点,普通 Pod 仍然可能被调度到 GPU 节点(当其他节点资源不足时)。加上 Taint 才能确保普通 Pod 永远不调度到 GPU 节点。反过来,如果只用 Taint 给 GPU 节点打上 gpu=true:NoSchedule,GPU Pod 虽然配置了 Toleration,但调度器可能把它调度到普通节点,浪费了 GPU 资源。

生产最佳实践: 两者配合使用——Taint 排斥不需要的 Pod,Node Affinity 吸引需要的 Pod。这是"专用节点池"的标准做法。


Q3:一个 Burstable Pod 的 requests 是 256Mi,limits 是 1Gi,当前实际使用 800Mi。节点内存不足时,它和 BestEffort Pod 谁先被驱逐?为什么?

回答:

BestEffort Pod 先被驱逐。

K8s 驱逐顺序:BestEffort → Burstable(超过 requests 的)→ Burstable(未超过 requests 的)→ Guaranteed。

这个 Burstable Pod 实际使用 800Mi,超过了 requests 的 256Mi,属于"超过 requests 的 Burstable"。它在驱逐队列中排在 BestEffort 之后,但在"未超过 requests 的 Burstable"之前。

底层 OOM Score 计算逻辑:

  • BestEffort:OOM Score = 1000(最高优先级被杀)
  • Burstable:OOM Score = max(0, 1000 - 1000 × memoryRequestBytes / machineMemoryCapacityBytes) + 实际使用量超出 requests 的惩罚
  • Guaranteed:OOM Score = -998(几乎不会被杀)

追问: 如果这个 Burstable Pod 的实际使用降到 200Mi(低于 requests),驱逐顺序会变吗?→ 会,它变为"未超过 requests 的 Burstable",排在后面。但如果节点内存继续恶化,OOM Killer 最终还是会按实际使用量排序——使用 200Mi 的 Pod 和实际使用 10Gi 的 Pod 相比,后者 OOM Score 更高,更先被杀。OOM Score 并非只看 QoS 等级,实际内存使用量也是关键因子。


Q4:HPA 的 CPU 利用率是基于 requests 还是 limits 计算的?如果 Pod 没有设置 requests 会发生什么?

回答:

HPA 的 CPU 利用率基于 requests 计算。公式:

code
utilization = (当前所有 Pod 的 CPU 使用量总和) / (sum(Pod[i].spec.containers[*].resources.requests.cpu)) × 100%

计算 desiredReplicas 时:

code
desiredReplicas = ceil(currentReplicas × currentUtilization / targetUtilization)

如果 Pod 没有设置 requests,HPA 无法计算利用率——metrics API 返回的 utilization 值为空,HPA 控制器会跳过该指标,不会触发扩缩容。这正是为什么所有需要 HPA 的工作负载都必须设置 requests。

陷阱: 如果 HPA 基于 CPU utilization 时,一些 Pod 的 requests 设为 100m,另一些设为 1000m,平均值会失去意义。实际生产环境建议使用 AverageValue 类型的指标(如 averageValue: "500m")替代 Utilization 类型,避免因 requests 配置不均导致的误判。

追问: 如果使用 AverageValue 类型,HPA 的计算公式是什么?→ desiredReplicas = ceil(currentReplicas × currentAverageValue / targetAverageValue),不再依赖 requests 做分母。


Q5:你有一个 StatefulSet 部署的 Kafka 集群,3 个 Broker,要求每个 Broker 在不同可用区,且同一可用区最多一个 Broker。如何用亲和性/反亲和性实现?

回答:

核心思路是用 podAntiAffinity + topologyKey: topology.kubernetes.io/zone:

yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: kafka
spec:
  serviceName: kafka
  replicas: 3
  selector:
    matchLabels:
      app: kafka
  template:
    metadata:
      labels:
        app: kafka
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchLabels:
                app: kafka
            topologyKey: topology.kubernetes.io/zone
      containers:
      - name: kafka
        image: confluentinc/cp-kafka:7.5.0

为什么用 StatefulSet 而非 Deployment? Kafka Broker 需要稳定的网络标识和持久存储——每个 Broker 的 broker.id 需要与 Pod 序号绑定,数据目录需要 PVC 持久化。Deployment 的 Pod 是"无状态可互换"的,不适合 Kafka。

注意事项:

  • 如果集群只有 2 个可用区但 replicas: 3,第 3 个 Pod 会永久 Pending。解决方案:用 preferredDuringScheduling 替代 required,或者确保可用区数 ≥ 副本数。
  • Kafka 的 advertised.listeners 需要配置为每个 Pod 的稳定域名,如 kafka-0.kafka.default.svc.cluster.local:9092。

Q6:Cluster Autoscaler 判断节点是否可以缩容的依据是什么?如果一个节点上运行着 DaemonSet 的 Pod,会影响缩容吗?

回答:

CA 判断节点可缩容需同时满足以下条件:

  1. 节点上所有 Pod(除 DaemonSet Pod 和 Mirror Pod 外)的 requests 总和 < 节点可分配资源的 50%(默认阈值,可通过 --scale-down-utilization-threshold 调整)。
  2. 节点上所有 Pod 可以安全迁移到其他节点——CA 会模拟将这些 Pod 调度到其他节点,确认不会导致 Pending。
  3. 节点上没有阻止缩容的注解,如 "cluster-autoscaler.kubernetes.io/safe-to-evict": "false"。
  4. 节点不在缩容冷却期内(默认 10 分钟)。

DaemonSet Pod 的影响: CA 在判断时会忽略 DaemonSet Pod 的 requests——因为 DaemonSet Pod 是每个节点必有的,迁移到其他节点后目标节点上本来就有一个,所以不需要额外计算它们的资源。但 DaemonSet Pod 的存在本身不影响缩容判断,CA 会正常缩容。

补充: 如果 Pod 有本地存储(EmptyDir 除外),或设置了 PDB 导致迁移会导致 PDB 违规,该 Pod 也会阻止所在节点的缩容。


Q7:VPA 在 Auto 模式下会驱逐 Pod 重建,这会对服务可用性造成什么影响?如何规避?

回答:

影响:

  1. 服务中断:Pod 被驱逐到新 Pod 启动就绪之间存在空窗期,期间该 Pod 不提供服务。
  2. 级联风险:如果 VPA 同时调整多个 Pod,可能导致服务整体不可用。
  3. 与 HPA 冲突:VPA 修改 requests 后,HPA 的 CPU 利用率计算基准变化,可能导致 HPA 出现非预期的扩缩容行为。
  4. 状态丢失:如果 Pod 不依赖持久化存储(如内存缓存),驱逐后状态丢失。

规避措施:

  1. 配置 PodDisruptionBudget(PDB):确保驱逐期间至少有 minAvailable 个 Pod 在运行。
  2. 使用**updateMode: Initial**:只在 Pod 首次创建时设置 resources,不驱逐运行中的 Pod。
  3. 限制 VPA 的调整范围:通过 minAllowed/maxAllowed 防止极端调整。
  4. VPA 与 HPA 分离:如果必须同时使用,让 VPA 只调整内存(controlledResources: ["memory"]),HPA 基于 CPU 伸缩。
  5. 逐步放量:先使用 Off 模式观察 VPA 的建议值,验证合理后再切换到 Auto。

Q8:一个 Pod 设置了**requiredDuringSchedulingIgnoredDuringExecution**的 Node Affinity 指向**zone=zone-a**,但同时 zone-a 所有节点都被打上了**maintenance=true:NoSchedule**的污点,且 Pod 没有对应的 toleration。Pod 最终能被调度吗?

回答:

不能。Pod 会永久 Pending。

调度流程:

  1. 预选阶段,PodMatchNodeSelector 检查节点标签 → zone-a 的节点通过(匹配 zone=zone-a),其他 zone 的节点被过滤。
  2. 预选阶段,PodToleratesNodeTaints 检查污点容忍 → zone-a 的节点全部失败(Pod 没有 toleration for maintenance=true:NoSchedule)。
  3. 预选后,可用节点数为 0 → Pod 进入 Pending 状态,事件中显示 0/N nodes are available。

关键点: 预选规则是 AND 关系——所有规则都必须通过。Node Affinity 和 Taint/Toleration 是两套独立的过滤机制,不会互相"覆盖"或"抵消"。

实际场景: 这正好说明了为什么 Node Affinity 和 Taint/Toleration 需要配合使用——如果你想独占 zone-a 节点做维护,不加 Taint 的 Pod 仍然可能被调度上去(只要资源够),加了 Taint 才能确保只有配置了 Toleration 的 Pod 才被准入。


Q9:描述从用户提交一个 Deployment 到 Pod 运行在节点上的完整流程,重点说明调度和资源管理相关的每个步骤。

回答:

完整流程如下(按时间顺序):

code
1. kubectl apply -f deployment.yaml
   → 客户端将 YAML 序列化为 JSON,发送 POST 请求到 API Server

2. API Server 接收请求
   → 认证(Authentication):验证用户身份
   → 授权(Authorization):RBAC 检查用户是否有 create deployments 权限
   → 准入控制(Admission Control):例如 LimitRanger 设置默认 requests/limits,
     ResourceQuota 检查命名空间配额是否超限
   → 写入 etcd,返回 Deployment 对象

3. Deployment Controller(kube-controller-manager 内)
   → Watch 到新 Deployment → 创建 ReplicaSet → ReplicaSet Controller 创建 Pod
   → Pod 写入 etcd 时 nodeName 为空,status.phase = Pending

4. Scheduler 介入
   → Watch 到 nodeName 为空的 Pod,放入调度队列
   → Sort 阶段:按优先级排序(PriorityClass)
   → PreFilter:预处理 Pod 信息
   → Filter(预选):
     · PodFitsResources:节点剩余 CPU/内存 >= Pod 的 requests
     · PodMatchNodeSelector:匹配 nodeSelector / Node Affinity
     · PodToleratesNodeTaints:匹配 Taint/Toleration
     · CheckNodeCondition:节点 Ready?
     · PodFitsHostPorts:HostPort 冲突?
     · 其他 Filter 插件...
   → PostFilter:如果 Filter 后无可用节点 → 抢占(Preemption)低优先级 Pod
   → Score(优选):
     · NodeResourcesFit:资源均衡度打分
     · ImageLocality:镜像已有加分
     · InterPodAffinity:Pod 亲和性匹配加分
     · 其他 Score 插件...
   → Reserve:预留资源(乐观并发)
   → Permit:等待/批准
   → Bind:向 API Server 提交 Binding 对象,设置 nodeName

5. 目标节点 kubelet
   → Watch 到 nodeName 指向自己的 Pod
   → 再次执行 Admission(如 PodSecurityPolicy 检查)
   → 调用 CRI(Container Runtime Interface)拉取镜像、创建容器
   → 调用 CNI(Container Network Interface)配置网络
   → 调用 CSI(Container Storage Interface)挂载存储卷
   → 更新 Pod status 为 Running

6. 资源管理相关
   → kubelet 通过 Cgroup 设置 CPU/内存限制(limits)
   → kubelet 上报 Node Status(包含剩余资源)给 API Server
   → 如果发生 OOM:内核 OOM Killer 按 oom_score_adj 杀进程
   → kubelet 监控资源使用,触发驱逐(Eviction)

面试官视角追问: Admission Control 环节中,如果 LimitRanger 给 Pod 设置了默认 limits=500m CPU,但 Pod 自己的 requests=1000m,最终生效的是什么?→ requests 不会超过 limits(单容器),LimitRanger 会将 limits 提升到至少等于 requests,所以最终 limits=1000m,requests=1000m。


Q10:你如何设计一个大规模 K8s 集群的调度策略,以确保 GPU 训练任务、在线服务和批处理任务互不干扰?请给出具体的亲和性、污点、QoS 和伸缩策略。

回答:

设计思路: 三种工作负载的资源需求和 SLA 完全不同,需要从物理隔离(节点池)→ 调度策略(准入+倾向)→ 资源保障(QoS)→ 弹性伸缩四个层面设计。

1. 节点池划分(物理隔离)

节点池 标签 用途
gpu-pool workload-type=gpu GPU 训练任务
service-pool workload-type=online 在线服务(低延迟)
batch-pool workload-type=batch 批处理/离线任务

2. 污点与容忍(准入控制)

bash
# GPU 节点:只接受 GPU 任务
kubectl taint node gpu-{1..8} workload-type=gpu:NoSchedule

# 在线服务节点:拒绝批处理任务
kubectl taint node service-{1..20} workload-type=online:NoSchedule

# 批处理节点:可以被抢占,接受在线服务溢出
kubectl taint node batch-{1..10} workload-type=batch:NoSchedule

3. 亲和性策略(倾向控制)

yaml
# GPU 训练任务 Pod
spec:
  tolerations:
  - key: workload-type
    operator: Equal
    value: gpu
    effect: NoSchedule
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: workload-type
            operator: In
            values: [gpu]
    # 高优先级训练任务可以抢占批处理
  priorityClassName: high-priority

# 在线服务 Pod
spec:
  tolerations:
  - key: workload-type
    operator: Equal
    value: online
    effect: NoSchedule
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: workload-type
            operator: In
            values: [online]
    # 反亲和性:同一服务的 Pod 分散到不同节点
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchLabels:
              app: my-service
          topologyKey: kubernetes.io/hostname

# 批处理任务 Pod
spec:
  tolerations:
  - key: workload-type
    operator: Equal
    value: batch
    effect: NoSchedule
  priorityClassName: low-priority  # 可被抢占

4. QoS 策略

工作负载 QoS 原因
在线服务 Guaranteed 核心服务不能因 OOM 被驱逐
GPU 训练 Guaranteed 或 Burstable(上限设置好) 训练任务中断代价高(Checkpoint 开销大)
批处理 Burstable 或 BestEffort 可接受被驱逐,重试即可

5. 自动伸缩策略

工作负载 伸缩策略 说明
在线服务 HPA(CPU/Memory + 自定义指标) 基于 QPS/延迟伸缩
GPU 训练 KEDA + 任务队列 基于待训练任务数伸缩,无任务时缩容到 0
批处理 KEDA + 消息队列 基于 Kafka lag/Redis 队列长度伸缩
集群节点 Cluster Autoscaler + 多节点组 每个节点组独立设置 min/max

6. 关键补充

  • 优先级与抢占(PriorityClass):在线服务 > GPU 训练 > 批处理。当资源紧张时,低优先级 Pod 被抢占腾出资源给高优先级 Pod。
  • ResourceQuota:限制每个命名空间的资源总量,防止某个团队过度消耗。
  • LimitRange:给未设置 requests/limits 的 Pod 默认值,防止"裸 Pod"影响调度。
  • 调度器插件:对于 GPU 拓扑感知调度(如 NVLink 亲和性),使用 Volcano 等第三方调度器替代默认调度器。

本文是「Java 技术专家与架构师面试:250 个高频考点总览」系列第 236 篇。