安全模型控制谁能做什么,资源管理控制能用多少。这两个维度共同决定了集群的多租户能力。requests 和 limits 是 Kubernetes 资源模型的基础,理解它们的精确语义是做好容量规划的前提。HPA 和 VPA 在此基础上提供了自动化的弹性能力。

在传统物理机和虚拟机时代,资源隔离靠硬件分区或 hypervisor,资源弹性靠人工扩容。Kubernetes 把资源管理内置为一等公民,通过 cgroup 在内核层面强制执行,通过控制器实现自动化弹性。这一篇的核心问题是:requests 和 limits 如何影响调度和运行时行为,HPA 和 VPA 如何根据负载自动伸缩?

资源模型全景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
调度时(kube-scheduler)
┌────────────────────────────────────────────────────────────┐
│ Node Allocatable = Node Capacity - System Reserved │
│ ───────────────────────────────────────────────────── │
│ kube-scheduler 只看 requests,不看 limits │
│ 确保:sum(Pod requests) <= Node Allocatable
└────────────────────────────────────────────────────────────┘

运行时(kubelet + 内核 cgroup)
┌────────────────────────────────────────────────────────────┐
│ CPU:CFS Bandwidth │
│ ────────── │
│ cpu.shares ∝ requests │
│ cpu.cfs_quota_us / cpu.cfs_period_us = limits │
│ 超过 quota → throttling(进程不会被杀,只是限速) │
│ │
│ Memory:cgroup memory limit │
│ ────────────────────────── │
│ memory.limit_in_bytes = limits │
│ 超过 limit → OOM kill(内核直接杀进程) │
└────────────────────────────────────────────────────────────┘

QoS 与驱逐优先级
BestEffort(无 requests/limits)→ 最先驱逐
Burstable(requests < limits) → 次之
Guaranteed(requests == limits)→ 最后驱逐
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
自动伸缩层次

┌──────────────────────────────────────────┐
│ KEDA(事件驱动伸缩) │
│ 队列深度 / 消息积压 → 副本数 │
└──────────────────────────────────────────┘
┌───────────────────┐ ┌────────────────────┐
│ HPA(水平伸缩) │ │ VPA(垂直伸缩) │
│ 调整副本数 │ │ 调整 requests/limits│
│ 基于 CPU/内存 │ │ 建议或自动生效 │
│ 或自定义指标 │ │ │
└───────────────────┘ └────────────────────┘

┌──────────────────────────────────────────┐
│ Pod(运行中) │
requests: cpu 100m, memory 128Mi │
limits: cpu 500m, memory 512Mi │
└──────────────────────────────────────────┘

核心概念

requests 与 limits 精确语义

1
2
3
4
5
6
7
resources:
requests:
cpu: "250m" # 0.25 个 CPU 核,调度时保证
memory: "512Mi" # 512 MiB,调度时保证
limits:
cpu: "1000m" # 1 个 CPU 核,运行时上限
memory: "1Gi" # 1 GiB,运行时上限,超过触发 OOM kill

CPU requests 转化为 cgroup cpu.shares(值 = requests * 1024 / 1000,最小 2),在节点 CPU 竞争时按比例分配时间片。CPU limits 转化为 CFS bandwidth(cpu.cfs_quota_us),以 100ms 为周期(cpu.cfs_period_us = 100000),若进程在周期内用完配额,剩余时间被 throttle。

Memory requests 只影响调度,运行时不限制实际内存使用(直到超过 limits)。Memory limits 转化为 cgroup memory.limit_in_bytes,超过后内核的 OOM killer 会选择杀死该 cgroup 中的进程。

CPU throttling 是一个常被忽视的性能问题。container_cpu_throttled_seconds_totalcontainer_cpu_cfs_throttled_periods_total 是两个重要的 Prometheus 指标,throttling 比例超过 25% 通常意味着 CPU limits 设置过低。

QoS 类型

