探索PromQL

PromQL基础

Prometheus会将所有采集到的样本数据以时间序列(time-series)的方式保存在内存数据库中,并且定时保存到硬盘上,每个数据称为一个样本。时间序列(time-series)是按照时间戳和值的序列顺序存放的,我们称之为向量(vector). 每条时间序列(time-series)通过指标名称(metrics name)和一组标签集(labelset)命名。如下所示,可以将时间序列(time-series)理解为将多个时间序列(time-series)放在同一个坐标系内(以时间为横轴,以序列为纵轴),将形成一个由数据点组成的矩阵;

可以把以下图想象成Promethus存储

  • 每一行代表一个时间序列(time-series)我们也称为一个向量(Vector)
  • 每一列代表时间的流逝时间点

Prometheus-PromQL-1

在时间序列(time-series)中的每一个点称为一个样本(sample),样本由以下三部分组成:

  • 指标名(metric name):指标名(metric name)和描述当前样本特征的标签(labels);
  • 时间戳(timestamp):一个精确到毫秒的时间戳;
  • 样本值(value): 一个float64的浮点型数据表示当前样本的值。

Prometheus-PromQL-2

Prometheus数据模型

Prometheus中,每个时间序列都由指标名称(Metric Name)和标签(Label)来唯一标识(注意:指标名称相同,但标签不同的组合分别代表着不同的时间序列。不同的指标名称自然更是代表着不同的时间序列。),格式为:

1
<metric name>{<label name>=<label value>, ...}

指标名称(metric name)可以反映被监控样本的含义(比如,http_request_total - 表示当前系统接收到的HTTP请求总量)支持使用字母、数字、下划线和冒号,且必须能匹配RE2规范的正则表达式;
标签(label)键值型数据,附加在指标名称之后,反映了当前样本的特征维度。通过这些维度Prometheus可以对样本数据进行过滤,聚合等。例如,http_requests_total{method=GET}和http_requests_total{method=POST}代表着两个不同的时间序列;标签名称可使用字母、数字和下划线,且必须能匹配RE2规范的正则表达式; 以__为前缀的名称为Prometheus系统预留使用;

在Prometheus的底层实现中指标名称实际上是以__name__=<metric name>的形式保存在数据库中的,因此以下两种方式均表示的同一条时间序列(time-series):

1
api_http_requests_total{method="POST", handler="/messages"}
1
{__name__="api_http_requests_total",method="POST", handler="/messages"}

初识PromQL

Prometheus通过指标名称(metrics name)以及对应的一组标签(labels)唯一定义一条时间序列。

  • 指标名称反映了监控样本的基本标识
  • 而label则在这个基本特征上为采集到的数据提供了多种特征维度。

基于PromQL表达式,用户可以针对指定的特征(指标名)及其细分的纬度(标签)进行过滤、聚合、 统计等运算从而产生期望的计算结果。

PromQL的数据类型

PromQL的表达式中支持4种数据类型

  • 即时向量(Instant Vector):特定或全部的时间序列集合上,具有相同时间戳的一组样本值称为即时向量;
  • 范围向量(Range Vector):特定或全部的时间序列集合上,在指定的同一时间范围内的 所有样本值;
  • 标量(Scalar):一个浮点型的数据值;
  • 字符串(String):支持使用单引号、双引号或反引号进行引用,但反引号中不会对转义 字符进行转义;

时间序列选择器

用户可使用向量表达式来查询某个指标名称下的所有时间序列或部分时间序列(根据标签判断不同的标签标示不同的时间序列)的即时(当前)样本值或过去某个时间范围内的样本值,前者称为即时向量选择器,后者称为范围向量选择器

  • 即时向量选择器(Instant Vector Selectors):返回值中只会包含该时间序列中的最新的一个样本值,这样的返回结果我们称之为即时向量(瞬时向量)
  • 范围向量选择器(Range Vector Selectors) :返回值中只会包含该时间序列中的最新的一组样本值;范围选择器 [] 进行定义。

