跳转至

调度、资源与弹性

Pod 能不能跑起来,取决于调度器是否找得到合适的节点;跑起来稳不稳,取决于 requests/limits 与 QoS;流量上来扛不扛得住,取决于 HPA 与 KEDA。本章把这三件事串成一条可验证的链路。

难度 · 进阶 预计 60 分钟 前置:Pod 与容器、工作负载

你将学到

  • 用 requests 让调度器预留资源,用 limits 划出运行时上限,并判断 Pod 的 QoS 等级。
  • 用 nodeSelector、nodeAffinity、taints/tolerations 控制 Pod 落在哪些节点。
  • 部署 metrics-server,编写 autoscaling/v2 HPA,压测观察扩缩容。
  • 安装 KEDA,用 cron 触发器缩容到 0,再用 CPU 触发器接管。

为什么需要它

Kubernetes 把集群抽象成共享资源池,但资源池不会自动变得高效。没有 requests,调度器只能按当前空闲量“猜”着放,几个 Pod 挤到同一节点后一起 OOM;没有 limits,一个内存泄漏的容器可以拖垮整台机器;没有弹性,流量高峰只能靠长期预留大量副本硬扛。

三者还会互相牵制:requests 写大了浪费资源,写小了调度器会把 Pod 堆到一起;limits 收得太紧会频繁 OOM,放得太松又失去隔离;HPA 的指标链路一旦断掉,副本数就停在上一次的计算结果。下面先建立概念,再用一套实验把链路走通。

核心概念

requests 与 limits:调度契约和运行时边界

requests 是写给调度器的承诺:调度器只把 Pod 放到“剩余可分配资源 ≥ requests 总和”的节点上。limits 是写给内核的硬边界:CPU 超限被 cgroup 节流,表现为变慢;内存超限被 OOM Killer 杀掉,退出码通常是 137。CPU 可压缩,内存不可压缩,这是两者最根本的差别。

QoS 等级由 requests/limits 的组合自动推导,不能手工指定:

QoS 判定条件 驱逐优先级 适用场景
Guaranteed 所有容器都设置 CPU/内存的 requests 和 limits,且两者相等 最低 数据库、核心网关
Burstable 至少一个容器设置了 requests 或 limits,但不满足 Guaranteed 中等 大多数无状态业务
BestEffort 所有容器都没有设置 requests 和 limits 最高 临时任务、批处理

节点资源紧张时,kubelet 按 BestEffort → Burstable → Guaranteed 的顺序驱逐。给关键服务设置 requests == limits 确实能降低被驱逐的概率。

节点调度:从硬约束到软偏好

调度器先过滤掉不满足硬约束的节点,再给剩余节点打分并绑定。nodeSelector 只匹配节点标签;nodeAffinity 的 requiredDuringSchedulingIgnoredDuringExecution 是硬约束,preferred... 只加分。taints/tolerations 方向相反:节点用污点排斥 Pod,Pod 用容忍声明“我可以接受”。污点的 effect 有三种:NoSchedule 拒绝新 Pod,PreferNoSchedule 尽量不调度,NoExecute 还会驱逐不容忍该污点的已有 Pod。

HPA:把指标变成副本数

HPA 的 CPU/内存指标不来自 Prometheus,而来自 metrics-server。它通过 Aggregation Layer 注册 metrics.k8s.io/v1beta1,定期从 kubelet 的 Summary API 读取数据,默认 15 秒一次(--metric-resolution=15s),只保留最近的数据点。长期存储、查询和告警交给 Prometheus,详见 Prometheus 监控;日志侧见 日志与 ELK / Loki。

HPA 默认每 15 秒读取一次指标,按 desiredReplicas = ceil(currentReplicas × currentMetric / desiredMetric) 计算期望副本数,再写入工作负载;把 metrics 的 name 换成 memory 即可按内存扩容,但内存不会随负载下降自动释放,缩容容易滞后。计算结果有 10% 容差:比值在 0.9 到 1.1 之间不触发扩缩容,避免阈值附近抖动。autoscaling/v2 的 behavior 用 stabilizationWindowSeconds 和 policies 控制速度;缩容窗口默认 300 秒,表示“过去 5 分钟出现过更高副本建议就先不缩”。

