Kubernetes 基础体系 · 第 69/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

Kubernetes 容量规划:Request、利用率、碎片、故障余量和压测

容量规划不是回答“集群还有多少 CPU”这么简单,而是要同时回答以下问题:

  • 调度器还能不能放下下一个 Pod?
  • 现有 Pod 的 requests 是否接近真实需求?
  • 节点剩余资源是否因为碎片而无法使用?
  • 丢失一个节点后,副本能否重新调度?
  • 业务高峰、滚动发布和故障恢复是否会同时发生?
  • 压测得到的数字,是否能支持生产容量决策?

Kubernetes 中至少存在三种容易混淆的“容量”:

  1. 调度容量:由 Pod 的 requests 和节点的 Allocatable 决定。
  2. 运行时容量:由 CPU、内存、磁盘、网络和内核等实际资源决定。
  3. 服务容量:由延迟、错误率、吞吐量、队列长度等业务 SLO 决定。

一个节点可能还有 30% 的 CPU 空闲,但因为内存不足、Pod 请求过大或拓扑约束限制,仍然无法调度新的 Pod。反过来,调度器可能认为容量充足,但实际运行时已经出现 CPU Throttle、内存回收或业务延迟恶化。


一、先定义容量规划中的几个核心量

1. Capacity、Allocatable 和 Available 不是同一个概念

对节点来说,可以先把资源分成三个层次:

  • Capacity:节点向 Kubernetes 报告的总资源。
  • Allocatable:允许普通 Pod 使用的资源上限。
  • Available:某个时刻尚未被已调度 Pod 的 requests 消耗的资源。

近似关系是:

A=node=1NAllocatable(node)A = \sum_{node=1}^{N} Allocatable(node)

Available=Apod=1MRequest(pod)Available = A - \sum_{pod=1}^{M} Request(pod)

其中:

  • AA 是集群的调度容量;
  • Request(pod) 是 Pod 的有效资源请求;
  • Available 只是调度意义上的剩余量,不等于操作系统此刻的空闲量。

节点的 Allocatable 通常小于 Capacity,因为节点需要为以下组件预留资源:

  • kubelet;
  • 容器运行时;
  • 操作系统进程;
  • Kubernetes 系统 Pod;
  • 内存压力和磁盘压力相关的系统余量。

节点可以通过 kubeReservedsystemReservedevictionHard 等配置控制部分预留,但具体生效方式取决于 Kubernetes 发行版、kubelet 配置和云厂商节点镜像。不能简单地用云主机规格减去业务 Pod 请求来计算可用容量。

查看节点容量:

kubectl get nodes -o custom-columns=\
NAME:.metadata.name,\
CPU_CAPACITY:.status.capacity.cpu,\
CPU_ALLOCATABLE:.status.allocatable.cpu,\
MEM_CAPACITY:.status.capacity.memory,\
MEM_ALLOCATABLE:.status.allocatable.memory

可能得到:

NAME       CPU_CAPACITY   CPU_ALLOCATABLE   MEM_CAPACITY   MEM_ALLOCATABLE
worker-01  8              7600m             32Gi           29Gi
worker-02  8              7600m             32Gi           29Gi

这里不能把 8 CPU7600m 混为一谈。调度器使用的是 Allocatable,因此两个节点的集群调度 CPU 容量是:

Acpu=7600m+7600m=15200mA_{cpu}=7600m+7600m=15200m

不是 16000m

2. Request 是调度承诺,不是实际使用量

容器资源声明通常包含:

resources:
  requests:
    cpu: "500m"
    memory: "512Mi"
  limits:
    cpu: "1"
    memory: "1Gi"

request 表示调度器在放置 Pod 时必须为其“预留”的资源额度。对 CPU 而言,500m 表示 0.5 个逻辑 CPU;对内存而言,512Mi 表示 512 MiB 的调度请求。

request 不意味着:

  • 容器启动后一定立刻获得这么多 CPU;
  • 节点上真的切出一块独占内存给它;
  • 容器实际持续使用这么多资源;
  • 内存使用超过 request 就一定被杀死。

limit 才是运行时的上限语义,但 CPU 和内存的行为不同:

  • CPU 超过 limit,通常受到 CFS 配额限制,表现为 CPU Throttle;
  • 内存超过 limit,可能触发该容器的 cgroup OOM;
  • Pod 没有内存 limit 时,仍可能受到节点内存压力和驱逐影响;
  • 具体 OOM 选择还会受到 QoS、oom_score_adj、节点压力和进程行为影响。

因此,容量规划必须同时记录:

R=requestsR = \sum requests

U=actual usageU = \sum actual\ usage

L=limitsL = \sum limits

这三个量分别回答“调度占用”“当前使用”和“理论运行上限”。


二、Pod 的有效 Request 如何计算

不能只把 YAML 中所有容器的数字机械相加,因为 Pod 还可能包含 Init Container 和 Pod Overhead。

对某个资源维度 rr,一个 Pod 的有效请求近似为:

Requestpod,r=max(app containersRequestr,maxinit containersRequestr)+OverheadrRequest_{pod,r} = max\left( \sum_{app\ containers} Request_{r}, \max_{init\ containers} Request_{r} \right) + Overhead_r

原因是:

  • 普通容器通常并行运行,因此请求需要求和;
  • Init Container 通常串行运行,因此它们之间取最大值;
  • Pod Overhead 用于运行时或沙箱等额外开销,适用于配置了相应 RuntimeClass 的场景。

例如:

spec:
  initContainers:
  - name: migrate
    resources:
      requests:
        cpu: "2"
        memory: "1Gi"

  containers:
  - name: api
    resources:
      requests:
        cpu: "500m"
        memory: "512Mi"
  - name: sidecar
    resources:
      requests:
        cpu: "200m"
        memory: "256Mi"

CPU 的有效请求是:

max(500m+200m, 2000m)=2000mmax(500m+200m,\ 2000m)=2000m

内存的有效请求是:

max(512Mi+256Mi, 1Gi)=1Gimax(512Mi+256Mi,\ 1Gi)=1Gi

这会造成一个常见现象:业务容器运行后只需要 700m CPU,但 Init Container 的 2 CPU 请求会在调度阶段把整个 Pod 当成 2 CPU。如果 Init Container 长期存在于 Deployment 模板中,滚动更新时还会持续影响调度和扩容。

验证 Pod 实际请求:

kubectl describe pod <pod-name>

重点查看:

Requests:
  cpu:     2
  memory:  1Gi

也可以通过 Prometheus 的 kube-state-metrics 指标统计:

sum by (namespace, pod) (
  kube_pod_container_resource_requests{
    resource="cpu",
    unit="core"
  }
)

但要注意,这类指标通常按容器暴露,Init Container 和 Pod Overhead 的处理不能仅凭一条简单查询完全还原。容量审计工具必须明确是否实现了 Kubernetes 对 Pod 有效请求的计算规则。


三、利用率:分母不同,结论就不同

“CPU 利用率 70%”没有上下文时几乎没有意义。至少要明确分母。

1. 相对 Capacity 的利用率

Utilizationcapacity=UsageCapacityUtilization_{capacity} = \frac{Usage}{Capacity}

例如节点有 8 个 CPU,实际使用 4 个 CPU:

Utilizationcapacity=48=50%Utilization_{capacity}=\frac{4}{8}=50\%

这反映硬件或虚拟机总能力的使用程度。

2. 相对 Allocatable 的利用率

Utilizationallocatable=UsageAllocatableUtilization_{allocatable} = \frac{Usage}{Allocatable}

如果 Allocatable=7.6 CPU

Utilizationallocatable=47.652.6%Utilization_{allocatable}=\frac{4}{7.6}\approx52.6\%

对于 Kubernetes 调度容量,这个分母通常比 Capacity 更合理。

3. 相对 Request 的利用率

Utilizationrequest=UsageRequestUtilization_{request} = \frac{Usage}{Request}

如果 Pod 的 CPU request 是 500m,实际使用 250m

Utilizationrequest=250m500m=50%Utilization_{request}=\frac{250m}{500m}=50\%

HPA 的 CPU 利用率通常就是“实际 CPU 使用量相对于 Pod CPU request 的比例”,而不是相对于节点总 CPU。也因此,request 的大小会直接影响 HPA 的行为:

  • request 过小:相同实际使用量会产生更高的利用率,HPA 更容易扩容;
  • request 过大:利用率看起来偏低,HPA 可能扩容较慢;
  • 没有合适的 CPU request:基于资源利用率的 HPA 可能无法正确计算。

4. 一个完整算例

假设节点:

  • Capacity:8 CPU;
  • Allocatable:7.6 CPU;
  • 所有 Pod 的 CPU request 总和:6 CPU;
  • 实际 CPU 使用:3 CPU。

那么:

Utilizationcapacity=38=37.5%Utilization_{capacity}=\frac{3}{8}=37.5\%

Utilizationallocatable=37.639.5%Utilization_{allocatable}=\frac{3}{7.6}\approx39.5\%

Utilizationrequest=36=50%Utilization_{request}=\frac{3}{6}=50\%

三者都正确,但含义不同:

  • 37.5%:相对整台机器;
  • 39.5%:相对可被 Kubernetes 使用的节点资源;
  • 50%:已声明请求被实际使用了一半。

因此,“节点 CPU 只有 40%,为什么 HPA 已经扩容?”并不矛盾。HPA 可能看到的是 Pod 相对 request 的 80%,而节点总 CPU 仍然只有 40%。


四、Request 应该如何从实际数据推导

1. CPU Request 的基本推导

CPU 使用具有较强的时间波动性。可以定义一个采样窗口内的 CPU 使用量随机变量 XcpuX_{cpu},然后选择目标利用率 uu

RequestcpuQuantilep(Xcpu)uRequest_{cpu} \ge \frac{Quantile_p(X_{cpu})}{u}

例如:

  • 业务高峰期间 CPU 使用量的 P95 为 350m
  • 希望高峰时 request 利用率不超过 70%;
  • 则建议初始 request:

Requestcpu=350m0.7=500mRequest_{cpu} = \frac{350m}{0.7} =500m

这个推导的直觉是:即使达到 P95 使用量,仍然保留约 30% 的 request 余量。

但这不是 Kubernetes 的规范要求,而是容量建模方法。选择 P95、P99 还是最大值,需要结合:

  • SLO 对延迟尖峰的容忍程度;
  • HPA 的扩容速度;
  • 单 Pod 承载的并发量;
  • CPU limit 是否存在;
  • 节点是否允许 CPU 超卖;
  • 业务是否对 CPU Throttle 敏感。

如果业务是低延迟交易服务,CPU P99 可能比 P95 更有意义;如果是异步批处理,允许排队,则可以选择更高的超卖比例。

2. 内存 Request 不能只看平均值

内存通常不像 CPU 那样适合依靠短时间超卖,因为内存不可压缩。CPU 不够时,进程可能变慢;内存不够时,进程可能被 OOM。

可以使用:

RequestmemoryPeakWorkingSet×(1+ϵ)Request_{memory} \ge PeakWorkingSet \times (1 + \epsilon)

其中:

  • PeakWorkingSet 是观测周期内的峰值工作集;
  • ϵ\epsilon 是应用增长、分配抖动和测量误差余量。

例如一个服务在 14 天内的工作集峰值为 1.4Gi,预留 25%:

Requestmemory=1.4Gi×1.25=1.75GiRequest_{memory}=1.4Gi\times1.25=1.75Gi

可以向上取整为 2Gi

这里要区分 RSS、working set、cache 和 page fault。监控系统中的“内存使用量”可能包含不同 cgroup 指标,不能直接把某一条 kubectl top 数值当成唯一事实。生产上应明确:

  • 使用的是 cgroup memory current、working set 还是 RSS;
  • 是否包含文件缓存;
  • 容器运行时和监控组件的采样周期;
  • 是否覆盖启动、GC、缓存预热和流量切换阶段。

3. Request 与 Limit 的关系

一种常见配置是:

resources:
  requests:
    cpu: "500m"
    memory: "1Gi"
  limits:
    cpu: "1"
    memory: "2Gi"

其含义是:

  • 调度阶段按 500m CPU1Gi memory 计算;
  • 运行时 CPU 最高允许接近 1 CPU,超过后可能被 throttle;
  • 运行时内存达到 2Gi 附近可能触发 cgroup OOM;
  • 节点压力下,该 Pod 的 QoS 和实际使用会影响驱逐风险。

CPU limit 不是“可持续获得的 CPU 保证”。当节点繁忙时,容器即使没有达到 CPU limit,也可能只能获得接近 request 所对应的权重;当 CPU limit 较低时,周期性负载可能出现明显 CFS throttling。

内存 limit 也不是容量规划中的“可额外借用空间”。如果所有 Pod 的 memory request 很小、limit 很大,调度器可能允许大量 Pod 进入节点,但高峰时它们会竞争同一块物理内存,最终导致 OOM 或 Eviction。


五、碎片:总剩余资源足够,但仍然调度失败

1. 一维碎片的基本例子

假设有两个节点,每个节点的 CPU Allocatable 是 8 CPU

当前状态:

node-a: 已用 request 5 CPU,剩余 3 CPU
node-b: 已用 request 5 CPU,剩余 3 CPU

集群剩余 CPU 总量:

3+3=6CPU3+3=6 CPU

此时来了一个需要 4 CPU 的 Pod。虽然集群总剩余量是 6 CPU,但每个节点都只有 3 CPU,因此 Pod 无法放置。

这就是外部碎片:总空间足够,但没有足够大的连续可放置单元。

如果把一组小 Pod 合并或重新调度,可能变成:

node-a: 已用 request 8 CPU,剩余 0 CPU
node-b: 已用 request 2 CPU,剩余 6 CPU

此时同一个 4 CPU Pod 可以调度。集群总容量没有变化,只改变了资源分布。

2. 多资源维度下的碎片

Kubernetes 调度不是只看 CPU。假设:

node-a: 剩余 2 CPU,剩余 8Gi memory
node-b: 剩余 8 CPU,剩余 1Gi memory

新 Pod 请求:

cpu: 4
memory: 4Gi

总剩余资源为:

CPU: 10
Memory: 9Gi

但没有任何节点同时满足 4 CPU4Gi memory,所以仍然无法调度。

这可以形式化为:Pod pp 能否放到节点 nn,必须对每一个资源维度 rr 都满足:

Requestp,rAllocatablen,rRequestedn,rRequest_{p,r} \le Allocatable_{n,r} - Requested_{n,r}

只要有一个维度不满足,该节点就是不可行节点。

3. 影响碎片的调度约束

以下因素会把“可用资源”进一步切碎:

  • Pod 的 nodeSelector 和 Node Affinity;
  • Pod Anti-Affinity;
  • topology spread constraints;
  • Taint 和 Toleration;
  • 可用区、机型、架构等节点标签;
  • GPU、FPGA 等不可超卖的扩展资源;
  • PersistentVolume 的拓扑限制;
  • 节点最大 Pod 数;
  • CNI 地址池和 Pod IP 数量;
  • DaemonSet 在每个节点上消耗的请求;
  • PodDisruptionBudget 对驱逐和滚动操作的限制。

因此,容量模型不能只写成:

NodeCount=TotalRequestNodeAllocatableNodeCount= \left\lceil \frac{TotalRequest}{NodeAllocatable} \right\rceil

这个公式只适合非常理想的一维场景。

