# k8s 调度与自动伸缩


## 导读

Kubernetes 把 Pod 放到哪个节点上运行，由调度器决定；Pod 数量随负载增减，则由自动伸缩机制负责。调度和伸缩是集群资源利用率的两个核心杠杆，前者决定资源分配的合理性，后者决定系统面对流量波动的弹性能力。掌握这两层机制，才能在成本与稳定性之间找到平衡点。

## 核心概念

调度器为每个新建的 Pod 选择最合适的节点，整个过程分为过滤与打分两个阶段。过滤阶段执行 Predicate 函数，淘汰不符合硬性条件的节点，比如资源不足、污点不匹配、亲和性冲突；打分阶段对剩余候选节点执行 Priority 函数，按资源均衡度、亲和性权重等维度逐项评分，最终把 Pod 绑定到得分最高的节点。这套两段式设计让调度器既能快速缩小候选范围，又能在可行节点中挑选最优解。

```mermaid
flowchart TD
    A[新 Pod 进入调度队列] --> B[Predicate 过滤阶段]
    B --> C{资源是否充足?}
    C -->|否| X[淘汰该节点]
    C -->|是| D{污点是否容忍?}
    D -->|否| X
    D -->|是| E{亲和性是否满足?}
    E -->|否| X
    E -->|是| F[节点进入候选列表]
    F --> G[Priority 打分阶段]
    G --> H[资源均衡评分]
    G --> I[亲和性权重评分]
    G --> J[节点亲和性评分]
    H --> K[综合加权汇总]
    I --> K
    J --> K
    K --> L[选择得分最高节点]
    L --> M[绑定 Pod 到节点]
    X --> N[调度失败重试]
```

*图 1：调度器过滤与打分两阶段决策流程*

图 1 描绘了调度器从入队到绑定的完整路径，Predicate 阶段的三道关卡逐一筛除不符合硬性条件的节点，Priority 阶段三类评分加权汇总后选出最终节点。整个流程先保正确再求最优，被过滤淘汰的节点不再参与打分。资源请求与限制是过滤阶段的第一道依据，容器通过 `resources.requests` 声明运行所需 CPU 和内存，调度器据此判断节点剩余资源是否充足；`resources.limits` 约束容器能使用的上限，超过内存限制会触发 OOMKilled，超过 CPU 限制会被节流。请求值是调度的输入，限制值是运行期的硬约束，二者不要混为一谈。

节点亲和性让 Pod 表达对节点偏好的硬约束与软偏好。`requiredDuringSchedulingIgnoredDuringExecution` 属于硬约束，必须满足才能调度，相当于带标签增强版的 nodeSelector；`preferredDuringSchedulingIgnoredDuringExecution` 属于软偏好，打分阶段给予加权倾斜。Pod 亲和性与反亲和性则把约束对象从节点转移到其他 Pod，比如把缓存服务与 Web 服务调度到同节点降低访问延迟，或把同一应用的副本打散到不同节点避免单点故障。

污点与容忍度从节点角度控制谁能调度上来。节点通过 `kubectl taint` 打上污点，默认效果 NoSchedule 拒绝新 Pod，NoExecute 还会驱逐已在运行的 Pod。Pod 通过 `tolerations` 声明能容忍哪些污点，只有匹配的污点才允许调度。这套机制常用于独占节点场景，比如把 GPU 节点打污点，只让训练任务通过容忍度占用。

## 图解原理

HPA（Horizontal Pod Autoscaler）根据指标波动自动调整 Deployment 副本数，把人工扩容变成持续运行的闭环。它不直接操作 Pod，而是周期性拉取指标、计算目标副本数、更新 Deployment 的 replicas 字段，再由 Deployment Controller 真正创建或删除 Pod。整个链路依赖 Metrics Server 提供 CPU 与内存数据，自定义指标则需要部署 Prometheus Adapter 把业务指标转换成 Metrics API 能识别的格式。

```mermaid
sequenceDiagram
    participant HPA as HPA Controller
    participant MA as Metrics API
    participant CALC as HPA 计算逻辑
    participant DC as Deployment Controller
    participant Pod as Pod

    HPA->>MA: 拉取 Pod 指标 CPU/内存
    MA-->>HPA: 返回当前指标值
    HPA->>CALC: 计算目标副本数
    CALC-->>HPA: desired = current * avg / target
    alt 扩容 目标副本 > 当前副本
        HPA->>DC: 更新 Deployment replicas
        DC->>Pod: 创建新 Pod
    else 缩容 目标副本 < 当前副本
        HPA->>DC: 更新 Deployment replicas
        DC->>Pod: 删除多余 Pod
    end
```

*图 2：HPA 采集指标并触发 Deployment 扩缩容的交互过程*

图 2 展示了 HPA 的控制回路，目标副本数由当前副本、平均指标与目标利用率的乘除关系得出，超过阈值即触发扩容或缩容。目标副本数的计算公式为 `desiredReplicas = currentReplicas * (currentMetric / targetMetric)`，当所有 Pod 的平均 CPU 利用率超过目标值，副本数按比例放大；低于目标值时按比例缩小。HPA 默认 15 秒轮询一次，并引入冷却窗口防止指标抖动导致副本数频繁震荡，扩容动作较快，缩容相对保守，默认要等 5 分钟稳定窗口才执行。