v1.31 只使用 autoscaling/v2

autoscaling/v2beta2 在 v1.26 已被移除。旧清单把 apiVersion 改成 autoscaling/v2 即可,字段结构基本一致。HPA 不能把副本缩到 0,这是它和 KEDA 最本质的区别。

KEDA:事件驱动与 scale-to-zero

KEDA 是 CNCF 毕业项目,不替代 HPA,而是在它前面加一层事件感知。KEDA Operator 监听 ScaledObject,轮询 Kafka 积压、RabbitMQ 队列、Prometheus 查询或 cron 表达式,把结果通过自己的 metrics apiserver 暴露成 External Metrics,同时自动创建并管理一个 HPA。没有事件时,KEDA 绕过 HPA 直接把 Deployment 缩到 0;有新事件再拉起。代价是冷启动,因此适合间歇性任务,不适合对首包延迟极敏感的核心 API。

flowchart LR
  SRC[事件源:cron / Kafka / Prometheus] --> OP[KEDA Operator]
  OP --> SO[ScaledObject]
  SO --> HPA[KEDA 托管的 HPA]
  HPA --> DEP[Deployment]
  OP --> MA[KEDA Metrics API]
  MA --> HPA

图:KEDA 感知事件并生成外部指标,HPA 执行扩缩容,二者通过 ScaledObject 绑定。

PDB:自愿中断的预算

PDB 只限制自愿中断,例如 kubectl drain、节点升级、集群缩容;节点宕机、OOM 这类非自愿中断它管不了。minAvailable 和 maxUnavailable 只能二选一,值可以是整数或百分比。控制器通过 Eviction API 驱逐 Pod,预算不足时驱逐被拒绝,drain 就会卡住。v1.31 使用 policy/v1,policy/v1beta1 在 v1.25 已移除;空 selector 会匹配命名空间内所有 Pod,生产环境必须写明确的标签选择器。

动手实验

实验:从 metrics-server 到 KEDA 缩容到 0

目标:打通指标链路,给 demoapp 配置 requests/limits,用 HPA 压测扩容,再用 KEDA 的 cron 触发器缩容到 0。

步骤 1 · 部署 metrics-server

kubectl apply -f https://raw.githubusercontent.com/iKubernetes/learning-k8s/master/metrics-server/components.yaml
kubectl -n kube-system rollout status deploy/metrics-server
kubectl get apiservice v1beta1.metrics.k8s.io
kubectl top nodes

步骤 2 · 给 demoapp 配置 requests/limits

demoapp.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demoapp
spec:
  replicas: 2
  selector: {matchLabels: {app: demoapp}}
  template:
    metadata: {labels: {app: demoapp}}
    spec:
      containers:
      - name: demoapp
        image: registry.k8s.io/hpa-example
        resources:
          requests: {cpu: 100m, memory: 128Mi}
          limits: {cpu: 500m, memory: 256Mi}
kubectl apply -f demoapp.yaml
kubectl expose deployment demoapp --port=80 --target-port=80
kubectl get pod -l app=demoapp \
  -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.qosClass}{"\n"}{end}'

requests 和 limits 不相等,所以 QoS 是 Burstable;把两者改成相同值就会变成 Guaranteed。仓库里的 examples/pods/resource-limits-demo.yaml 就是这种写法,它用 ikubernetes/simmemleak 持续泄漏内存,最终会看到 OOMKilled 和退出码 137。

步骤 3 · 创建 HPA 并压测

demoapp-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: demoapp
spec:
  scaleTargetRef:
    kind: Deployment
    name: demoapp
  minReplicas: 2
  maxReplicas: 8
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 60
kubectl apply -f demoapp-hpa.yaml
kubectl get hpa demoapp -w
# 另开终端制造 CPU 负载
kubectl run load --image=busybox:1.36 --restart=Never -- \
  sh -c "while true; do wget -q -O- http://demoapp; done"
NAME      REFERENCE            TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
demoapp   Deployment/demoapp   2%/50%    2         8         2          40s
demoapp   Deployment/demoapp   78%/50%   2         8         4          90s
demoapp   Deployment/demoapp   41%/50%   2         8         8          2m

