Prometheus 中的一些关键设计,比如注重标准和生态、监控目标动态发现机制、PromQL等。
PromQL 是 Prometheus 的查询语言,使用灵活方便,但很多人不知道如何更好利用它,发挥不出优势。
PromQL主要用于时序数据的查询和二次计算场景。
1 时序数据
可理解成一个以时间为轴的矩阵,如下案例有三个时间序列,在时间轴对应不同值:
代码语言:javascript复制^
│ . . . . . . . . . . node_load1{host="host01",zone="bj"}
│ . . . . . . . . . . node_load1{host="host02",zone="sh"}
│ . . . . . . . . . . node_load1{host="host11",zone="sh"}
v
<------- 时间 ---------->
每一个点称为一个
1.1 样本(sample)
1.1.1 组成
- 指标(metric):metric name和描述当前样本特征的labelsets
- 时间戳(timestamp):ms的时间戳
- 值(value):该时间样本的值
PromQL就是对这样一批样本数据做查询、计算。
2 应用场景
时序数据的查询和二次计算。
PromQL第一个核心价值
2.1 筛选
查询靠的查询选择器
查询选择器
每个监控图表的渲染或者每条告警规则的处理,都只是针对有限的几条数据,所以 PromQL第一个需求就是过滤。
假设我有两个需求:
- 查询上海所有机器1min的负载
- 查询所有以host0为前缀的机器1min的负载
# = 做 zone 的匹配过滤
node_load1{zone="sh"}
# =~ 做 host 的正则过滤
node_load1{host=~"host0.*"}
大括号里写过滤条件,主要是针对标签过滤,操作符除了等于号和正则匹配之外,还有不等于 !=
和正则非 !~
。
metric name可以写到大括号,比如我想【同时查看上海机器的 load1、load5、load15三指标】,可对 __name__
,即 metric 名字做正则过滤:
{__name__=~"node_load.*", zone="sh"}
上面例子给出的3条PromQL都叫即时查询(Instant Query),返回内容叫即时向量( Instant Vector)。
即时查询返回的是当前最新值,如 10:00发起的查询,返回的就是 10:00 这时刻对应数据。但监控数据是周期性上报,并非每时每刻都有数据上报,10:00时可能恰无数据进来,此时 Prometheus 就会往前看,看看9:59、9:58、9:57等时间点有没有上报数据。
最多应该往前看多久?
由Prometheus启动参数 --query.lookback-delta
控制,默认 5 min。从监控角度,建议调短,改成 1 min --query.lookback-delta=1m
?
某人使用 Telegraf 做 HTTP 探测,配置了一个告警规则: response_code 连续 3 min != 200 才告警。实际上只有一个数据点的 response_code!=200,过了3min还是报警。
主要原因
- Telegraf 的 HTTP 探测,会默认把 status code 放到标签,导致标签非稳态结构(这行为不好,最好是把这类标签直接丢弃或使用categraf、blackbox_exporter做采集器),平时 code=200,出问题时 code=500,在 Prometheus 生态里,标签变了就是新的时间序列
- 跟
query.lookback-delta
有关,虽只有一个点异常,即code=500的这个时间序列只有一个点,但告警规则每次执行查询时,都是查到这个异常点,连续5min都如此。所以就满足规则里连续3min才告警的条件。这就是为何建议把--query.lookback-delta
调短
除了即时查询,PromQL还有范围查询(Range Query),返回内容叫 Range Vector。
代码语言:javascript复制{__name__=~"node_load.*", zone="sh"}[1m]
范围查询就是多加个时间范围1min。即时查询每个指标返回一个点,范围查询会返回多个点。假设数据10秒钟采集一次,1分钟有6个点,都会返回。
Prometheus 官方文档 介绍各函数时,都会讲解函数参数,标明 Range Vector or Instant Vector。
PromQL的另一个核心价值
2.2 计算
有算术、比较、逻辑、聚合运算符等。
算术运算符
就常用的加减乘除、取模之类的符号。
代码语言:javascript复制# 计算内存可用率,就是内存可用量除以内存总量,又希望按照百分比呈现,所以最后乘以100
mem_available{app="clickhouse"} / mem_total{app="clickhouse"} * 100
# 计算北京区网口出向的速率,原始数据的单位是byte,网络流量单位一般用bit,所以乘以8
irate(net_sent_bytes_total{zone="beijing"}[1m]) * 8
比较运算符
比较运算符就是大于、小于、等于、不等于之类的,简单但意义重大, 告警规则的逻辑就是靠比较运算符支撑的。
代码语言:javascript复制mem_available{app="clickhouse"} / mem_total{app="clickhouse"} * 100 < 20
irate(net_sent_bytes_total{zone="beijing"}[1m]) * 8 / 1024 / 1024 > 700
带比较运算符的 PromQL 就是告警规则的核心,如内存可用率的告警,在 Prometheus 这样配置:
代码语言:javascript复制groups:
- name: host
rules:
- alert: MemUtil
# 指定了查询用的 PromQL
expr: mem_available{app="clickhouse"} / mem_total{app="clickhouse"} * 100 < 20
# 偶尔一次低于 20% 不是啥大事,只有连续1min每次查询都低于20%才告警,这就是 `for: 1m` 意义
for: 1m
labels:
severity: warn
annotations:
summary: Mem available less than 20%, host:{{ $labels.ident }}
告警引擎会根据用户的配置,周期执行查询:
- 查不到就说明一切正常,没有机器的内存可用率低于20%
- 查到了,说明触发了告警,查到几条就触发几条告警
逻辑运算符
and、or和unless,用于 instant-vector 间的运算。and 是求交集,or是求并集,unless是求差集。
and使用场景,关于磁盘使用率,有的分区大如16T,有的分区小如50G,只用磁盘使用率做告警就不合理,如 disk_used_percent{app="clickhouse"} > 70
表示磁盘使用率大于70%就告警。对于小盘,这策略合理,但对大盘,70%使用率表示还有很多空间,就不合理。这时我们希望给这个策略加个限制,只有小于200G的硬盘在使用率超过70%时才告警,就能用 and:
disk_used_percent{app="clickhouse"} > 70 and disk_total{app="clickhouse"}/1024/1024/1024 < 200
向量匹配
向量间的操作想要在右侧的向量中,为左侧向量的每个条目找到一个匹配的元素,匹配行为分为:one-to-one、many-to-one、one-to-many。刚才介绍的磁盘使用率的例子,就是典型的 one-to-one 类型,左右两侧的指标,除了指标名,其余标签都是一样的,非常容易找到对应关系。但是有时候,我们希望用 and 求交集,但是两侧向量标签不同,怎么办呢?
此时我们可以使用关键字 on 和 ignoring 来限制用于做匹配的标签集。
代码语言:javascript复制mysql_slave_status_slave_sql_running == 0
and ON (instance)
mysql_slave_status_master_server_id > 0
这个PromQL想表达的意思是如果这个MySQL实例是个slave(master_server_id>0),就检查其slave_sql_running的值,如果slave_sql_running==0,就表示slave sql线程没有在运行。
但mysql_slave_status_slave_sql_running和mysql_slave_status_master_server_id这两个metric的标签,可能并非完全一致。不过好在二者都有个instance标签,且相同的instance标签的数据从语义上来看就表示一个实例的多个指标数据,那我们就可以用关键字on来指定只使用instance标签做匹配,忽略其他标签。
与on相反的是关键字ignoring,顾名思义,ignoring是忽略掉某些标签,用剩下的标签来做匹配。我们拿 Prometheus 文档中的例子来说明。
代码语言:javascript复制## example series
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
## promql
method_code:http_errors:rate5m{code="500"}
/ ignoring(code)
method:http_requests:rate5m
## result
{method="get"} 0.04 // 24 / 600
{method="post"} 0.05 // 6 / 120
例子里都是 one-to-one 的对应关系,这个好理解。难理解的是 one-to-many 和 many-to-one,这种情况下,做指标运算时就要借助关键字 group_left 和 group_right 了。left、right 指向高基数那一侧的向量。还是用上面method_code:http_errors:rate5m和method:http_requests:rate5m 这两个指标来举例,你可以看一下使用 group_left 的PromQL和输出的结果。
代码语言:javascript复制## promql
method_code:http_errors:rate5m
/ ignoring(code) group_left
method:http_requests:rate5m
## result
{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
比如针对 method="get"
的条目,右侧的向量中只有一个记录,但是左侧的向量中有两个记录,所以高基数的一侧是左侧,故而使用 group_left。
这里我再举一个例子,来说明 group_left、group_right 的一个常见用法。比如我们使用 kube-state-metrics 来采集 Kubernetes 各个对象的指标数据,其中针对 pod 有个指标是 kube_pod_labels,该指标会把 pod 的一些信息放到标签里,指标值是1,相当于一个元信息。
代码语言:javascript复制kube_pod_labels{
[...]
label_name="frontdoor",
label_version="1.0.1",
label_team="blue"
namespace="default",
pod="frontdoor-xxxxxxxxx-xxxxxx",
} = 1
假设某个 Pod 是接入层的,统计了很多 HTTP 请求相关的指标,我们想统计 5xx 的请求数量,希望能按 Pod 的 version 画一个饼图。这里有个难点:接入层的请求类指标没有 version 标签,version 信息只出现在 kube_pod_labels 里,怎么让二者联动?
代码语言:javascript复制sum(
rate(http_request_count{code=~"^(?:5..)$"}[5m])) by (pod)
*
on (pod) group_left(label_version) kube_pod_labels
我们把这个 PromQL掰开揉碎,乘号前面的部分,是一个统计每秒 5xx 数量的典型语法,按照 pod 维度做分组统计。
然后乘以 kube_pod_labels,这个值是1。任何值乘以1都是原来的值,所以对整体数值没有影响,而 kube_pod_labels 有多个标签,而且和 sum 语句的结果向量的标签不一致,所以通过 on(pod) 语法来指定只按照 pod 标签来建立对应关系。
最后,利用 group_left(label_version),把 label_version 附加到了结果向量里,高基数的部分显然是 sum 的部分,所以使用 group_left 而非 group_right。
聚合运算
针对单指标的多个 series,还有聚合需求。如查看100台机器的平均内存可用率或排序,取数值最小的10台。
这种需求使用 PromQL 内置聚合函数。
代码语言:javascript复制# 求取 clickhouse 的机器的平均内存可用率
avg(mem_available_percent{app="clickhouse"})
# 把 clickhouse 的机器的内存可用率排个序,取最小的两条记录
bottomk(2, mem_available_percent{app="clickhouse"})
分组统计
分别统计 clickhouse 和 canal 的机器内存可用率,使用 by 指定分组统计的维度(与 by 相反的without)。
代码语言:javascript复制avg(mem_available_percent{app=~"clickhouse|canal"}) by (app)
这些聚合运算可理解为 纵向拟合。100 台机器的内存可用率,在折线图上有100条线,如想把这100条线拟合成一条线,就相当于把每个时刻的100个点拟合成1个点。咋让100个点变成1个点?求平均值或最大值之类,所以有了这些聚合运算符。
横向拟合
即 <aggregation>_over_time
等函数。这些函数接收范围向量,因为范围向量是一个时段内有多个值, <aggregation>
就是对这多个值做运算。
# [2m]:获取这个指标最近 2 分钟的所有数据点。若15秒采集一个点,2min就是8个点
# max_over_time:对这8个点求最大值,相当于对各个时间序列做横向拟合
max_over_time(target_up[2m])
3 容易误解的函数
increase 函数
字面意求取一个增量,接收一个 range-vector,range-vector 显然会返回多个 value timestamp 的组合。直观理解就是,直接把时间范围内的最后一个值减去第一个值,不就可以得到增量?No!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-14wDpCXz-1682918864743)(images/623851/5fe64d3408bba9b26b88f556865b2983.png)]
代码语言:javascript复制promql: net_bytes_recv{interface="eth0"}[1m] @ 1661570908
965304237246 @1661570850
965307953982 @1661570860
965311949925 @1661570870
965315732812 @1661570880
965319998347 @1661570890
965323899880 @1661570900
promql: increase(net_bytes_recv{interface="eth0"}[1m]) @1661570909
23595160.8
监控数据10s上报一次,所以虽然两次 PromQL 查询时间不同:
- 一次 1661570908
- 一次 1661570909,但查询的原始数据内容一样,就是 1661570850~1661570900 几个时间点对应数据。
直观理解,在这几个时间点对应的数据上求取 increase,无非就是最后一个值减去第一个值,即965323899880-965304237246=19662634。但实际23595160.8,差别大。
实际上,increase 这个 PromQL 发起请求的时间是1661570909,时间范围是[1m],相当于告诉Prometheus,我要查询1661570849(由1661570909-60得出)~1661570909之间的 increase 数值。但原始监控数据并没有 1661570849、1661570909 这两个时刻的数值,咋办?Prometheus只能基于现有数据做外推,即使用最后一个点数值 - 第一个点的数值的结果,除以时间差,再乘60。
这样最终就得到1分钟的 increase 值,是小数。
rate 函数
increase是求取的时间段内增量,且有数据外推
rate函数则求取每s的变化率,也有数据外推,increase结果除以 range-vector 的时间段大小=rate值。
代码语言:javascript复制rate(net_bytes_recv{interface="eth0"}[1m])
== bool
increase(net_bytes_recv{interface="eth0"}[1m])/60.0
== 后跟个bool,表示希望返回一个 bool 值,如果是 True 就会返回 1,如果是 False 就返回 0。我们观察结果后发现,这个表达式永远都会返回 1,即等号前后的两个 PromQL 语义上是相同的。
rate 函数求取的变化率,相对平滑。因为是拿时间范围内的最后一个值和第一个值做数据外推,一些毛刺现象就会被平滑掉。如果想要得到更敏感的数据,我们可以使用 irate 函数。irate 是拿时间范围内的最后两个值来做计算,变化就会更剧烈,我们拿网卡入向流量这个指标来做个对比。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jjA7oHHy-1682918864746)(images/623851/68ac7b49091b60970b9a8f8a106500c3.png)]
蓝色的变化得更剧烈的线是用 irate 函数计算得到的,而紫色的相对平滑的线是用 rate 函数计算得到的,对比还是很强烈的。
4 总结
PromQL核心价值:
- 筛选 靠查询选择器,查询分为即时查询和范围查询
- 计算 有算术、比较、逻辑、聚合运算符,还有向量匹配逻辑
5 FAQ
Prometheus 中提供了一个函数叫 absent,用于做数据缺失告警,使用得也很广泛,但是坑也挺大。这里我留给你一个问题:如果我想对 100 台机器的 node_load1 做数据缺失告警,应该如何配置?这个需求用 absent 解决合适吗?你能否给出 absent 的最佳使用场景?
对于100台机器的node_load1数据缺失告警需求,使用absent函数不太适合,原因在于:
- Absent函数是用于监控某个指标是否消失(即不存在),而非该指标数据是否有缺失。如果只是某一个节点出现了一段时间的缺失,Absent将会误报为不存在。
- 在涉及到多个节点的情况下,每个节点可能由于各种原因导致其不能向Prometheus发送监控数据,从而触发错误的告警。所以要针对每个节点单独设置告警,即设置一个请求每个节点数据的查询语句,同时确保查询结果正常,并且对于每个节点的告警条件进行区分。
对于 Absent 函数最佳使用场景,它可以过滤掉一些无效的告警,比如对于一些稀有的事件或者异常数据点,当这些事件或者数据出现时我们就需要告警,但是如果出现的很少,我们就会被一堆“false”positive告警搞得心烦意乱。此时, Absent 函数就派上了用场,可以排除掉这些稀有数据并避免告警的的误判,提高告警的可靠性和准确性。