调度、资源与弹性¶
Pod 能不能跑起来,取决于调度器是否找得到合适的节点;跑起来稳不稳,取决于 requests/limits 与 QoS;流量上来扛不扛得住,取决于 HPA 与 KEDA。本章把这三件事串成一条可验证的链路。
你将学到¶
- 用 requests 让调度器预留资源,用 limits 划出运行时上限,并判断 Pod 的 QoS 等级。
- 用 nodeSelector、nodeAffinity、taints/tolerations 控制 Pod 落在哪些节点。
- 部署 metrics-server,编写
autoscaling/v2HPA,压测观察扩缩容。 - 安装 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
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 并压测
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
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 对象后换成:
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 |
进阶挑战¶
- 给 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 很危险。