更现实的模型是先为每个节点类型 tt 定义可分配向量:

Ct=(CPUt,Memoryt,EphemeralStoraget,)C_t=(CPU_t, Memory_t, EphemeralStorage_t,\ldots)

然后检查每个 Pod 是否能找到满足所有约束的节点。这个问题本质上接近多维装箱问题,通常没有简单的精确闭式解,因此必须使用实际调度模拟、历史调度结果和压测验证。

4. 选择调度策略也会改变碎片

Kubernetes 调度器通常会先过滤不满足约束的节点,再对可行节点打分。与资源分布相关的调度插件会影响 Pod 是倾向于“填满已有节点”还是“摊开到更多节点”。

  • 偏向填充节点,可以减少空闲节点数量,但可能形成大块节点级碎片,也会降低单节点故障后的弹性;
  • 偏向分散,可以提高故障隔离和副本分布,但会留下更多小块空闲资源;
  • topology spread 能改善跨区或跨节点均衡,但均衡约束本身可能降低装箱效率。

所以“减少碎片”和“提高故障容余”经常存在张力,不能只优化其中一个指标。


六、故障余量:不是把平均利用率降到某个百分比

1. N+1 的形式化条件

设集群有 NN 个节点,每个节点的可调度容量为 CiC_i。业务在目标高峰下需要的请求为 RR

如果要求任意一个节点故障后,剩余节点仍能容纳业务请求,则至少需要满足:

Ri=1NCimax(Ci)R \le \sum_{i=1}^{N} C_i - \max(C_i)

在同规格节点场景中,节点容量为 CC,则:

R(N1)CR \le (N-1)C

如果还要求保留额外安全余量 HH,可以写为:

R(N1)C(1H)R \le (N-1)C(1-H)

例如:

  • 5 个节点;
  • 每个节点 Allocatable 为 7.6 CPU
  • 目标高峰请求为 25 CPU
  • 要求任意一个节点故障后再保留 15% CPU 余量。

故障后的有效容量:

(51)×7.6×(10.15)=25.84CPU(5-1)\times7.6\times(1-0.15) =25.84 CPU

因为:

2525.8425 \le 25.84

所以在纯 CPU、无其他约束的理想模型中满足 N+1。但如果内存、拓扑、DaemonSet、Pod 数量或大 Pod 碎片不满足,CPU 计算仍然不能证明系统安全。

2. 故障余量必须按资源维度分别计算

假设故障后剩余节点提供:

CPU:    30 CPU
Memory: 100 Gi

业务高峰请求:

CPU:    20 CPU
Memory: 95 Gi

CPU 余量是 33%,但内存余量只有 5%。集群的有效故障余量应由更紧张的维度决定,不能只看 CPU。

可以对每种资源计算:

Headroomr=1RrCrfailureHeadroom_r= 1-\frac{R_r}{C^{failure}_r}

本例中:

Headroomcpu=12030=33.3%Headroom_{cpu}=1-\frac{20}{30}=33.3\%

Headroommemory=195100=5%Headroom_{memory}=1-\frac{95}{100}=5\%

内存才是故障场景下的主要瓶颈。

3. 故障余量还要覆盖恢复过程

“丢失一个节点后还能运行”不等于“能够自动恢复”。

故障恢复通常经历:

  1. 节点失联;
  2. Node Controller 观察到状态变化;
  3. Pod 被判定为不可继续运行;
  4. ReplicaSet、StatefulSet 或其他控制器创建替代 Pod;
  5. 调度器寻找可行节点;
  6. 镜像拉取、容器启动、探针通过;
  7. Service Endpoint 更新;
  8. 流量逐步恢复。

在此期间还可能有:

  • 节点故障检测延迟;
  • 镜像仓库限流;
  • 多个 Pod 同时拉镜像造成网络拥塞;
  • PVC 跨区挂载限制;
  • PodDisruptionBudget 或拓扑约束导致替代 Pod 无法放置;
  • HPA/VPA 或 Cluster Autoscaler 的决策延迟。

因此,故障余量至少应覆盖两个阶段:

  • 静态容量余量:故障后请求能否放下;
  • 恢复容量余量:替代副本启动和预热期间是否会产生额外负载。

滚动发布也会制造额外容量需求。对于 Deployment,滚动更新期间可能同时存在旧 Pod 和新 Pod。maxSurge 越大,更新期间的瞬时请求越高;如果节点没有足够余量,更新可能卡在 Pending。

检查 Pending Pod 的真实原因:

kubectl describe pod <pending-pod>

重点看 Events,例如:

0/5 nodes are available:
2 Insufficient cpu,
1 Insufficient memory,
2 node(s) didn't match Pod's node affinity/selector.

这三类信息分别说明:

  • 总资源不足;
  • 某个资源维度不足;
  • 约束造成的碎片或隔离。

七、利用率目标如何与容量目标同时成立

容量规划中常用的两类目标经常互相冲突:

  1. 正常运行时,希望节点利用率较高,减少空闲成本;
  2. 故障和高峰时,希望保留足够余量。

设正常业务请求为 RnormalR_{normal},高峰请求为 RpeakR_{peak},故障后的有效容量为 CfailureC_{failure}。同时要求:

Rnormalunormal×CnormalR_{normal}\le u_{normal} \times C_{normal}

RpeakCfailure×(1H)R_{peak}\le C_{failure}\times(1-H)

其中:

  • unormalu_{normal} 是正常调度容量目标;
  • HH 是故障后的额外余量;
  • CnormalC_{normal} 是正常状态下集群容量。

如果只根据平均流量计算节点数量:

N=RaverageCN= \left\lceil \frac{R_{average}}{C} \right\rceil

可能得到很高的平均利用率,但高峰或故障时完全没有恢复空间。

一个完整算例

假设:

  • 单节点 CPU Allocatable:7.6 CPU
  • 正常高峰业务 request:20 CPU
  • 希望故障后仍保留 20% CPU 余量;
  • 要求任意一个节点故障后继续运行。

需要节点数 NN 满足:

20(N1)×7.6×0.820\le (N-1)\times7.6\times0.8

因此:

N1206.083.29N-1\ge \frac{20}{6.08}\approx3.29

N5N\ge5

至少需要 5 个节点。

如果只按无故障容量计算:

N207.6=3N\ge\left\lceil\frac{20}{7.6}\right\rceil=3

3 个节点能够承载正常高峰,但丢失一个节点后只剩:

2×7.6=15.2CPU2\times7.6=15.2 CPU

无法承载 20 CPU。这就是“平均容量足够,但故障余量不足”。


八、QoS、Eviction 和容量规划的关系

Kubernetes 根据容器的 requests 和 limits 为 Pod 分配 QoS 类别:

Guaranteed

通常要求 Pod 中每个容器对 CPU 和内存都同时设置 request 和 limit,且两者相等。

resources:
  requests:
    cpu: "1"
    memory: "1Gi"
  limits:
    cpu: "1"
    memory: "1Gi"

Burstable

设置了部分或全部 requests/limits,但不满足 Guaranteed 条件。

BestEffort

Pod 中没有任何容器设置 CPU 或内存 requests/limits。

QoS 影响节点内存压力下的驱逐优先级,但不能把它理解为“Guaranteed Pod 永远不会被杀”。如果节点自身不可恢复、内核发生 OOM、容器触发自己的 memory limit,Guaranteed 也可能终止。

容量规划中的关键误区是:

“Memory request 小于 memory limit,所以多出来的部分可以当成安全容量。”

不能这样计算。假设节点 Allocatable 是 16Gi,调度上的 Pod memory request 总和为 12Gi,但实际高峰使用可能达到 18Gi。调度器认为还有 4Gi 空间,运行时却会发生节点内存压力。

因此应同时检查:

sum(memory requests) <= allocatable memory
peak(actual memory usage) <= allocatable memory - system reserve

并且为故障、滚动更新和突发分配保留余量。

查看 Pod QoS:

kubectl get pod <pod-name> \
  -o jsonpath='{.status.qosClass}{"\n"}'

查看节点是否发生压力:

kubectl describe node <node-name>

关注:

Conditions:
  MemoryPressure   False
  DiskPressure     False
  PIDPressure      False

也要检查事件:

kubectl get events --all-namespaces \
  --field-selector reason=Evicted \
  --sort-by=.lastTimestamp

Eviction 是 kubelet 在节点压力下主动终止 Pod 的机制,和容器因 memory limit 触发 cgroup OOM 不是同一条路径:

  • cgroup OOM:某个 cgroup 的内存限制被触发;
  • 节点 Eviction:节点整体资源压力达到 kubelet 阈值,kubelet 主动驱逐 Pod。

诊断时应同时查看 Pod 终止原因、容器状态、节点事件和内核日志,不能只看到 OOMKilled 就断言是节点驱逐。


九、HPA 和 VPA 会改变容量模型

1. HPA 的 request 依赖

基于 CPU 利用率的 HPA 通常计算:

UtilizationHPA=Pod ActualCPUUsagePod CPURequestUtilization_{HPA} = \frac{Pod\ ActualCPUUsage}{Pod\ CPURequest}

假设每个 Pod:

CPU request = 500m
实际使用   = 400m
目标利用率 = 70%

则当前利用率:

400500=80%\frac{400}{500}=80\%

期望副本数近似为:

DesiredReplicas=CurrentReplicas×CurrentUtilizationTargetUtilizationDesiredReplicas = \left\lceil CurrentReplicas \times \frac{CurrentUtilization}{TargetUtilization} \right\rceil

如果当前有 3 个副本:

3×0.80.7=4\left\lceil 3\times\frac{0.8}{0.7} \right\rceil = 4

实际 HPA 还会受到采样窗口、缺失指标、初始化期间、缩容稳定窗口和多指标取最大值等因素影响,因此不能把公式当成精确的即时控制器实现。

request 过小会使 HPA 过度敏感;request 过大则可能让 HPA 长期认为利用率不高。容量规划应先让 request 接近真实稳态需求,再调整 HPA 目标,而不是用错误 request 来“校准”扩缩容。

2. HPA 扩容本身需要容量余量

HPA 产生更多副本后,新增 Pod 需要经过:

  1. 控制器修改副本数;
  2. 创建 Pod;
  3. 调度;
  4. 拉取镜像;
  5. 启动容器;
  6. 通过 Readiness;
  7. 接收流量。

如果集群没有可调度空间,HPA 只会创建更多 Pending Pod,不能自动解决底层节点容量不足。此时通常需要 Cluster Autoscaler 或其他节点供应机制,但它们也有启动延迟、节点组约束和云厂商配额限制。

因此,HPA 容量规划要验证:

CapacityavailableCurrentRequests+RequestsscaleoutCapacity_{available} \ge CurrentRequests + Requests_{scaleout}

并且该条件要在高峰和单节点故障场景下成立。

3. VPA 与 HPA 的冲突

VPA 会根据历史使用情况给出或应用新的资源建议。若 VPA 调整 CPU request,而 HPA 又根据 CPU request 计算利用率,可能产生反馈变化:

  • VPA 增大 request;
  • 相同实际使用量对应的利用率下降;
  • HPA 缩容或扩容变慢;
  • VPA 再根据新的使用轨迹调整 request。

CPU 维度同时使用 HPA 和 VPA 时需要谨慎设计,常见做法是让 HPA 基于外部业务指标或并发量扩缩容,而让 VPA 主要负责内存建议;具体策略取决于实现和版本。VPA 并非 Kubernetes 核心稳定 API,其安装方式、更新模式和重启行为必须以所用项目版本为准,不能把它当成所有集群默认内置能力。


十、容量观测:不要只使用 kubectl top

kubectl top 依赖 Metrics Server,适合查看当前资源使用概况:

kubectl top nodes
kubectl top pods -A --sort-by=cpu
kubectl top pods -A --sort-by=memory

但它存在几个边界:

  • 主要反映当前或短窗口数据;
  • 不提供完整的 P95/P99 历史分布;
  • 不能直接解释 CPU Throttle;
  • 不能替代业务延迟、吞吐和错误率;
  • 不能直接告诉你碎片、拓扑约束和 Pending 原因;
  • Metrics Server 不等同于完整监控系统。

生产容量规划至少需要同时采集以下数据:

1. 调度层数据

  • 节点 Capacity 和 Allocatable;
  • Pod requests 和 limits;
  • Pending Pod 数量及原因;
  • 节点不可调度状态;
  • Pod 分布、拓扑和亲和性;
  • DaemonSet 资源开销;
  • 最大 Pod 数和 IP 使用量。

2. 运行时数据

  • CPU 使用量;
  • CPU Throttle 时间或比例;
  • 内存 working set、RSS、cgroup current;
  • OOMKilled;
  • 节点 MemoryPressure;
  • 磁盘空间和 inode;
  • ephemeral-storage 使用;
  • 网络带宽、丢包和连接数;
  • PID 使用量。

3. 服务层数据

  • 请求吞吐;
  • 并发数;
  • P50、P95、P99 延迟;
  • 5xx 和业务错误率;
  • 队列长度;
  • 数据库连接池;
  • 下游依赖延迟;
  • GC、线程池和文件描述符。

一个典型 Prometheus 查询示例:

sum(
  rate(container_cpu_usage_seconds_total{
    container!="",
    image!=""
  }[5m])
)

它表示容器 CPU 使用率的总和,单位近似为 CPU core。若要计算相对于 request 的利用率,需要把它与对应容器或 Pod 的 CPU request 对齐。实际生产查询必须处理:

  • 容器重启;
  • Pod 名称变化;
  • Init Container;
  • 多容器 Pod;
  • 指标标签重复;
  • 缺失 request;
  • kube-state-metrics 与 kubelet 指标的时间对齐。

简单地把全局使用量除以全局 request,可能掩盖某个节点已经过载而另一个节点空闲的情况。因此,容量仪表盘应至少同时展示:

集群总量
节点分布
命名空间分布
工作负载分布
资源维度
峰值和分位数
Pending 与驱逐

十一、压测:验证“能运行”之外的服务容量

容量压测的目标不是找一个单独的 QPS 数字,而是确定在给定 SLO 下,系统能稳定承载多少负载,以及增加容量后瓶颈是否真的移动。

1. 先定义压测变量

一个可复现的压测模型至少要固定:

  • 请求类型和请求比例;
  • payload 大小;
  • 并发模型;
  • 到达率;
  • 依赖服务;
  • 数据集规模;
  • 缓存命中率;
  • 副本数;
  • 节点规格;
  • CPU 和内存 request/limit;
  • HPA 是否启用;
  • 是否包含发布和故障注入。

压测需要区分两种负载模型:

闭环模型

固定并发数。一个请求结束后,客户端才发出下一个请求。

并发数固定 → 响应变慢 → 实际到达率下降

它适合观察固定并发下的响应时间,但在系统变慢时会自动降低压力,可能掩盖排队问题。

开环模型

固定到达率。无论前面的请求是否完成,测试器都按设定速率继续发送。

到达率固定 → 系统变慢 → 队列增长 → 延迟和错误暴露

它更适合验证“每秒进入多少请求时系统开始失稳”,但需要限制测试器自身的连接和内存,否则测试器会先成为瓶颈。

2. 压测阶段

