👉️URL: https://grafana.com/blog/2020/02/04/introduction-to-promql-the-prometheus-query-language/
📝Description:
对学习 PromQL 感兴趣吗?这里有一个介绍性的教程。
PromQL 是查询语言,是 Prometheus 的一部分。除了 PromQL 之外,Prometheus 还提供了一个从实例(任何提供指标的应用程序)中获取指标的刮刀和一个时间序列数据库(TSDB),该数据库在一段时间内存储这些指标。
对 PromQL 的介绍将在很大程度上与具体的工具和 Promheus 的非 PromQL 部分脱钩,以便将重点放在语言本身的特点上。
我建议阅读 Ivana Huckova 的博文 如何用简单的 "Hello World " 项目探索普罗米修斯?,其中有关于用 Grafana 一起设置系列和数据库的有用提示和链接。另外,我们最近在 Grafana 9 中引入了 Prometheus 查询生成器,这使得建立简单和复杂的 Prometheus 查询变得容易–不需要 PromQL 经验。
你也可以通过 Grafana Cloud 在几分钟内开始使用 Prometheus。我们有新的免费和付费的 Grafana Cloud 计划,以满足各种使用情况 - 现在就免费注册。
作为这篇文章的补充,请查看 Ian Billett 在 2019 年 PromCon EU 上的精彩演讲录音:PromQL for Mere Mortals。如果你对 PromQL 小抄感兴趣,可以看看 Timber 的 PromQL for Humans 或 PromLabs 的 PromQL Cheat Sheet。
数据类型
Prometheus 使用三种数据类型来衡量:标量、瞬时向量和范围向量。普罗米修斯最基本的数据类型是标量–它代表一个浮点值。标量的例子包括 0、18.12 和 1000000。普罗米修斯中的所有计算都是浮点运算。
当你在一个时间点上把标量作为一组指标组合在一起时,你会得到即时向量数据类型。当你运行一个查询,只要求一个度量的名称,例如bicycle_distance_meters_total
,响应是一个即时向量。由于度量有名称和标签(我稍后会介绍),一个名称可能包含许多值,这就是为什么它是一个向量而不仅仅是一个标量。
一个随时间变化的向量数组给你带来了范围向量。Grafana 和内置的 Prometheus 表达式浏览器都不能直接用范围向量做图,而是使用针对不同时间点独立计算的即时向量或标量。
正因为如此,范围向量通常被包裹在一个函数中,该函数将其转换为一个即时向量(如速率、三角函数或 increase)来绘制。从语法上讲,当你查询一个即时向量并附加一个时间选择器(如[5m]
)时,你会得到一个范围向量。最简单的范围向量查询是一个带有时间选择器的瞬时向量,如 bicycle_distance_meters_total[1h]
。
标签
一些早期的指标系统只有指标名称来区分不同的指标。这意味着你可能会用诸如 bicycle.distance.miles.monark.7
这样的指标名称来区分 7 档的 Monark 自行车和 2 档的 Brompton 自行车(bicycle.distance.miles.brompton.2
)。在 Prometheus 中,我们使用标签来实现。标签以 {label=value}
的格式写在指标名称后面。
这意味着我们之前的两辆自行车被改写为 bicycle_distance_meters_total{brand="monark", gears="7"}
和bicycle_distance_meters_total{brand="brompton", gears="2"}
。
当基于指标进行查询时,使用同样的格式。因此,bicycle_distance_meters_total
会给我们这个例子中所有自行车的里程数;bicycle_distance_meters_total{gears="7"}
会将结果限制在所有 7 档自行车上。这让我们有了更多的灵活性,而不必像老的格式那样求助于奇怪的正则表达式魔法。
否定和正则表达式(在 go 的 RE2 格式中)通过用 !=
、!~
或=~
或 =
来支持,分别表示不等于、不匹配和匹配。(当在 Grafana 中为变量选择多个值时,它们将以与 =~
兼容的格式表示)。
标签的缺点是,看似无害的查询 bicycle_distance_meters_total
实际上可以返回成千上万的值,而且有时并不直观,哪些查询最终会在 Prometheus 服务器上或在你查询 Prometheus 的客户端上变得很重。(即高基数)
指标类型
普罗米修斯在概念上有四种不同的指标类型。这些指标类型都是由一个或多个标量值表示的,有一些不同的约定,决定了如何使用它们以及它们有什么用处。
**Counter 和 Gauge ** 是基本的度量类型,它们都存储一个标量。计数器 (Counter) 总是向上计数(重启时可能会重置为零),而仪表 (Gauge) 则既可以向上也可以向下。bicycle_distance_meters_total
是一个计数器,因为自行车行驶的公里数不能减少,而 bicycle_speed_meters_per_second
则必须是一个仪表,以允许速度减少。按照惯例,计数器以 _total
结尾,以帮助用户区分计数器和仪表,一目了然。
第三种数据类型是 直方图 (Histogram),它为测量三种不同的东西提供了一个接口。
<metric>_count
是一个计数器,用于存储数据点的总数。<metric>_sum
是一个存储所有数据点相加的值的仪表。总和可以作为所有直方图的计数器,其中负值是不可能的。<metric>_bucket
是一个计数器的集合,其中的标签是用来支持计算数值的分布的。这些桶是累积的,所以所有适用于一个值的桶在插入一个样本时都会增加一个。有一个+Inf
桶,它应该持有与_count
相同的值。
对于一场自行车比赛,完成比赛的自行车手的数量和他们完成比赛所需的小时数可以存储在一个直方图中,其中有 21600, 25200, 28800, 32400, 36000, 39600, +Inf.(时间按惯例存储为秒;这是在 [6, 11]
小时范围内每小时一个桶。)
如果有 2 名自行车运动员在略低于 7 小时内完成比赛,5 名在低于 8 小时内完成比赛,3 名在低于 10 小时内完成比赛,而唯一的一名自行车运动员在两天后完成比赛,那么这个桶将被表示为这样的东西。(在这个例子中,我编造了总和的数值,当然,它可以是任何东西,取决于每个桶中的数值)。
race_duration_seconds_bucket{le="21600"} 0 race_duration_seconds_bucket{le="25200"} 2 race_duration_seconds_bucket{le="28800"} 7 race_duration_seconds_bucket{le="32400"} 7 race_duration_seconds_bucket{le="36000"} 10 race_duration_seconds_bucket{le="39600"} 10 race_duration_seconds_bucket{le="+Inf"} 11 race_duration_seconds_count 11 race_duration_seconds_sum 511200 APACHE |
通过对如何存储直方图有一个共同的约定,Prometheus 可以提供诸如 histogram_quantile
这样的函数(计算直方图的量值–我将进一步讨论这方面的细节),外部工具如 Grafana 可以识别这种格式并提供直方图功能。由于直方图 " 只是 " 一个计数器的集合,所以直方图并没有增加 Prometheus 的整体复杂性。
当使用直方图时,知道桶是如何工作的,以及最大的桶以上的一切都被简单地存储为 “超过最大的桶”,可以帮助你了解你可以从直方图中得到什么样的准确性,并延伸到你可以从计算中期望的准确性。
例如,有一个 +Inf
桶的值明显高于最大的桶,这可能表明你的桶的配置是错误的(而且你从 Prometheus 得到的值是不可靠的)。
最后一种指标类型是 摘要 (summary)。它类似于直方图,但被定义为一个量化指标,由客户计算,以获得更高的量化指标精度。预先计算出的汇总的 summary 不能以有意义的方式进行汇总。你可以研究你的服务的单个实例的 summary,但你不能把它们汇总到整个车队的 summary。
Summary 的一个常见用途是作为服务等级指标(即 SLI/SLO/ SLA),以了解进入服务器的请求中有多大一部分慢于 50ms。有了一个直方图,其中的一个桶是 <0.05 秒,就可以很准确地说出有多少请求没有在这个时间内被处理。添加更多的桶,就有可能计算出量值,这让你对性能有一个概念。对于 Summary 来说,这种 Summary 是完全不可能的。
总而言之,直方图需要你对你的值的分布有一定程度的洞察力,以便开始设置适当的桶,而 Summary 则缺乏可靠的聚合操作。
函数和操作符
衡量标准本身可能是有用的,但为了使其效用最大化,某种操作是必要的,这就是为什么 Prometheus 提供了一些操作符和函数用于操作。
聚合操作符
聚合运算符将一个瞬时向量减少到另一个瞬时向量,代表相同或更少的标签集,通过聚合不同标签集的值或保留一个或多个不同的标签集,取决于其值。聚合运算符的最简单形式如avg(bicycle_speed_meters_per_second)
,它给你集合中自行车的总体平均速度。
如果你想通过标签、品牌和齿轮数来区分自行车,你可以使用 avg(bicycle_speed_meters_per_second) by (brand, gears)
。如果你想为新的向量丢弃一个标签,而不是选择你想保留的标签,可以用without
代替by
。
有许多可用的聚合,最突出的是希望不言自明的 sum
、min
、max
和avg
。一些更复杂的聚合器需要额外的参数,例如topk(3, bicycle_speed_meters_per_second)
,它给你总体的三个最高速度。
二进制运算符
二进制算术运算符(+、-、*
、/
、%[模 / 时钟计数]
、^[幂]
)可以对即时向量和标量的组合进行运算,如果你想在数学上做到合理,这可能会导致一些麻烦。所以我将总结不同的情况,以及如何处理做向量算术时出现的奇怪情况。
标量到标量的算术,其核心是小学时的算术。标量到向量的算术几乎一样简单。对于向量中的每一个值,应用标量进行计算。(如果你有 bicycle_speed_meters_per_second
,并想用更多的公里 / 小时(km/h)来表示,那就用 bicycle_speed_meters_per_second*3.6
来完成)。
向量到向量的算术是它变得真正有趣的地方。正在进行标签匹配,具有完全匹配的标签的向量被相互计算。所有其他的值都被丢弃了。例如:bicycle_speed_meters_per_second / bicycle_cadence_revolutions_per_minute
.
由于这往往不是你想要的,你可以在二进制运算符的右边添加一个on
(或ignoring
)运算符,最终你会在运行前限制被用于比较的标签集。然而,所有没有用于比较的标签都会被丢弃。这将是bicycle_speed_meters_per_second / on (gears) bicycle_cadence_revolutions_per_minute
.
如果你想保留这些标签呢?那么,你可以通过在 on
或ignoring
关键字后面添加 group_left
来保留所有来自左边的标签。当你这样做的时候,右边的值将被应用到每个与 on
的标签相匹配的左边的标签上。在实践中,这看起来像bicycle_speed_meters_per_second / on (gears) group_left bicycle_cadence_revolutions_per_minute
。还有一个group_right
,它是按右手边分组的。
除了算术运算符之外,还有比较(==
, !=
, >
, <
, >=
, <=
)和集合(and
, or
, unless
)运算。
比较操作是为即时向量和标量定义的。两个标量之间的比较,假的返回 0,真的返回 1,需要在比较器后面加上 bool
关键字。对于瞬时向量,当与标量比较时,每一个比较为真的数据点都被保留,而其他的则被丢弃。
当比较两个瞬时向量时,逻辑是类似的,但是每一组标签,标签和值都要进行比较。当操作返回 false 时,或者对面没有与之匹配的标签集的度量,值就被扔掉;否则就被保留。如果你想要 0 或 1,而不是保留或扔掉数值,你可以在比较器后面添加关键字bool
。
集合运算符对即时向量进行操作,通过检查指标上的标签集进行工作。对于 and
,如果左手边和右手边都存在一个标签集,则返回左手边的值;否则,不返回。对于 or
,左边的所有标签集都被返回,右边的标签集如果不存在于左边,也会被返回。最后,unless
返回左侧的标签集不存在于右侧的值。
函数
普罗米修斯中的函数与一般编程中的函数工作原理很相似,但仅限于预先定义的一组。重要的是要知道,Prometheus 的大多数函数都是近似的,并对结果进行推断–这偶尔会把本应是整数的计算变成浮点值,也意味着 Prometheus 在需要精确性的时候确实不好用(例如,为了计费)。
一些特别有用的函数是 delta
, increase
, 和rate
。每个函数都接受一个范围向量作为输入,并返回一个瞬间向量。delta
对 gauge 进行操作,并返回范围的起点和终点之间的差。increase
和 rate
对计数器进行操作,返回计数器在指定时间内增加的数量。increase
给出了时间内的总增长量,rate 给出了每秒的增长量。rate(bicycle_distance_meters_total[1h])
应该与 increase(bicycle_distance_meters_total[1h]) / 3600
相同。因为 increase
和rate
有逻辑来处理值被重置为零时的重启,所以要避免将它们用于上下波动的仪表。这可能最终看起来像是函数的重启,导致一个无意义的值。
为了使直方图桶有意义,histogram_quantile
函数需要两个参数:第一,应该计算的量化,第二,桶的即时向量。对于我们之前的比赛例子,再加上一些标签,这可能是 histogram_quantile(0.95, sum(race_duration_seconds_bucket) by (le))
,它返回 95 分位数的赛车手的完成时间。在进行量化计算之前,我们用by (le)
对完成时间进行求和,是因为量化是按标签的唯一组合计算的。这使得我们可以用 histogram_quantile(0.5, sum(race_duration_seconds_bucket) by (le, gears))
来绘制诸如自行车上每个齿轮数的中位时间。
PromQL 扩展阅读
PromQL 是一种特定领域的语言,它的语法隐藏着一些惊喜,而且并不总是能直观地理解。在我加入 Grafana 实验室之前,我最初写了这篇文章的早期版本,因为我想更好地理解 Prometheus 使用的语法,也因为我注意到很多野路子 PromQL 查询与作者的意图不一致。这篇文章是我对实验和阅读 Prometheus 的文档和源代码的总结。我建议阅读文档中的这些页面,它们几乎涵盖了我在这里写的内容,而且还有很多。