PromQL进阶

Prometheus 聚合函数

一般说来,单个指标的价值不大,监控场景中往往需要将一些具有相同指标名的指标值进行聚合计算(这些指标名很有可能来自不同target只是拥有同样的指标名,也有可能是某一个target上某一个指标的多个维度,例如:有的一个指标名有多个时间序列)。例如计数、求和、平均值、分位数、标准 差及方差等。

什么是聚合计算:对查询结果事先按照某种分类机制进行分组(group by)并将查询结果按组进行计算,例如分组统计、分组求平均值、分组求和等;

下图我们首先通过PromQL过滤出来具有指标名为node_disk_free的所有节点,然后可以看到有很多target都有这个指标名,这也就跟上面提到的有可能来自不同target只是拥有同样的指标名,也可以看到其中每个target有两个时间序列就例如10.191.184.164这个IP。这也就符合了我们上面说的也有可能是某一个target上某一个指标的多个维度
Prometheus-PromQL-18

下图是用instance作为分组,且做了标签匹配仅符合条件的才会被过滤出来。然后在进行求和运算。以IP10.191.184.164为例计算方式如下:

1
0.9446315040088761+0.738403637258654=1.6830351413

Prometheus-PromQL-19

Prometheus-PromQL-20

聚合操作由聚合函数针对一组值进行计算并返回单个值或少量几值作为结果(聚合是越聚越少,不会越聚越多):

  • Prometheus内置提供的11个聚合函数也称为聚合运算符
  • 这些运算符仅支持应用于单个即时向量的元素,其返回值也是具有少量元素的新向量或标量
  • 这些聚合运行符既可以基于向量表达式返回结果中的时间序列的所有标签维度进行分组聚合,也可以仅基于指定的标签维度分组后再进行分组聚合

聚合表达式

PromQL中的聚合操作语法格式可采用如下面两种格式之一:

1
2
<aggr-op>([parameter,] <vector expression>) [without|by (<label list>)] 
<aggr-op> [without|by (<label list>)] ([parameter,] <vector expression>)

分组聚合:先分组、后聚合

  • without:从结果向量中删除由without子句指定的标签,未指定的那部分标签则用作分组标准;
  • by:功能与without刚好相反,它仅使用by子句中指定的标签进行聚合,结果向量中出现但未被by子句指定的标签则会被忽略;
    • 为了保留上下文信息,使用by子句时需要显式指定其结果中原本出现的job、instance等一类的标签

11个聚合函数

1
2
3
4
5
6
7
8
9
10
11
sum():对样本值求和;
avg():对样本值求平均值,这是进行指标数据分析的标准方法;
count():对分组内的时间序列进行数量统计;
stddev():对样本值求标准差,以帮助用户了解数据的波动大小(或称之为波动程度);
stdvar():对样本值求方差,它是求取标准差过程中的中间状态;
min():求取样本值中的最小者;
max():求取样本值中的最大者;
topk():逆序返回分组内的样本值最大的前k个时间序列及其值;
bottomk():顺序返回分组内的样本值最小的前k个时间序列及其值;
quantile():分位数用于评估数据的分布状态,该函数会返回分组内指定的分位数的值,即 数值落在小于等于指定的分位区间的比例;
count_values():对分组内的时间序列的样本值进行数量统计;

注意:其中只有count_values, quantile, topk, bottomk支持参数(parameter)。事实上,各函数工作机制的不同之处也仅在于计算操作本身,PromQL对于它们的执行逻辑相似;

示例讲解

Prometheus-PromQL-11

通过上面的语句过滤出来所有节点上指标名 node_filesystem_avail_bytes 的值,我们可以用聚合函数进行直接聚合。

sum() 求和

他会把所有节点的node_filesystem_avail_bytes的值全部相加
Prometheus-PromQL-12

我们也可以指定标签进行聚合,指定instance则是以instance进行分组聚合,这样就计算出每台主机可用空间。
Prometheus-PromQL-13

我们可以看到有一些device="rootfs"device="tmpfs" 这样的标签。这种标签如果我们不想统计应该怎么写呢?
Prometheus-PromQL-14

过滤掉我们不想统计的标签后在做分组聚合
Prometheus-PromQL-15

avg() 求平均值

首先我们先找一个值少一些的等会好手动计算,我们注意看下面有这些标签。我们使用service标签做分组聚合。
Prometheus-PromQL-16

分组集合后的结果
Prometheus-PromQL-17

1
计算方式:(9661+17171+15679)/3=14,170.3333333333

PromQL运算符

当用户需要使用不同的监控指标进行更多操作时,PromQL聚合操作会出现无法满足使用的情况。这时Prometheus提供了多种运算符。这些运算符不仅允许对即时向量进行简单的算术运算,还可以将运算符应用于两个基于标签分组的即时向量。下面我们对Prometheus查询语言提供的算术运算符、关系运算符、向量匹配模式和逻辑运算符进行介绍。

