【前情提要】 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 过滤,只看你关心的分区。”

排错思路总结:

  1. PromQL 查询结果为空,先分别查左右两边的指标,确认都有数据。
  2. 检查两边的标签是否完全一致(instance、job、device、mountpoint)。
  3. 如果标签不一致,加过滤条件,让两边标签匹配。
  4. 常见原因:一边有数据,另一边没有;或者标签值不同。
  5. 用 count by(...) 统计两边的标签组合,对比差异。

“记住,”老周说,“PromQL 的运算,是按标签匹配的。标签不一致,结果就是空。 你以后遇到空结果,先查标签。”

小林在笔记本上写下:PromQL 空结果 = 标签不匹配。

十、练习

  1. 练习一:在 Prometheus 查询框里,分别查询 node_memory_MemTotal_bytes 和 node_memory_MemAvailable_bytes,然后用 PromQL 算出内存使用率。把结果截图或记录数值。
  2. 练习二:用 PromQL 查询 CPU 使用率(所有核心平均),以及按实例分组的 CPU 使用率。然后查 topk(3, 1 - rate(node_cpu_seconds_total{mode="idle"}[1m])),看哪 3 个核心最忙。
  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:把数字变成“仪表盘”》