1. 基本概念
1.1 指标(metric)
- 在形式上,所有的 指标(Metric) 都通过如下格式标示:
<metric name>{<label name>=<label value>, ...} - 一个典型的指标如下所示:
1.2 时间序列(time-series)
- 一个时间序列(time series)是一系列带有时间戳信息的值,且它们具有相同的指标名(metric name)和标签集(labels)。
- 每个时间序列都有:
1)一个标识identifier,即指标metric(其由 metric name 和 一组唯一的key="value"标签对 构成)
2)一组样本。每个样本(也称为数据点Point)都有 一个以毫秒为单位的int64 Unix 时间戳 和 一个float64 样本值,换句话说是一个包含 (Timestamp:int64, Value:float64) 的二元组。
- 假设同一个指标,不同时间的值都一样,则可以抽象为如下所示的图来理解时间序列、样本、瞬时向量、区间/范围向量等概念。
2. 指标的逻辑类型
https://www.youtube.com/watch?v=fhx0ehppMGM
2.1 Counter(计数器)
Counter类型的指标其工作方式和计数器一样,只增不减(除非系统发生重置)。
常见的监控指标,如:http_requests_total,node_cpu都是Counter类型的监控指标。
Counter是一个简单但有强大的工具,例如,我们可以在应用程序中记录某些事件发生的次数,通过以时序的形式存储这些数据,我们可以轻松的了解该事件产生速率的变化。
PromQL内置的聚合操作和函数可以让用户对这些数据进行进一步的分析,例如:
• 通过rate()函数获取HTTP请求量的增长率:rate( http_requests_total[5m] )
• 查询当前系统中,访问量前10的HTTP地址:topk( 10, http_requests_total )
2.2 Gauge(仪表盘)
与Counter不同,Gauge类型的指标侧重于反应系统的当前状态。因此这类指标的样本数据可增可减。
常见指标如:node_memory_MemFree(主机当前空闲的内容大小)、node_memory_MemAvailable(可用内存大小)都是Gauge类型的监控指标。
• 对于Gauge类型的监控指标,通过PromQL内置函数delta()可以获取样本在一段时间返回内的变化情况。
例如,计算CPU温度在两个小时内的差异:
delta( cpu_temp_celsius{host="zeus"}[2h] )
• 还可以使用deriv()计算样本的线性回归模型,甚至是直接使用predict_linear()对数据的变化趋势进行预测。
例如,预测系统磁盘空间在4个小时之后的剩余情况:
predict_linear( node_filesystem_free{job="node"}[1h], 4 * 3600 )
2.3 Summary(摘要)
当第一次在instrumentation(即被监控客户端应用)中创建Summary时,需要先指定要计算的分位数以及误差范围。然后每当您想要跟踪特定值时,可以调用摘要对象上的 Observe() 方法,并传入想要跟踪的特定值。
1)构建Summaries
requestDurations := promethus.NewSummary( prometheus.SummaryOpts {
Name: "http_request_duration_seconds",
Help: "A summary of the HTTP request durations in seconds.",
Objectives: map[float64] float64 {
// 最大第 50 个百分位数。绝对误差0.05。
0.5: 0.05,
// 最大第 90 个百分位数。绝对误差0.01。
0.9: 0.01,
// 最大第 99 个百分位数。绝对误差0.001。
0.99: 0.001,
},
})
2)跟踪特定值
例如,如果您刚刚处理了一个耗时 2.3 秒的 HTTP 请求,如下所示,调用Observe() 方法来记录该持续时间。
Summary度量对象将根据流算法自动更新输出分位数。
requestDurations.Observe(2.3)
3)查询
如下所示,摘要被扩展为一组时间序列,每个时间序列对应一个计算的目标分位数,观测值总数count 和 观测值的总和sum。
# HELP http_request_duration_seconds A summary of the HTTP request durations in seconds.
# TYPE http_request_duration_seconds summary
http_request_duration_seconds{quantile="0.5"} 0.052
http_request_duration_seconds{quantile="0.90"} 0.564
http_request_duration_seconds{quantile="0.99"} 2.372
http_request_duration_seconds_sum 88364.234
http_request_duration_seconds_count 227420
因此,在请求延迟(就是我们常说的请求耗时)的场景下,“_count”时间序列表示您已处理的请求总数,“_sum”时间序列是您处理这些请求所花费的总时间。
- 总结和注意事项
实际上,可以将Summary的输出视为Gauge和Counter指标的集合,因此,在 PromQL 中,您可以只使用Counter和Gauge的单独的序列。
只是请不要尝试对多个服务实例或其他标签维度的分位数进行平均或以其他方式聚合,因为没有统计上有效的方法来对百分位数进行平均。下面的查询是不允许的:
# Invalid quantile aggregation.
avg by(job) (http_request_duration_seconds{quantile="0.9"} )
例如,如果您有来自 10 个不同服务实例的 90% 延迟,您无法计算所有实例的总体 90% 延迟。
这就是为什么您通常只看到分布的摘要,而不关心跨维度的聚合。
如果您确实需要聚合,那么需要使用直方图。
还有,Summary是在instrumentation(即被监控客户端应用)中计算的分布,而Histogram是在服务端。
2.4 Histogram(直方图)
Histogram和Summary主用用于统计和分析样本的分布情况。
在大多数情况下人们都倾向于使用某些量化指标的平均值,例如CPU的平均使用率、页面的平均响应时间。这种方式的问题很明显,以系统API调用的平均响应时间为例:如果大多数API请求都维持在100ms的响应时间范围内,而个别请求的响应时间需要5s,那么就会导致某些WEB页面的响应时间落到中位数的情况,而这种现象被称为长尾问题。
为了区分是平均的慢还是长尾的慢,最简单的方式就是按照请求延迟的范围进行分组。例如,统计延迟在0~10ms之间的请求数有多少,而10~20ms之间的请求数又有多少。通过这种方式可以快速分析系统慢的原因。Histogram和Summary都是为了能够解决这样问题的存在,通过Histogram和Summary类型的监控指标,我们可以快速了解监控样本的分布情况。
Histogram不是输出预先计算的分位数,Histogram将输入值计数到一组范围存储桶(ranged buckets)中,以便您了解每个范围类别看到的值数量。
Prometheus 中Histogram的一个具体特点是它们是累积直方图(cumulative histograms),这意味着每个存储桶还包含之前较低范围存储桶的计数。累积直方图的好处是我们只需要定义每个桶范围的上限,因为每个桶隐式地从零开始。
如上图所示,桶边界在Histogram的时间序列中被编码为“le”标签。
当您在仪器中创建Histogram时,您必须向构造函数提供一组存储桶范围,然后就可以在直方图中观察,比如请求持续时间等值,就像处理Summary一样。
requestDurations := promethus.NewHistogram( prometheus.HistogramOpts {
Name: "http_request_duration_seconds",
Help: "A histogram of the HTTP request durations in seconds.",
// 桶上限:第一个桶包含 0.05 秒内完成的所有请求,最后一个桶包含 10 秒内完成的所有请求。
Buckets: []float64{0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10},
})
现在,直方图会自动为您增加正确的存储桶计数器。
requestDurations.Observe(2.3)
查询数据如下所示,每个直方图桶(histogram bucket) 都表示为 一个单独的Counter时间序列,并带有指示该桶的上限值边界的“le”标签。
“le”仅表示小于或等于,因此对于“le”标签为 0.2 的存储桶,这意味着它会计算所有耗时小于或等于 0.2 秒的请求。
您还可以再次获得所有观察值的总和sum 和 计数count,就像Summary一样。
# HELP http_request_duration_seconds A histogram of the HTTP request durations in seconds.
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.05"} 4599
http_request_duration_seconds_bucket{le="0.1"} 24128
http_request_duration_seconds_bucket{le="0.25"} 45311
http_request_duration_seconds_bucket{le="0.5"} 59983
http_request_duration_seconds_bucket{le="0.1"} 60345
http_request_duration_seconds_bucket{le="2.5"} 114003
http_request_duration_seconds_bucket{le="5"} 201325
http_request_duration_seconds_bucket{le="+Inf"} 227420
http_request_duration_seconds_sum 88364.234
http_request_duration_seconds_count 227420
对于 PromQL,您可以将直方图视为一组计数器(Counter)并单独查询其中的每个计数器,但您最常见的事情是根据直方图计算近似百分位数(approximated percentiles)。
您可以使用 histogram_quantile() 函数来做到这一点,这是 PromQL 中唯一真正查看并理解那些“le”存储桶边界标签含义的函数。
histogram_quantile(<target_quantile>, <histogram>)
由于桶是计数器(buckets are counters),在将直方图存储桶传递给histogram_quantile之前,一般总是将rate()或increase()函数包装在直方图桶周围,如下所示。
histogram_quantile(0.9, rate(http_request_duration_seconds_bucket[5m]) )
这样,您就可以 将输入直方图限制为 仅在已知的最近时间范围内发生的事件,而不是从应用程序上次重新启动时起的某个任意长的时间范围。
直方图还允许使用求和运算符在实例或其他标签维度之间进行聚合。
只要始终确保在聚合中保留特殊的“le”标签,这样您最终仍然可以将有效的直方图传递到 histogram_quantile() 函数中。
histogram_quantile(0.9, sum by(path, method, le) (
rate(http_request_duration_seconds_bucket[5m])
))
顺便说一句,由于histogram和summary都会提供观察结果的总和sum 和 计数count,即使没有任何存储桶或分位数,您也可以使用这两种指标类型来计算平均请求延迟。
# 过去 5 分钟的平均请求持续时间
rate(http_request_duration_seconds_sum[5m]) / rate(http_request_duration_seconds_count[5m])
#按照path,method分组,每组聚合的平均请求持续时间
sum by(path, method) (rate(http_request_duration_seconds_sum[5m])) / sum by(path, method) (rate(http_request_duration_seconds_count[5m]))
3. PromQL
3.1 瞬时查询
- 当Prometheus通过Exporter采集到相应的监控指标样本数据后,我们就可以通过PromQL对监控样本数据进行查询。
- 当我们直接使用监控指标名称查询时,可以查询该指标下的所有时间序列(这里的“所有”指的是metri name 和 labels的所有组合,但是每一种组合只返回最新的一条样本)。如:
http_requests_total
等同于:
http_requests_total{}
该表达式会返回指标名称为http_requests_total的所有时间序列:
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)
如上结果所示,20889@1518096812.326 表示“值@时间戳”。由于返回的是瞬时向量,所以@后面的时间戳的值都一样。
- PromQL还支持用户根据时间序列的标签匹配模式来对时间序列进行过滤,目前主要支持两种匹配模式:完全匹配和正则匹配。
1)PromQL支持使用 = 和 != 两种完全匹配模式:
§ 通过使用label = value可以选择那些标签满足表达式定义的时间序列;
§ 反之使用label != value则可以根据标签匹配排除时间序列;
例如,如果我们只需要查询所有http_requests_total时间序列中满足标签instance为localhost:9090的时间序列,则可以使用如下表达式:
http_requests_total{instance="localhost:9090"}
反之使用instance!="localhost:9090"则可以排除这些时间序列:
http_requests_total{instance!="localhost:9090"}
2)除了使用完全匹配的方式对时间序列进行过滤以外,PromQL还可以支持使用正则表达式作为匹配条件,多个表达式之间使用|进行分离:
§ 使用label =~ regx表示选择那些标签符合正则表达式定义的时间序列;
§ 反之使用label !~ regx进行排除;
例如,如果想查询多个环节下的时间序列序列可以使用如下表达式:
http_requests_total { environment =~ "staging|testing|development", method != "GET" }
3.2 范围查询
- 直接通过类似于PromQL表达式http_requests_total查询时间序列时,返回值中只会包含该时间序列中的最新的一个样本值,这样的返回结果我们称之为瞬时向量。而相应的这样的表达式称之为瞬时向量表达式。
- 而如果我们想过去一段时间范围内的样本数据时,我们则需要使用区间向量表达式。区间向量表达式和瞬时向量表达式之间的差异在于在区间向量表达式中我们需要定义时间选择的范围,时间范围通过时间范围选择器[]进行定义。
例如,通过以下表达式可以选择最近5分钟内的所有样本数据:
http_requests_total{}[5m]
该表达式将会返回查询到的时间序列中最近5分钟的所有样本数据:
http_requests_total{code="200", handler="alerts", instance="localhost:9090", job="prometheus", method="get"} = [
1@1518096812.326
1@1518096817.326
1@1518096822.326
...
]
http_requests_total{code="200", handler="graph", instance="localhost:9090", job="prometheus", method="get"} = [
4 @1518096812.326
4@1518096817.326
4@1518096822.326
...
]
通过区间向量表达式查询到的结果我们称为区间向量。
注意,中括号[]中的值,例如:1@1518096812.326表示“值@时间戳”,由于返回值是区间向量,所以中括号中,@后面的时间戳由小到大变化,且互相之间都是相同的时间间隔(scrape interval)。
- 除了使用m表示分钟以外,PromQL的时间范围选择器支持其它时间单位:
• s - 秒
• m - 分钟
• h - 小时
• d - 天
• w - 周
• y - 年
3.3 时间位移操作
- 在瞬时向量表达式或者区间向量表达式中,都是以当前时间为基准:
http_request_total{} # 瞬时向量表达式,选择当前最新的数据
http_request_total{}[5m] # 区间向量表达式,选择以当前时间为基准,向前5分钟内的数据
- 而如果我们想查询,5分钟前的瞬时样本数据,或昨天一天的区间内的样本数据呢? 这个时候我们就可以使用位移操作,位移操作的关键字为offset。
可以使用offset时间位移操作:
http_request_total{} offset 5m
http_request_total{}[1d] offset 1d
3.4 修饰符@
@修饰符允许修改瞬时向量和范围向量的查询时间。@后面的时间是一个unix时间戳,并用float字面量描述。
例如,下面的表达式返回http_requests_total指标在2021-01-04T07:40:00+00:00这个时间点的值:
http_requests_total @ 1609746000
注意,@修饰符总是需要紧跟在选择器后面,下面的语句是正确的:
sum(http_requests_total{method="GET"} @ 1609746000) // GOOD.
下面的语句是不正确的:
sum(http_requests_total{method="GET"}) @ 1609746000 // INVALID.
这同样适用于范围/区间向量。下面的查询,返回http_requests_total指标,在2021-01-04T07:40:00+00:00之前5分钟范围的速率。
rate(http_requests_total[5m] @ 1609746000)
@修饰符支持单独与offset一起使用。偏移量是相对于@修饰符后面的时间。不管@是写在offset的前面还是后面,下面两个查询是等效的:
# offset after @
http_requests_total @ 1609746000 offset 5m
# offset before @
http_requests_total offset 5m @ 1609746000
3.5 返回类型
3.5.1 瞬时向量instant vector
同一时间点的数据点
3.5.2 区间向量Range vector
同一个指标的一段时间范围内的数据点
3.5.3 标量Scalar
标量只有一个数字(浮点型的数字值),没有时序(不带时间戳信息)。例如:10
需要注意的是,当使用 表达式count(http_requests_total),返回的数据类型,依然是瞬时向量(带时间戳)。
用户可以通过内置函数scalar()将单个瞬时向量转换为标量。
3.5.4 字符串
一个简单的字符串值,没有时序(不带时间戳信息)。
例如,直接使用字符串,作为PromQL表达式,则会直接返回字符串:
"this is a string"
'these are unescaped: \n \\ \t'
`these are not unescaped: \n ' " \t
3.6 操作符
3.6.1 算术/数学运算
3.6.1.1 瞬时向量 与 标量
当 瞬时向量 与 标量 之间进行数学运算时,数学运算符会依次作用于瞬时向量中的每一个样本值,从而得到一组新的时间序列。
例如,我们可以通过指标node_memory_free_bytes_total获取当前主机可用的内存空间大小,其样本单位为Bytes。这是如果客户端要求使用MB作为单位响应数据,那只需要将查询到的时间序列的样本值进行单位换算即可:
node_memory_free_bytes_total / (1024 * 1024)
node_memory_free_bytes_total表达式会查询出所有满足表达式条件的时间序列,我们 称该表达式为瞬时向量表达式,而返回的结果成为瞬时向量。
3.6.1.2 瞬时向量 与 瞬时向量
瞬时向量 与 瞬时向量 之间进行数学运算时,过程会相对复杂一点。 例如,如果我们想根据node_disk_bytes_written和node_disk_bytes_read获取主机磁盘IO的总量,可以使用如下表达式:
node_disk_bytes_written + node_disk_bytes_read
那这个表达式是如何工作的呢?依次找到与左边向量元素匹配(标签labels完全一致)的右边向量元素进行运算,如果没找到匹配元素,则直接丢弃。同时新的时间序列将不会包含指标名称。 该表达式返回结果的示例如下所示:
{device="sda",instance="localhost:9100",job="node_exporter"} => 1634967552@1518146427.807 + 864551424@1518146427.807
{device="sdb",instance="localhost:9100",job="node_exporter"} => 0@1518146427.807 + 1744384@1518146427.807
3.6.2 比较/布尔运算
3.6.2.1 瞬时向量 与 标量
瞬时向量 与 标量 进行布尔运算时,PromQL依次比较向量中的所有时间序列样本的值,如果比较结果为true则保留,反之丢弃。
例如,通过数学运算符我们可以很方便的计算出,当前所有主机节点的内存使用率:
(node_memory_bytes_total - node_memory_free_bytes_total) / node_memory_bytes_total
而系统管理员在排查问题的时候可能只想知道当前内存使用率超过95%的主机呢?通过使用布尔运算符可以方便的获取到该结果:
(node_memory_bytes_total - node_memory_free_bytes_total) / node_memory_bytes_total > 0.95
3.6.2.2 瞬时向量 与 瞬时向量
瞬时向量 与 瞬时向量 直接进行布尔运算时,同样遵循默认的匹配模式:依次找到与左边向量元素匹配(标签完全一致)的右边向量元素进行相应的操作,如果没找到匹配元素,则直接丢弃。
3.6.2.3 使用bool修饰符改变布尔运算符的行为
- 布尔运算符的默认行为是对时序数据进行过滤。而在其它的情况下我们可能需要的是真正的布尔结果。
例如,只需要知道当前模块的HTTP请求量是否>=1000,如果大于等于1000则返回1(true)否则返回0(false)。这时可以使用bool修饰符改变布尔运算的默认行为。 例如:
http_requests_total > bool 1000
- 使用bool修改符后,布尔运算不会对时间序列进行过滤,而是直接依次瞬时向量中的各个样本数据与标量的比较结果0或者1。从而形成一条新的时间序列。
http_requests_total{code="200",handler="query",instance="localhost:9090",job="prometheus",method="get"} 1
http_requests_total{code="200",handler="query_range",instance="localhost:9090",job="prometheus",method="get"} 0
- 同时需要注意的是,如果是在两个标量之间使用布尔运算,则必须使用bool修饰符。
2 == bool 2 # 结果为1
- 总结
不加bool修饰符,默认会过滤数据,返回值是真实值;
加上bool修饰符,不会过滤数据,但是返回值只有0或1,布尔运算为true的返回1,为false的返回0。
3.6.3 逻辑/集合运算
- Prometheus支持以下集合运算符:
• and (并且)
• or (或者)
• unless (排除)
- 逻辑/集合运算符仅在瞬时向量之间定义
1)instant-vector1 and instant-vector2
会产生一个由vector1的元素组成的新的向量。该向量包含vector1中完全匹配vector2中的元素组成。
2)instant-vector1 or instant-vector2
会产生一个新的向量,该向量包含vector1中所有的样本数据,以及vector2中没有与vector1匹配到的样本数据。
3)instant-vector1 unless instant-vector2
会产生一个新的向量,新向量中的元素由vector1中没有与vector2匹配的元素组成。
3.6.4 操作符优先级
- 对于复杂类型的表达式,需要了解运算操作的运行优先级
例如,查询主机的CPU使用率,可以使用表达式:
100 * (1 - avg( irate( node_cpu{mode='idle'}[5m] ) ) by(job) )
其中irate是PromQL中的内置函数,用于计算区间向量中时间序列每秒的即时增长率。
- 在PromQL操作符中优先级由高到低依次为:
1) ^
2) *, /, %
3) +, -
4) ==, !=, <=, <, >=, >
5) and, unless
6) or
3.6.5 匹配模式
向量与向量之间进行运算操作时会基于默认的匹配规则:依次找到与左边向量元素匹配(标签labels完全一致)的右边向量元素进行运算,如果没找到匹配元素,则直接丢弃。
接下来将介绍在PromQL中有两种典型的匹配模式:
1)一对一(one-to-one)
2)多对一(many-to-one)或 一对多(one-to-many)。
3.6.5.1 一对一
- 一对一匹配模式 会从操作符两边表达式获取的瞬时向量依次比较,并找到唯一匹配(标签完全一致)的样本值。默认情况下,使用表达式:
vector1 <operator> vector2
- 在操作符两边表达式标签不一致的情况下,可以使用 on(label list) 或者 ignoring(label list)来修改便签的匹配行为。
使用ignoreing可以在匹配时忽略某些便签。而on则用于将匹配行为限定在某些便签之内。
<vector expr> <bin-op> ignoring(<label list>) <vector expr>
<vector expr> <bin-op> on(<label list>) <vector expr>
例如,当存在样本:
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
该表达式会返回在过去5分钟内,HTTP请求状态码为500的在所有请求中的比例。如果没有使用ignoring(code),操作符两边表达式返回的瞬时向量中将找不到任何一个标签完全相同的匹配项。
因此结果如下:
{method="get"} 0.04 // 24 / 600
{method="post"} 0.05 // 6 / 120
同时由于method为put和del的样本找不到匹配项,因此不会出现在结果当中。
3.6.5.2 多对一 和 一对多
- 多对一和一对多两种匹配模式指的是“一”侧的每一个向量元素可以与"多"侧的多个元素匹配的情况。
在这种情况下,必须使用group修饰符:group_left 或者 group_right 来确定哪一个向量具有更高的基数(充当“多”的角色)。
<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>
- 多对一和一对多两种模式,一定是出现在操作符两侧表达式返回的向量标签不一致的情况。因此需要使用ignoring和on修饰符来排除或者限定匹配的标签列表。
例如,当存在样本:
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
使用表达式:
method_code:http_errors:rate5m / ignoring(code) group_left method:http_requests:rate5m
该表达式中,左向量method_code:http_errors:rate5m包含两个标签method和code。而右向量method:http_requests:rate5m中只包含一个标签method,因此匹配时需要使用ignoring限定忽略匹配的标签为code。 在限定匹配标签后,右向量中的元素可能匹配到多个左向量中的元素,因此该表达式的匹配模式为多对一,需要使用group修饰符group_left指定左向量具有更好的基数。
最终的运算结果如下:
{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
提醒:group修饰符只能在比较和数学运算符中使用。在逻辑运算and,unless和or操作中默认与右向量中的所有元素进行匹配。
3.7 聚合函数
3.7.1 适用于瞬时向量
使用聚合操作的语法如下:
<aggr-op> [without|by (<label list>)] ([parameter,] <vector expression>)
或者
<aggr-op>([parameter,] <vector expression>) [without|by (<label list>)]
其中只有count_values, quantile, topk, bottomk支持参数(parameter)。
3.7.1.1 without 和 by
- without用于从计算结果中移除列举的标签,而保留其它标签。
- by则正好相反,结果向量中只保留列出的标签,其余标签则移除。
例如,假设 process_resident_memory_bytes
metric 有job,
instance
和 datacenter
这三个标签:
process_resident_memory_bytes{job="job1",instance="host1",datacenter="dc1"} N1
process_resident_memory_bytes{job="job1",instance="host2",datacenter="dc1"} N2
process_resident_memory_bytes{job="job1",instance="host1",datacenter="dc2"} N3
process_resident_memory_bytes{job="job2",instance="host1",datacenter="dc1"} N4
那么 sum(process_resident_memory_bytes) by (datacenter) 将按照datacenter进行分组,返回每个datacenter组的总内存使用率;
而 sum(process_resident_memory_bytes) without (instance) 将按照 job和datacenter进行分组,返回每个job和datacenter分组的 总内存使用率。
3.7.1.2 常用聚合函数
-
sum
(calculate sum over dimensions)min
(select minimum over dimensions)max
(select maximum over dimensions)avg
(calculate the average over dimensions)group
(all values in the resulting vector are 1)stddev
(calculate population standard deviation over dimensions)stdvar
(calculate population standard variance over dimensions)count
(count number of elements in the vector)count_values
(count number of elements with the same value)bottomk
(smallest k elements by sample value)topk
(largest k elements by sample value)quantile
(calculate φ-quantile (0 ≤ φ ≤ 1) over dimensions)
3.7.2 适用于区间向量
Prometheus有一系列被称为 <smth>_over_time() 的函数。它们仅可以被应用于区间向量。本质上就是窗口函数。
每个这样的函数都接受一个范围向量,并生成一个瞬时向量,其中的元素是每个序列聚合的结果。
在 PromQL 中构造范围向量的唯一方法是在向量选择器后添加“[持续时间]”。例如:http_requests_total[5m]。
3.7.2.1 常用函数
-
avg_over_time(range-vector)
: the average value of all points in the specified interval.min_over_time(range-vector)
: the minimum value of all points in the specified interval.max_over_time(range-vector)
: the maximum value of all points in the specified interval.sum_over_time(range-vector)
: the sum of all values in the specified interval.count_over_time(range-vector)
: the count of all values in the specified interval.quantile_over_time(scalar, range-vector)
: the φ-quantile (0 ≤ φ ≤ 1) of the values in the specified interval.stddev_over_time(range-vector)
: the population standard deviation of the values in the specified interval.stdvar_over_time(range-vector)
: the population standard variance of the values in the specified interval.last_over_time(range-vector)
: the most recent point value in the specified interval.present_over_time(range-vector)
: the value 1 for any series in the specified interval.
avg_over_time
, sum_over_time
, count_over_time
, last_over_time和
present_over_time
按预期处理 native histograms。所有其他函数忽略 histogram样本。
3.7.3 图像化比较 瞬时向量聚合 和 区间向量聚合 的区别
3.8 其他常用函数
3.8.1 增长率
counter指标的原始形式几乎毫无用处。除了进程重新启动时偶尔重置为零之外,它们只会上升。因此,它们的绝对值完全取决于计数器何时从零开始,并且不会告诉您底层系统当前发生的情况。所以,您想知道的是计数器在您选择的某个指定的最近时间窗口内上升的速度有多快。例如,获取当前的请求速率或当前的CPU使用率。
在Prometheus中,有三个函数用于计算计数器指标的增长率——rate,irate和increase函数。这些函数中的每一个都将范围向量作为输入,并返回该窗口下计数器Counter的增长率。
顺便说一句,所有这些函数都需要在提供的窗口下为每个系列找到至少两个样本,以便能够判断计数器上升的速度。否则,他们只会从结果中忽略该系列。因此,请务必确保输入窗口足够大,以保证至少有两个样本。
一般来说,在这三个功能中,我总是建议使用rate函数,除非您有特定原因使用 irate 或increase。
这是因为rate在整个输入时间范围内平滑输出,并且它还始终返回与平滑窗口的大小分离的可预测的每秒基本单位。
如果您将结果保留为基本单位,那么您通常可以更轻松地解释结果,甚至可以将它们与 PromQL 中的其他指标相结合。
3.8.1.1 rate 和 irate
rate函数计算每秒的增长率,该增长率在整个输入范围窗口上进行平滑或平均。
相比之下,irate 函数仅考虑提供的速率计算窗口下的最后两个样本,从而计算出反应更快的瞬时速率。
想象一下,我们有一个经过几次重置的计数器指标,并且我们希望计算其在单个时间点的增长率。
因此,我们从中选择一个五分钟范围窗口,将其输入到rate或increase函数中。出现的第一个问题是这些函数如何处理计数器重置。所以我们知道计数器通常只会上升,因此,我们不想将重置视为实际概念值减少,甚至可能返回无意义的负利率值。因此,为了补偿重置,我们首先扫描整个输入范围以查找值低于前一个值的样本,并假设任何此类值的降低都肯定是重置。
由于计数器在重置后总是从零开始,然后我们可以假设实际增加的数量至少是重置之前的样本值加上随后的较低值。因此,我们可以通过将重置后的样本添加到我们之前看到的值来纠正它们。然后我们可以对窗口下的所有重置执行相同的操作,以尽可能地补偿它们。
然后,我们最终得到一系列经过重置校正的样本,现在可以将其用于最终的速率计算。
注意,上图向您展示了语义上正在发生的事情,但实现效率更高,并且不必生成 校正样本的完整列表。
现在,重置补偿并不完美,因为重置发生时您仍然可能会丢失增量。重置之前计数器发生的任何增量,但那些还没有被拉取的数据将会永远消失,并且最终的rate会稍微太低。但这通常不是什么大问题,只要您确保重置只是偶尔发生并且远不及抓取间隔(scrape interval)的频率。
好的,现在我们有了重置校正的样本,rate和increase函数实际上不再关心除了该范围内的第一个和最后一个样本之外的任何样本。然后,我们计算这些边界样本之间的斜率,该斜率告诉我们计数器在窗口下平均上升的速度。现在对于rate函数,我们已经基本完成了,我们可以将斜率返回为每秒增加的次数。
最后来谈谈irate函数,代表瞬时速率。就像rate一样,irate也计算每秒的增长率,但 irate 不是在整个输入时间窗口上平滑结果,而是只查看窗口下的最后两个样本,无论您传入多少样本。
因此您只需要选择足够长的时间窗口以始终选择至少两个样本,但除此之外,不同时间窗口您会得到完全相同的结果。
顺便说一下, irate 还补偿了窗口下最后两个样本之间发生的重置。
因此,这为您提供了一个增长率,该增长率可以对最新值做出尽可能快的反应,并且尽可能少地平滑。这意味着 irate 会给你非常尖锐的结果,只有对于超级放大的图表才真正有意义,在这种情况下你想看到系统行为的立即变化。
而且您可能永远不想在警报中使用 irate,因为在警报中具有一定程度的平滑和时间容差总是好的。
3.8.1.2 increase
最后,increase函数的行为与rate完全相同,只是它返回提供的窗口上的绝对值增加而不是 每秒增加。
rate函数和increase函数,因为它们的行为几乎完全相同,只是输出单位不同。
3.8.1.3 外推extrapolation
https://www.youtube.com/watch?v=7uy_yovtyqw
如果考虑一下,在上例中,increase函数应该为您提供五分钟窗口内的总值增量,但我们可以看到,我们在窗口下实际拥有的第一个和最后一个样本相距不到五分钟,实际只有四分半钟。
这是一种常见的情况,因为选择窗口通常会相对于基础数据任意对齐。因此,如果我们只返回第一个和最后一个样本之间的原始值差异,我们实际上会给您一个结果,该结果小于底层系统在这五分钟内发生的实际增长。
您可以将其视为假装我们已经在这些时间点上抓取了指标,然后我们将最终的输出值增量基于这些推断的样本。这也意味着可以从仅具有整数增量的请求或事件计数器的增加中获得非整数结果。
不幸的是,当窗口内的计数器行为不能代表窗口边界周围的行为时,这种推断不太理想。对于缓慢移动的计数器(每隔几个小时左右才会增加一次)来说,这可能尤其令人困惑。
看一下这个极端的例子,我们计算了这样一个计数器在一分钟的范围窗口内的增加。外推距离最终与两个样本之间的间隔一样长,因此,虽然窗口下只有 1 个增量,但increase 返回值 2。
对于较长的输入窗口,这种影响不会那么极端,但了解这种行为是有好处的。
现在,我还向您保证涵盖所有边缘情况行为,但有两个例外,我们不会将值一直外推到窗口边界。
首先,一个序列实际上可能在选择窗口的中间开始或结束,在这种情况下,我们可能不想将其一直推断到其各自的边界。否则,我们会产生太多的增量。
为了猜测序列是在窗口下开始还是结束,increase函数会查看第一个和最后一个样本到各自窗口边界的距离。
如果该距离大于窗口下其余样本之间的平均间隔的 1.1 倍,则它推断出朝向各自边界的斜率仅为平均样本间隔的一半。
另外,想象一下在这种情况下会发生什么,其中计数器从窗口下方的低值开始,然后急剧增加,如果我们将其一直延伸到范围窗口的开头,我们会将斜率推断为负样本值。
负样本值对于反度量没有意义,因此,我们也总是在样本值为零时切断样本值的外推。
3.8.2 abs
abs(v instant-vector) 返回输入向量的所有样本的绝对值。
3.8.3 delta和idelta
- delta(v range-vector)
delta(v range-vector) 计算范围向量 v 中每个时间序列元素的第一个值和最后一个值之间的差,返回具有给定deltas和equivalent标签的即时向量。delta 被外推以覆盖范围向量选择器中指定的完整时间范围,因此即使样本值都是整数,也可以获得非整数结果。 以下示例表达式返回现在与 2 小时前的 CPU 温度差异:
delta(cpu_temp_celsius{host="zeus"}[2h])
delta 通过计算一个新的直方图来作用于原生直方图,其中每个component(观测值的总和和计数、桶)是 v 中第一个和最后一个原生直方图中相应component之间的差异。然而,v 中的每个元素都包含结果向量中将缺少该范围内的浮点数和本机直方图样本。
delta 只能与仪表gauges 和本机直方图一起使用,其中component 的行为类似于仪表(所谓的仪表直方图)。所以,delta不会像increase一样存在重置补偿。
- idelta(v range-vector)
idelta(v range-vector) 计算范围向量 v 中最后两个样本之间的差异,返回具有给定deltas和equivalent标签的即时向量。 idelta 只能与Gauge类型指标一起使用。
举例:应用接入流量 (Appid Flow Rate) 每分钟打点量
sum( idelta( hickwall_dispatcher_line_channel_application_output_count{appid=""}[1m] ) ) by (appid) >=0
sum( idelta( ( keep_last_value( hickwall_dispatcher_lineudp_channel_defaultchannel_datapoint_count{appid=""}[1m] ) ) ) ) by (appid) >=0
3.8.4 标签替换——label_replace 和 label_join
- 语法格式
1)label_replace
label_replace(
v instant-vector,
dst_label string,
replacement string,
src_label string,
regex string
)
2) label_join
label_join(
v instant-vector,
dst_label string,
separator string,
src_label_1 string,
src_label_2 string,
...
)
一般来说来说,使用PromQL查询到时间序列后,可视化工具会根据时间序列的标签来渲染图表。例如通过up指标可以获取到当前所有运行的Exporter实例以及其状态:
up{instance="localhost:8080", job="cadvisor"} 1
up{instance="localhost:9090", job="prometheus"} 1
up{instance="localhost:9100", job="node"} 1
这是可视化工具渲染图标时可能根据,instance和job的值进行渲染,为了能够让客户端的图标更具有可读性,可以通过label_replace标签为时间序列添加额外的标签。label_replace的具体参数如下:
label_replace(v instant-vector, dst_label string, replacement string, src_label string, regex string)
该函数会依次对v中的每一条时间序列进行处理,通过regex匹配src_label的值,并将匹配部分relacement写入到dst_label标签中。如下所示:
label_replace(up, "host", "$1", "instance", "(.*):.*")
函数处理后,时间序列将包含一个host标签,host标签的值为Exporter实例的IP地址:
up{host="localhost",instance="localhost:8080",job="cadvisor"} 1
up{host="localhost",instance="localhost:9090",job="prometheus"} 1
up{host="localhost",instance="localhost:9100",job="node"} 1
除了label_replace以外,Prometheus还提供了label_join函数,该函数可以将时间序列中v多个标签src_label的值,通过separator作为连接符写入到一个新的标签dst_label中:
label_join(v instant-vector, dst_label string, separator string, src_label_1 string, src_label_2 string, ...)
label_replace 和 label_join函数提供了对时间序列标签的自定义能力,从而能够更好的于客户端或者可视化工具配合。
3.8.5 histogram_quantile
histogram_quantile(φ scalar, b instant-vector) 注意histogram_quantile仅适用于瞬时向量
https://github.com/prometheus/prometheus/blob/main/promql/quantile.go#L109
histogram_quantile(0.99,rate(server_handle_seconds_bucket{}[1m]))
我们需要计算指标名为server_handle_seconds_bucket 在过去1分钟内的数据的百分之99分位数。
histogram_quantile计算步骤如下:
1) 首先会拿最后一个桶中(因为最后一个桶包含了所有样本的个数)的统计的次数去乘以分位数,看下第99分位是所有样本数据中的第几个,假设用rank变量存储这个结果。
2)拿上一步的计算结果rank值挨个桶比较统计次数,找到第一个桶的次数大于等于rank值的桶。这一步就计算出了99分位的样本是在哪个桶里,假设这个桶的索引位置是b。
3)计算b桶范围的实际容量数量count = buckets[b].count - buckets[b-1].count
4)计算99分位数在b桶中属于第几位,得到新的rank =buckets[len(buckets)-1].count * 0.99 - buckets[b-1].count
5)最后通过下面的计算估算99分位数是多少
bucketStart + (bucketEnd-bucketStart)*(rank/count)
bucketEnd 和bucketStart是桶的上下边界值,估算分位数是多少时,假设数据是线性均匀分布的,所以拿(bucketEnd-bucketStart)*(rank/count) 估算出99分为的数在这个桶内的偏移量。
- 其他函数参见官方文档:Query functions | Prometheus
4. 在HTTP API中使用PromQL
4.1 API响应格式
Prometheus API使用了JSON格式的响应内容。 当API调用成功后将会返回2xx的HTTP状态码。
反之,当API调用失败时可能返回以下几种不同的HTTP状态码:
• 404 Bad Request:当参数错误或者缺失时。
• 422 Unprocessable Entity 当表达式无法执行时。
• 503 Service Unavailiable 当请求超时或者被中断时。
所有的API请求均使用以下的JSON格式:
{
"status": "success" | "error",
"data": {
"resultType": "matrix" | "vector" | "scalar" | "string",
"result": <value>
},
// Only set if status is "error". The data field may still hold additional data.
"errorType": "<string>",
"error": "<string>"
}
4.2 在HTTP API中使用PromQL
通过HTTP API我们可以分别通过 /api/v1/query 和 /api/v1/query_range 查询PromQL表达式当前 或者 一定时间范围内的计算结果。
PromQL表达式可能返回多种数据类型,在响应内容中使用resultType表示当前返回的数据类型,包括以下4种。
4.2.1 瞬时向量:vector
- 当返回数据类型resultType为vector时,result响应格式如下:
[
{
"metric": { "<label_name>": "<label_value>", ... },
"value": [ <unix_time>, "<sample_value>" ]
},
...
]
其中metrics表示当前时间序列的特征维度,value只包含一个唯一的样本。
- 通过使用QUERY API我们可以查询PromQL在特定时间点下的计算结果。
- GET /api/v1/query?query=...&time=...&timeout=...
URL请求参数:
• query=:PromQL表达式。
• time=:用于指定用于计算PromQL的时间戳。可选参数,默认情况下使用当前系统时间。
• timeout=:超时设置。可选参数,默认情况下使用-query,timeout的全局设置。
例如,使用以下表达式查询表达式up在时间点2015-07-01T20:10:51.781Z的计算结果:
$ curl 'http://localhost:9090/api/v1/query?query=up&time=2015-07-01T20:10:51.781Z'
{
"status" : "success",
"data" : {
"resultType" : "vector",
"result" : [
{
"metric" : {
"__name__" : "up",
"job" : "prometheus",
"instance" : "localhost:9090"
},
"value": [ 1435781451.781, "1" ]
},
{
"metric" : {
"__name__" : "up",
"job" : "node",
"instance" : "localhost:9100"
},
"value" : [ 1435781451.781, "0" ]
}
]
}
}
4.2.2 区间向量:matrix
- 当返回数据类型resultType为matrix时,result响应格式如下:
[
{
"metric": { "<label_name>": "<label_value>", ... },
"values": [ [ <unix_time>, "<sample_value>" ], ... ]
},
...
]
其中metrics表示当前时间序列的特征维度,values包含当前事件序列的一组样本。
- 使用QUERY_RANGE API我们则可以直接查询PromQL表达式在一段时间返回内的计算结果。
- GET /api/v1/query_range?query=...&start=...&end=...&step=...&timeout=
URL请求参数:
• query=: PromQL表达式。
• start=: 起始时间。
• end=: 结束时间。
• step=: 查询步长。
• timeout=: 超时设置。可选参数,默认情况下使用-query,timeout的全局设置。
当使用QUERY_RANGE API查询PromQL表达式时,返回结果一定是一个区间向量。
需要注意的是,在QUERY_RANGE API中PromQL只能使用瞬时向量选择器类型的表达式。
例如使用以下表达式查询表达式up在30秒范围内以15秒为间隔计算PromQL表达式的结果。
$ curl 'http://localhost:9090/api/v1/query_range?query=up&start=2015-07-01T20:10:30.781Z&end=2015-07-01T20:11:00.781Z&step=15s'
{
"status" : "success",
"data" : {
"resultType" : "matrix",
"result" : [
{
"metric" : {
"__name__" : "up",
"job" : "prometheus",
"instance" : "localhost:9090"
},
"values" : [
[ 1435781430.781, "1" ],
[ 1435781445.781, "1" ],
[ 1435781460.781, "1" ]
]
},
{
"metric" : {
"__name__" : "up",
"job" : "node",
"instance" : "localhost:9091"
},
"values" : [
[ 1435781430.781, "0" ],
[ 1435781445.781, "0" ],
[ 1435781460.781, "1" ]
]
}
]
}
}
4.2.2.1 绘图描点原理
拿截图的表达式「rate(go_memstats_other_sys_bytes[1m])」 举例,假设时间区间[start,end]被step分成了3小段。
如上图,其中每段的开始时间戳分别是A1,A2,A3,按step进行累加,这3个小的时间段将会产生3个描点,每个描点计算规则如下:
val=rate函数(每个小的时间段开始时间 到 之前的1m的时间范围内 的所有样本)
每个描点,都会执行一次rate函数得到描点的value值,描点的时间戳则是每个小的时间段开始的时间,而计算的样本则是 每个小的时间段开始时间到 之前的1m的时间范围内筛选出来的。
4.2.3 标量:scalar
当返回数据类型resultType为scalar时,result响应格式如下:
[ <unix_time>, "<scalar_value>" ]
由于标量不存在时间序列一说,因此result表示为当前系统时间一个标量的值。
4.2.4 字符串:string
当返回数据类型resultType为string时,result响应格式如下:
[ <unix_time>, "<string_value>" ]
字符串类型的响应内容格式和标量相同。