jimyag's Blog

Kubernetes HPA 调优与排障:从指标计算到副本就绪

最后更新:

HPA 的 status.desiredReplicas 已经是 4,业务监控却只有一个实例能接收流量。在一次慢启动实验中,新增的 3 个 Pod 已经 Running,但约一分钟后才全部 Ready。副本建议、Pod 数量和可服务的实例数分别处于不同阶段。

本文是系列第四篇,沿着指标计算、行为策略和工作负载启动定位这些差异,最后用一个“已经全部 Ready,吞吐仍未提升”的实验检查扩容后的瓶颈。指标计算基础见指标如何变成期望副本。

HPA 写入期望副本后,谁继续执行

HPA 控制器读取指标、计算建议并应用行为策略,最终通过目标工作负载的 Scale 子资源修改副本数。Deployment 控制器协调 ReplicaSet,ReplicaSet 控制器创建 Pod,scheduler 选择节点,kubelet 启动容器并报告就绪状态。每个控制循环都可能等待自己的条件,HPA 不会替代后续组件执行这些动作。机制见 HPA 工作原理。

默认 kubectl get hpa 的表格没有 DESIRED 列。需要确认建议与执行结果时,分别读取 HPA 的 status.desiredReplicas、Deployment 的 spec.replicas 和 status.readyReplicas:

1
2
3
4
kubectl --context kind-hpa-lab -n hpa-demo get hpa worker \
  -o jsonpath='{.status.desiredReplicas}{"\n"}'
kubectl --context kind-hpa-lab -n hpa-demo get deployment worker \
  -o jsonpath='{.spec.replicas}{"\t"}{.status.readyReplicas}{"\n"}'
1
2
3
4
5
6
7
8
9
指标有值?
   ↓
原始副本建议是多少?
   ↓
是否受稳定窗口、限速、min/max 或多指标约束?
   ↓
Deployment 期望副本是否更新?
   ↓
Pod 能否调度、启动并通过 readiness?

排障时依次记录:

1
2
3
4
kubectl --context kind-hpa-lab -n hpa-demo get hpa
kubectl --context kind-hpa-lab -n hpa-demo describe hpa worker
kubectl --context kind-hpa-lab -n hpa-demo get deployment,pod
kubectl --context kind-hpa-lab -n hpa-demo get events --sort-by=.lastTimestamp

自定义指标还要分别查询 Prometheus 与指标 API,避免把「没采到」「没映射」「HPA 没采用」混成一个问题。

第一步:确认原始建议不是碰巧撞到上限

队列每副本目标为 10,依次把总量改成 15、25、35,再降到 25、15、5:

队列总量ceil(Q/10)Ready 副本
1522
2533
3544
2533
1522
511

完整走过 1 → 2 → 3 → 4 → 3 → 2 → 1,说明中间副本建议也符合公式;只验证 1 → 4 → 1,可能只是每次都撞到 maxReplicas。

重跑中 Q=35、4 个副本时,HPA 的 current.averageValue 是 8750m,即 35 / 4 = 8.75:

1
2
3
4
5
metric:          queue_depth
averageValue:    8750m
target:          10
desiredReplicas: 4
Ready:           4

第二步:看建议是否被稳定窗口保留

实验将缩容稳定窗口改为 60 秒:先用 Q=35 保持 4 个副本,把队列降到 0 约 25 秒,再恢复到 35。整个低值脉冲期间,期望副本始终是 4;最后持续置零才缩回 1。

1
2
最近 60 秒的原始建议:4, 4, 1, 1, 4
缩容采用的约束:max(...) = 4

缩容稳定窗口保存近期的副本建议,采用窗口内较高的建议来抑制短暂下降。它不会对指标求平均,也不会在每次指标变化后重新开始一个固定倒计时。实现见 Kubernetes v1.35.0 的 stabilizeRecommendationWithBehaviors。

正在加载 GitHub 文件…

第三步:看变化速度是否被限制