QoS 类型 条件 驱逐优先级
Guaranteed 所有容器:requests == limits,且两者都已设置 最低(最后被驱逐)
Burstable 有 requests,但不等于 limits;或只设置了 limits 中等
BestEffort 没有任何 requests 和 limits 最高(第一个被驱逐)

节点内存压力时,kubelet 按 BestEffort → Burstable → Guaranteed 的顺序驱逐 Pod。Guaranteed Pod 只有在节点极端内存压力(超出所有 limits)时才会被驱逐。

ResourceQuota

ResourceQuota 限制命名空间内的资源总量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: v1
kind: ResourceQuota
metadata:
name: production-quota
namespace: production
spec:
hard:
requests.cpu: "10" # namespace 内所有 Pod 的 CPU requests 总和
requests.memory: 20Gi
limits.cpu: "40"
limits.memory: 80Gi
pods: "50" # 最大 Pod 数
services: "20"
persistentvolumeclaims: "10"
count/deployments.apps: "20"

当 namespace 有 ResourceQuota 时,所有新创建的 Pod 必须显式设置 requests 和 limits,否则被拒绝。

LimitRange

LimitRange 定义命名空间的默认值和约束范围,自动注入没有设置资源的 Pod:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: production
spec:
limits:
- type: Container
default: # 没有设置 limits 时使用
cpu: "500m"
memory: "512Mi"
defaultRequest: # 没有设置 requests 时使用
cpu: "100m"
memory: "128Mi"
max: # 允许的最大值
cpu: "4"
memory: "8Gi"
min: # 允许的最小值
cpu: "50m"
memory: "64Mi"

HPA(Horizontal Pod Autoscaler)

HPA 根据指标自动调整 Deployment/StatefulSet/ReplicaSet 的副本数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-server-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api-server
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60 # 目标 CPU 使用率 60%
- type: Resource
resource:
name: memory
target:
type: AverageValue
averageValue: 512Mi
behavior:
scaleUp:
stabilizationWindowSeconds: 60 # 扩容稳定窗口:60 秒内不重复扩容
policies:
- type: Percent
value: 100 # 每次最多扩容一倍
periodSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300 # 缩容稳定窗口:5 分钟,防止抖动
policies:
- type: Pods
value: 2 # 每次最多缩容 2 个
periodSeconds: 60

HPA 控制算法

1
2
3
4
targetReplicas = ceil(currentReplicas × currentMetricValue / desiredMetricValue)

例:当前 3 个副本,CPU 使用率 90%,目标 60%
targetReplicas = ceil(3 × 90 / 60) = ceil(4.5) = 5

HPA 每 15 秒(默认)从 metrics-server 或 Prometheus Adapter 获取指标,计算目标副本数,通过 Scale 子资源更新 Deployment。scaleDown 有 5 分钟默认稳定窗口,防止指标短暂下降导致频繁扩缩容。

自定义指标 HPA

需要安装 Prometheus Adapter,把 Prometheus 指标注册为 Custom Metrics API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
metrics:
- type: Pods
pods:
metric:
name: http_requests_per_second # Prometheus 自定义指标
target:
type: AverageValue
averageValue: "100" # 每个 Pod 每秒处理 100 个请求
- type: External
external:
metric:
name: queue_depth
selector:
matchLabels:
queue: orders
target:
type: AverageValue
averageValue: "30" # 队列深度超过 30 时扩容

VPA(Vertical Pod Autoscaler)

VPA 自动推荐或调整 Pod 的 requests/limits:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: api-server-vpa
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: api-server
updatePolicy:
updateMode: "Auto" # Off / Initial / Recreate / Auto
resourcePolicy:
containerPolicies:
- containerName: api-server
minAllowed:
cpu: 100m
memory: 128Mi
maxAllowed:
cpu: 4
memory: 8Gi
controlledResources: ["cpu", "memory"]

VPA 的四种模式:

模式 行为
Off 只计算推荐值,不做任何修改,用于观察
Initial 只在 Pod 创建时应用推荐值,运行中不变
Recreate 当推荐值与当前值差异超过阈值时,驱逐 Pod 重建
Auto 与 Recreate 相同(未来可能支持 in-place 更新)

VPA 和 HPA 的冲突:两者同时对 CPU 指标操作时,HPA 根据当前 requests 计算扩容目标,VPA 同时调整 requests,导致相互干扰。标准做法:HPA 用 CPU,VPA 用内存(设置 controlledResources: ["memory"]),或用 KEDA 完全替代 HPA。

KEDA(Kubernetes Event-Driven Autoscaling)

KEDA 把任意外部指标(消息队列深度、数据库行数、HTTP 请求速率)转化为 HPA 兼容的自定义指标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: order-processor
spec:
scaleTargetRef:
name: order-processor-deployment
minReplicaCount: 0 # 支持缩容到 0(HPA 最小 1)
maxReplicaCount: 50
triggers:
- type: rabbitmq
metadata:
queueName: orders
host: amqp://rabbitmq.production
queueLength: "10" # 每个 Pod 处理 10 条消息
- type: prometheus
metadata:
serverAddress: http://prometheus:9090
metricName: pending_jobs
threshold: "5"
query: sum(pending_jobs{service="order"})

KEDA 支持缩容到 0(scale-to-zero),在无流量时彻底停止工作负载,节省资源。

实验:HPA 在压力下的扩缩容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# 安装 metrics-server(kind 集群需要)
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

# kind 集群需要追加 --kubelet-insecure-tls 参数
kubectl patch deployment metrics-server -n kube-system \
--type=json \
-p='[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]'

# 部署 CPU 密集型应用
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: load-test
spec:
replicas: 1
selector:
matchLabels:
app: load-test
template:
metadata:
labels:
app: load-test
spec:
containers:
- name: cpu-burner
image: nginx
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
EOF

# 创建 HPA
kubectl autoscale deployment load-test \
--cpu-percent=50 \
--min=1 \
--max=10

# 观察 HPA 状态
kubectl get hpa load-test -w
# NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS
# load-test Deployment/load-test 0%/50% 1 10 1

# 注入负载
kubectl run load-gen --image=busybox --rm -it -- \
sh -c "while true; do wget -q -O- http://load-test; done" &

# 几分钟后观察扩容
kubectl get hpa load-test -w
kubectl get pods -l app=load-test -w

# 停止负载后观察缩容(约 5 分钟后)
kubectl delete pod load-gen

# 查看 VPA 推荐(需要先安装 VPA)
kubectl describe vpa load-test-vpa
# Recommendation:
# Container Recommendations:
# Container Name: cpu-burner
# Lower Bound: cpu: 150m, memory: 80Mi
# Target: cpu: 350m, memory: 96Mi
# Upper Bound: cpu: 1000m, memory: 512Mi

映射到 Kubernetes 内部机制

HPA Controller 是 kube-controller-manager 中的一个控制器,每 15 秒(--horizontal-pod-autoscaler-sync-period)执行一次调谐循环:获取当前副本数 → 从 metrics API 获取指标 → 计算目标副本数 → 调用 Scale 子资源更新副本数。

VPA 由三个组件组成:VPA Recommender(持续从 metrics API 收集历史数据,使用指数加权移动平均计算推荐值)、VPA Updater(监控 VPA 对象,决定何时驱逐 Pod 使新配置生效)、VPA Admission Plugin(Pod 创建时,根据推荐值修改 requests/limits)。

节点驱逐的触发机制:kubelet 监控节点内存和磁盘压力,设置 Node Condition(MemoryPressure、DiskPressure)。当压力超过 soft eviction threshold 并持续一段时间,或超过 hard eviction threshold 时,kubelet 立即开始按 QoS 优先级驱逐 Pod。