基于 CPU 的伸缩要求容器必须配置 `resources.requests.cpu`，否则 HPA 无法计算利用率，状态会显示为 unknown。自定义指标伸缩覆盖两类场景：Pod 级别指标如每秒请求数 QPS，适合按业务负载扩容；Object 级别指标如消息队列堆积深度，适合按外部依赖状态扩容。自定义指标的接入要部署 Prometheus Adapter 或 KEDA，把外部监控系统对接到 Kubernetes 的 Metrics API 聚合层，HPA 才能像读 CPU 一样读取这些业务指标。

VPA（Vertical Pod Autoscaler）做纵向伸缩，自动调整容器的 CPU 与内存请求值。它适用于无法水平扩展的有状态应用，比如单实例数据库。VPA 有 Off、Initial、Auto 三种更新模式，Auto 模式会驱逐并重建 Pod 以应用新请求值，对在线服务有中断风险。VPA 与 HPA 不宜同时作用于同一维度的资源，二者会相互冲突，官方建议 VPA 管内存、HPA 管 CPU。

Cluster Autoscaler 从集群层面伸缩节点数量。当 Pod 因资源不足处于 Pending 状态，它会向云厂商申请扩容节点；当节点利用率长期偏低，它会迁移 Pod 并回收节点。该组件与云厂商深度耦合，依赖 Cluster API 或各厂商的节点组接口。在节点扩容延迟较大的环境里，可以配合 HPA 的冷却窗口预留缓冲，避免流量激增时 Pod 等待节点就绪。

## 动手实操

先给 Deployment 配置资源请求与限制，这是 HPA 计算利用率的前提条件。

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web-app
  template:
    metadata:
      labels:
        app: web-app
    spec:
      containers:
      - name: nginx
        image: nginx:1.25
        resources:
          requests:
            cpu: 200m
            memory: 256Mi
          limits:
            cpu: 500m
            memory: 512Mi
```

创建基于 CPU 利用率的 HPA，目标值设为 50%，副本数在 1 到 10 之间波动。

```yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web-app-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web-app
  minReplicas: 1
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50
```

查看 HPA 状态与实时指标，`-w` 参数持续 watch 副本数与利用率的变化。

```bash
kubectl get hpa web-app-hpa -w
kubectl describe hpa web-app-hpa
```

自定义指标伸缩需要对接 Metrics API 聚合层。以下示例按每秒请求数扩容，前提是集群已部署 Prometheus Adapter 并注册了 `http_requests_per_second` 指标。

```yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web-app-custom-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web-app
  minReplicas: 2
  maxReplicas: 20
  metrics:
  - type: Pods
    pods:
      metric:
        name: http_requests_per_second
      target:
        type: AverageValue
        averageValue: "100"
```

节点亲和性示例，把 Pod 调度到带 `disktype=ssd` 标签的节点。

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: affinity-demo
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: disktype
            operator: In
            values:
            - ssd
  containers:
  - name: app
    image: nginx:1.25
```

污点与容忍度配合使用。先给节点打污点，再让特定 Pod 通过容忍度调度上去。

```bash
kubectl taint nodes node-1 dedicated=gpu:NoSchedule
```

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: gpu-job
spec:
  tolerations:
  - key: "dedicated"
    operator: "Equal"
    value: "gpu"
    effect: "NoSchedule"
  containers:
  - name: trainer
    image: tensorflow/tensorflow:2.15.0-gpu
```

压测验证 HPA 扩容效果。开一个临时 Pod 发压，观察副本数随 CPU 上升而增长。

```bash
kubectl run load-generator --image=busybox:1.36 -- \
  /bin/sh -c "while true; do wget -q -O- http://web-app; done"
kubectl get deployment web-app -w
```

## 常见问题与避坑

**HPA 报缺少资源请求导致无法计算利用率**。基于 CPU 的伸缩依赖容器声明 `resources.requests.cpu`，缺失时 HPA 状态会显示 unknown 并拒绝扩容。给所有容器补上请求值即可恢复。

指标延迟导致扩容滞后。Metrics Server 采集周期与 HPA 轮询间隔叠加，从流量上升到新 Pod 就绪往往需要一到两分钟。对突发流量敏感的服务，调高 minReplicas 作为预热缓冲，或配合 Cluster Autoscaler 预留节点容量。

**不要在同一维度同时启用 VPA 和 HPA**。两者都对 CPU 请求值做决策时，VPA 的调整会干扰 HPA 的利用率计算，造成副本数反复震荡。官方建议二者分管不同资源维度。

调度失败排查思路。Pod 长期 Pending 时用 `kubectl describe pod <name>` 查看 Events，常见原因包括资源不足、节点污点未匹配、亲和性无候选节点。结合 `kubectl get nodes -o wide` 的节点资源水位能快速定位瓶颈。

NoExecute 污点会驱逐在跑 Pod。给节点打 NoExecute 污点后，没有对应容忍度的 Pod 会被立即或延迟驱逐，生产环境慎用以免误伤在线服务。

## 小结与进阶

调度器用过滤打分两段式选节点，HPA、VPA、Cluster Autoscaler 分别在 Pod 副本、容器规格、节点数量三个层面伸缩资源，四者配合构成完整的弹性体系。下一篇将进入网络与服务暴露，讲解 Service、Ingress 与 DNS 机制如何在集群内外打通流量。

