ᕕ( ᐛ )ᕗ Jimyag's Blog

HPA 自定义指标:请求数不变,为什么副本会减半?

Last modified:

一个上传服务始终收到约 35 RPS。只把每个请求体从 1 MiB 改成 512 KiB,HPA 却把副本从 4 个缩到 2 个。这正常吗?

请求数没有减少,副本为什么会减半?

答案取决于 HPA 实际读取的指标。本次实验不用 CPU,而是让应用导出请求数和已处理字节数,再分别用 RPS 与 B/s 控制副本。

本文是 HPA 实验系列的第二篇。第一篇先解释了一个 CPU 指标如何变成 Ready Pod;本文只关注应用吞吐指标。

自定义指标从哪里来

1
2
3
4
5
6
7
业务请求
应用 Counter → /metrics → Prometheus → rate(...[30s])
Pod 指标 ← custom.metrics.k8s.io ← Adapter
HPA → Deployment /scale

HPA 不会调用应用业务接口,也不会自动数 HTTP 请求。应用先定义指标口径,Prometheus 采集累计值,Adapter 把它转换并映射为 Kubernetes 指标。

本例的两个 Counter:

Counter 何时增加 HPA 使用的速率
demo_http_requests_total /request/upload 成功处理一次 http_requests_per_second
demo_processed_bytes_total /upload 完整读取请求体后 processed_bytes_per_second

Counter 只增不减,进程重启时可能归零。直接把累计值交给 HPA,会让历史流量一直影响副本数。因此先用 rate() 计算每秒速率:

1
2
rate(demo_http_requests_total[30s])
rate(demo_processed_bytes_total[30s])

较长窗口更平滑,但降载反映更慢;较短窗口更敏感,新 Pod 却可能还没有足够样本。Prometheus 的 rate 还会处理 Counter 重置和窗口边界外推。

为什么这里用 Pods,而不是 Object

Prometheus 通过 Pod 服务发现逐个抓取 app=web 的实例,并保留 namespacepod 标签。Adapter 因此能返回每个 Pod 的独立指标:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 把每个 Pod 的累计处理字节数转换成 B/s。
- seriesQuery: 'demo_processed_bytes_total{namespace!="",pod!=""}'
  resources:
    overrides:
      namespace: {resource: namespace} # 映射命名空间
      pod: {resource: pod} # 保留 Pod 维度
  name:
    matches: '^demo_processed_bytes_total$'
    as: processed_bytes_per_second
  # 先对 Counter 求速率,再按 Pod 聚合。
  metricsQuery: 'sum(rate(<<.Series>>{<<.LabelMatchers>>}[30s])) by (<<.GroupBy>>)'

HPA 使用 Pods + AverageValue

1
2
3
4
5
6
7
8
9
# 按 web Pod 的平均处理带宽扩缩容。
metrics:
  - type: Pods # 指标来源是扩缩目标中的各个 Pod
    pods:
      metric:
        name: processed_bytes_per_second # 指标语义是 B/s
      target:
        type: AverageValue # 每个 Pod 的平均绝对值
        averageValue: 10Mi # 每 Pod 目标为 10 MiB/s

Mi 只是 Kubernetes Quantity 的数值倍率;B/s 来自指标定义。10Mi 在这里表示 10485760 B/s,约等于 83.89 Mbit/s

自定义指标不能直接使用 CPU 那样的 Utilization,因为它没有 requests.cpu 这样的分母。不同指标来源支持的目标类型如下:

metrics[].type 可用的 target.type
Resource / ContainerResource UtilizationAverageValue
Pods AverageValue
Object / External ValueAverageValue

如果业务要表达「处理能力使用率 70%」,应先在应用或 Prometheus 中定义分母和比例,再把结果作为一个数值指标交给 HPA;HPA 不会替自定义指标寻找容量分母。

两个 HPA 不能同时控制一个 Deployment

请求速率实验使用的指标配置只有名称和目标不同:

1
2
3
4
5
6
7
8
9
# 按 web Pod 的平均请求速率扩缩容。
metrics:
  - type: Pods
    pods:
      metric:
        name: http_requests_per_second
      target:
        type: AverageValue
        averageValue: "10" # 每 Pod 目标为 10 RPS

实验顺序替换同一个 web HPA。不要创建两个 HPA 同时写同一个 Deployment:它们会相互覆盖期望副本数。如果确实要同时考虑 RPS 和带宽,应把两项写进同一个 metrics 数组,HPA 分别计算后选择较大的副本建议。

动手验证:保持 RPS,只改变请求体大小

实验文件固定在 k8sdev c48f140。先按第一篇的环境步骤创建集群,再执行:

1
2
3
4
5
6
cd ../advanced
./setup.sh
HPA_OUTPUT=$(mktemp -d "${TMPDIR:-/tmp}/hpa-throughput.XXXXXX")
python3 check-upload.py --output "$HPA_OUTPUT/upload-api.json"
python3 run.py requests --output "$HPA_OUTPUT"
python3 run.py bandwidth --output "$HPA_OUTPUT"

每个场景同时检查 HPA 建议、Deployment 期望副本、Ready 副本、Prometheus 查询和 custom metrics API。首次带宽运行被 macOS 休眠打断,采样出现长时间空档;失败记录被保留,重跑使用 caffeinate -i 并加入连续采样检查。

结果:指标选择改变了答案

请求速率 HPA:

实测 RPS Ready 副本 估算
35.08 4 ceil(35.08 / 10)
15.12 2 ceil(15.12 / 10)
0 1 minReplicas 限制

处理带宽 HPA:

请求条件 实测 RPS 实测带宽 Ready 副本
1 MiB × 35 RPS 35.16 35.16 MiB/s 4
512 KiB × 35 RPS 34.88 17.44 MiB/s 2
停止上传 0 0 1

第二阶段的计算是:

1
ceil(17.44 MiB/s ÷ 10 MiB/s/Pod) = 2 Pods

此时 RPS 仍约为 35。如果 HPA 依据请求速率,它会继续建议 4 个副本;实际缩到 2,证明本阶段参与决策的是处理带宽,而请求数只用于核对实验条件。

同一轮 custom metrics API 的返回值也保留了 Pod 维度。高带宽阶段的四个值为:

1
2
3
4
5
web-...-7zgx5  processed_bytes_per_second = 8640957516m
web-...-85xh5  processed_bytes_per_second = 9101639680m
web-...-db7tr  processed_bytes_per_second = 10402706136m
web-...-w7wcf  processed_bytes_per_second = 8725199343m
Prometheus sum                              = 36870502.68 B/s

Kubernetes Quantity 中的 m 表示千分之一,所以第一项是 8640957.516 B/s,不是 86 亿 B/s。半请求体阶段只剩两个 Pod,API 返回 8682209280m9604956160m,Prometheus 总值为 18287165.44 B/s

原始证据:

这个实验没有证明什么

processed_bytes_per_second 统计的是程序完整读取的请求体,不是节点网卡吞吐,也不含 HTTP 头和 TCP 重传。程序读取后直接丢弃数据,没有写盘和业务计算。

生产指标必须回答两个问题:

  1. 增加 Pod 后,这个负载能否被更多实例分担?
  2. 服务饱和时,指标是否仍能表达尚未处理的压力?

共享网卡、数据库或下游已经饱和时,增加 Pod 可能没有收益。完成请求数也可能在过载时不再增长,这时应结合排队、在途请求或延迟指标。

回答开头的问题

请求数不变而副本减半,是因为 HPA 当时没有按请求数决策。请求体减半让应用实际处理的字节速率从 35.16 MiB/s 降到 17.44 MiB/s,每 Pod 目标仍是 10 MiB/s,所以建议从 4 变成 2。

导出一个指标,不等于它会参与扩缩容;只有写入当前 HPA spec.metrics 的指标才参与计算。 下一篇将把指标从「每 Pod 吞吐」换成「一个队列还有多少任务」,并处理缩容时的在途任务。

#Kubernetes #HPA #Prometheus #Kind