一个完整流程通常包含:

  1. 基线测试:单副本或固定副本,确认功能和指标链路正确;
  2. 预热:等待 JVM、Go runtime、缓存、连接池和 HPA 指标稳定;
  3. 阶梯加压:逐步提高并发或到达率;
  4. 稳态观察:每个阶梯保持足够时间,观察资源、延迟和错误;
  5. 峰值保持:验证目标峰值是否能持续;
  6. 突发测试:观察短时间流量冲击;
  7. 扩容测试:记录 HPA、调度、节点扩容和 Ready 延迟;
  8. 故障测试:删除节点或阻断依赖,验证 N+1 和恢复能力;
  9. 回落测试:观察缩容和资源释放是否稳定。

不能只记录压测工具输出的平均延迟。平均值可能是:

P50 = 20ms
P95 = 30ms
P99 = 2s

此时平均值仍然很好,但尾延迟已经明显违反很多业务 SLO。

3. 一个可运行的 Kubernetes 压测示例

以下 Deployment 使用 httpbin 作为示例 HTTP 服务:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: httpbin
spec:
  replicas: 2
  selector:
    matchLabels:
      app: httpbin
  template:
    metadata:
      labels:
        app: httpbin
    spec:
      containers:
      - name: httpbin
        image: kennethreitz/httpbin:latest
        ports:
        - containerPort: 80
        resources:
          requests:
            cpu: "100m"
            memory: "128Mi"
          limits:
            cpu: "500m"
            memory: "256Mi"
---
apiVersion: v1
kind: Service
metadata:
  name: httpbin
spec:
  selector:
    app: httpbin
  ports:
  - port: 80
    targetPort: 80

应用并等待 Ready:

kubectl apply -f httpbin.yaml
kubectl rollout status deployment/httpbin
kubectl get pods -l app=httpbin -o wide

启动一个临时客户端:

kubectl run loadgen \
  --rm -it \
  --image=busybox:1.36 \
  --restart=Never \
  -- sh

在容器内执行:

while true; do
  wget -q -O- http://httpbin/get >/dev/null
done

这个示例只适合验证 Service 连通性和产生非常粗糙的请求流量,不适合正式性能测试,因为:

  • BusyBox 循环不是严格的并发或固定到达率模型;
  • 没有可靠的延迟分位数统计;
  • 客户端本身可能成为瓶颈;
  • httpbin 不是业务应用;
  • 镜像标签使用 latest 不利于复现。

正式压测应使用固定版本的压测工具或专用压测平台,并把压测客户端运行在独立节点或集群之外。否则客户端 CPU、网络和连接池会污染结果。

4. 如何从压测结果反推 Request

假设压测得到以下结果:

目标业务 SLO:P99 < 200ms,错误率 < 0.1%
稳定吞吐:每个 Pod 120 RPS
稳定状态 CPU P95:420m
稳定状态内存峰值:900Mi

如果目标 CPU request 利用率为 70%,则:

Requestcpu420m0.7=600mRequest_{cpu}\ge\frac{420m}{0.7}=600m

内存按峰值加 25%:

Requestmemory900Mi×1.25=1125MiRequest_{memory}\ge900Mi\times1.25=1125Mi

可以设置为:

resources:
  requests:
    cpu: "600m"
    memory: "1200Mi"

如果目标高峰为 1000 RPS,则按每 Pod 120 RPS 计算:

Replicasload=1000120=9Replicas_{load} = \left\lceil \frac{1000}{120} \right\rceil =9

如果还要求单节点故障后继续承载,并且每个节点只能放下 4 个这样的 Pod,则至少需要:

94+1=4 个节点\left\lceil\frac{9}{4}\right\rceil+1=4\text{ 个节点}

但这仍然只是粗略上界。实际还要检查副本是否能跨区分布、是否有足够内存、是否存在滚动发布的 surge,以及 HPA 从 9 扩到目标副本数的时间。


十二、常见失败表现与诊断路径

1. 节点 CPU 利用率不高,但 Pod 一直 Pending

优先检查:

kubectl describe pod <pod-name>
kubectl get nodes
kubectl describe node <node-name>

常见原因:

  • 单个 Pod 的 CPU 或内存 request 大于任意节点剩余量;
  • 节点存在 taint;
  • Pod 有 node affinity;
  • Pod Anti-Affinity 或 topology spread 无法满足;
  • 可用区或架构不匹配;
  • 节点最大 Pod 数已达上限;
  • 需要 GPU 等扩展资源;
  • PVC 无法在目标拓扑挂载。

此时查看集群总 CPU 利用率通常没有帮助,因为调度是按节点和约束逐个判断的。

2. Request 利用率很低,但节点经常 MemoryPressure

可能是 memory request 过大或监控分母不一致,也可能是 memory limit 过高导致运行时过度超卖。

排查:

kubectl top nodes
kubectl top pods -A --sort-by=memory
kubectl describe node <node-name>
kubectl get events -A --sort-by=.lastTimestamp

同时核对:

  • Pod memory request 总和;
  • 实际 cgroup 内存;
  • 文件缓存;
  • kubelet 和系统预留;
  • evictionHard
  • 是否有突发批任务;
  • 是否发生 OOMKilled。

3. CPU 利用率不高,但延迟上升

可能原因包括:

  • CPU limit 触发 Throttle;
  • 单线程程序无法利用多个 CPU;
  • 锁竞争;
  • GC;
  • 下游数据库或网络成为瓶颈;
  • 请求排队在连接池、线程池或队列中;
  • 节点 CPU steal 或虚拟化争用;
  • 某个关键 Pod 被压缩到少数节点。

