理解 node_cpu_seconds_total 与 CPU 利用率的计算本质
1. 这不是“又一个监控教程”而是你第一次真正看懂 CPU 利用率的起点如果你刚在公司内网看到同事贴出一张 Grafana 面板上面跳动着一条标着100%的红色曲线而运维大哥皱着眉说“CPU 跑满了查下 node_exporter 拉取的数据对不对”你心里大概率是懵的——这 100% 到底是哪个 CPU是单核跑满还是所有核加起来为什么top显示才 40%Prometheus 却报 95%node_cpu_seconds_total这个指标名里带“seconds”却算出百分比时间单位哪去了这些不是配置错误而是你还没跨过 Prometheus 监控体系里最基础、也最容易被跳过的那道门槛理解指标背后的物理意义与计算逻辑。本文不讲 Docker 怎么拉镜像、不教docker-compose.yml里怎么写volumes只聚焦一件事从零拆解node_exporter如何采集 CPU 数据、Prometheus 如何存储与计算、以及你看到的那个“CPU 利用率”数字究竟是怎么从 Linux 内核的/proc/stat文件里一层层变成你 Grafana 面板上那条曲线的。适合刚接触监控栈的 SRE、运维工程师、后端开发也适合那些已经配好环境但总在告警阈值设置上反复试错的中级使用者。你不需要会写 Go但得愿意花 15 分钟跟着我一起打开终端cat /proc/stat亲手算一遍那个“100%”。2. 核心设计思路为什么不用top或htop非要用node_exporter Prometheus2.1 本质区别采样方式决定数据可信度很多人第一次部署时会疑惑“我本地top不就看得清清楚楚吗为啥还要搞一套复杂的 exporter TSDB” 这问题问到了根子上。top是交互式进程查看器它默认每 3 秒刷新一次显示的是最近 3 秒内的平均负载且这个平均值是基于getrusage()和/proc/[pid]/stat等接口动态聚合的不保存历史。而node_exporter的设计哲学完全不同它是一个被动、无状态、只读的指标暴露器。它不“监控”只“快照”。每 15 秒可配置精确读取一次/proc/stat把原始计数器counter值原封不动地通过 HTTP 接口暴露出来。Prometheus 则以固定间隔如 15s主动抓取这些计数器并将它们持久化为时间序列。关键点在于/proc/stat里的cpu行记录的是自系统启动以来各个 CPU 模式所消耗的总 jiffies 数Linux 内核调度的基本时间单位通常 1 jiffy 10ms这是一个单调递增的整数。node_exporter暴露的node_cpu_seconds_total就是把这个 jiffies 数除以USER_HZ通常是 100换算成秒再按 CPU 模式user, system, idle, iowait 等拆分后的结果。所以它不是“利用率”而是一个绝对时间累加值。真正的“利用率”是 Prometheus 在查询时用两个时间点的差值除以时间窗口长度算出来的相对变化率。这个设计规避了top类工具最大的缺陷无法回溯、无法对比、无法做同比环比。你想知道“上周三下午 3 点 CPU idle 时间是否比前一周同期少了 15%”top给不出答案但 Prometheus 可以。2.2 架构选型为什么是node_exporter而不是自己写脚本有人会想“我写个 Python 脚本cat /proc/stat解析一下发到 InfluxDB 不就行了” 理论上可行但实践中会踩三个深坑。第一是精度漂移Python 的time.sleep(15)实际休眠时间受 GIL 和系统调度影响可能偏差 50-100ms而node_exporter是用 Go 写的goroutine 调度精准且其内部使用ticker机制能保证抓取间隔的抖动控制在毫秒级。第二是指标一致性/proc/stat里cpu行有 10 个字段user, nice, system, idle, iowait, irq, softirq, steal, guest, guest_nice不同内核版本字段顺序和含义有细微差别。node_exporter经过十年迭代内置了对 2.6.x 到 6.x 内核的全兼容解析逻辑比如它会自动识别guest字段是否应计入user避免因内核升级导致指标突变。第三是生态集成node_exporter暴露的是标准的 OpenMetrics 格式Prometheus 抓取时自带服务发现、目标健康检查、抓取失败重试、样本去重等能力。你手写的脚本要实现这些工作量远超预期。我见过最典型的反面案例某团队用 Bash 脚本curl http://localhost:9100/metrics结果因curl超时未设-m 5导致单次抓取卡住 30 秒Prometheus 认为该 target 失联连续触发告警。而node_exporter自身就是一个健壮的 HTTP server它的/metrics端点响应时间稳定在 2ms 以内这是经过千万级节点验证的工业级可靠性。2.3 CPU 利用率的“陷阱”没有一个单一的正确答案这是最常被忽略却最致命的认知偏差。Linux 系统根本没有一个叫“CPU 利用率”的官方定义。top显示的%Cpu(s)是100 - idle%即“非空闲时间占比”iostat -c默认显示的是100 - %idle但vmstat的ussy列加起来又不等于top的数值。为什么因为它们统计的“空闲”定义不同。top的idle包含了idle和iowait认为磁盘等待也是 CPU 无所事事的时间而有些性能分析场景iowait被视为一种“有效等待”CPU 正在为 I/O 做准备不应计入“浪费”。node_exporter的聪明之处在于它不替你做判断只提供原子事实。它把/proc/stat里的每个字段都暴露为独立指标node_cpu_seconds_total{modeuser}、node_cpu_seconds_total{modesystem}、node_cpu_seconds_total{modeidle}、node_cpu_seconds_total{modeiowait}…… 你可以根据业务需求自由组合。例如你的数据库服务器核心瓶颈是磁盘 I/O那么100 * (1 - avg by(instance) (rate(node_cpu_seconds_total{modeidle}[5m]) / rate(node_cpu_seconds_total[5m])))这个公式就把iowait当作了有效时间得到的是“真正被 CPU 计算占用”的比例而对 Web 应用服务器你更关心整体响应能力用100 * (1 - avg by(instance) (rate(node_cpu_seconds_total{modeidle}[5m]) / ignoring(mode) rate(node_cpu_seconds_total[5m])))把iowait算进空闲得到的是“用户请求能被及时处理”的能力指标。这种灵活性是任何预设好公式的监控工具都无法提供的。3. 核心细节解析从/proc/stat到 Grafana 面板的完整链路3.1/proc/stat的原始数据长什么样我们来亲手解析别急着开 Prometheus先回到源头。在任意一台 Linux 机器上执行cat /proc/stat | head -n 5你会看到类似这样的输出cpu 12345678 98765 456789 123456789 54321 12345 6789 0 0 0 cpu0 1234567 9876 45678 12345678 5432 1234 678 0 0 0 cpu1 1111111 8888 41111 11011111 4899 1111 600 0 0 0 intr 123456789 ... ctxt 987654321第一行cpu无数字后缀是所有 CPU 的累加值后面cpu0,cpu1是各逻辑 CPU 的单独值。我们聚焦cpu行12345678 98765 456789 123456789 54321 12345 6789 0 0 0。这 10 个数字依次代表user: 在用户态运行普通进程消耗的 jiffiesnice: 在用户态运行 niced低优先级进程消耗的 jiffiessystem: 在内核态运行系统进程消耗的 jiffiesidle: CPU 完全空闲未等待任何事件的 jiffiesiowait: CPU 空闲且正在等待 I/O 完成的 jiffiesirq: 处理硬中断消耗的 jiffiessoftirq: 处理软中断消耗的 jiffiessteal: 被其他虚拟机在虚拟化环境中偷走的 jiffiesguest: 运行虚拟 CPUguest OS消耗的 jiffies计入userguest_nice: 运行 niced guest OS 消耗的 jiffies计入nice注意guest和guest_nice是为了兼容虚拟化而添加的node_exporter默认会将它们从user和nice中减去避免重复计算。这就是为什么你在 Prometheus 里看到的node_cpu_seconds_total{modeuser}的值会略小于直接用cat /proc/stat解析出的user字段值。这个细节决定了你后续计算的准确性。我曾经在一个 KVM 虚拟机上因为没注意到guest字段的存在用原始user值计算利用率结果发现数值比top高出 8%排查了两天才发现是node_exporter的自动修正逻辑在起作用。3.2node_exporter的指标映射如何把 jiffies 变成 secondsnode_exporter的核心工作就是把上面这些 jiffies 数字转换成人类可读的seconds。它的转换公式非常简单seconds jiffies / USER_HZ。USER_HZ是一个编译时确定的常量对于 x86_64 架构默认是 100。这意味着 1 个 jiffy 10ms。所以如果user字段是12345678那么node_cpu_seconds_total{modeuser}的值就是123456.78。这个转换发生在node_exporter的 Go 代码里位于collector/cpu_linux.go文件中。它读取/proc/stat后对每一行进行strconv.ParseUint解析然后除以userHZ硬编码为 100。这里有个重要提示node_exporter不会对idle和iowait做任何特殊处理它只是忠实地转换。所以node_cpu_seconds_total{modeidle}就是/proc/stat里第 4 个数字除以 100 的结果。这个“忠实”是优点也是责任——它要求你必须理解idle时间长不代表系统健康如果iowait也同时很高那说明 CPU 在傻等磁盘这才是真正的瓶颈。我在一家电商公司做压测时就遇到过idle高达 95%但iowait也高达 90% 的诡异情况最后定位到是 NFS 存储的元数据锁争用idle高是假象iowait高才是真相。3.3 Prometheus 的抓取与存储计数器Counter的魔力与诅咒当你在 Prometheus 的targets页面看到node_exporter的状态是UP意味着 Prometheus 成功抓取到了http://node-ip:9100/metrics的响应。这个响应体里会有成百上千行指标其中关于 CPU 的核心是# HELP node_cpu_seconds_total Seconds the cpus spent in each mode. # TYPE node_cpu_seconds_total counter node_cpu_seconds_total{cpucpu0,modeidle} 123456789.0 node_cpu_seconds_total{cpucpu0,modeiowait} 54321.0 node_cpu_seconds_total{cpucpu0,modesystem} 456789.0 ... node_cpu_seconds_total{cpucpu-total,modeidle} 1234567890.0注意TYPE是counter。这是 Prometheus 最重要的数据类型之一它的特点是只增不减且代表一个累积总量。node_cpu_seconds_total不是“当前 CPU 使用了多少秒”而是“从系统启动到现在CPU 在idle模式下总共运行了多少秒”。这个设计带来了两个关键后果。第一是重置处理当 Linux 系统重启或者node_exporter进程崩溃重启/proc/stat的计数器会归零node_cpu_seconds_total的值也会从一个大数突然跳回接近 0。Prometheus 的rate()函数正是为了解决这个问题而生。rate(node_cpu_seconds_total{modeidle}[5m])的意思是“计算过去 5 分钟内idle时间的平均每秒增长速率”。它会自动检测并忽略掉那些因重置造成的负数跳跃只取上升沿的有效增量。第二是时间窗口选择[5m]这个区间不能随便写。太短如[30s]会放大瞬时抖动一条毛刺线毫无参考价值太长如[1h]会抹平真实的峰值让你错过 2 分钟的 CPU 飙升。经验法则是窗口长度至少是抓取间隔的 4 倍。如果你的scrape_interval是 15s那么[1m]是底线[5m]是推荐值。我曾在一个金融交易系统里把窗口设成了[30s]结果 Grafana 面板上全是锯齿状的尖峰运维同学以为是高频交易导致的 CPU 波动花了半天排查应用代码最后发现只是rate()计算过于敏感。3.4 Grafana 查询从rate()到最终百分比的数学推导现在我们终于来到最关键的一步如何写出那个能正确显示“CPU 利用率”的 PromQL 查询记住Prometheus 本身不提供“利用率”这个指标它只提供rate()计算出的“每秒消耗的秒数”也就是一个无量纲的比率。rate(node_cpu_seconds_total{modeidle}[5m])的单位是seconds/second即 1。所以100 * (1 - rate(node_cpu_seconds_total{modeidle}[5m]) / rate(node_cpu_seconds_total[5m]))这个表达式分子分母的seconds/second单位约掉了剩下纯数字再乘以 100就是百分比。但这里有个极易被忽略的陷阱分母rate(node_cpu_seconds_total[5m])。node_cpu_seconds_total是一个带modelabel 的指标当你不指定mode时Prometheus 会尝试匹配所有mode的时间序列然后对它们求和。但rate()函数要求输入的时间序列必须有完全相同的 label 集合。node_cpu_seconds_total{modeidle}和node_cpu_seconds_total{modeuser}的 label 集合不同前者有modeidle后者有modeuser所以rate(node_cpu_seconds_total[5m])会返回空。正确的写法是ignoring(mode) rate(node_cpu_seconds_total[5m])。ignoring(mode)告诉 Prometheus“在计算rate时忽略mode这个 label把所有模式的值加起来再算速率”。这才是总 CPU 时间的正确速率。完整的、生产环境推荐的查询是100 * (1 - avg by(instance) ( rate(node_cpu_seconds_total{modeidle}[5m]) ) / avg by(instance) ( ignoring(mode) rate(node_cpu_seconds_total[5m]) ) )这个查询做了三件事第一用rate(...[5m])计算每个mode的每秒速率第二用avg by(instance)对同一台机器的所有 CPU 核心cpu0,cpu1...取平均得到该实例的平均idle速率和平均总速率第三相除再100 * (1 - ...)得到利用率。为什么用avg by(instance)而不是sum因为sum会把 32 核 CPU 的idle速率加起来得到一个巨大的数再除以总速率结果还是 1失去了“单核平均”的意义。avg才是反映“平均每个 CPU 核心的空闲程度”的正确操作。这个细节决定了你看到的数字是“系统级平均”还是“核级平均”直接影响容量规划的准确性。4. 实操过程从零部署、验证、调优的全流程详解4.1 最小化部署三行命令搞定node_exporter与 Prometheus别被网上那些几十行的docker-compose.yml吓到。一个真正能跑起来、用于学习的最小环境只需要三步。首先下载并启动node_exporter以 Linux x86_64 为例# 下载最新版截至2024年v1.6.1 是稳定版 wget https://github.com/prometheus/node_exporter/releases/download/v1.6.1/node_exporter-1.6.1.linux-amd64.tar.gz tar xvfz node_exporter-1.6.1.linux-amd64.tar.gz cd node_exporter-1.6.1.linux-amd64 # 启动监听 9100 端口--no-collector.wifi 禁用无线网卡采集避免权限问题 ./node_exporter --no-collector.wifi 此时访问http://localhost:9100/metrics你应该能看到数千行指标文本搜索node_cpu_seconds_total确认它存在且有数值。第二步下载 Prometheuswget https://github.com/prometheus/prometheus/releases/download/v2.47.2/prometheus-2.47.2.linux-amd64.tar.gz tar xvfz prometheus-2.47.2.linux-amd64.tar.gz cd prometheus-2.47.2.linux-amd64第三步创建最简prometheus.yml配置文件global: scrape_interval: 15s evaluation_interval: 15s scrape_configs: - job_name: node static_configs: - targets: [localhost:9100]启动 Prometheus./prometheus --config.fileprometheus.yml --web.listen-address:9090 访问http://localhost:9090/targets看到nodejob 状态为UP表示抓取成功。这是最干净的“裸金属”部署没有 Docker、没有 systemd、没有 TLS纯粹为了理解数据流。很多初学者卡在第一步是因为node_exporter默认需要CAP_NET_BIND_SERVICE权限才能绑定 9100 端口而普通用户没有。解决方案有两个一是用sudo ./node_exporter不推荐安全风险二是用--web.listen-address:9101换一个大于 1024 的端口推荐。我建议始终用非特权端口养成安全习惯。4.2 数据验证用curl和Prometheus UI交叉验证部署完别急着开 Grafana。先用最原始的方式验证数据是否正确。打开终端执行# 获取当前 node_cpu_seconds_total{modeidle} 的值 curl -s http://localhost:9090/api/v1/query?querynode_cpu_seconds_total{mode\idle\} | jq .data.result[0].value[1] # 假设返回 123456789.0 # 等待 15 秒后再执行一次 curl -s http://localhost:9090/api/v1/query?querynode_cpu_seconds_total{mode\idle\} | jq .data.result[0].value[1] # 假设返回 123456804.0 # 差值是 15.0说明 15 秒内 idle 时间增加了 15 秒即 CPU 完全空闲了这 15 秒利用率是 0%这个手动计算是建立信任的基石。接着用 Prometheus 自带的 Graph 功能输入查询rate(node_cpu_seconds_total{modeidle}[15s])你应该看到一条接近1.0的水平线因为 15 秒内 idle 了 15 秒速率是 1 秒/秒。再输入100 * (1 - rate(node_cpu_seconds_total{modeidle}[15s]) / ignoring(mode) rate(node_cpu_seconds_total[15s]))如果一切正常应该显示0。然后你可以在另一台终端里用stress-ng --cpu 4 --timeout 60s命令需apt install stress-ng给 CPU 加压再观察这个查询的值是否飙升到 95% 以上。这种“手动加压 - 观察变化”的闭环验证比任何文档都管用。我带新人时一定会让他们亲手做这三遍空闲时看 0%加压时看 100%再killall stress-ng后看回落直到他们能预测出数值变化才算真正入门。4.3 Grafana 面板配置不只是拖拽更要理解每个选项的含义假设你已经安装了 Grafanadocker run -d -p 3000:3000 grafana/grafana-enterprise添加 Prometheus 数据源后创建新 Dashboard。添加一个 Time series 面板Query 设置如下Query A:100 * (1 - avg by(instance) (rate(node_cpu_seconds_total{modeidle}[5m])) / avg by(instance) (ignoring(mode) rate(node_cpu_seconds_total[5m])))Legend:{{instance}} - CPU UsageUnit:percent (0-100)Min:0Max:100这里的关键是Unit和Min/Max。Unit设为percent (0-100)Grafana 会自动在 Y 轴上显示%符号并按百分比刻度渲染。如果不设它会显示0.95你需要手动乘以 100非常容易出错。Min/Max锁定坐标轴范围避免因某个异常峰值如 200%导致整个图表压缩看不清正常波动。另一个常被忽视的选项是Repeat options。如果你的node_exporter部署在多台机器上开启Repeat选择instanceGrafana 会为每一台机器自动生成一个独立的子面板而不是把所有数据画在同一张图上互相遮挡。这比手动复制粘贴 Query 高效十倍。我管理着 200 台服务器所有 CPU 面板都用Repeat一眼就能扫出哪台机器在告警边缘。4.4 告警规则配置从“看到”到“知道”的最后一公里监控的价值不在于图表有多漂亮而在于能否在问题发生前预警。在 Prometheus 的prometheus.yml里添加rule_filesrule_files: - alerts.yml然后创建alerts.ymlgroups: - name: cpu-alerts rules: - alert: HighCPUUsage expr: 100 * (1 - avg by(instance) (rate(node_cpu_seconds_total{modeidle}[5m])) / avg by(instance) (ignoring(mode) rate(node_cpu_seconds_total[5m]))) 80 for: 10m labels: severity: warning annotations: summary: High CPU usage on {{ $labels.instance }} description: CPU usage is above 80% for more than 10 minutes.关键参数解释expr: 就是前面验证过的查询阈值设为80。for: 10m: 这个条件必须持续满足 10 分钟才真正触发告警。这是防止毛刺误报的核心。for不是“延迟发送”而是“持续观察”。如果 CPU 在 85% 波动了 9 分钟然后降到 75%告警永远不会发出。labels:severity是告警级别Grafana Alerting 或 Alertmanager 会据此路由。annotations:summary是标题description是详情会出现在邮件、钉钉、企业微信的通知里。部署后用stress-ng模拟高负载你会看到 Prometheus 的Alerts页面里HighCPUUsage状态从inactive变成pending已满足expr但未满for时间再变成firing已满for时间。这个状态流转是你理解告警生命周期的最好教材。我见过太多人把for设为1m结果每天收到上百条“CPU 瞬时飙高”告警最后全部被静音监控形同虚设。for的时间必须根据你的业务 SLA 来定对实时交易系统for: 2m可能合理对后台批处理任务for: 30m更合适。5. 常见问题与排查技巧实录那些文档里不会写的“血泪教训”5.1 问题速查表5 分钟定位绝大多数 CPU 监控异常现象可能原因快速验证命令解决方案node_cpu_seconds_total完全不出现node_exporter未运行或防火墙拦截 9100 端口curl -v http://localhost:9100/metrics | head -n 5检查node_exporter进程ss -tuln | grep 9100rate(...[5m])返回空值scrape_interval大于5m或node_exporter抓取失败curl -s http://localhost:9090/api/v1/targets | jq .data.activeTargets[] | select(.healthdown)确保scrape_interval 5m检查targets页面的Last Scrape时间CPU 利用率长期显示0%idle速率计算错误分母ignoring(mode) rate(...)缺失在 Prometheus UI 输入ignoring(mode) rate(node_cpu_seconds_total[5m])看是否有值严格按本文 3.4 节的完整查询书写不可省略ignoring(mode)top显示 30%Prometheus 显示 95%top默认显示的是100 - idle%但idle不包含iowaitPrometheus 计算包含了iowaitiostat -c 1 3看%iowait是否很高根据业务需求改用100 * (1 - rate(node_cpu_seconds_total{mode~idle|iowait}[5m]) / ignoring(mode) rate(node_cpu_seconds_total[5m]))多核 CPU 显示的利用率超过100%查询中用了sum而非avg把 32 核的 idle 速率加起来了sum by(instance) (rate(node_cpu_seconds_total{modeidle}[5m]))vsavg by(instance) (...)坚决使用avg by(instance)这是唯一正确的聚合方式这张表是我过去三年在客户现场处理上百次监控故障后提炼出的最高频、最有效的排查路径。它不讲原理只给最直接的命令和动作帮你把平均故障定位时间MTTR从 30 分钟缩短到 5 分钟。5.2 “CPU 利用率”之外你必须关注的 3 个关联指标仅仅盯着一个“CPU 利用率”百分比就像只看汽车仪表盘上的“油量”却不管发动机温度、转速、变速箱油压。以下是与 CPU 紧密耦合、必须一起看的黄金三指标1.node_load11 分钟平均负载node_load1是/proc/loadavg的第一个数字它表示过去 1 分钟内处于RRunning或DUninterruptible Sleep通常是 I/O 等待状态的进程平均数量。它和 CPU 利用率的关系是load1 ≈ CPU 利用率% / 100 * 逻辑 CPU 核心数。例如一台 8 核机器load18通常意味着 CPU 已饱和。但如果load18而CPU 利用率40%那几乎可以断定是D状态进程I/O 等待占了大头CPU 其实很空闲瓶颈在磁盘或网络。我的排查口诀是“看load1知瓶颈类型看CPU%知计算占用”。2.node_context_switches_total上下文切换次数这个指标记录了系统每秒发生的上下文切换Context Switch次数。正常值在几百到几千次/秒。如果它突然飙升到 50,000/秒而CPU 利用率并不高那说明系统正被大量短生命周期的进程或线程“折磨”比如一个 Bug 导致的无限循环创建 goroutine。perf top或pidstat -w 1可以帮你定位是哪个进程在疯狂切换。3.node_intr_total中断次数/proc/stat里的intr行记录了系统自启动以来处理的硬件中断总数。rate(node_intr_total[5m])可以看出每秒中断频率。网络设备网卡是中断大户。如果node_intr_total的速率和网络流量node_network_receive_bytes_total的速率同步飙升那很可能是网卡中断风暴需要考虑开启RPSReceive Packet Steering或更换支持RSSReceive Side Scaling的网卡。我曾在一个 CDN 边缘节点上发现intr速率高达 200,000/sCPU利用率只有 30%但sys时间占比 90%最终通过ethtool -l eth0发现网卡队列数只有 1调整为 8 后intr降到了 25,000/ssys时间降到 15%。5.3 实操心得那些让老手也拍大腿的“小技巧”技巧一用irate()替代rate()查看瞬时峰值rate()是为长期趋势设计的它会平滑掉尖峰。如果你想捕捉一个持续 1 秒的 CPU 爆发比如 GC STW用irate(node_cpu_seconds_total{modeidle}[1m])。irate()只取窗口内最后两个样本点计算对瞬时变化极其敏感。但它不稳定不能用于告警只适合调试。技巧二modelabel 的终极过滤法node_cpu_seconds_total有 10 个mode你永远记不全。在 Prometheus UI 的 Query 输入框输入node_cpu_seconds_total然后按CtrlSpaceMac 是CmdSpace它会自动弹出所有可用的mode值供你选择。这是最高效的学习方式。技巧三node_exporter的--collector.disable-defaults是性能利器默认情况下node_exporter会启用所有 collector磁盘、内存、网络、CPU 等。如果你只关心 CPU启动时加上--collector.disable-defaults --collector.cpu它会
上一篇/下一篇内容由系统自动关联
返回资讯列表 →