1、PromQL 简介
PromQL作为Prometheus监控系统的核心查询语言,它是一种专门为时间序列数据设计的表达式语言。通过PromQL,我们能够以极其灵活和精确的方式,从海量的监控指标数据中筛选、聚合、计算和提取我们所关心的信息。
(1) PromQL 的基本语法
1)指标名称和标签选择器
Prometheus 基于指标名称和标签选择器唯一定义一条时间序列。
指标名称犹如数据海洋中的指南针,明确地指向我们所需的特定类型的数据。而标签选择器则像是精细的滤网,能够根据丰富的标签信息,精准地筛选出符合特定条件的数据子集。
例如,通过 prometheus_http_requests_total{code="200", handler="/targets"} 这样的表达式,我们可以精确获取到访问 /targets 且响应状态码为 200 的 HTTP 请求总数。
2)时间范围选择
时间范围的选择为我们的数据分析赋予了时间维度的视角。通过灵活指定起始时间和结束时间,或者使用相对时间的表达方式,如 5m(表示过去 5 分钟)、1h(表示过去 1 小时)等, 我们可以轻松获取特定时间段内的数据。这使得我们能够对比不同时间段的数据表现,发现潜在的周期性规律和异常波动。
3)函数和操作符
PromQL 提供了琳琅满目的函数,如 sum() 用于求和、avg() 计算平均值、max() 找出最大值、min() 确定最小值,以及 rate() 计算每秒的变化率等。
这些函数让我们能够对数据进行多维度的聚合和分析。同时,操作符如 + 、- 、* 、/ 等数学运算符号,使我们能够对数据进行自定义的计算和转换,满足各种复杂的业务需求。
(2)数据类型与指标
1)瞬时向量(Instant Vector)
瞬时向量是在特定时间点上的一组数据样本,每个样本都包含一个指标名称、一组标签和一个具体的值。它能够瞬间捕捉系统在某一时刻的状态快照,帮助我们快速了解各项指标的即时情况。
2)区间向量(Range Vector)
与瞬时向量不同,区间向量涵盖了一段时间范围内的数据样本。这对于分析指标的变化趋势、计算一段时间内的累计值或者检测数据的稳定性非常有用。通过观察区间向量,我们可以更全面地了解系统在一段时间内的性能表现。
3)标量(Scalar)
标量则是单个的数值,通常作为函数计算的结果返回。例如,通过 count(prometheus_http_requests_total{code="200"}) 统计状态码为 200 的数据点的数量,其结果就是一个标量值。
样本数据格式
Prometheus 的每个时间序列数据样本由两部分组成
●毫秒精度的时间戳
●浮点型的样本值
prometheus_http_requests_total{code="200", handler="/targets"} @1434317560885 28
prometheus_http_requests_total{code="200", handler="/targets"} @1434317561483 35
prometheus_http_requests_total{code="200", handler="/targets"} @1434317562589 42
prometheus_http_requests_total{code="200", handler="/targets"} @1434317563654 50
| || | | | |
--------- 指标名称 --------- ------------ 标签 ------------ -- 时间戳 -- 样本值
(3)时间序列操作
PromQL 的查询操作可能需要针对若干个时间序列上的样本数据进行,挑选出目标时间序列是构建表达式时最为关键的一步。
用户可使用向量选择器表达式来挑选出给定指标名称下在某一时刻的样本值或至过去某个时间范围内的样本值,前者称为瞬时向量选择器,后者称为区间向量选择器。
(1)瞬时向量选择器(Instant Vector Selectors)
瞬时向量选择器可以返回 0 个、1 个或多个时间序列上在某一时刻的各自的一个样本。
瞬时向量选择器由两部分组成;
◆指标名称:用于限定特定指标下的时间序列,即负责过滤指标;可选
◆标签选择器:用于过滤时间序列上的标签;定义在 {} 之中;可选
定义瞬时向量选择器时,以上两个部分应该至少给出一个;因此存在以下三种组合:
1、仅给定指标名称,或在标签名称上使用了空值的标签选择器:返回给定的指标下的所有时间序列各自的即时样本
例如,prometheus_http_requests_total 和 prometheus_http_requests_total{} 的功能相同,都是用于返回这个指标下各时间序列的即时样本
2、仅给定标签选择器:返回所有符合给定的标签选择器的所有时间序列上的即时样本
例如,{code="200", handler="/targets"} ,这样的时间序列可能会有着不同的指标名称
3、指标名称和标签选择器的组合:返回给定的指标下的,且符合给定的标签过滤器的所有时间序列上的即时样本
例如,prometheus_http_requests_total{code="200", handler="/targets"},用于返回这个指标 code 为 200, 并且 handler 为 targets 的时间序列的即时样本
#标签选择器用于定义标签过滤条件,目前支持如下几种匹配操作符:
= :完全相等
!= : 不相等
=~ : 正则表达式匹配
!~ : 正则表达式不匹配
注意事项:
◆匹配到空标签值的标签选择器时,所有未定义该标签的时间序列同样符合条件
例如,prometheus_http_requests_total{handler=""},则该指标名称上所有未使用该标签(handler)的时间序列也符合条件
◆正则表达式将执行完全锚定机制,它需要匹配指定的标签的整个值
◆向量选择器至少要包含一个指标名称,或者至少有一个不会匹配到空字符串的标签选择器
例如,{job=""} 为非法的向量选择器
◆使用 __name__ 做为标签名称,还能够对指标名称进行过滤
例如,{__name__=~".*http_requests_total"} 能够匹配所有以 http_requests_total 为后缀的所有指标
(2)区间向量选择器(Range Vector Selectors)
区间向量选择器可以返回 0 个、1 个或多个时间序列上在给定时间范围内的各自的一组样本。
区间向量选择器的不同之处在于,需要通过在瞬时向量选择器表达式后面添加包含在 [] 里的时长来表达需在时间序列上返回的样本所处的时间范围。
时间范围:以当前时间为基准时间点,指向过去一个特定的时间长度;例如,[5m] 是指过去 5 分钟之内。
◆可用的时间单位有 ms(毫秒)、s(秒)、m(分钟)、h(小时)、d(天)、w(周)和 y(年)
◆必须使用整数时间,且能够将多个不同级别的单位进行串联组合,以时间单位由大到小为顺序,例如 1h30m,但不能使用 1.5h
(3)偏移向量选择器
前面介绍的选择器默认都是以当前时间为基准时间,偏移修饰器用来调整基准时间,使其往前偏移一段时间。偏移修饰器紧跟在选择器后面,使用关键字 offset 来指定要偏移的量。
例如,prometheus_http_requests_total offset 5m ,表示获取以 prometheus_http_requests_total 为指标名称的所有时间序列在过去 5 分钟之时的即时样本;
prometheus_http_requests_total[5m] offset 1d ,表示获取距此刻 1 天时间之前的 5 分钟之内的所有样本
(4)PromQL 的指标类型
Prometheus 底层存储上其实并没有对指标做类型的区分,都是以时间序列的形式存储,但是为了方便用户的使用和理解不同监控指标之间的差异,Prometheus 定义了 4 种不同的指标类型:计数器 counter,仪表盘 gauge,直方图 histogram,摘要 summary。
PromQL 有四个指标类型:
(1)Counter:计数器,用于保存单调递增型的数据;例如站点访问次数、http请求错误数、接口调用次数等单调递增的数据。
通常,Counter 的总数并没有直接作用,而是需要借助于 rate、irate、topk、bottomk 和 increase 等函数来统计变化速率(增长率/变化率):
topk(3, prometheus_http_requests_total),获取该指标下 http 请求总数排名前 3 的时间序列
increase(prometheus_http_requests_total[1m]),获取区间向量中过去 1 分钟内的增量总量
rate(prometheus_http_requests_total[1m]),获取 1 分钟内,该指标下各时间序列上的 http 总请求数的增长速率
rate函数 = 1分钟内的增量 除以 60秒 得到的平均每秒钟的增量速率
rate 函数只是算出来了某个时间区间内的平均速率,没办法反映突发变化,假设在一分钟的时间区间里,前 50 秒的请求量都是 0 到 10 左右,但是最后 10 秒的请求量暴增到 100 以上,这时候算出来的值可能无法很好的反映这个峰值变化。这个问题可以通过 irate 函数解决,irate 函数求出来的就是瞬时变化率。
irate(prometheus_http_requests_total[1m]),irate为高灵敏度函数,用于计算指标的瞬时速率,相较于rate函数来说,irate更适用于短期时间范围内的变化速率分析
irate函数 = 时间区间内最后两个样本值的差 / 最后两个样本的时间差
(2)Gauge:仪表盘,用于存储有着起伏特征的指标数据,可以反映一些动态变化的数据,例如当前内存占用率、CPU利用率、GC次数等动态可上升可下降的数据。
Gauge 用于存储其值可增可减的指标的样本数据,可以不用经过内置函数直观的反映数据的变化情况,也可以用于进行求和、取平均值、最小值、最大值等聚合计算;
也可以结合 PromQL 的 delta 和 predict_linear 函数使用:
delta 函数计算范围向量中每个时间序列元素的第一个值与最后一个值之差,从而展示不同时间点上的样本值的差值
例如,delta(cpu_temp_celsius{host="node01"}[2h]) ,返回该服务器上的CPU温度与2小时之前的差异
●predict_linear 函数可以预测时间序列 v 在 t 秒后的值,它通过线性回归的方式,对样本数据的变化趋势做出预测
例如,predict_linear(node_filesystem_free{job="node"}[2h], 4 * 3600) ,基于 2 小时的样本数据,来预测主机可用磁盘空间在 4 个小时之后的剩余情况
注:基于历史数据进行未来趋势的预测是PromQL的一项高级功能。虽然其准确性受到多种因素的影响,但它可以为我们提供一个大致的趋势方向和可能的范围,能够为我们提前做好资源规划和容量准备。
(3)Histogram:累计直方图,可以观察到指标在各个不同的区间范围的分布情况;比如将时间范围内的数据划分成不同的时间段,并各自评估其样本个数及样本值之和,因而可计算出百分位数;例如延迟时间、响应大小。0~1秒内的延迟时间、0~5秒内的延迟时间、0~1kb之内的响应大小、0~5kb之内的响应大小。
对于 Prometheus 来说,Histogram 会在一段时间范围内对数据进行采样(通常是请求持续时长或响应大小等),并将其计入可配置的 bucket(存储桶)中,后续可通过指定区间筛选样本, 也可以统计样本总数,最后一般将数据展示为直方图。
Prometheus 取值间隔的划分采用的是累积区间间隔机制,即每个 bucket 中的样本均包含了其前面所有 bucket 中的样本,因而也称为累积直方图。
Histogram 类型的每个指标有一个基础指标名称 <basename>,它会提供多个时间序列:
<basename>_sum :所有样本值的总和
<basename>_count :总的采样次数,它自身本质上是一个 Counter 类型的指标
<basename>_bucket{le="<上边界>"} :观测桶的上边界,即样本统计区间,表示样本值小于等于上边界的所有样本数量
<basename>_bucket{le="+Inf"} :最大区间(包含所有样本)的样本数量
●用于分析因异常值而引起的平均值过大的问题;
在大多数情况下人们一般倾向于使用某些量化指标的平均值,例如 CPU 的平均使用率、网页的平均响应时间。这种方式的问题很明显,以系统 API 调用的平均响应时间为例:如果大多数 API 请求都维持在 100ms 的响应时间范围内,而个别请求的响应时间需要 5s,那么就会导致某些 Web 页面的响应时间落到中位数的情况,而这种现象被称为长尾问题。
为了区分是平均的慢还是长尾的慢,最简单的方式就是按照请求延迟的范围进行分组。例如,统计延迟在 0~10 ms 之间的请求数有多少,而 10~20 ms 之间的请求数又有多少。 通过这种方式可以快速分析系统慢的原因。Histogram 和 Summary 都是为了能够解决这样问题的存在,通过 Histogram 和 Summary 类型的监控指标,我们可以快速了解监控样本的分布情况。
http 请求响应时间 <= 0.005 秒 的请求次数为 10
prometheus_http_request_duration_seconds_bucket{handler="/metrics",le="0.005"} 10
http 请求响应时间 <= 0.01 秒 的请求次数为 15
prometheus_http_request_duration_seconds_bucket{handler="/metrics",le="0.01"} 15
http 请求响应时间 <= 0.025 秒 的请求次数为 25
prometheus_http_request_duration_seconds_bucket{handler="/metrics",le="0.025"} 18
prometheus_http_request_duration_seconds_bucket{handler="/metrics",le="0.05"} 18
prometheus_http_request_duration_seconds_bucket{handler="/metrics",le="0.075"} 18
prometheus_http_request_duration_seconds_bucket{handler="/metrics",le="0.1"} 18
prometheus_http_request_duration_seconds_bucket{handler="/metrics",le="0.25"} 25
prometheus_http_request_duration_seconds_bucket{handler="/metrics",le="0.5"} 25
prometheus_http_request_duration_seconds_bucket{handler="/metrics",le="0.75"} 25
prometheus_http_request_duration_seconds_bucket{handler="/metrics",le="1.0"} 25
prometheus_http_request_duration_seconds_bucket{handler="/metrics",le="2.5"} 25
prometheus_http_request_duration_seconds_bucket{handler="/metrics",le="5.0"} 30
prometheus_http_request_duration_seconds_bucket{handler="/metrics",le="7.5"} 30
prometheus_http_request_duration_seconds_bucket{handler="/metrics",le="+Inf"} 30
所有样本值的大小总和,命名为 <basename>_sum
prometheus_http_request_duration_seconds_sum{handler="/metrics"} 10.107670803000001
样本总数,命名为 <basename>_count ,效果与 <basename>_bucket{le="+Inf"} 相同
prometheus_http_request_duration_seconds_count{handler="/metrics"} 20
注意:
bucket(桶)可以理解为是对数据指标值域的一个划分,划分的依据应该基于数据值的分布。即后面的样本是包含前面的样本,例如小于 0.1 毫秒(le="0.1")的请求数量是 18 个,小于 0.25 毫秒(le="0.25")的请求是 25 个,在 le="0.25" 这个桶中是包含了 le="0.1" 这个桶的数据,如果我们要拿到 0.1 毫秒到 0.25 毫秒的请求数量,可以通过两个桶想减得到。
●还可以通过 histogram_quantile() 函数求出百分位数,比如 P50,P90,P99 等数据
histogram_quantile(0.5, prometheus_http_request_duration_seconds_bucket{handler="/metrics"}) 0.05
histogram_quantile() 函数中的第一个参数是百分位,第二个参数是 histogram 指标,这样计算出来的就是中位数,即 P50(排在第 50% 位置的样本值)。
注:
histogram_quantile() 函数在计算分位数时会假定每个区间内的样本满足线性分布状态,因而它的结果仅是一个预估值,并不完全准确
预估的准确度取决于bucket区间划分的粒度:粒度越大,准确度越低
(4)Summary:摘要,类似于 Histogram,也是用来做统计分析的,但 Summary 直接存储的就是百分位数,Summary 的百分位数是客户端计算好直接让 Prometheus 抓取的,不需要 Prometheus 计算即可观察到样本的百分位数,Histogram 是通过内置函数 histogram_quantile 在 Prometheus 服务端计算求出百分位数。
例如:延迟时间、响应大小。超过百分之多少的人要满足需求的话,需要多长时间完成。
Histogram 在客户端仅是简单的桶划分和分桶计数,百分位数计算由 Prometheus Server 基于样本数据进行估算,因而其结果未必准确,甚至不合理的 bucket 划分会导致较大的误差。
Summary 是一种类似于 Histogram 的指标类型,但它在客户端于一段时间内(默认为 10 分钟)的每个采样点进行统计,计算并存储了百分位数数值,Server 端直接抓取相应值即可。
对于每个指标,Summary 以指标名称 <basename> 为前缀,生成如下几个指标序列:
<basename>_sum :统计所有样本值之和
<basename>_count :统计所有样本总数
<basename>{quantile="x"} :统计样本值的百分位数分布情况,分位数范围:0 ≤ x ≤ 1
示例:
prometheus_tsdb_wal_fsync_duration_seconds{quantile="0.5"} 0.012352463
prometheus_tsdb_wal_fsync_duration_seconds{quantile="0.9"} 0.014458005
prometheus_tsdb_wal_fsync_duration_seconds{quantile="0.99"} 0.017316173
prometheus_tsdb_wal_fsync_duration_seconds_sum 2.888716127000002
prometheus_tsdb_wal_fsync_duration_seconds_count 216
从上面的样本中可以得知当前 Promtheus Server 进行 wal_fsync 操作的总次数为 216 次,耗时 2.888716127000002 s。 其中中位数(quantile=0.5)的耗时为 0.012352463 s, 9分位数(quantile=0.9)的耗时为 0.014458005 s。
Histogram 与 Summary 的异同:
它们都包含了 <basename>_sum 和 <basename>_count 指标,Histogram 需要通过 histogram_quantile 函数来计算分位数的值,而 Summary 则直接存储了分位数的值。
2、聚合操作
一般说来,单个指标的价值不大,监控场景中往往需要联合并可视化一组指标,这种联合机制即是指聚合操作,例如,将计数、求和、平均值、分位数等统计函数应用于时间序列的样本之上生成具有统计学意义的结果等。
对查询结果事先按照某种分类机制进行分组(group by)并将查询结果按组进行聚合计算也是较为常见的需求,例如分组统计、分组求平均值、分组求和等。
Prometheus 的聚合操作由聚合函数针对一组值进行计算并返回值作为结果。
Prometheus 内置提供的 11 个聚合函数,也称为聚合运算符:
●sum() :用于将指定指标的所有样本值相加,得到总和。这在计算资源使用总量、请求总数等场景中非常实用。
例如,要计算所有服务器的总内存使用量,可以使用 sum(node_memory_usage_bytes)
●min() :用于确定指定指标中的最小值,有助于发现系统的最低性能水平或者资源使用下限。
例如,查找数据库连接池在过去一天中的最小空闲连接数可以使用 min(db_connection_pool_free_count[1d])
●max() :用于找出指定指标中的最大值,适用于检测系统在特定时间段内的峰值性能。
例如,要获取服务器在过去一小时内的最高 CPU 使用率,可以使用 max(node_cpu_usage[1h])
●avg() :用于保存平均值的数据。
例如,计算平均的网络延迟可以使用 avg(network_latency_ms)
●count() :用于对分组内的时间序列进行数量统计
例如,要计算出请求的状态码为 200 的请求数量可以使用 count(requests_duration_ms{status_code="200"})
●topk() :逆序返回分组内的样本值最大的前 k 个时间序列及其值,即最大的 k 个样本值
●bottomk() :顺序返回分组内的样本值最小的前 k 个时间序列及其值,即最小的 k 个样本值
●quantile() :百分位数,用于评估数据的分布状态,该函数会返回分组内指定的百分位数的值,即数值落在小于等于指定的分位区间的比例
2.1 PromQL 的聚合表达式
PromQL 中的聚合操作语法格式可采用如下面两种格式之一:
● <聚合函数>(向量表达式) by|without (标签)
● <聚合函数> by|without (标签) (向量表达式)
分组聚合:先分组、后聚合
by :仅使用by子句中指定的标签进行分组聚合,结果向量中出现但未被 by 指定的标签则会被忽略;
为了保留上下文信息,使用 by 子句时需要显式指定其结果中原本出现的 job、instance 等一类的标签。
without:不根据指定的标签分组聚合,从结果向量中删除由 without 指定的标签,未指定的那部分标签则用作分组标准
示例:
(1)每台主机 CPU 在最近 5 分钟内的平均使用率
(1 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) by (instance)) * 100
(2)查询 1 分钟的 load average 的时间序列是否超过主机 CPU 数量 2 倍
node_load1 > on (instance) 2 * count (node_cpu_seconds_total{mode="idle"}) by (instance)
(3)计算主机内存使用率
可用内存空间:空闲内存、buffer、cache 指标之和
node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes
已用内存空间:总内存空间减去可用空间
node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)
使用率:已用空间除以总空间
(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) / node_memory_MemTotal_bytes * 100
(4)计算每个 node 节点所有容器总计内存:
sum by (instance) (container_memory_usage_bytes{instance=~"node*"})/1024/1024/1024
(5)计算 node01 节点最近 1m 所有容器 cpu 使用率:
sum (rate(container_cpu_usage_seconds_total{instance="node01"}[1m])) / sum (machine_cpu_cores{instance="node01"}) * 100
#container_cpu_usage_seconds_total 代表容器占用CPU的时间总和
(6)计算最近 5m 每个容器 cpu 使用情况变化率
sum (rate(container_cpu_usage_seconds_total[5m])) by (container_name)
(7)查询 K8S 集群中最近 1m 每个 Pod 的 CPU 使用情况变化率
sum (rate(container_cpu_usage_seconds_total{image!="", pod_name!=""}[1m])) by (pod_name)
#由于查询到的数据都是容器相关的,所以最好按照 Pod 分组聚合