算术运算符

Prometheus提供的所有算术运算的工作原理都是类似的,有编程基础的读者可以看到其语义与其他编程语言的语义也相同。Prometheus的6种算术运算符如下:

运算符 描述
+
-
*
/
% 取模
^ 幂运算

运算操作符支持三类操作:

  • scalar/scalar(标量/标量)之间的操作;
  • vector/scalar(即时向量/标量)之间的操作;
  • vector/vector(即时向量/即时向量)之间的操作;

下面我们分别对表达式进行调试,通过Prometheus Web UI上直观的输出信息观察三类操作。

两个标量之间

在两个标量之间进行算术运算,得到的结果还是标量
Prometheus-PromQL-21

即时向量与标量之间

当即时向量与标量之间进行算术运算时,算术运算符会依次作用于即时向量中的每一个样本值,从而得到一组新的时间序列。

例如,我们可以通过监控指标node_memory_MemTotal_bytes获取主机内存总空间的大小,其样本单位为byte。现在把样本单位换算为MB时,表达式为 node_memory_MemTotal_bytes/(1024*1024),如下图:
Prometheus-PromQL-22

即时向量与即时向量之间

即时向量与即时向量进行算术运算的过程相对复杂一些。

例如,node_disk_read_time_msnode_disk_write_time_ms 获取磁盘读写时间使用表达式:node_disk_write_time_ms + node_disk_read_time_ms 如下图所示:

Prometheus-PromQL-23

该表达式工作过程是依次找到与左边向量元素匹配(标签完全一致)的右边向量元素进行运算,如果没找到匹配元素,则直接丢弃,同时,计算后新的时间序列将不会包含指标名称。

Prometheus-PromQL-24

Prometheus-PromQL-25

关系运算符

Prometheus同样提供了关系运算符,也称为比较运算符,其语义很容易理解。Prometheus的6种关系运算符如下所示:

运算符 描述
== 等值比较
!= 不等
> 大于
< 小于
>= 大于等于
<= 小于等于

关系运算符同样被应用于scalar/scalar(标量/标量)、vector/scalar(即时向量/标量),和vector/vector(即时向量/即时向量)之间。默认情况下,运算符用于对时序数据进行过滤。但是在有些情况下,可以通过在运算符之后使用bool修饰符,从而不对时间序列进行过滤,直接返回0(false)或者1(true)。

下面我们使用Prometheus Web UI分别对表达式进行调试后,通过直观的输出信息观察三类操作

两个标量之间

在两个标量之间进行关系运算,必须使用bool修饰符,并且这些运算符会产生另一个标量,即0(false)或者1(true),如图

Prometheus-PromQL-26

即时向量与标量之间

当即时向量与标量之间进行关系运算时,这个运算符会应用到某个当前时刻的每个时间序列数据上,如果一个时间序列数据值与这个标量的比较结果是false,则这个时间序列数据被丢弃,如果是true,则这个时间序列数据被保留在结果中。

例如,通过监控指标node_netstat_Tcp_CurrEstab获取主机网络状态为ESTABLISHED,数量大于30的表达式为:node_netstat_Tcp_CurrEstab ≥ 30
Prometheus-PromQL-27

还有我们经常使用的被监控主机告警表达式,例如,up{job="node_exporter"}==0

即时向量与即时向量之间

在即时向量与即时向量之间进行关系运算时,运算符默认情况下是过滤的,用于匹配条目。表达式不是true或在表达式的另一侧找不到匹配项的向量元素将被从结果中删除,不在结果中显示;否则将保留左侧的度量指标和标签的样本数据写入即时向量。如果提供了bool修饰符,则删除的向量元素的值为0,而保留的向量元素的值为1,左侧标签值为1。

向量匹配

在标量和即时向量之间使用运算符可以满足很多需求,但是在两个即时向量之间使用运算符时PromQL会为左侧向量中的每个元素找到匹配的元素,如果没找到匹配元素则直接丢弃,其匹配行为有两种基本类型:

  • 一对一 (One-to-One)
  • 一对多或多对一 (Many-to-One, One-to-Many)

one-to-one(一对一)

即一对一向量匹配模式,它从运算符的两侧表达式中获取即时向量,依次比较并找到唯一匹配(标签完全一致)的样本值,如果两个条目具有完全相同的标签和对应的值,则它们匹配。一般默认表达式格式为 vector1 <运算符> vector2

如:

1
2
3
process_open_fds{instance="192.168.3.21:9100",job="centos8"}    
/
process_max_fds{instance="192.168.3.21:9100",job="centos8"}

Prometheus-PromQL-28

