专栏文章 普罗米修斯 -- PromQL 进阶

孙高飞 · 2021年12月07日 · 最后由 Thirty-Thirty 回复于 2022年03月04日 · 6398 次阅读

理解 instant 类型和 range 类型

在 PromQL 中我们可以使用很多的操作符和内置函数来计算我们的监控数据, 而这些操作符和内置函数在计算的 时候要求输入的参数是有类型要求的。 在 PromQL 中计算的参数分为标量 (scalar) 和向量 (vector), 标量就是普通的值, 比如 1,2,3,4 这些值就是标量。 很好理解,不好理解的是向量类型的。 什么是向量呢?比如我们随便找到一个指标, 他的数据张下面这个样子:

{device="sda",instance="localhost:9100",job="node_exporter"}=>1634967552@1518146427.807 + 864551424@1518146427.807
{device="sdb",instance="localhost:9100",job="node_exporter"}=>1634967552@1518146427.807 + 864551424@1518146427.807`

这就是一个向量类型, 这里补充解释一下,之所以我们这里返回了两条数据, 是因为一个监控指标也就是 metric 是含有 label 的, 我们之前说过监控数据会根据 label 进行分类, 所以我们这里可以看到有多条数据返回, 这也是为什么叫做向量,一个向量是多个监控数据组合而成的。而向量又分为 instant(瞬时向量)和 range(范围向量)类型。 他们的含义也就是字面上的意思,瞬时向量反应是某一个时刻的监控数据,上面那条数据就是一个即时向量。 而如果我们使用 [time] 这样的语法获取一个时间段内的监控数据的话, 就是一个范围向量了。 比如我们使用process_cpu_seconds_total[5m] 来查询 5 分钟内所有的监控数据就会返回如下的数据

process_cpu_seconds_total{GPUName="T4", beta_kubernetes_io_arch="amd64"} 
457290.56 @1636449224.643
457292.18 @1636449239.643
457294.08 @1636449254.643
457295.48 @1636449269.643
457297.15 @1636449284.643
457298.74 @1636449299.643
457300 @1636449314.643
457301.55 @1636449329.643
457302.9 @1636449344.643
457304.77 @1636449359.643
457306.26 @1636449374.643
457307.93 @1636449389.643
457309.26 @1636449404.643

process_cpu_seconds_total{GPUName="T5", beta_kubernetes_io_arch="amd64"} 
457290.56 @1636449224.643
457292.18 @1636449239.643
457294.08 @1636449254.643
457295.48 @1636449269.643
457297.15 @1636449284.643
457298.74 @1636449299.643
457300 @1636449314.643
457301.55 @1636449329.643
457302.9 @1636449344.643
457304.77 @1636449359.643
457306.26 @1636449374.643
457307.93 @1636449389.643
457309.26 @1636449404.643

上面通过 [time] 的语法就查询到了过去 5 分钟内监控数据了 。 每一条数据里都记录了 5 分钟内所有的数据值。

之所以要搞清楚标量和向量(瞬时向量,范围向量)的定义是因为 PromQL 中的操作符和内置函数在使用 的时候对参数是有要求的。 有的要求是标量, 有的要求是瞬时向量有的要求是范围向量。所以我们要搞明白要调用的函数和操作符要求的变量是什么类型的, 然后才能去查询出这个类型的变量来。 比如上次写的例子 avg(process_cpu_seconds_total{}) by (kubernetes_io_hostname) avg 这个内置函数要求的就是一个瞬时向量, 计算向量中数据的平均值。 如果我们用 avg(process_cpu_seconds_total{}[5m]) by (kubernetes_io_hostname) 去把查询出的 5 分钟内的范围向量输入给 avg 函数的话,就会抛出异常:

Error executing query: 1:5: parse error: expected type instant vector in aggregation expression, got range vector

异常里说了期望的类型是 instant vector 但是传递进去的是一个 range vector。

理解常用聚合操作符

先列一下常用的聚合操作符。 具体的每个函数和其他的操作符的文档在:https://prometheus.io/docs/prometheus/latest/querying/operators/

  • sum (求和)
  • min (最小值)
  • max (最大值)
  • avg (平均值)
  • stddev (标准差)
  • stdvar (标准方差)
  • count (计数)
  • count_values (对 value 进行计数)
  • bottomk (后 n 条时序)
  • topk (前 n 条时序)
  • quantile (分位数)

使用聚合操作的语法如下:

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

其中只有 count_values , quantile , topk , bottomk 支持参数 (parameter)。
without 用于从计算结果中移除列举的标签,而保留其它标签。by 则正好相反,结果向量中只保留列出的标签,其余标签则移除。通过 without 和 by 可以按照样本的问题对数据进行聚合。
例如:

sum(http_requests_total) without (instance)

等价于
sum(http_requests_total) by (code,handler,job,method)

注意这些聚合函数要求的类型都是瞬时向量的类型。 可能有些同学会觉得奇怪,我们一般希望得到的数据是反应当前系统现状的监控结果。 那要这些聚合函数有什么用, 比如 sum 函数是用来计算瞬时向量中所有数据的累加值的。 这有什么用? 比如还是拿process_cpu_seconds_total 这个指标来说, 这个指标是通过 process exporter 监控一些进程的。 我们这么查询出来的结果是把所有机器上的监控的所有进程都查询出来了。 那如果我想计算出每台机器的 CPU 使用总数呢? 那么就需要 sum 函数 配合 by 关键字来解决问题了:sum(process_cpu_seconds_total) by (kubernetes_io_hostname)

查询结果会根据 by 关键字针对 hostname 进行分组, 然后每组里都使用 sum 函数进行计算 。 这样就能分别计算出每台机器 上进程占用 CPU 的总和了。

理解常用内置函数

首先所有内置函数的文档链接:https://prometheus.io/docs/prometheus/latest/querying/functions/ 函数比较多, 我这类就介绍最常用的。
计算 counter 类型数据的增长率的常用函数 -- rate 理论上,rate 用于计算某个指标每秒的增长率。 因为 counter 类型都是只增不减的, 比如统计某个进程使用的 CPU 数据,其实统计的就是这个进程到目前位置使用 cpu 时间的总量, 又或者统计某块网卡的网络包,指标返回的是这块网卡从启动到现在收发网络包 size 的总和, 这些都很难反应数据当前系统的性能情况。 而 rate 的这个函数就是用来在 counter 类型的数据中计算某段时间内, 这个指标平均每秒都增长了多少数字。 比如我们要统计某个节点的 CPU 使用率的话,PromQL 语句是100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[1m])) * 100)

这里需要好好解释一下这个语句的含义, 首先我们需要先知道。 关于 CPU 的指标统计是一个只增不减的 ounter 类型,查询结果是每一块 CPU 自启动以来一共 运行了多少秒。 如下:

|node_cpu_seconds_total{cpu="0", instance="cs1", mode="idle"} |8327793.95 |
|node_cpu_seconds_total{cpu="0", instance="cs1", mode="system"} |8327793.95 |
|node_cpu_seconds_total{cpu="0", instance="cs1", mode="user"} |8327793.95 |
|node_cpu_seconds_total{cpu="1", instance="cs1", mode="idle"} |6327793.95 |
|node_cpu_seconds_total{cpu="0", instance="cs2", mode="idle"} |9327793.95 |
|node_cpu_seconds_total{cpu="1", instance="cs2", mode="idle"} |4327793.95 |

上面是我 mock 的数据,我把很多数据给删除了, 免得占用太大的篇幅。 我们可以认为当我们使用node_cpu_seconds_total 查询后, 就会返回上面那样的数据。 上面 分别展示了 2 台机器上的 4 块卡的性能信息,分别用 3 个 label 进行区分。 cpu 是卡的编号, instance 是机器名字, model 是这块 cpu 的模式,idel 代表空闲时间, user 代表用户态时间,system 代表内核态时间。 而这个指标的含义就是某台机器上某块卡在某个模式下自启动以来的时间总和(精确到秒)。 那么这个时候要统计某台机器的 CPU 的使用率我们就需要利用 rate 函数来解决了。 rate 函数能计算出平均每秒 CPU 使用时间增长多少。 比如我们统计最近 5 分钟的数据, 然后用 rate 函数计算出 CPU 这段时间内在空闲状态下,每秒增长了 0.9。 也就是说过去 5 分钟内,平均每一秒里 CPU 有 0.9 秒都处于空闲状态。 那么 CPU 的使用率就是 10% 了。 这样我们就理解了为什么我们会使用100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[1m])) * 100) 来计算 CPU 的使用率了吧。 这里再简单介绍一下几个细节:

  • avg by (instance) 是因为我们要统计每台机器的 cpu 使用率。 所以先按机器分组。 但是每台机器上又有很多块 cpu 卡, 所以 avg 用来计算平均的 cpu 使用率。
  • model="idel" 是要筛选出 cpu 空闲时间的指标。
  • 用 rate 计算出平均增长率后乘以 100 再用 100 减去这个结果, 是因为我们要计算出一个百分比。

需要注意的是使用 rate 函数去计算样本的平均增长速率,容易陷入 “长尾问题” 当中,其无法反应在时间窗口内样本数据的突发变化。 例如,对于主机而言在 2 分钟的时间窗口内,可能在某一个由于访问量或者其它问题导致 CPU 占用 100% 的情况,但是通过计算在时间窗口内的平均增长率却无法反应出该问题。
为了解决该问题,PromQL 提供了另外一个灵敏度更高的函数 irate(v range-vector)。irate 同样用于计算区间向量的计算率,但是其反应出的是瞬时增长率。irate 函数是通过区间向量中最后两个样本数据来计算区间向量的增长速率。这种方式可以避免在时间窗口范围内的 “长尾问题”,并且体现出更好的灵敏度,通过 irate 函数绘制的图标能够更好的反应样本数据的瞬时变化状态。

irate(node_cpu_seconds_total[2m])

irate 函数相比于 rate 函数提供了更高的灵敏度,不过当需要分析长期趋势或者在告警规则中,irate 的这种灵敏度反而容易造成干扰。因此在长期趋势分析或者告警中更推荐使用 rate 函数。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 2 条回复 时间 点赞
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册