Prometheus-PromQL-3

即时向量

当我们直接使用监控指标名称查询时,可以查询该指标下的所有时间序列。如:

1
http_requests_total

等同于:

1
http_requests_total{}

该表达式会返回指标名称为http_requests_total的所有时间序列:(以下是两个不同的时间序列,因为他们的标签不同)

1
2
http_requests_total{code="200",handler="alerts",instance="localhost:9090",job="prometheus",method="get"}=(20889@1518096812.326)
http_requests_total{code="200",handler="graph",instance="localhost:9090",job="prometheus",method="get"}=(21287@1518096812.326)

直接通过类似于PromQL表达式http_requests_total查询时间序列时,返回值中只会包含该时间序列中的最新的一个样本值,这样的返回结果我们称之为瞬时向量。而相应的这样的表达式称之为瞬时向量表达式。

范围向量

如果我们想过去一段时间范围内的样本数据时,我们则需要使用范围向量表达式。范围向量表达式和瞬时向量表达式之间的差异在于在范围向量表达式中我们需要定义时间选择的范围,时间范围通过时间范围选择器 [] 进行定义。例如,通过以下表达式可以选择最近5分钟内的所有样本数据:

1
http_requests_total{}[5m]

该表达式将会返回查询到的时间序列中最近5分钟的所有样本数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
http_requests_total{code="200",handler="alerts",instance="localhost:9090",job="prometheus",method="get"}=[
1@1518096812.326
1@1518096817.326
1@1518096822.326
1@1518096827.326
1@1518096832.326
1@1518096837.325
]
http_requests_total{code="200",handler="graph",instance="localhost:9090",job="prometheus",method="get"}=[
4 @1518096812.326
4@1518096817.326
4@1518096822.326
4@1518096827.326
4@1518096832.326
4@1518096837.325
]

通过范围向量表达式查询到的结果我们称为范围向量。

除了使用m表示分钟以外,PromQL的时间范围选择器支持其它时间单位:

1
2
3
4
5
6
s - 秒
m - 分钟
h - 小时
d - 天
w - 周
y - 年

标量(Scalar):一个浮点型的数字值

标量只有一个数字,没有时序。
例如:

1
10

需要注意的是,当使用表达式count(http_requests_total),返回的数据类型,依然是瞬时向量。用户可以通过内置函数scalar()将单个瞬时向量转换为标量。

字符串(String):一个简单的字符串值

直接使用字符串,作为PromQL表达式,则会直接返回字符串。

1
2
3
"this is a string"
'these are unescaped: \n \\ \t'
`these are not unescaped: \n ' " \t`

注意

需要将返回值绘制成图形时,仅支持即时向量类型和标量类型的数据;由于范围向量选择器的返回的是范围向量型数据,它不能用于表达式浏览器中图形绘制功能,否则,表达式浏览器会返回Error executing query: invalid expression type "range vector" for range query, must be Scalar or instant Vector一类的错误,但事实上,范围向量选择几乎总是结合速率类的函数 rateirate 一同使用。

Prometheus-PromQL-4

Prometheus 中有四种指标类型其中一种 Counters(计数型),其值只增不减,除非监控系统发生了重置。你可以使用 counter 指标类型来表示服务的请求数、已完成的任务数、错误发生的次数等。需要使用counter绘图或做任何事之前,通常要借助rate(), irate()或 increase() 函数来查看它的rate(速率)。以下先介绍两个函数:

rate()

此函数计算整个采样周期内每秒的增长率。

例如:rate(http_requests_total[5m]) 得出的是HTTP在5分钟窗口内,平均每秒的请求率。作为最常见的函数,它以可预测的每秒输出单位产生平滑的rate。

手动计算rate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ node_time_seconds[1m]
node_time_seconds{instance="exporter:9100",job="node-resources"}
1596077182.3093214 @1596077182.307 // 第一个点
1596077192.3132203 @1596077192.307
1596077202.311446 @1596077202.307
1596077212.309673 @1596077212.307
1596077222.316771 @1596077222.307
1596077232.3151288 @1596077232.307 // 最后一个点
node_time_seconds{instance="10.0.23.29:9100",job="node-resources"}
1596077178.6314309 @1596077178.633 // 第一个点
1596077188.6312084 @1596077188.633
1596077198.633293 @1596077198.634
1596077208.6332283 @1596077208.634
1596077218.6320524 @1596077218.633
1596077228.635078 @1596077228.633 // 最后一个点

$ rate(node_time_seconds[1m])
{instance="exporter:9100",job="node-resources"} 1.0001161479949952
{instance="10.0.23.29:9100",job="node-resources"} 1.0000729417800902

计算公式

1
2
3
4
最后一个点  -  第一个点  = vulue
最后一个时间戳 - 第一个时间戳 = time

resultValue = value / time

先用10.0.23.29这个 instance 算

1
2
3
4
(1596077228.635078 - 1596077178.6314309) / (1596077228.633 - 1596077178.633)
// web上的时间是秒数的,go的time是多了三个单位,所以代码里/1000转换成秒这里不需要除以1000
上面式子左边和右边算是下面结果:
50.003647089 / 50 = 1.00007294178

谷歌搜的在线计算器算的(比windows的calc精度高一些),由于是float64,所以精度丢失了一些。结果一样。再算下另一个 instance

1
2
(1596077232.3151288 - 1596077182.3093214) / (1596077232.307 - 1596077182.307)
50.0058073997 / 50 = 1.00011614799

irate()

即 “瞬时rate”,此函数和rate()一样,计算每秒的增长率,但只对规定采样周期内的最后两个样本进行计算,而忽略前面所有样本。

例如:irate(http_requests_total[5m]) 选取规定5分钟窗口内的最后两个样本,并计算两者之间每秒的增长率。如果想让一个放大的图形显示出对rate变化的快速响应,那这个函数就很有用,它呈现的结果会比rate()的有更多的峰值。

手动计算irate

流程图
Prometheus-PromQL-5

1
2
3
4
5
6
7
8
9
$ process_cpu_seconds_total[1m]
process_cpu_seconds_total{app="spring-boot", instance="192.168.28.13:9100", job="consul-node-exporter", project="bigdata", team="appgroup"}
46.26 @1622430320.268
46.42 @1622430335.267
46.53 @1622430350.268 # end1 time1
46.65 @1622430365.266 # end2 time2

$ irate(process_cpu_seconds_total[1m])
{app="spring-boot", instance="192.168.28.13:9100", job="consul-node-exporter", project="bigdata", team="appgroup"} 0.008001066808907685

计算公式:选取时间范围内最后两个点:end1,end2

1
2
3
value = end2 - end1
time = time2 - time1
resultValue = value / time

计算结果如下

1
2
(46.65  - 46.53 ) / (1622430365.266 - 1622430350.268)
0.12 / 14.998 = 0.008001066808907855

increase(v range-vector)

此函数和 rate() 完全一样,只是它没有将最终单位转换为 “每秒”(1/s)。每个规定的采样周期就是它的最终单位。

例如:increase(http_requests_total[5m]) 得出的是5分钟的采样周期内处理完成的HTTP请求的总增长量(单位1/5m)。因此increase(foo[5m])/ (5 * 60) 等同于rate(foo[5m])。

1
increase(<metric>[<labels>], <duration>)
  • <metric>:要计算增长量的指标名称。
  • <labels>:指标的标签。可选项,如果指标有标签,可以通过该参数指定标签过滤条件。
  • <duration>:时间段,用于指定增长量的计算范围。

使用increase函数时,需要注意以下几点:

  • 在使用increase函数之前,需要先确保指标具有单调递增的属性。如果指标值出现回退,使用该函数计算的结果将不准确。
  • <duration>参数的单位可以是秒(s)、分钟(m)、小时(h)或天(d)。例如,increase(my_metric[5m])表示计算my_metric指标在过去5分钟内的增长量。

手动计算increase

1
2
3
4
5
6
7
$ node_network_receive_bytes_total{instance=~"10.168.128.11:.+",device=~"(bond0)"}[5m]
node_network_receive_bytes_total{device="bond0", instance="10.168.128.11:9100", job="node"}
6177757842399 @1698832380.983 // 第一个点
6177809129780 @1698832440.983
6177858461314 @1698832500.983
6177937348431 @1698832560.983
6177987251822 @1698832620.983 // 最后一个点

计算公式

1
2
3
最后一个点  -  第一个点  = vulue
最后一个时间戳 - 第一个时间戳 = time
resultValue = value / time * (range-vector * 60) # range-vector表示时间范围,将分钟转换成秒

计算结果

1
2
(6177987251822 - 6177757842399) / (1698832620.983 - 1698832380.983) * (5 * 60)
229409423 / 240 * 300 = 286761778.75

上面提到increase(foo[5m])/ (5 * 60) 等同于rate(foo[5m])

1
2
3
4
5
increase函数计算平均单位每5分钟
(6177987251822 - 6177757842399) / (1698832620.983 - 1698832380.983) * (5 * 60)

rate函数计算的是每秒,所以要将5分钟换乘成秒
(6177987251822 - 6177757842399) / (1698832620.983 - 1698832380.983) * (5 * 60) / (5 * 60)

即时向量选择器

即时向量选择器由两部分组成;

  • 指标名称:用于限定特定指标下的时间序列,即负责过滤指标;可选;
  • 匹配器(Matcher):或称为标签选择器,用于过滤时间序列上的标签;定义在{}之中; 可选;

显然,定义即时向量选择器时,以上两个部分应该至少给出一个;于是,这将存在以下三种组合;

  • 仅给定指标名称,或在标签名称上使用了空值的匹配器:返回给定的指标下的所有时间 序列各自的即时样本;
    • 例如,http_requests_totalhttp_requests_total{}的功能相同,都是用于返回http_requests_total指标下 各时间序列的即时样本;
  • 仅给定匹配器:返回所有符合给定的匹配器的所有时间序列上的即时样本;
    • 注意:这些时间序列可能会有着不同的指标名称;
    • 例如, {team=~".*",project="docker-service"}
  • 指标名称和匹配器的组合:返回给定的指标下的,且符合给定的标签过滤器的所有时间序列上的即时样本;
    • 例如, http_requests_total{method="get"}

合法的PromQL表达式

1
2
3
4
5
http_request_total        # 合法
http_request_total{} # 合法
{method="get"} # 合法
{job=~".+"} # 合法 因为它们都有一个不匹配空标签值的选择器
{job=~".*",method="get"} # 合法 因为它们都有一个不匹配空标签值的选择器
1
{job=~".*"}               # 不合法

标签匹配器也可以通过与内部__name__标签匹配来应用于指标名称 。例如,表达式http_requests_total等效于 {__name__="http_requests_total"}。除了 =、!=、=~、!~ 也可以使用以下表达式选择名称以job开头的所有指标:

1
{__name__=~"job:.*"}

指标名称不能是关键字bool, on, ignored, group_left和group_right中的一个。以下表达式是非法的:

1
2
on{}               # 不合法
{__name__="on"} # 合法

匹配器(Matcher)

匹配器用于定义标签过滤条件,目前支持如下4种匹配操作符;

1
2
3
4
=: 精确地匹配标签给定的值
!=: 不等于给定的标签值
=~: 正则表达匹配给定的标签值(注意是完全匹配)
!~: 给定的标签值不符合正则表达式

注意事项

  • 匹配到空标签值的匹配器时,所有未定义该标签的时间序列同样符合条件;
    • 例如,我们并没有给某个时间序列设置 env 标签但是我们使用 http_requests_total{env=""} 去匹配,则表示未使用该标签(env)的时间序列也符合条件,比如时间序列 http_requests_total{method="get"} 也会被匹配到;
  • 正则表达式将执行完全锚定机制,它需要匹配指定的标签的整个值;
    • 例如,时间序列 prom_http{job=node_exporter} 使用表达式 prom_http{job=~"node"} 无法匹配,必须使用{job=~node.*} 才能匹配到。
  • 向量选择器至少要包含一个指标名称,或者至少有一个不会匹配到空字符串的匹配器;
    • 例如,{job=~""}为非法的选择器;
  • 使用__name__做为标签名称,还能够对指标名称进行过滤;
    • 例如,{__name__=~"http_requests_.*"}能够匹配所有以 http_requests_ 为前缀的所有指标

范围向量选择器

同即时向量选择器的唯一不同之处在于,范围向量选择器需要在表达式后紧跟一个方括号[ ]来表达需在时间时序上返回的样本所处的时间范围;

  • 时间范围:以当前时间为基准时间点,指向过去一个特定的时间长度;例如[5m]便是指过去5分钟之内;

时间格式:一个整数后紧跟一个时间单位,例如“5m”中的“m”即是时间单位 ;

  • 可用的时间单位有ms(毫秒)、s(秒)、m(分钟)、h(小时)、d(天)、w(周) 和y(年);
  • 必须使用整数时间,且能够将多个不同级别的单位进行串联组合,以时间单位由大到小为顺序,例如1h30m,但不能使用1.5h;

需要注意的是,范围向量选择器返回的是一定时间范围内的数据样本,虽然不同时间序列的数据抓取时间点相同,但它们的时间戳并不会严格对齐;

  • 多个Target上的数据抓取需要分散在抓取时间点前后一定的时间范围内(不会同时对所有target进行数据抓取,会分散开抓取数据),以均衡 Prometheus Server的负载;
  • 因而,Prometheus在趋势上准确,但并非绝对精准;

偏移量修改器

  • 默认情况下,即时向量选择器和范围向量选择器都以当前时间为基准时间点,而偏移量修改器能够修改该基准;
  • 偏移量修改器的使用方法是紧跟在选择器表达式之后使用 offset 关键字指定
    • http_requests_total offset 5m,表示获取以http_requests_total为指标名称的所有时间序列在过去5分钟之时的即时样本;
      Prometheus-PromQL-6

    • http_requests_total[5m] offset 30m,表示获取距此刻30分钟之前的5分钟之内的所有样本;

Prometheus-PromQL-7

PromQL的指标类型

PromQL有四个指标类型,它们主要由Prometheus的客户端库使用

  • Counter:计数器,单调递增,除非重置(例如服务器或进程重启);
  • Gauge:仪表盘,可增可减的数据;
  • Histogram:直方图,将时间范围内的数据划分成不同的时间段,并各自评估其样本个数及样本值之和,因而可计算出分位数;
    • 可用于分析因异常值而引起的平均值过大的问题;
    • 分位数计算要使用专用的histogram_quantile函数;
  • Summary:类似于Histogram,但客户端会直接计算并上报分位数;

Prometheus Server并不使用类型信息,而是将所有数据展平为时间序列

Prometheus-PromQL-8

Counter和Gauge

通常,Counter的总数并没有直接作用,而是需要借助于rate、topk、increase和irate 等函数来生成样本数据的变化状况(增长率);

  • rate(http_requests_total[2h]),获取2小内,该指标下各时间序列上的http总请求数的增长速 率;
  • topk(3, http_requests_total),获取该指标下http请求总数排名前3的时间序列;
  • irate(http_requests_total[2h]),高灵敏度函数,用于计算指标的瞬时速率;
    • 基于样本范围内的最后两个样本进行计算,相较于rate函数来说,irate更适用于短期时间范围内的 变化速率分析;

Gauge用于存储其值可增可减的指标的样本数据,常用于进行求和、取平均值、最小值、最大值等聚合计算;也会经常结合PromQL的predict_linear和delta函数使用;

  • predict_linear(v range-vector, t, scalar)函数可以预测时间序列v在t秒后的值,它通过线性回归 的方式来预测样本数据的Gauge变化趋势;
  • delta(v range-vector)函数计算范围向量中每个时间序列元素的第一个值与最后一个值之差,从而展示不同时间点上的样本值的差值;
    • delta(cpu_temp_celsius{host=”web01.magedu.com”}[2h]),返回该服务器上的CPU温度与2小时之前的差异;

Histogram和Summary

对于Prometheus来说,Histogram会在一段时间范围内对数据进行采样(通常是请 求持续时长或响应大小等),并将其计入可配置的bucket(存储桶)中
Histogram事先将特定测度可能的取值范围分隔为多个bucket,并通过对落入bucket内的观测值进行计数以及求和操作
与常规方式略有不同的是,Prometheus取值间隔的划分采用的是累积(Cumulative)区间间隔机制,即每个bucket中的样本均包含了其前面所有bucket中的样本,因而也称为累积直方图

  • 可降低Histogram的维护成本
  • 支持粗略计算样本值的分位数
  • 单独提供了_sum和_count指标,从而支持计算平均值

Prometheus-PromQL-9

根据上图假设我们想监控某个应用在一段时间内的响应时间,最后监控到的样本的响应时间范围为 0s~10s。现在我们将样本的值域划分为不同的区间,即不同的 bucket,每个 bucket 的宽度是 0.2s。那么第一个 bucket 表示响应时间小于等于 0.2s 的请求数量,第二个 bucket 表示响应时间小于等于 0.4s 的请求数量,以此类推。也就是说,每一个 bucket 的样本包含了之前所有 bucket 的样本,所以叫累积直方图。

histogram例子

Prometheus-PromQL-10

如上表,设置bucket=[1,5,10],当实际采样数据如是采样点所示, Observe表示采样点落在该bucket中的数量,即落在[-,1]的样本数为2,即落在[1,5]的样本数为3,即落在[5,10]的杨样本数为1,write是得到的最终结果(histogram的最终结果bucket计数是向下包含的):

1
2
3
4
5
6
[basename]_bucket{le=1”} = 2
[basename]_bucket{le=5”} = 5
[basename]_bucket{le=10”} = 6
[basename]_bucket{le="+Inf"} = 6
[basename]_count = 6
[basename]_sum = 18.8378745

histogram并不会保存数据采样点值,每个bucket只有个记录样本数的counter(float64),即histogram存储的是区间的样本数统计值,因此客户端性能开销相比 Counter 和 Gauge 而言没有明显改变,适合高并发的数据收集。

Histogram 类型的样本会提供三种指标(假设指标名称为 )每个指标有一个基础指标名称,它会提供多个时间序列:

  • 样本的值分布在 bucket 中的数量,命名为 <basename>_bucket{le="<上边界>"}。解释得更通俗易懂一点,这个值表示指标值小于等于上边界的所有样本数量。最大的上边界名称为 <basename>_bucket{le="+Inf"}表示无穷大;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 在总共2次请求当中。http 请求响应时间 <=0.005 秒 的请求次数为0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.005",} 0.0
// 在总共2次请求当中。http 请求响应时间 <=0.01 秒 的请求次数为0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.01",} 0.0
// 在总共2次请求当中。http 请求响应时间 <=0.025 秒 的请求次数为0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.025",} 0.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.05",} 0.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.075",} 0.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.1",} 0.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.25",} 0.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.5",} 0.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.75",} 0.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="1.0",} 0.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="2.5",} 0.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="5.0",} 0.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="7.5",} 2.0
// 在总共2次请求当中。http 请求响应时间 <=10 秒 的请求次数为 2
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="10.0",} 2.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="+Inf",} 2.0
  • 所有样本值的大小总和,命名为<basename>_sum
1
2
// 实际含义:发生的2次 http 请求总的响应时间为 13.107670803000001
io_namespace_http_requests_latency_seconds_histogram_sum{path="/",method="GET",code="200",} 13.107670803000001
  • 总的观测次数,命名为<basename>_count。值和 <basename>_bucket{le="+Inf"}相同。
1
2
// 实际含义:当前一共发生了 2 次 http 请求
io_namespace_http_requests_latency_seconds_histogram_count{path="/",method="GET",code="200",} 2.0

累积间隔机制生成的样本数据需要额外使用内置的histogram_quantile()函数即可根据Histogram指标来计算相应的分位数(quantile),即某个bucket的样本数在所有样本数中占据的比例。分位数可能不太好理解,我举个例子,假设你要计算样本的 9 分位数(quantile=0.9),即表示 90% 的样本的值。

  • histogram_quantile()函数在计算分位数时会假定每个区间内的样本满足线性分布状态,因而它的结果仅是一个预估值,并不完全准确;
  • 预估的准确度取决于bucket区间划分的粒度;粒度越大,准确度越低;
histogram计算方式
  • 其中 bucket 代表事先定义好的bucket
  • upperBound代表这个bucket的上限值
  • count 代表这个小于等于这个upperBound的个数/次数
  • workqueue_work_duration_seconds_bucket{name="crd_openapi_controller",le="10"} 65246
  • 所以上述表达式含义为 workqueue_work_duration_seconds 小于10秒的有65246个

我们现在有这些数据,然后求75分位值

1
2
3
4
5
6
7
8
9
10
11
12
a := []bucket{
{upperBound: 0.05, count: 199881},
{upperBound: 0.1, count: 212210},
{upperBound: 0.2, count: 215395},
{upperBound: 0.4, count: 319435},
{upperBound: 0.8, count: 419576},
{upperBound: 1.6, count: 469593},
{upperBound: math.Inf(1), count: 519593},
}

q75 := bucketQuantile(0.75, a)

  • 其计算逻辑为:根据记录总数和分位值求目标落在第几个bucket段b
  • 根据b得到起始bucket大小bucketStart,终止bucket大小bucketStart ,本bucket宽度 ,本bucket记录数。
  • 根据本段记录数和分位值算出目标分位数在本bucket排行rank
  • 最终的计算方式为分位值=起始bucket大小+(本bucket宽度)*(目标分位数在本bucket排行/本bucket记录数)
  • 换成本例中: q75=0.4+(0.8-0.4)*(70259.75/100141) = 0.6806432929569308
1
return bucketStart + (bucketEnd-bucketStart)*(rank/count)   最终的计算公式

1、根据记录总数和分位值求目标落在第几个bucket段
519593 * 0.75 = 389,694.75 (根据这个值我们可以判断他是在0.4和0.8之间的一个值,也就是落到了第四个bucket中)

2、根据本段记录数和分位值算出目标分位数在本bucket排行rank
389,694.75 - 319435 = 70,259.75 (根据这个值可以算出75%他在第四个bucket中属于哪个位置)

3、最终的计算公式
0.4+(0.8-0.4)*(70259.75/100141) = 0.6806432929569308 (计算出来的最终结果就是75%的请求小于0.6806432929569308s)

1
2
3
4
5
6
7
8
9
2021/02/02 19:08:55 记录总数 = 519593
2021/02/02 19:08:55 目标落在第几个bucket段= 4
2021/02/02 19:08:55 起始bucket大小= 0.4
2021/02/02 19:08:55 终止bucket大小= 0.8
2021/02/02 19:08:55 本bucket宽度= 0.4
2021/02/02 19:08:55 本bucket记录数= 100141
2021/02/02 19:08:55 目标分位数在本bucket排行= 70259.75
2021/02/02 19:08:55 分位值=起始bucket大小+(本bucket宽度)*(目标分位数在本bucket排行/本bucket记录数)
2021/02/02 19:08:55 0.4+(0.8-0.4)*(70259.75/100141) = 0.6806432929569308
Summary

与 Histogram 类型类似,用于表示一段时间内的数据采样结果(通常是请求持续时间或响应大小等),但它直接存储了分位数(通过客户端计算,然后展示出来),而不是通过区间来计算。其中φ是分位点,其取值范围是(0 ≤φ≤ 1);计数器类型指标;如下是几种典型的常用分位点;

  • 0、0.25、0.5、0.75和1几个分位点;
  • 0.5、0.9和0.99几个分位点;
  • 0.01、0.05、0.5、0.9和0.99几个分位点;

Summary 类型的样本也会提供三种指标(假设指标名称为 ):

  • 样本值的分位数分布情况,命名为 <basename>{quantile="<φ>"}
1
2
3
4
// 含义:这 12 次 http 请求中有 50% 的请求响应时间是 3.052404983s
io_namespace_http_requests_latency_seconds_summary{path="/",method="GET",code="200",quantile="0.5",} 3.052404983
// 含义:这 12 次 http 请求中有 90% 的请求响应时间是 8.003261666s
io_namespace_http_requests_latency_seconds_summary{path="/",method="GET",code="200",quantile="0.9",} 8.003261666
  • 所有样本值的大小总和,命名为 <basename>_sum
1
2
// 含义:这12次 http 请求的总响应时间为 51.029495508s
io_namespace_http_requests_latency_seconds_summary_sum{path="/",method="GET",code="200",} 51.029495508
  • 样本总数,命名为 <basename>_count
1
2
// 含义:当前一共发生了 12 次 http 请求
io_namespace_http_requests_latency_seconds_summary_count{path="/",method="GET",code="200",} 12.0

根据前面分别对Summary和Histogram的描述,很显然Summary和Histogram计算quantile有很大的差别。另外,它们之间一个重要的区别在于,Summary对quantile的计算是在client端完成的,而Histogram对quantile的计算是在server端完成的

如果需要了解某个时间段内的请求的响应时间,则通常使用平均响应时间,但这样做无法体现数据的长尾效应。如,一个HTTP服务器的正常响应时间是30ms,但是有很少几次请求耗时3s,通过平均响应时间很难甄别长尾效应。这时候可以通过Histogram或者Summary展现。Histogram和Summary这两种指标类型在本质上是可以相互转化的。
在Summary中使用到了分位数,φ代表分位数,0≤φ≤1。例如,0.9分为数代表第90%位置上的数,0.95分位代表95%位置上的数。如Promethues_tsdb_wal_fsync_duration_seconds 指标为Promthues Server中WAL写入磁盘的同步时间:

1
2
3
4
5
6
7
# HELP prometheus_tsdb_wal_fsync_duration_seconds Duration of WAL fsync.
# TYPE prometheus_tsdb_wal_fsync_duration_seconds summary
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

从而得出,当前Prometheus Server进行wal_fsync操作的总次数为216次,总共耗时2.888716127000002s。有50%的同步时间低于0.012352463s,有90%的同步时间低于0.014458005s。

现在可以总结一下 Histogram 与 Summary 的异同:

  • 它们都包含了 <basename>_sum<basename>_count 指标
  • Histogram 需要通过 <basename>_bucket 来计算分位数,而 Summary 则直接存储了分位数的值。
  • Summary不支持sum或avg一类的聚合运算,而且其分位数由客户端计算并生成, Server端无法获取客户端未定义的分位数,而Histogram可通过PromQL任意定义,有着较好的灵活性;

探索PromQL
https://system51.github.io/2021/05/27/PromQL/
作者
Mr.Ye
发布于
2021年5月27日
许可协议