从上图看到,除了标签__name__中的指标名称之外,他们具有完全相同标签名和标签值,这样他们就被匹配到了一起。也就是说,该示例中都有标签{instance=”192.168.3.21:9100”,job=”centos8”}将匹配在一起。

如果运算符两侧表达式标签不一致,可以使用关键字on或ignoring修改标签间的匹配行为。其中,on用于在指定标签上进行匹配,ignoring可以忽略指定标签进行匹配。表达格式分别为:

1
2
3
4
5
<vector expr> <bin-op> ignoring(<label list>) <vector expr>
<vector expr> <bin-op> on(<label list>) <vector expr>

ignore:定义匹配检测时要忽略的标签;
on:定义匹配检测时只使用的标签;

这里我们列举一个表达式:

我们希望下面两个node_disk_read_time_ms相加,但显然是不行的。因为他们两个标签不完全匹配一个device=sda一个标签是device=sdb,除此别的标签可以完全一致。

Prometheus-PromQL-29

计算node_disk_read_time_ms{device="sda"} + node_disk_read_time_ms{device="sdb"} 可以看到无数据,这是因为他们两个的标签不能匹配。
Prometheus-PromQL-30

我们使用ignoring忽略掉他们不一样的标签后他们就能完全匹配了,这样就能计算了。(总之记住一句话两个向量之间做运算必须两边的标签名和标签值必须匹配才能做计算)
Prometheus-PromQL-31

在来一个稍微复杂一点的表达式:

1
2
3
#表达式中使用到了rate()函数,即计算某个时间序列范围内的平均增长率

sum (rate(node_cpu_seconds_total{mode="idle"}[5m])) by (instance) / sum (rate(node_cpu_seconds_total[5m])) by (instance)

以下表达式是过滤出来标签mode="idle"的并计算出他们5分钟的一个平均值
Prometheus-PromQL-34

然后可以看到一个节点有多个核心,那在把他们几个核心的值相加,所以我们需要用到 sum()函数做聚合运算,而且必须基于主机做聚合所以用instance标签
Prometheus-PromQL-32

这里我们计算整个CPU 5分钟的一个平均值
Prometheus-PromQL-35

可以看到下面有很多mode标签不一样的,所以我们也需要聚合一下
Prometheus-PromQL-33

最终计算结果
Prometheus-PromQL-36

但是我们注意到左边表达式和右边表达式中标签完全一致,所以能计算,如果稍微修改一下呢,多一个Job标签

Prometheus-PromQL-37

此时我们需要使用on关键字 仅使用某一个或多个标签做匹配
Prometheus-PromQL-38

many-to-one (多对一)and one-to-many(一对多)

多对一和一对多的匹配模式,可以理解为如果两个瞬时向量数量不一致时可通过group_leftgroup_right指定以那一侧为准:

完整的语法形式:

1
2
3
4
<vector expr> <bin-op> ignoring(<label list>) group_left(<label list>) <vector expr>
<vector expr> <bin-op> ignoring(<label list>) group_right(<label list>) <vector expr>
<vector expr> <bin-op> on(<label list>) group_left(<label list>) <vector expr>
<vector expr> <bin-op> on(<label list>) group_right(<label list>) <vector expr>

输入示例:

1
2
3
4
5
6
7
8
9
method_code:http_errors:rate5m{method="get", code="500"}  24
method_code:http_errors:rate5m{method="get", code="404"} 30
method_code:http_errors:rate5m{method="put", code="501"} 3
method_code:http_errors:rate5m{method="post", code="500"} 6
method_code:http_errors:rate5m{method="post", code="404"} 21

method:http_requests:rate5m{method="get"} 600
method:http_requests:rate5m{method="del"} 34
method:http_requests:rate5m{method="post"} 120

查询示例:

group_left以左侧为准

1
method_code:http_errors:rate5m / ignoring(code) group_left method:http_requests:rate5m

结果示例:

1
2
3
4
{method="get", code="500"}  0.04            //  24 / 600
{method="get", code="404"} 0.05           // 30 / 600
{method="post", code="500"} 0.05           //   6 / 120
{method="post", code="404"} 0.175           // 21 / 120

表达式分析:

1
sum by (node) ((kube_pod_status_phase{phase="Running"} == 1) + on(pod) group_left(node) (0 * kube_pod_info{pod_template_hash=""})) / sum by (node) (kube_node_status_allocatable{resource="pods"}) * 100 > 90

expr1表达式:

1
(kube_pod_status_phase{phase="Running"} == 1)

执行结果:
Prometheus-PromQL-48

expr2表达式:

1
(0 * kube_pod_info{pod_template_hash=""})

执行结果:
Prometheus-PromQL-46

group_left(node)结果:

1
(kube_pod_status_phase{phase="Running"} == 1) + on(pod) group_left(node) (0 * kube_pod_info{pod_template_hash=""})

Prometheus-PromQL-47

group_left(node) 的参数是右侧向量的标签名,其会将此标签值传递给左侧,也就是出现在运算的结果中出现的node的label。原来expr1中并没有node的标签。

如group_left()参数中的标签两边都存在则覆盖左侧同名标签。比如此例中如果 group_left(job),那么最终结果的标签集中将会存在 job="node" 而不是 job="process"

1
2
3
node_process_namegroup_memory_bytes{job="process",ip="10.1.1.1",memtype="resident"}

node_memory_MemTotal_bytes{job="node",ip="10.1.1.1"}
1
node_process_namegroup_memory_bytes{job="process",ip="10.1.1.1",memtype="resident"} / on(ip) group_left(job) node_memory_MemTotal_bytes{job="node",ip="10.1.1.1"}

group_left()结果:
Prometheus-PromQL-39

没有node标签

逻辑运算符

Prometheus提供了三种逻辑运算符:and、or和unless。逻辑运算符仅用于瞬时向量之间。所有逻辑运算符都以多对多的方式工作,它们是唯一能工作于多对多方式的运算符。不同于算术运算符和比较运算符,因为没有执行任何数学计算,所以重点是描述一个组是否包含样本。下面我们根据官方提供的内容分别对三种逻辑运算进行介绍。

vector1 and vector2

and逻辑运算会产生一个由vector1的元素组成的新的向量。该向量包含vector1中完全匹配vector2中的元素。表达式示例如下:

1
2
3
node_cpu_seconds_total{job="centos8"} 
and
node_cpu_seconds_total{mode="idle"}

Prometheus-PromQL-40

vector1 or vector2

or逻辑运算会产生一个新的向量,该向量包含vector1的所有原始元素(标签集+值)的向量,以及vector2中没有与vector1匹配标签集的所有元素。假设判断node_test_metric指标是否存在,如果指标不存在则返回0。在这种情况下,我们可以使用与每个目标关联的up指标进行表达式操作:

1
2
3
node_test_metric
or
(up{job="centos8"} == 1) * 0

Prometheus-PromQL-41

vector1 unless vector2

unless逻辑运算会产生一个由vector1的元素组成的向量,而这些元素在vector2中没有与标签集完全匹配的元素,两个向量中的所有匹配元素都被删除。表达式示例如下:

1
2
3
4
5
6
node_cpu_seconds_total{job="centos8"} 
and
node_cpu_seconds_total{mode="idle"}
#把master节点去掉
unless
node_cpu_seconds_total{instance="192.168.3.82:9100"}

Prometheus-PromQL-42

运算符优先级

编号 运算符
1 ^
2 *,/,%
3 +,-
4 ==,!=,<=,<,>=,>
5 and,unless
6 or

PromQL内置函数

下面的函数列表允许传入一个范围向量,它们会聚合每个时间序列的范围,并返回一个即时向量:

  • avg_over_time(range-vector) : 范围向量内每个度量指标的平均值。
  • min_over_time(range-vector) : 范围向量内每个度量指标的最小值。
  • max_over_time(range-vector) : 范围向量内每个度量指标的最大值。
  • sum_over_time(range-vector) : 范围向量内每个度量指标的求和。
  • count_over_time(range-vector) : 范围向量内每个度量指标的样本数据个数。
  • quantile_over_time(scalar, range-vector) : 范围向量内每个度量指标的样本数据值分位数,φ-quantile (0 ≤ φ ≤ 1)
  • stddev_over_time(range-vector) : 范围向量内每个度量指标的总体标准差。
  • stdvar_over_time(range-vector) : 范围向量内每个度量指标的总体标准方差。

下面的示例中我们看到另外一种写法[15m:1m],这表示取15分钟的一个范围数据,并按每分钟分为一段,最后分为15段。在15段中取一个最小值。

1
2
3
4
5
6
7
8
- alert: KubernetesPodNotHealthy
expr: min_over_time(sum by (namespace, pod) (kube_pod_status_phase{phase=~"Pending|Unknown|Failed"})[15m:1m]) > 0
for: 0m
labels:
severity: critical
annotations:
summary: Kubernetes Pod not healthy (instance {{ $labels.instance }})
description: "Pod has been in a non-ready state for longer than 15 minutes.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"

演示

首先使用范围向量获取过去一段时间内的数据
Prometheus-PromQL-43

按每2分钟为一段切割6/2=3
Prometheus-PromQL-44

使用min_over_time函数计算出3个数据中最小的一个
Prometheus-PromQL-45


PromQL进阶
https://system51.github.io/2021/06/15/Use-advanced-PromQL/
作者
Mr.Ye
发布于
2021年6月15日
许可协议