如何通过Prometheus正确测量K8s微服务CPU使用率百分比
我需要通过Prometheus指标获取Kubernetes集群内各运行微服务的CPU使用率百分比,用于优化CPU资源配置与limits设置。
我的部署架构为每位客户对应4个运行在服务器上的微服务,每个微服务都配置了独立的内存资源request/limit与CPU资源request/limit。当前我使用如下PromQL查询24小时维度的平均CPU使用率百分比:
avg_over_time(sum(rate(container_cpu_usage_seconds_total{name=~"^k8s_.*", namespace=~"$namespace", container_name!="POD", pod=~"^$Deployment.*$"}[5m]))[24h:5m]) / avg_over_time(sum(container_spec_cpu_quota{name=~"^k8s_.*", namespace=~"$namespace",container_name!="POD", pod=~"^$Deployment.*$"}/container_spec_cpu_period{name=~"^k8s_.*",namespace=~"$namespace", container_name!="POD", pod=~"^$Deployment.*$"})[24h:5m]) * 100
为校验上述查询结果的准确性,我通过Kubernetes命令获取对应数值做对比:
- 使用
kubectl -n {namespace} top pod {Deployment}命令查看Pod CPU使用率 - 使用
kubectl -n {namespace} describe pod {Deployment}命令查看Pod CPU limit值
我按照「CPU使用率 / CPU limit * 100 = 当前CPU使用率百分比」的公式计算Kubernetes侧的CPU使用率百分比,但发现Prometheus查询得到的数值与Kubernetes侧计算得到的数值存在偏差,部分结果接近,部分差异较大,两类来源的CPU使用率百分比如下:
| 客户 | 服务 | Prometheus | Kubernetes |
|---|---|---|---|
| 客户A | 服务1 | 0.216 | 0.2 |
| 服务2 | 0.137 | 0.2 | |
| 服务3 | 0.445 | 0.45 | |
| 服务4 | 0.165 | 0.2 | |
| 客户B | 服务1 | 0.139 | 0.2 |
| 服务2 | 0.0917 | 0.2 | |
| 服务3 | 0.5739 | 0.5 | |
| 服务4 | 0.0972 | 0.2 |
需要确认测量方式是否正确:是所用的Prometheus查询语句存在错误,还是从Kubernetes获取数值的方式有误,以及通过Prometheus正确测量CPU使用率百分比的方法。
偏差来自三个核心问题,没有一个是单一原因导致的:
1. 两边统计的时间口径完全不一致
你写的PromQL计算的是24小时跨度下、5分钟粒度的平均CPU使用率,而kubectl top pod返回的是命令执行瞬间的瞬时值(本质是metrics-server采集的最近15-30秒的平均使用率,具体值取决于metrics-server的采集间隔配置)。拿24小时长周期平均值和单点瞬时值对比,出现偏差是必然结果。你表格里大部分Prometheus值低于kubectl值,本质是服务大部分时间CPU负载较低,只是你执行kubectl命令时刚好碰到业务小高峰;客户B服务3的Prometheus值高于kubectl值,是长周期统计把业务高峰、发布期的高负载算进了平均值,而你执行命令时刚好处于低峰。
2. 你的PromQL存在逻辑错误,会直接导致计算不准
- 聚合顺序错误:你把
sum放在了avg_over_time内层,当Deployment发生滚动重启、扩缩容时,新旧Pod会有短暂的共存期,sum会把同时存在的多份Pod CPU、配额重复加总,平均后结果必然失真,这也是客户B服务3结果偏高的核心原因。 - 过滤条件不全:旧版本cAdvisor会保留已终止Pod的指标数小时,你用
pod=~"^$Deployment.*$"的宽松正则,会匹配到已删除的旧Pod、同名其他工作负载的Pod;同时没有过滤container_spec_cpu_quota<0的场景——未配置CPU limit的容器该指标值为-1,直接做除法会得到负数结果拉低均值。 - 指标选择稳定性差:手动计算
container_spec_cpu_quota/container_spec_cpu_period获取limit的方式,会受cgroup版本、内核版本、cAdvisor版本差异影响出现异常值,不如直接用kube-state-metrics提供的资源配额指标稳定。
3. kubectl侧取值逻辑也有误区
- 单位容易算错:
kubectl top pod返回的CPU单位是millicores(千分之一核),如果直接拿这个值除以以核为单位的limit(比如limit配1核对应1000m),单位不统一会直接算错结果。 - 统计范围容易漏:如果Pod里注入了sidecar(比如istio-proxy、日志采集代理),
kubectl top pod默认返回的是Pod内所有容器的CPU总和,如果你PromQL里没把sidecar的配额和使用率算进去,或者反过来过滤掉了sidecar的指标,两边统计范围不一致就会出偏差。
正确的PromQL写法
(1)24小时平均CPU使用率百分比(匹配你做资源优化的需求)
优先用kube-state-metrics的配额指标,稳定性更高:
( avg_over_time( sum( rate(container_cpu_usage_seconds_total{namespace=~"$namespace", pod=~"$Deployment-[a-z0-9]+-[a-z0-9]+", container!="POD", container!="", image!=""}[5m]) ) by (pod, container) [24h:5m] ) ) / ( avg_over_time( sum( kube_pod_container_resource_limits{resource="cpu", namespace=~"$namespace", pod=~"$Deployment-[a-z0-9]+-[a-z0-9]+"} ) by (pod, container) [24h:5m] ) ) * 100
如果集群没有部署kube-state-metrics,仍然用cAdvisor原生指标的话,把配额部分替换为如下内容即可,记得加quota大于0的过滤:
sum( container_spec_cpu_quota{namespace=~"$namespace", pod=~"$Deployment-[a-z0-9]+-[a-z0-9]+", container!="POD", container!="", container_spec_cpu_quota>0} / container_spec_cpu_period{namespace=~"$namespace", pod=~"$Deployment-[a-z0-9]+-[a-z0-9]+", container!="POD", container!=""} ) by (pod,container)
(2)和kubectl top做结果对齐的瞬时值查询
要和kubectl结果做校验,必须把时间窗口对齐到metrics-server的采集粒度(一般用1分钟窗口即可),去掉长周期平均逻辑:
sum(rate(container_cpu_usage_seconds_total{namespace=~"$namespace", pod=~"$Deployment-[a-z0-9]+-[a-z0-9]+", container!="POD", container!="", image!=""}[1m])) / sum(kube_pod_container_resource_limits{resource="cpu", namespace=~"$namespace", pod=~"$Deployment-[a-z0-9]+-[a-z0-9]+"}) * 100
该查询结果和kubectl top的误差会在5%以内,属于采集时间差带来的正常偏差。
校验注意事项
- 对比数据必须保证时间口径完全一致:长周期平均值只能和长周期平均值比,瞬时值只能和瞬时值比,跨粒度对比没有任何参考意义。
- 确认两边统计的容器范围一致:如果Pod存在sidecar,要么PromQL里指定业务容器名做过滤,要么kubectl top加
--containers参数把所有容器的CPU值加总后再计算,避免漏算、多算。 - Pod名匹配不要用宽松正则:Deployment生成的Pod名固定格式为
Deployment名-replicaset哈希-实例哈希,用$Deployment-[a-z0-9]+-[a-z0-9]+的正则可以精准匹配对应Pod,避免捞到已终止的旧Pod数据。
内容的提问来源于stack exchange,提问作者user6285790