步骤 4 · 安装 KEDA 并用 cron 缩容到 0

先删除手工 HPA,避免和 KEDA 托管的 HPA 争夺同一个 Deployment:

kubectl delete hpa demoapp
helm repo add kedacore https://kedacore.github.io/charts && helm repo update
helm install keda kedacore/keda --namespace keda --create-namespace
kubectl -n keda get pods
demoapp-cron-scaledobject.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: demoapp-cron
spec:
  scaleTargetRef:
    name: demoapp
  minReplicaCount: 0
  maxReplicaCount: 6
  cooldownPeriod: 60
  triggers:
  - type: cron
    metadata:
      timezone: Asia/Shanghai
      start: "0 9 * * *"
      end: "0 10 * * *"
      desiredReplicas: "4"

这段 cron 表示每天 09:00 到 10:00 保持 4 个副本,其余时间回到 minReplicaCount。在窗口外执行时,KEDA 会直接把 Deployment 缩到 0:

kubectl apply -f demoapp-cron-scaledobject.yaml
kubectl get scaledobject demoapp-cron
kubectl get deploy demoapp

把 start/end 改成当前时间前后各 5 分钟,KEDA 会把副本拉到 desiredReplicas: 4;改回窗口外并等待 cooldownPeriod 后再次归零。KEDA 托管的 HPA 名为 keda-hpa-demoapp-cron,它的 MINPODS 显示 1 是正常的,真正归零由 KEDA 控制器完成,此时 kubectl get deploy demoapp 显示 0/0。

步骤 5 · 换成 CPU 触发器

CPU 触发器仍依赖 metrics-server,但不能缩到 0。删除 cron 对象后换成:

demoapp-cpu-scaledobject.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: demoapp-cpu
spec:
  scaleTargetRef:
    name: demoapp
  minReplicaCount: 1
  triggers:
  - type: cpu
    metricType: Utilization
    metadata:
      value: "50"

应用前先 kubectl delete scaledobject demoapp-cron,再 kubectl apply -f demoapp-cpu-scaledobject.yaml。

验证清单

  • kubectl top nodes 有数据,v1beta1.metrics.k8s.io 状态为 True。
  • demoapp 的 QoS 是 Burstable,kubectl describe pod 中的 requests/limits 与清单一致。
  • 压测后 HPA 的 REPLICAS 上升,停止压测后在缩容窗口后回落。
  • kubectl get scaledobject 的 READY 为 True,窗口外 Deployment 显示 0/0。

常见问题与排错

现象 可能原因 处理方式
kubectl top 报 Metrics API not available metrics-server 未就绪或 APIService 不可用 查 kubectl -n kube-system logs deploy/metrics-server,确认 APIService 为 True
Pod 一直 Pending,事件为 Insufficient cpu requests 总量超过节点可分配资源 看 kubectl describe node 的 Allocated resources,降低 requests 或扩容
Pod 频繁 OOMKilled,退出码 137 内存 limit 太小或应用泄漏 用 kubectl top 定位峰值,调整 limit 或修应用
HPA 显示 <unknown>/50% metrics-server 不可用,或 Pod 没设 requests 先修指标链路;Utilization 必须基于 requests 计算
HPA 反复震荡 缩容窗口太短或指标波动大 增大 scaleDown.stabilizationWindowSeconds,必要时用 selectPolicy: Min
drain 报 would violate the pod's disruption budget PDB 预算已耗尽 等 Pod 恢复或调整 PDB,检查是否单副本加 minAvailable: 1

进阶挑战

  1. 给 demoapp 增加 topologySpreadConstraints,让副本尽量分散到不同节点,并用 kubectl describe pod 验证结果。

小结

  • requests 决定调度,limits 决定运行时上限;QoS 由二者组合推导,决定节点压力下的驱逐顺序。
  • nodeSelector/affinity 是 Pod 选节点,taints/tolerations 是节点排斥 Pod;硬约束不满足时 Pod 会 Pending。
  • HPA 用 autoscaling/v2,靠公式和容差调整副本,behavior 控制速度,但无法缩到 0。
  • PDB 只保护自愿中断,policy/v1 是 v1.31 的唯一写法,空 selector 很危险。

延伸阅读