下面的策略关闭稳定等待,只观察每 30 秒最多改变一个 Pod:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 只验证速度限制,不引入稳定窗口。
behavior:
  scaleUp:
    stabilizationWindowSeconds: 0
    policies:
      - type: Pods # 使用绝对 Pod 数限制变化
        value: 1
        periodSeconds: 30
  scaleDown:
    stabilizationWindowSeconds: 0
    policies:
      - type: Pods
        value: 1
        periodSeconds: 30

与 Percent: 100 对照,并在方向切换前等待上一轮历史离开窗口:

策略扩容序列缩容序列
Pods: 1 / 30s1 → 2 → 3 → 44 → 3 → 2 → 1
Percent: 100 / 30s1 → 2 → 44 → 1

periodSeconds 是统计近期副本变化的窗口,不是控制器严格每 30 秒执行一次。方向切换时,窗口内的历史变化仍会进入基数计算;第一次未隔离历史的探索结果因此没有用作上表结论。

第四步:多指标不是投票

CPU 与队列可以放进同一个 HPA。控制器分别计算副本建议,再选择较大的值,不是取平均,也不要求两项都超过阈值。

CPU / 队列状态本次结果
CPU 约 1%,Q=35队列建议占优,保持 4
CPU 约 1%,队列指标失败拒绝按低 CPU 缩容,保持 4
CPU 高负载,队列指标失败有效 CPU 建议触发 1 → 4
CPU 高负载,Q=0CPU 独立触发 1 → 4

部分指标失败时,HPA 可以使用仍然有效的扩容建议,但不会仅凭其他指标的低值冒险缩容。因此 ScalingActive=True 也不等于每一条指标链路都健康;要同时查看 status.currentMetrics 和事件。

第五步:指标在哪一层消失

实验在 Q=35、4 个副本时分别暂停 exporter、Prometheus 和 Adapter:

暂停组件Prometheus 查询custom metrics APIworker
queue exporter先读到旧值,后为空向量先返回 35,后 NotFound4
Prometheus无可用 endpointInternalError4
Adapter仍返回 35ServiceUnavailable4

对应的关键错误日志为:

1
2
3
4
5
6
exporter 停止: Prometheus result=[]
               custom metrics API: NotFound: could not find metric queue_depth
Prometheus 停止: ServiceUnavailable: no endpoints available for service "prometheus"
                 InternalError: unable to fetch metrics
Adapter 停止: Prometheus value=35
              custom metrics API: ServiceUnavailable

恢复组件并明确让应用返回 Q=0 后,worker 才缩回 1。排障时需要区分四种输入状态:

1
旧样本 ≠ 没有样本 ≠ 指标 API 不可用 ≠ 应用明确报告 0

Prometheus 有值而指标 API 没值时,检查 Adapter 的 seriesQuery、资源标签和映射;指标 API 有值但副本不变时,再检查目标、condition、稳定窗口和限速。

少几个指标时,HPA 为什么还要重新算一次

“部分 Pod 没有采样”和“整个指标查询失败”是不同情况。对于 CPU 利用率,v1.35.1 的 GetResourceReplicas会先分组处理未就绪、缺失指标和应忽略的 Pod,用可用数据计算初步比例;需要时再补入保守估计,重新计算。

初步结果要求缩容时,缺失指标的 Pod 按 request 的 100% 估算;若目标利用率高于 100%,则使用更高的目标值。初步结果要求扩容时,缺失指标的 Pod 按零用量估算,相关未就绪 Pod 也可能按零补入。两个方向采用不同估计,都是为了减弱样本不全导致的调整:避免因没看到部分负载就贸然缩容,也避免只看到少数高负载 Pod 就过度扩容。

重新计算后,如果落入容忍范围或反转了扩缩方向,函数保留当前副本数。因而把界面上的平均利用率直接代入最简单的副本公式,不一定得到最终建议。这一段解释的是源码计算规则;本文瓶颈实验没有故意删除指标样本来验证这些分支。

稳定窗口和速率限制也记录不同的历史。HPA 控制器用 recommendations 保存过去的副本建议,稳定窗口从中选择保守值;scaleUpEvents、scaleDownEvents 记录已发生的副本调整,用于计算某个策略周期内还能改变多少副本。因此 periodSeconds 不是“每隔这么久才执行一次”,稳定窗口也不是收到新建议后简单等待一个定时器到期。