模式提炼

requests 是"预订",limits 是"上限",QoS 是"驱逐优先级"。HPA 解决"需要多少个 Pod"的问题,VPA 解决"每个 Pod 需要多大"的问题,两者在不同维度互补。KEDA 扩展了 HPA 的指标来源,特别适合消息驱动的异步处理场景。

资源管理的本质是在"资源利用率"和"服务稳定性"之间找平衡点。requests 设得太低,调度过多 Pod 到节点导致资源争用;limits 设得太低,正常负载下出现 CPU throttling 或 OOM kill;limits 设得太高,资源浪费。正确的做法是从 Off 模式的 VPA 推荐开始,结合压测数据调整。

工程迁移表

资源机制 工程类比
requests / limits 线程池 coreSize / maxSize,数据库连接池 minIdle / maxActive
CPU throttling(CFS) 进程 nice 值调度,CPU 时间片轮转
OOM kill JVM -Xmx 超出后的 OutOfMemoryError,进程 crash
QoS Guaranteed 高优先级服务,独占资源,类似实时任务调度
HPA 云厂商 Auto Scaling Group,按负载自动增减实例
VPA 数据库实例垂直扩容,升级到更大规格的 VM
KEDA 消息队列消费者动态扩缩,AWS Lambda 函数触发
ResourceQuota 云账号 Service Quota,部门 cost center 限额

常见误解

误解一:requests 等于容器实际使用的资源量,设高了浪费,设低了不够用。

requests 只影响调度,不限制实际使用。一个 requests 100m CPU 的容器,如果节点有空闲,可以使用更多 CPU(直到 limits)。在资源充足时,所有容器都能获得比 requests 更多的资源;只有在竞争时,requests 才决定各容器的分配比例。因此,requests 应该设置为正常负载下的典型使用量,不是峰值,也不是最低值。

误解二:CPU limits 会像内存 OOM 一样杀死进程。

CPU 超出 limits 的结果是 throttling(限速),不是进程终止。内核会在 CFS 周期(100ms)内限制该 cgroup 的 CPU 时间,进程继续运行,但速度变慢。这对延迟敏感的应用影响很大(P99 延迟升高),但不会导致 crash。内存超 limits 才会触发 OOM kill。两种超限的后果完全不同。

误解三:HPA 和 VPA 可以同时作用于同一应用的 CPU 指标,互相补充。

HPA 基于 CPU 使用率(相对于 requests)计算目标副本数。如果 VPA 同时调整 requests,HPA 的基准值发生变化,计算出的目标副本数也随之变化,两个控制器会相互干扰,导致副本数震荡。标准做法是:HPA 管副本数(基于 CPU 指标),VPA 只管内存(设置 controlledResources: ["memory"]),或者放弃 HPA,用 KEDA + VPA 的组合。

练习

练习一:CPU throttling 分析。部署一个设置了较低 CPU limits 的应用,用 kubectl exec 进入容器运行 CPU 密集计算,同时通过 kubectl top pod 和 cgroup 文件(/sys/fs/cgroup/cpu/cpu.stat 中的 nr_throttled)观察 throttling 现象。对比提高 limits 前后的延迟变化。

练习二:QoS 驱逐实验。在一个内存有限的测试节点上,分别创建 Guaranteed、Burstable、BestEffort 三类 Pod,然后逐渐填满节点内存(使用内存泄漏测试工具),观察 kubelet 的驱逐顺序是否符合 QoS 规则,查看 kubelet 日志中的驱逐决策。

练习三:HPA 冷却参数调优。部署一个 HPA,默认配置下产生一次负载尖峰,观察扩缩容的过程和时间线。调整 scaleDown.stabilizationWindowSecondsscaleUp.stabilizationWindowSeconds,对比不同参数下系统对负载变化的响应速度和稳定性,理解冷却参数的权衡。

系列导航

参考资料