CPU 使用量低并不能证明服务有容量。必须同时观察 CPU Throttle、运行队列、应用线程、依赖延迟和业务 P99。

4. HPA 扩容了,但延迟继续恶化

可能是:

  • 新 Pod 处于 Pending;
  • 节点没有可用 request 容量;
  • Cluster Autoscaler 尚未完成节点创建;
  • 镜像拉取慢;
  • Readiness 探针迟迟不通过;
  • 新副本没有真正接收流量;
  • 瓶颈在数据库、限流器或外部 API;
  • HPA 使用了不合适的指标或 request。

检查:

kubectl get hpa
kubectl describe hpa <hpa-name>
kubectl get pods -l app=<app> -o wide
kubectl get events --sort-by=.lastTimestamp

容量规划不能只验证“副本数增加”,还要验证“新副本从创建到真正承载流量的完整时间”。


十三、把容量规划落成一个可审计的模型

一个可用于生产评审的容量表,至少应包含以下字段:

节点类型
节点数量
Capacity
Allocatable
系统与 DaemonSet 开销
业务 Request
业务实际 P50/P95/P99
业务峰值
滚动发布额外副本
HPA 最大副本数
单节点故障后的容量
内存压力阈值
Pending Pod
Pod 数量/IP 余量
可用区和拓扑约束

可以按以下步骤计算:

第一步:计算调度容量

每种节点类型的 Allocatable
- DaemonSet requests
- 系统预留影响
= 普通工作负载可用空间

不能重复扣除已经体现在 Allocatable 中的系统预留。应先确认节点状态中的 Allocatable 已包含哪些配置。

第二步:计算有效业务请求

对每个 Pod:

普通容器 request 之和
与 Init Container request 最大值取较大者
再加 Pod Overhead

然后按命名空间、工作负载、节点和可用区聚合。

第三步:加入运行时峰值约束

分别检查:

峰值 CPU 使用
峰值内存使用
CPU limit 和 Throttle
内存 limit 和 OOM
磁盘、网络、PID

CPU 可以有一定程度的超卖,但内存超卖的风险通常更高。任何超卖比例都必须通过压测和历史故障数据验证,而不是从 CPU 经验直接复制到内存。

第四步:加入故障和变更场景

至少计算:

  • 正常高峰;
  • 单节点故障;
  • 单可用区故障;
  • HPA 扩容;
  • Deployment 滚动发布;
  • 镜像缓存为空;
  • 关键 DaemonSet 重启;
  • 节点维护和主动驱逐。

第五步:检查碎片而不是只看总和

对大 request Pod、GPU Pod、StatefulSet 和有拓扑约束的工作负载,执行实际调度验证。总量公式只能证明“理论上可能足够”,不能证明“存在一个可行放置方案”。


十四、规范、实现和经验必须分开

以下结论属于 Kubernetes 资源模型的基本规范语义:

  • 调度器主要依据 Pod 的 requests 和节点 Allocatable 做资源可行性判断;
  • 容器的 CPU 和内存 limit 由运行时执行,但具体 cgroup 行为依赖内核和容器运行时;
  • HPA 的资源利用率依赖相应资源 request;
  • QoS 类别由资源声明模式决定,并影响压力场景下的处理。

以下内容属于常见实现或部署相关行为:

  • kubectl top 依赖 Metrics Server;
  • kubelet 的预留和驱逐阈值受发行版和节点配置影响;
  • CPU Throttle 的指标名称和采集方式受 cAdvisor、kubelet、Prometheus 配置影响;
  • 节点自动扩容的启动时间和配额受云厂商影响;
  • CNI 的 Pod IP 容量可能先于 CPU 或内存成为瓶颈。

以下内容属于经验建议,不能视为 Kubernetes 保证:

  • CPU request 使用 P95 除以目标利用率;
  • 内存 request 在峰值上增加固定百分比;
  • 预留 15% 或 20% 故障余量;
  • 让 CPU request 等于某个固定分位数;
  • 通过设置更高 limit 消除所有性能问题。

这些数字只有在明确负载模型、SLO、节点规格和故障目标后才有意义。


结语

Kubernetes 容量规划的核心不是寻找一个“集群利用率目标”,而是建立从业务负载到调度和故障恢复的完整约束链:

业务负载实际资源使用Request/Limit节点Allocatable调度可行性运行时压力SLO与故障恢复业务负载 \rightarrow 实际资源使用 \rightarrow Request/Limit \rightarrow 节点 Allocatable \rightarrow 调度可行性 \rightarrow 运行时压力 \rightarrow SLO 与故障恢复

Request 决定 Pod 能不能被放下;利用率说明当前使用情况,但必须明确分母;碎片解释了为什么总剩余资源不等于可调度资源;故障余量决定节点丢失或滚动发布时是否还能恢复;压测则用真实负载验证这些计算是否符合业务 SLO。

只有把这几个层次放在同一个模型中,容量规划结果才不仅能解释“现在为什么 Pending”,也能回答“高峰、扩容、发布和故障同时发生时,系统是否仍然可用”。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。