Prometheus CPU利用率指标原理与正确计算方法
1. 为什么“CPU利用率”在 Prometheus 生态里不是个简单数字刚接触 Prometheus 监控体系的新手常会卡在第一个指标上node_cpu_seconds_total。你查文档、看 Grafana 面板、甚至复制粘贴别人写的rate(node_cpu_seconds_total[5m])结果发现——它压根不等于你用top或htop看到的那个“87% CPU 使用率”。这不是配置错了也不是数据不准而是根本就不是同一个东西。我第一次部署 node_exporter 时也栽在这儿。当时给一台 4 核 Web 服务器配告警按直觉写了avg by (instance) (1 - avg by (instance, cpu) (rate(node_cpu_seconds_total{modeidle}[5m]))) 0.9结果凌晨三点告警炸了登录上去一看top显示 CPU 才 32%uptime负载才 1.2。折腾两小时才发现这个表达式算出来的是“所有 CPU 核心的加权平均忙时占比”而top默认显示的是“当前所有核心的瞬时综合负载率”两者统计口径、时间窗口、归一化方式全都不一样。这背后是 Linux 内核暴露指标的底层逻辑决定的node_cpu_seconds_total是一个累积计数器counter记录的是每个 CPU 模式user/system/idle/iowait 等自系统启动以来消耗的总秒数。它本身不带“百分比”属性更不带“实时性”。所谓“CPU 利用率”是你用 PromQL 对这个原始计数器做两次数学变换的结果先用rate()计算单位时间内的增量即每秒消耗多少秒再用1 - idle_rate做归一化。这个过程里rate()的时间窗口选 5m 还是 1msum by (instance)和avg by (instance)有什么区别without (cpu)是去掉哪个 label这些细节直接决定你看到的数字是接近运维直觉还是偏离真实业务压力。所以“CPU 利用率入门”真正的起点不是写第一条查询语句而是理解Prometheus 不提供现成的“利用率”它只提供最原始、最精确的内核时间片计数所有“百分比”都是你根据监控目标主动构造的视图。这就像给你一整套精密钟表零件而不是一块指针表盘——你得自己决定怎么组装、怎么校准、怎么读数。这也是为什么网上大量教程教你怎么画曲线、怎么配告警却很少讲清楚为什么100 * (1 - avg by (instance) (rate(node_cpu_seconds_total{modeidle}[5m])))这个经典公式在双路 32 核服务器上可能高估实际瓶颈在容器密集型节点上又可能严重低估——因为avg by (instance)把所有 CPU 核心的 idle 时间简单平均了而现实中一个核心跑满 100%其余 31 个空闲 99%平均 idle 率是 98.7%算出来利用率才 1.3%完全掩盖了真实的单核瓶颈。提示别急着抄公式。先打开 Prometheus 表达式浏览器输入node_cpu_seconds_total{jobnode, modeidle}观察它的值每秒增长多少。你会发现在空闲机器上它每秒涨约 1.0单核或 4.0四核在满载时它几乎不涨。这个“每秒涨多少”才是rate()真正在算的东西——它本质是“CPU 在 idle 模式下工作的秒数/总秒数”不是“CPU 忙碌的百分比”。2. node_exporter 的 CPU 指标从哪来不是/proc/stat而是/proc/stat 内核时钟很多人以为 node_exporter 的 CPU 数据就是直接读/proc/stat文件然后原样上报。这是个常见误解。实际上node_exporter 的node_cpu_seconds_total指标其数据源确实是/proc/stat但它的采集逻辑远比“读文件→转 Prometheus 格式”复杂得多关键在于如何解析/proc/stat中的多行 CPU 记录以及如何处理内核时钟漂移与计数器重置。我们来看/proc/stat的典型内容cpu 123456789 12345 67890 987654321 123456 0 12345 0 0 0 cpu0 12345678 1234 6789 98765432 12345 0 1234 0 0 0 cpu1 23456789 2345 7890 87654321 23456 0 2345 0 0 0 ...第一行cpu是所有 CPU 核心的累加值后面cpu0,cpu1... 是各核心独立值。每行有 10 个数字分别对应user, nice, system, idle, iowait, irq, softirq, steal, guest, guest_nice。注意idle字段包含 CPU 空闲时间但不包含内核中断处理等不可调度时间iowait是 CPU 等待 I/O 完成的时间但它不是 CPU 忙碌时间而是“可运行但无事可做”的等待时间——这点常被误读为“I/O 瓶颈”实则需结合iowait占比与磁盘队列深度综合判断。node_exporter 的采集器cpu_linux.go会逐行解析/proc/stat对每一行包括cpu和cpu*提取前 10 个字段转换为浮点数构建时间戳与差值逻辑它不会直接上报原始数值而是维护一个内存缓存记录上次采集时各字段的值。本次采集时计算每个字段的增量 delta当前值 - 上次值处理 counter 重置Linux 内核在某些极端情况如长时间运行后计数器溢出下可能重置/proc/stat数值。node_exporter 通过检测 delta 为负来识别重置并自动跳过该次增量避免rate()计算出巨大负值映射为 Prometheus 指标将每个cpu*行的 10 个字段分别生成node_cpu_seconds_total{modeuser, cpucpu0}、node_cpu_seconds_total{modeidle, cpucpu0}等指标。cpu空字符串对应第一行cpu的累加值。这里有个关键细节node_cpu_seconds_total的单位是秒seconds不是“ticks”或“jiffies”。node_exporter 在解析时会将/proc/stat的原始 jiffies通常 100Hz自动转换为秒。例如若某核心user字段在 1 秒内增加了 100 jiffies则上报node_cpu_seconds_total{modeuser, cpucpu0} 1.0。这个转换由procfs库完成确保了跨不同内核版本jiffies 频率可能不同的数据一致性。注意node_cpu_seconds_total的joblabel 来自 Prometheus 的 scrape 配置instancelabel 来自 target 的地址cpu和modelabel 则来自/proc/stat解析结果。这意味着如果你在 Prometheus 中执行sum by (instance) (rate(node_cpu_seconds_total{modeidle}[5m]))得到的是该实例所有 CPU 核心的 idle 时间总和单位秒/秒即“该服务器每秒平均有多少秒处于 idle 状态”。这个值理论上应在 0 到 CPU 总核数之间浮动。比如 4 核服务器空闲时约为 4.0满载时趋近于 0。实测验证方法在一台 2 核虚拟机上执行stress-ng --cpu 2 --timeout 60s满载 CPU同时用curl http://localhost:9100/metrics | grep node_cpu_seconds_total | grep idle观察cpu行的idle字段。你会发现满载期间该字段几乎停滞增长而user和system字段飞速上升——这正是rate()计算的基础。3. 构造“真正有用”的 CPU 利用率从rate()到1 - idle_rate的三步推导现在我们有了原始计数器node_cpu_seconds_total也理解了它的数据来源。下一步是把它变成运维人员能一眼看懂的“CPU 利用率”。但这里没有标准答案只有针对不同监控目标的合理构造。我见过太多人把100 * (1 - avg by (instance) (rate(node_cpu_seconds_total{modeidle}[5m])))当作万能公式结果在生产环境踩坑。下面拆解这个公式的完整推导链并指出每个环节的取舍逻辑。3.1 第一步为什么必须用rate()而不是increase()或直接相减node_cpu_seconds_total是 Counter 类型指标其值单调递增除重置外。要得到“单位时间内的变化量”必须用rate()或increase()。二者区别在于rate(v[5m])计算过去 5 分钟内该时间序列的平均每秒增长率。它采用线性插值拟合斜率对短时间突刺如 1 秒内 CPU 爆涨有平滑作用且能自动处理 counter 重置。increase(v[5m])计算过去 5 分钟内该时间序列的总增量单位原始值单位这里是秒。它本质是rate() * 300但结果是一个绝对值不是速率。对于 CPU 利用率我们需要的是“每秒有多少秒在忙”这是一个比率ratio单位是“秒/秒”即无量纲的 0~1 区间。rate()直接给出这个比率而increase()给出的是“5 分钟内总共忙了多少秒”需要再除以 300 才能归一化。因此rate()是更自然、更少出错的选择。实操对比在 Prometheus 表达式浏览器中分别输入rate(node_cpu_seconds_total{modeidle, instanceyour-server:9100}[1m])和increase(node_cpu_seconds_total{modeidle, instanceyour-server:9100}[1m]) / 60。你会发现两者数值几乎一致因 1m 窗口小插值影响小但前者更简洁、语义更清晰。3.2 第二步为什么用idle模式而不是usersystemiowait直观想法是CPU 利用率 (user system iowait irq softirq) / total_time。但total_time并非直接存在需用sum of all modes计算。而idle模式是唯一一个明确表示“CPU 可用但未被使用”的状态。因此更稳健的做法是利用率 1 - (idle_time / total_time)。total_time就是所有模式时间之和即sum(rate(node_cpu_seconds_total[5m])) by (instance, cpu)。但由于idle是其中最大项且其他模式如steal,guest在多数场景下占比极小1 - rate(idle)/rate(total)与rate(usersystemiowait)/rate(total)结果高度接近但前者计算更稳定避免多个小项累加引入浮点误差且语义更纯粹——它直接反映 CPU 的“空闲资源比例”。关键验证在top命令中%Cpu(s)行显示的ididle值正是内核通过相同逻辑计算得出的。你可以用cat /proc/stat手动计算取两组间隔 1 秒的快照计算idledelta 与totaldelta所有字段和的比值结果应与top的id值基本一致。3.3 第三步聚合策略——avg by (instance)vssum by (instance)vs1 - avg(idle_rate)这才是最容易出错的地方。假设你有一台 8 核服务器node_cpu_seconds_total{modeidle, cpucpu0}到cpu7共 8 条时间序列。avg by (instance) (rate(node_cpu_seconds_total{modeidle}[5m]))先对每个cpu*的idle_rate求平均。结果是一个标量代表“平均每个核心的 idle 比率”。例如若 4 个核心 idle 99%4 个核心 idle 1%则平均 idle 率为 50%利用率也是 50%。这掩盖了核心间的不均衡。sum by (instance) (rate(node_cpu_seconds_total{modeidle}[5m]))对每个cpu*的idle_rate求和。结果是“所有核心 idle 时间的总和”单位是“核·秒/秒”。对于 8 核机器空闲时该值约为 8.0满载时趋近于 0。要得到利用率需1 - sum_idle_rate / 8。这反映了整体资源池的占用率适合容量规划。1 - avg by (instance) (rate(node_cpu_seconds_total{modeidle}[5m]))这就是网上最常见的公式。它等价于avg by (instance) (1 - rate(idle)[5m])即“每个核心利用率的平均值”。它平衡了各核心负载适合快速感知整体健康度。我的经验是告警用1 - avg by (instance) (rate(...))容量分析用sum by (instance) (rate(...)) / count by (instance) (node_cpu_seconds_total{modeidle})。后者分母count by (instance) (...)动态获取该实例的 CPU 核心数通过统计cpulabel 的数量避免硬编码适配不同规格的机器。踩坑实录曾有一个 Kubernetes 集群Node 节点有的 4 核有的 16 核。用固定除数1 - sum(idle)/4监控 16 核节点导致利用率永远显示低于 25%告警失效。后来改用count by (instance) (node_cpu_seconds_total{modeidle})动态获取核数问题解决。4. Grafana 面板里的“CPU 利用率”曲线为什么和top差那么多当你终于写出正确的 PromQL 查询在 Grafana 里画出一条漂亮的 CPU 利用率曲线兴冲冲地跟同事说“看我们监控到位了”结果对方打开top一看“咦这会儿明明 95%你图上才 60%”——这种场景太常见了。这不是数据错误而是采样频率、时间窗口、统计粒度和可视化渲染的天然差异造成的。4.1 时间窗口差异5 分钟 vs 3 秒 vs 实时top默认每 3 秒刷新一次显示的是最近 3 秒内的平均利用率。它读取/proc/stat的两个快照计算 delta非常接近瞬时值。而 Prometheus 的rate(node_cpu_seconds_total[5m])计算的是过去 5 分钟的平均速率。如果 CPU 在这 5 分钟内有 1 分钟 100%、4 分钟 0%rate()结果是 20%而top在那 1 分钟内会持续显示 100%。这是平滑与锐利的根本矛盾。解决方案不是消灭差异而是理解并利用差异用rate(...[1m])替代[5m]让曲线更贴近top的响应速度但会增加噪声需配合 Grafana 的“Min/Max”模式或max_over_time()函数过滤毛刺在 Grafana 面板中为同一指标添加两条曲线一条[5m]用于趋势分析一条[1m]用于异常定位用不同颜色区分对于告警坚持用[5m]避免因瞬时抖动误报对于故障排查切到[1m]或[30s]。4.2 统计粒度差异核心级 vs 进程级 vs 全局级top默认显示的是所有 CPU 核心的综合利用率%Cpu(s)行但它同时列出每个进程的%CPU这个值是该进程在所有核心上消耗的 CPU 时间占总时间的比例。而 Prometheus 的node_cpu_seconds_total是内核级指标无法直接对应到某个进程。你想知道“nginx 占用了多少 CPU”必须用process_exporter或cAdvisor而非node_exporter。更隐蔽的差异是top的%CPU值对多线程进程如 Java 应用会显示为超过 100%如 210% 表示占用了 2.1 个核心。而 Prometheus 的1 - idle_rate永远在 0~100% 之间因为它统计的是硬件资源池的占用率不是进程的“虚拟 CPU 时间”。4.3 可视化渲染差异折线图插值 vs 离散点Grafana 的 Time Series 面板默认使用“Linear”插值将离散的 Prometheus 采样点如每 15 秒一个点连成平滑曲线。而top是离散刷新每次显示一个确定的数字。当 CPU 利用率在 10% 和 90% 之间剧烈跳变时Grafana 的曲线会画出一条“斜坡”给人“缓慢上升”的错觉而top则是“啪”一下从 10% 跳到 90%。解决方法在 Grafana 面板设置中将Draw style改为Line Steps让曲线呈现阶梯状更忠实反映采样点的真实值启用Staircase模式使每个数据点的值保持到下一个点到来前消除插值带来的误导对于关键告警指标禁用插值只显示原始采样点Points设置为Always。实操技巧在 Grafana 中创建一个Stat面板查询100 * (1 - avg by (instance) (rate(node_cpu_seconds_total{modeidle}[1m])))设置Reduce options为LastThresholds设为0, 70, 90。这样你就能在面板右上角看到一个实时的、类似top的大数字旁边标注红/黄/绿状态兼顾直观性与准确性。5. 告警规则配置别只盯“90%”先定义什么是“异常”很多团队的 CPU 告警规则长这样- alert: HighCPUUsage expr: 100 * (1 - avg by (instance) (rate(node_cpu_seconds_total{modeidle}[5m]))) 90 for: 5m labels: severity: warning annotations: summary: High CPU usage on {{ $labels.instance }}看似合理实则隐患重重。它假设“90% 就是问题”但忽略了一台数据库服务器常年 85% 是健康的一台静态文件 CDN 服务器 30% 就可能意味着恶意爬虫攻击。真正的告警应该回答“什么行为模式超出了该服务的历史基线或设计预期”5.1 基于历史基线的动态阈值Prometheus 自带predict_linear()和stddev_over_time()函数可构建自适应告警。例如# 告警CPU 利用率比过去 7 天同时间段均值高出 3 个标准差 - alert: CPUUsageAnomaly expr: | 100 * (1 - avg by (instance) (rate(node_cpu_seconds_total{modeidle}[5m]))) avg_over_time(100 * (1 - avg by (instance) (rate(node_cpu_seconds_total{modeidle}[5m])))[$__range]) 3 * stddev_over_time(100 * (1 - avg by (instance) (rate(node_cpu_seconds_total{modeidle}[5m])))[$__range]) for: 10m labels: severity: critical这里$__range是 Grafana 的变量代表当前面板的时间范围如7d。该规则不依赖固定数字而是检测“显著偏离常态”的行为对周期性业务如每日批处理尤其有效。5.2 基于业务 SLA 的复合条件CPU 高不代表服务受损。需结合请求成功率、延迟等 SLO 指标。一个更健壮的告警可能是# 告警CPU 高 请求失败率上升 P95 延迟恶化 - alert: ServiceDegradation expr: | ( 100 * (1 - avg by (instance) (rate(node_cpu_seconds_total{modeidle}[5m]))) 80 and sum by (instance) (rate(http_request_duration_seconds_count{code~5..}[5m])) / sum by (instance) (rate(http_request_duration_seconds_count[5m])) 0.01 and histogram_quantile(0.95, sum by (le, instance) (rate(http_request_duration_seconds_bucket[5m]))) (histogram_quantile(0.95, sum by (le, instance) (rate(http_request_duration_seconds_bucket[1h]))) * 1.5) ) for: 3m labels: severity: critical这条规则只有在 CPU 高的同时失败率超过 1% 且延迟恶化 50%才触发。它过滤掉了“CPU 高但服务正常”的场景如后台计算任务聚焦于真正影响用户的故障。5.3 避免“告警风暴”的关键实践分层告警设置warning70%、critical95%两级warning不通知只写入日志critical才触发 PagerDuty静默期Silence对已知的维护窗口如每周二凌晨数据库备份提前配置静默避免无效告警抑制规则Inhibition当HostDown告警触发时自动抑制所有基于该主机的CPUUsage告警防止雪崩告警摘要在annotations中加入runbook_url链接到内部 Wiki 的《CPU 高排查手册》包含top,pidstat,perf top等命令速查。我的血泪教训曾因未配置抑制规则在一次网络分区故障中30 台服务器同时触发HighCPUUsage导致值班工程师手机被 30 条短信轰炸错过真正的HostDown主告警。后来在 Alertmanager 配置中加入inhibit_rules: - source_match: alertname: HostDown target_match: alertname: HighCPUUsage equal: [instance]从此再没发生过告警风暴。6. 从入门到进阶三个必须掌握的 CPU 监控延伸场景掌握了基础的node_cpu_seconds_total和rate()你已经能应对大部分场景。但真正的监控价值体现在对复杂问题的穿透能力。以下是三个高频、高价值的延伸方向每个都附带可直接复用的 PromQL 和实操要点。6.1 场景一识别“伪高 CPU”——I/O 等待导致的iowait误判现象top显示 CPU 利用率 95%但htop查看各进程 CPU% 总和不到 20%iostat -x 1却显示%util100%。这是典型的 I/O 瓶颈CPU 在iowait状态等待磁盘node_cpu_seconds_total{modeiowait}值飙升但user和system很低。正确做法单独监控iowait占比并与idle对比# iowait 占比排除 idle聚焦等待时间 100 * rate(node_cpu_seconds_total{modeiowait}[5m]) / ( rate(node_cpu_seconds_total{modeuser}[5m]) rate(node_cpu_seconds_total{modesystem}[5m]) rate(node_cpu_seconds_total{modeiowait}[5m]) rate(node_cpu_seconds_total{modeirq}[5m]) rate(node_cpu_seconds_total{modesoftirq}[5m]) )当iowait占比 50% 且usersystem 20% 时基本可判定为 I/O 瓶颈此时应检查磁盘、网络存储或数据库连接池而非优化应用代码。实操提示iowait高不一定代表磁盘慢。可能是应用频繁小文件读写如日志轮转、或 NFS 挂载点网络延迟。用iotop -p $(pgrep -f your-app)定位具体进程的 I/O 行为比单纯看iowait更有效。6.2 场景二容器化环境下的 CPU 限制识别在 Kubernetes 中Pod 的 CPU limit 为500m0.5 核但node_cpu_seconds_total统计的是宿主机物理核心时间。一个 Pod 即使只用了 0.5 核其container_cpu_usage_seconds_total来自 cAdvisor可能显示 0.5而node_cpu_seconds_total的user增量可能只有 0.1因 CPU throttling。这时node_exporter的利用率会低估容器的实际压力。解决方案用container_cpu_usage_seconds_total替代node_cpu_seconds_total并结合container_spec_cpu_quota和container_spec_cpu_period计算 throttling# 容器 CPU Throttling 比例越接近 1 越严重 sum by (container, pod, namespace) ( rate(container_cpu_cfs_throttled_periods_total[5m]) ) / sum by (container, pod, namespace) ( rate(container_cpu_cfs_periods_total[5m]) )当该值 0.110%时说明容器因 CPU limit 被频繁限频应考虑调高 limit 或优化代码。6.3 场景三多租户环境下的 CPU 公平性分析在共享宿主机的环境中如云厂商的 ECS你需要确认自己的业务是否被邻居“偷走”了 CPU 时间steal模式node_cpu_seconds_total{modesteal}正是为此而生——它表示 CPU 时间被 Hypervisor “偷走”分配给了其他虚拟机。监控steal占比100 * rate(node_cpu_seconds_total{modesteal}[5m]) / sum by (instance) (rate(node_cpu_seconds_total[5m]))在 AWS EC2 或阿里云 ECS 上steal 5% 通常意味着宿主机过载应申请迁移实例或升级规格。而在裸金属服务器上steal应恒为 0若非零则说明系统被错误地虚拟化了。最后分享一个小技巧在 Prometheus 的 Targets 页面为node_exporterjob 添加一个label_names参数如--collector.textfile.directory/var/log/node-exporter/然后定期用脚本将lscpu、cat /proc/cpuinfo | grep model name | head -1的输出写入文本文件。这样你就能在 Prometheus 中直接查node_cpu_info{model_name~.*}无需登录每台机器确认 CPU 型号极大提升排障效率。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →