【前情提要】
Prometheus 装好了,up 指标也会看了。老周说:“现在你能看到数据了,但 PromQL 才是你真正要学的。没有 PromQL,你只能看 up。”小林打开 Prometheus 查询框,准备学 PromQL。但他不知道,rate()、sum by()、avg by(),每一个函数都有讲究。
一、老周的“灵魂三问”
早上到工位,小林打开 Prometheus 的 Web UI,在查询框里敲了 up,看到两个 1,心满意足。
老周走过来,看了一眼:“就这?”
“我会查 up 了。”
“那我现在问你三个问题。”老周说,“第一,现在 CPU 使用率是多少?第二,现在内存还剩多少?第三,磁盘用了百分之几?”
小林盯着查询框,愣住了。
“up 只告诉你目标在不在,不告诉你机器健不健康。”老周说,“你要回答我刚才的问题,得学 PromQL。”
“PromQL?”
“Prometheus Query Language,Prometheus 的查询语言。”老周说,“你把它理解成‘跟 Prometheus 对话的语法’。你问它问题,它给你答案。监控的核心不是装软件,是能从数据里问出你想知道的东西。”
小林打开笔记本,准备记。
二、先认识指标:node_exporter 给了你什么
“学 PromQL 之前,你得先知道有哪些指标可以查。”老周说,“你在查询框里敲 node_,看看。”
小林敲了 node_,下拉列表弹出一长串:
node_boot_time_seconds
node_context_switches_total
node_cpu_seconds_total
node_disk_io_time_seconds_total
node_disk_read_bytes_total
node_disk_reads_completed_total
node_disk_written_bytes_total
node_filesystem_avail_bytes
node_filesystem_free_bytes
node_filesystem_size_bytes
node_load1
node_load5
node_load15
node_memory_Buffers_bytes
node_memory_Cached_bytes
node_memory_MemAvailable_bytes
node_memory_MemFree_bytes
node_memory_MemTotal_bytes
node_network_receive_bytes_total
node_network_transmit_bytes_total
...
“好几十个。”小林说。
“你不用全记。”老周说,“你记住这几类就行:”
- CPU:
node_cpu_seconds_total - 内存:
node_memory_MemTotal_bytes、node_memory_MemAvailable_bytes - 磁盘:
node_filesystem_size_bytes、node_filesystem_avail_bytes - 网络:
node_network_receive_bytes_total、node_network_transmit_bytes_total - 负载:
node_load1、node_load5、node_load15
“这几类,够你入门用了。”老周说,“现在开始,一个一个查。”
三、第一个 PromQL:查内存使用率
“先查内存。”老周说,“你敲 node_memory_MemTotal_bytes。”
小林敲进去,点 Execute。
结果:
node_memory_MemTotal_bytes{instance="localhost:9100", job="node"} 4102262784
“4.1 GB。”小林说,“我虚拟机给了 4G 内存。”
“对。”老周说,“再查 node_memory_MemAvailable_bytes。”
node_memory_MemAvailable_bytes{instance="localhost:9100", job="node"} 2567890123
“2.5 GB 可用。”小林说。
“现在,算内存使用率。”
小林想了想:“1 - MemAvailable / MemTotal?”
“对。”老周说,“但 PromQL 不能直接写 1 - MemAvailable / MemTotal,你得写完整的指标名。”
小林敲:
1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes
结果:
{instance="localhost:9100", job="node"} 0.3743
“0.374,就是 37.4%。”老周说,“这是内存使用率。你把这个查询保存下来,以后 Grafana 里就用它。”
“那为什么不用 MemFree?”
“我上次说过。”老周说,“MemFree 是‘完全没被用的内存’,Linux 会把大量内存用作缓存,所以 MemFree 通常很小。但缓存可以随时释放,所以可用内存要用 MemAvailable。MemAvailable / MemTotal 才是真实的内存使用率。”
小林记下:
# 内存使用率
1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes
四、rate():算 CPU 使用率
“内存搞定了,现在 CPU。”老周说,“CPU 比内存复杂一点。”
“为啥?”
“因为 node_cpu_seconds_total 是 Counter,只增不减。你直接看它的值,是‘累计用了多少秒’,不是‘现在用了多少’。你要算的是‘每秒增长了多少’,也就是速率。”
“那怎么算速率?”
“rate() 函数。”老周说,“rate(指标[时间范围]),表示‘在时间范围内,每秒的平均增长率’。”
小林敲:
rate(node_cpu_seconds_total[1m])
结果出来一长串,每个核心、每个模式都有一行:
{cpu="0", instance="localhost:9100", job="node", mode="idle"} 0.98
{cpu="0", instance="localhost:9100", job="node", mode="system"} 0.01
{cpu="0", instance="localhost:9100", job="node", mode="user"} 0.01
...
“看到了吗?”老周说,“mode="idle" 是 0.98,说明 CPU 有 98% 的时间在空闲。mode="system" 和 mode="user" 各 0.01,说明系统态和用户态各占 1%。”
“那怎么算 CPU 使用率?”
“1 - idle。”老周说,“但你要把所有核心的 idle 加起来平均。”
小林敲:
1 - avg(rate(node_cpu_seconds_total{mode="idle"}[1m]))
结果:
{} 0.0234
“0.0234,就是 2.34%。”老周说,“这是所有核心的平均 CPU 使用率。”
“那 avg 是啥?”
“聚合函数。”老周说,“avg 表示平均值。你还可以用 sum、max、min、count。avg(rate(...)) 表示把所有核心的速率取平均。”
“那 {} 是啥?为啥没有标签?”
“因为你用了 avg,它把所有标签都聚合掉了。结果没有 instance、cpu 这些标签,只剩一个空标签集。”老周说,“如果你想保留 instance,用 avg by(instance)。”
1 - avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[1m]))
结果:
{instance="localhost:9100"} 0.0234
“现在有 instance 标签了。”老周说,“你以后监控多台机器,by(instance) 就能区分每台机器。”
小林记下:
# CPU 使用率(所有核心平均)
1 - avg(rate(node_cpu_seconds_total{mode="idle"}[1m]))
# 按实例分组
1 - avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[1m]))
“注意,”老周说,“rate() 的时间范围,至少要是采集间隔的两倍。你采集间隔是 15 秒,rate() 最少用 [30s]。但一般用 [1m] 或 [5m] 更稳,数据更平滑。”
五、sum by():算磁盘使用率
“现在磁盘。”老周说,“你先查 node_filesystem_size_bytes。”
小林敲:
node_filesystem_size_bytes
结果:
node_filesystem_size_bytes{device="/dev/sda1", fstype="ext4", instance="localhost:9100", job="node", mountpoint="/"} 5.0e+10
node_filesystem_size_bytes{device="/dev/sda2", fstype="ext4", instance="localhost:9100", job="node", mountpoint="/boot"} 1.0e+09
...
“有好几行。”小林说,“mountpoint 不同。”
“对。”老周说,“/ 是根分区,/boot 是启动分区。你一般只关心根分区。”
小林敲:
node_filesystem_avail_bytes{mountpoint="/"}
结果:
node_filesystem_avail_bytes{device="/dev/sda1", fstype="ext4", instance="localhost:9100", job="node", mountpoint="/"} 3.0e+10
“现在算使用率。”
1 - node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}
结果:
{device="/dev/sda1", fstype="ext4", instance="localhost:9100", job="node", mountpoint="/"} 0.4
“40% 使用率。”小林说。
“对。”老周说,“但如果你有多个挂载点,想算总使用率,可以用 sum by()。”
1 - sum by(instance) (node_filesystem_avail_bytes) / sum by(instance) (node_filesystem_size_bytes)
“sum by(instance) 表示按 instance 分组求和。把所有分区的可用空间加起来,除以所有分区的总空间,就是总使用率。”
“那 sum 和 avg 有啥区别?”
“sum 是求和,avg 是求平均。”老周说,“CPU 用 avg,因为每个核心是平等的。磁盘用 sum,因为每个分区的空间可以加总。你选哪个,取决于物理意义。”
小林记下:
# 根分区使用率
1 - node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}
# 所有分区总使用率
1 - sum by(instance) (node_filesystem_avail_bytes) / sum by(instance) (node_filesystem_size_bytes)
六、by 和 without:分组和排除
“刚才你用了 by(instance)。”小林说,“有没有别的写法?”
“有。”老周说,“by 是‘保留哪些标签’,without 是‘排除哪些标签’。”
“举个例子。”
avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[1m]))
“这个只保留 instance 标签,其他 cpu、mode、job 都聚合掉。”
avg without(cpu, mode) (rate(node_cpu_seconds_total{mode="idle"}[1m]))
“这个排除 cpu 和 mode,保留 instance 和 job。效果和上面差不多,但保留了 job。”
“那用哪个?”
“by 更直观,你一眼看出结果里有什么标签。without 适合标签很多、你只想排除一两个的情况。”老周说,“你入门阶段,先用 by,等熟练了再用 without。”
七、topk():查 Top N
“现在查一个更有意思的。”老周说,“你想知道哪个 CPU 核心最忙,怎么看?”
“topk()?”
“对。”老周说,“topk(3, 指标) 表示取前 3 个最大值。”
小林敲:
topk(3, rate(node_cpu_seconds_total{mode="idle"}[1m]))
结果:
{cpu="0", instance="localhost:9100", job="node", mode="idle"} 0.98
{cpu="1", instance="localhost:9100", job="node", mode="idle"} 0.97
{cpu="2", instance="localhost:9100", job="node", mode="idle"} 0.96
“等等,这是 idle 最高的,不是最忙的。”小林说。
“对。”老周说,“topk 取最大值。idle 最高,说明最闲。你想找最忙的,得用 bottomk,或者把 idle 取反。”
topk(3, 1 - rate(node_cpu_seconds_total{mode="idle"}[1m]))
“这样取的是使用率最高的 3 个核心。”
“那 bottomk 呢?”
“bottomk(3, 指标) 取最小值。”老周说,“你以后查‘哪台机器内存最少’,就用 bottomk。”
小林记下:
topk(N, 指标):取前 N 个最大值bottomk(N, 指标):取前 N 个最小值
八、常用 PromQL 速查
老周拿过小林的笔记本,写了一个速查表。
# 目标是否在线(1 在线,0 离线)
up
# CPU 使用率(所有核心平均)
1 - avg(rate(node_cpu_seconds_total{mode="idle"}[1m]))
# CPU 使用率(按实例)
1 - avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[1m]))
# 内存使用率
1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes
# 根分区使用率
1 - node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}
# 所有分区总使用率
1 - sum by(instance) (node_filesystem_avail_bytes) / sum by(instance) (node_filesystem_size_bytes)
# 网络接收速率(每秒字节)
rate(node_network_receive_bytes_total[1m])
# 网络发送速率(每秒字节)
rate(node_network_transmit_bytes_total[1m])
# 系统负载
node_load1
# Top 3 CPU 使用率
topk(3, 1 - rate(node_cpu_seconds_total{mode="idle"}[1m]))
# 内存最少的 3 个实例
bottomk(3, node_memory_MemAvailable_bytes)
“这十几条,你背下来。”老周说,“以后配 Grafana 和告警,80% 的场景都用这些。”
小林把速查表抄了一遍。
九、排错挑战:查询结果为空
下午,小林想查一下磁盘使用率,敲:
1 - node_filesystem_avail_bytes / node_filesystem_size_bytes
结果:
Empty query result
“啥?”小林又敲了一遍。还是空。
他检查指标:
node_filesystem_avail_bytes
有数据。
node_filesystem_size_bytes
也有数据。
“都有数据,为啥一除就空?”
他喊来老周。
老周看了一眼:“你 node_filesystem_avail_bytes 有多少行?”
“好几行,不同 mountpoint。”
“node_filesystem_size_bytes 呢?”
“也是好几行。”
“问题就在这。”老周说,“PromQL 的除法,是按标签完全匹配的。node_filesystem_avail_bytes 的标签是 {device="/dev/sda1", fstype="ext4", mountpoint="/"},node_filesystem_size_bytes 的标签也是这一套。但如果某个 mountpoint 只有 avail 没有 size,或者反过来,除法的结果就是空。”
“那怎么办?”
“加筛选条件,让两边标签完全一致。”
1 - node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}
结果出来了。
“或者,你先查一边的标签,再查另一边,对比一下。”老周说,“但最简单的办法是加 mountpoint 过滤,只看你关心的分区。”
排错思路总结:
- PromQL 查询结果为空,先分别查左右两边的指标,确认都有数据。
- 检查两边的标签是否完全一致(
instance、job、device、mountpoint)。 - 如果标签不一致,加过滤条件,让两边标签匹配。
- 常见原因:一边有数据,另一边没有;或者标签值不同。
- 用
count by(...)统计两边的标签组合,对比差异。
“记住,”老周说,“PromQL 的运算,是按标签匹配的。标签不一致,结果就是空。 你以后遇到空结果,先查标签。”
小林在笔记本上写下:PromQL 空结果 = 标签不匹配。
十、练习
- 练习一:在 Prometheus 查询框里,分别查询
node_memory_MemTotal_bytes和node_memory_MemAvailable_bytes,然后用 PromQL 算出内存使用率。把结果截图或记录数值。 - 练习二:用 PromQL 查询 CPU 使用率(所有核心平均),以及按实例分组的 CPU 使用率。然后查
topk(3, 1 - rate(node_cpu_seconds_total{mode="idle"}[1m])),看哪 3 个核心最忙。 - 练习三:用 PromQL 查询根分区使用率。然后查询所有分区的总使用率(用
sum by(instance))。最后查询node_load1,记录当前系统负载。
参考答案
练习一:
node_memory_MemTotal_bytes
# 输出示例:node_memory_MemTotal_bytes{...} 4102262784
node_memory_MemAvailable_bytes
# 输出示例:node_memory_MemAvailable_bytes{...} 2567890123
1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes
# 输出示例:{...} 0.3743
练习二:
# CPU 使用率(所有核心平均)
1 - avg(rate(node_cpu_seconds_total{mode="idle"}[1m]))
# 按实例分组
1 - avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[1m]))
# Top 3 最忙核心
topk(3, 1 - rate(node_cpu_seconds_total{mode="idle"}[1m]))
练习三:
# 根分区使用率
1 - node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}
# 所有分区总使用率
1 - sum by(instance) (node_filesystem_avail_bytes) / sum by(instance) (node_filesystem_size_bytes)
# 系统负载
node_load1
十一、下集预告
PromQL 入门了,小林能查出 CPU、内存、磁盘的使用率了。老周说:“现在你能查数据了,但数据是一堆数字,没人愿意看。你得把它变成图。”小林打开 Grafana 官网,准备装 Grafana。但他不知道,数据源配置、Dashboard 导入、Panel 查询,每一个都可能让他折腾。
下一集:《Grafana:把数字变成“仪表盘”》