第六步:desired=4 后,Pod 卡在哪里

调度失败

独立实验把每个 Pod 的 CPU request 写成 1000,即 1000 核,而不是 1000m。HPA 正确写入 4,但 4 个 Pod 全部 Pending:

1
0/1 nodes are available: 1 Insufficient cpu

这是调度容量不足,不是 HPA 没有计算。实验没有启用节点自动扩容。

启动慢

另一个实验给 web 增加 60 秒 readiness 延迟:

时间(UTC)desiredRunningReady
14:50:49441
14:51:56444

这约 67 秒里,指标从 35 变为按 4 个副本展示的 8750m,但 Ready 一直为 1。差值包含容器启动、探针和采样间隔,不能全部算成 HPA 延迟。HPA、Deployment、调度器、kubelet 和 readiness 是不同的调谐过程。

4 个副本都 Ready,为什么吞吐仍没有提升

前面的检查解决的是“期望副本为什么没有变成可用副本”。但即使这一整条链路正常,业务性能也不一定随着副本数增加。

这次通过 SSH 在 zj 的 kind v0.33.0、Kubernetes v1.35.1 集群验证了一个下游瓶颈:前端每个请求先消耗约 10 毫秒 CPU,再同步调用单线程后端;后端每个请求固定等待 0.2 秒。客户端保持 8 个并发请求。

前端设置 requests.cpu: 5m,HPA 的 CPU utilization 目标为 50%,最多 4 个副本。很小的 request 用来让实验容易触发扩容,不是生产资源配置建议。Metrics Server 使用 v0.8.0,实验集群对测试 kubelet 证书显式配置了 --kubelet-insecure-tls。

先在 1 个副本时测量,再启动 HPA 并持续施压。脚本等到 Deployment 确实有 4 个 Ready 副本后,单独再测一轮:

状态成功吞吐成功请求 P50成功请求 P95错误数
1 个前端副本4.91 请求/秒1.403 秒1.404 秒1
4 个前端副本4.97 请求/秒1.403 秒1.406 秒2

每轮发起新请求的时间窗口为 20 秒,之后等待已发出的请求完成,所以总耗时约 21 秒。错误请求单独统计,不混进成功请求的延迟分位数;不能把这个小样本对照写成精确的生产容量结论。

结果与故障模型一致:单线程下游每 0.2 秒完成一个请求,服务能力约为每秒 5 个。增加前端副本没有增加下游处理能力,更多前端仍在等待同一个后端。HPA 的计算和副本创建都成功了,性能瓶颈却不在它扩容的那一层。

实验代码与使用说明见 k8sdev/deep-dive-hpa-bottleneck。

因此,副本已经 Ready 之后,还要继续观察请求在前端计算、连接池、下游处理和队列等待上分别花了多少时间。若瓶颈在下游,仅放宽前端 maxReplicas 可能增加下游压力;若新增副本还没接到流量,则应回到 Service、连接复用和负载分布检查,不能把两类问题混在一起。

稳定窗口、速率限制与多指标实验的代码和使用说明见 k8sdev/hpa/advanced。

排障速查

现象先检查
CPU 是 <unknown>metrics.k8s.io、Metrics Server、CPU request、Pod readiness
Prometheus 没有序列/metrics、target up、服务发现与标签
API 没发现自定义指标APIService、Adapter 日志、seriesQuery 与资源映射
API 有值,副本不变当前值/目标、condition、min/max、稳定窗口、限速
desired 增加,Pod PendingPod Events、资源 request、节点容量
Pod Running,Ready 未增加readiness、启动日志、依赖服务

用期望数、就绪数和吞吐区分故障

若 HPA 建议与 Deployment 期望数不同,检查 Scale 写入与控制器事件;期望数已经增加而 Ready 数没有增加,检查调度、启动与探针。Ready 数已经达到目标而吞吐未提升,则继续看新增副本是否收到请求,以及请求耗时是否集中在共享下游。前面的慢启动和单线程下游实验分别停在后两种情况,处理方式也随之不同。

#Kubernetes #HPA #Troubleshooting #Kind