vlambda博客
学习文章列表

读书笔记《hands-on-infrastructure-monitoring-with-prometheus》普罗米修斯查询语言--PromQL

Prometheus Query Language - PromQL

Prometheus 提供了一种强大而灵活的查询语言,以利用其多维数据模型,该模型允许临时聚合和时间序列数据的组合。在本章中,我们将介绍 PromQL、它的语法和语义。凭借这种语言的知识和特性,我们将能够释放 Prometheus 的真正潜力。

简而言之,本章将涵盖以下主题:

  • The test environment for this chapter
  • Getting to know the basics of PromQL
  • Common patterns and pitfalls
  • Moving on to more complex queries

The test environment for this chapter

在本章中,我们将使用基于 Kubernetes 的环境来生成测试本章中介绍的 PromQL 示例所需的所有指标。使用 Prometheus Operator,这个环境的设置非常简单;通过以下步骤让自己启动并运行:

  1. To start the Kubernetes test environment, we first must ensure there's no instance of minikube running:
minikube status
minikube delete
  1. Start a new minikube instance with the following specifications:
minikube start \
--cpus=2 \
--memory=3072 \
--kubernetes-version="v1.14.0" \
--vm-driver=virtualbox

上一条命令完成后,应该可以使用新的 Kubernetes 环境。

对于我们的 Kubernetes 测试环境,我们将基于我们在 第 5 章运行 Prometheus 服务器,并将在我们的工作流程中使用 Prometheus Operator。由于我们已经介绍了 Prometheus Operator 设置,因此我们将部署所有必需的组件,而无需详述每个组件。

  1. Step into this chapter number, relative to the code repository root path:
cd ./chapter07/
  1. Deploy the Prometheus Operator and validate the successful deploy:
kubectl apply -f ./provision/kubernetes/bootstrap/
kubectl rollout status deployment/prometheus-operator -n monitoring
  1. Wait a few seconds for the Prometheus Operator to be able to execute requests and deploy the Prometheus server:
kubectl apply -f ./provision/kubernetes/prometheus/
kubectl rollout status statefulset/prometheus-k8s -n monitoring

因此,我们有一些指标提供者,我们将部署我们在 第 6 章导出器和集成,具体如下:

  • Node Exporter
  • cAdvisor
  • kube-state-metrics

我们还将部署一种 Hello World 应用程序,Hey,我们在 第 5 章运行 Prometheus 服务器,以便 Prometheus 也收集 Web 应用程序指标。

为了简化部署所有组件和配置所需的工作,以下命令抽象了所需的所有步骤,我们在前面的章节中也经历了这些步骤:

kubectl apply -f ./provision/kubernetes/services/
kubectl get servicemonitors --all-namespaces

片刻之后,您应该准备好 Prometheus 和所有服务并可用。以下指令应在您的默认 Web 浏览器中打开 Prometheus Web 界面:

minikube service prometheus-service -n monitoring

如果您浏览 /targets 端点,您将看到类似于以下内容的内容:

读书笔记《hands-on-infrastructure-monitoring-with-prometheus》普罗米修斯查询语言--PromQL
Figure 7.1: Prometheus /targets endpoint showing all the configured targets

您现在可以使用这个新创建的测试环境来学习本章中的示例。

Getting to know the basics of PromQL

了解 Prometheus 查询语言对于能够执行富有洞察力的仪表板、容量规划和警报至关重要。但为此,我们需要从学习基础知识开始。以下主题将涵盖可用于构建查询的组件,并研究它们如何协同工作。

Selectors

Prometheus 旨在处理数十万个时间序列。每个指标名称可以有几个不同的时间序列,具体取决于标签的组合;当来自不同工作的名称相似的指标混合在一起时,查询正确的数据可能看起来很困难,甚至完全令人困惑。在 Prometheus 中,选择器指的是一组标签匹配器。度量名称也包含在此定义中,因为从技术上讲,它的内部表示也是一个标签,尽管是一个特殊的标签:__name__。选择器中的每个标签名称/值对称为标签匹配器,可以使用多个匹配器进一步过滤选择器匹配的时间序列。标签匹配器用大括号括起来。如果不需要匹配器,可以省略大括号。选择器可以返回即时或范围向量。这是一个示例选择器:

prometheus_build_info{version="2.9.2"}

此选择器等效于以下内容:

{__name__="prometheus_build_info", version="2.9.2"}

现在让我们看看标签匹配器是如何工作的。

Label matchers

匹配器用于将查询搜索限制为一组特定的标签值。我们将使用 node_cpu_seconds_total 指标来举例说明四个可用的标签匹配器运算符:=!==~ !~。在没有任何匹配规范的情况下,仅此指标会返回一个即时向量,其中包含包含指标名称的所有可用时间序列,以及 CPU 核心编号的所有组合(cpu=”0” cpu=”1”) 和 CPU 模式 (mode="idle", mode="iowait", mode="irq" , mode="nice", mode="softirq", mode="steal", mode="user" , mode="system"),总共有 16 个时间序列,如下图所示:

读书笔记《hands-on-infrastructure-monitoring-with-prometheus》普罗米修斯查询语言--PromQL
Figure 7.2: node_cpu_seconds_total query resulting in 16 time series being returned

现在,让我们分别使用四个可用的标签匹配器(=!==~ !~) 以不同的方式限制查询并分析产生的结果。

使用 =,我们可以对标签值进行精确匹配。例如,如果我们只匹配 CPU 内核 0,它将返回一个即时向量,其中包含上一个查询的一半时间序列:

读书笔记《hands-on-infrastructure-monitoring-with-prometheus》普罗米修斯查询语言--PromQL
Figure 7.3: Query node_cpu_seconds_total only on CPU core 0

我们还可以使用 != 匹配器否定匹配以获取所有剩余的时间序列。一旦应用于我们的示例,它将仅返回剩余的八个时间序列,如下所示:

读书笔记《hands-on-infrastructure-monitoring-with-prometheus》普罗米修斯查询语言--PromQL
Figure 7.4: Query node_cpu_seconds _total for all time series except core 0

在选择时间序列时,不要仅仅依赖精确匹配,能够应用正则表达式也很重要。因此,=~!~ 是此操作的 PromQL 匹配器,它们都接受 RE2 类型的正则表达式语法。请记住,使用这些匹配器时,正则表达式是锚定的。这意味着它们需要匹配完整的标签值。您可以通过在正则表达式的开头和结尾添加 .* 来取消锚定表达式。

The regular expression syntax that's accepted by RE2 can be found at: https://github.com/google/re2/wiki/Syntax.

查看我们的示例,如果我们只对两种 CPU 模式感兴趣,mode="user"mode="system",我们可以轻松执行如下查询,有效地只选择我们需要的模式:

读书笔记《hands-on-infrastructure-monitoring-with-prometheus》普罗米修斯查询语言--PromQL
Figure 7.5: Query node_cpu_seconds_total only for mode="user" and mode="system"

考虑到 RE2 不支持负前瞻,并且类似于否定匹配器,Prometheus 提供了一种方法来否定正则表达式匹配器,使用 !~。此匹配器排除与表达式匹配的结果并允许所有剩余的时间序列。这是一个例子:

读书笔记《hands-on-infrastructure-monitoring-with-prometheus》普罗米修斯查询语言--PromQL
Figure 7.6: Query node_cpu_seconds_total for all time series except mode="user" and mode="system"

现在我们已经很好地理解了标签匹配器的工作原理,让我们看看即时向量。

Instant vectors

即时向量选择器之所以这样命名,是因为它们返回与查询评估时间相关的样本列表,用于匹配它们的时间序列。该列表称为即时向量,因为它是给定瞬间的结果。

样本是时间序列的一个数据点,由一个值和一个时间戳组成。在大多数情况下,这个时间戳反映了抓取发生的时间以及该值被摄取的时间,但推送到 Pushgateway 的指标除外,由于其性质,这些指标永远不会有时间戳。但是,如果对时间序列应用函数或执行操作,则即时向量样本的时间戳将反映查询时间而不是摄取时间。

即时向量的运行方式——仅返回与查询时间匹配的与选择器匹配的最新样本——意味着 Prometheus 不会返回被认为是陈旧的时间序列(正如我们在 第 5 章运行 Prometheus 服务器)。当原始目标从发现机制中消失,或者如果它们在最后一次成功存在它们的位置后不存在于刮中时,就会插入一个过时标记(一种将时间序列标记为过时的特殊样本)。使用即时向量选择器时,将不会返回具有陈旧标记作为其最后一个样本的时间序列。

标签匹配器部分中的每个示例都是即时向量选择器,因此每个结果都是即时向量。

Range vectors

范围向量选择器类似于即时向量选择器,但它返回给定时间范围内的一组样本,以及与其匹配的每个时间序列。请记住,给定值的时间戳可能与不同目标的抓取时间不完全一致,因为 Prometheus 将抓取分布在其定义的时间间隔内,从而减少了同一时刻的重叠抓取。

要定义范围向量选择器查询,您必须设置一个即时向量选择器并使用方括号 [ ] 附加一个范围。

下表详细说明了用于定义范围的可用时间单位:

缩写

单位

s

m

分钟

h

小时

d

w

与持续时间类似,如 第 5 章中所述,运行Prometheus 服务器,时间范围始终是具有单个单位的整数值。例如,1.5d 和 1d12h 被视为错误,应表示为 36h。持续时间忽略闰秒和闰日:一周总是正好是 7 天,而一年是 365 天。

让我们把它付诸实践。以 Hey 应用程序为例,我们将检查在过去两分钟内收集的 HTTP 代码 200 的样本:

http_requests_total{code="200"}[2m]

以下是上述代码的输出:

读书笔记《hands-on-infrastructure-monitoring-with-prometheus》普罗米修斯查询语言--PromQL
Figure 7.7: Two minutes of samples of the http_requests_total metric for the HTTP code 200

正如我们在前面的屏幕截图中所见,范围向量选择器返回的 Hey 应用程序的每个实例都有四个可用样本(由 30 秒的抓取间隔定义)。

The offset modifier

offset 修饰符允许您查询过去的数据。这意味着我们可以相对于当前时间偏移即时或范围向量选择器的查询时间。它是在每个选择器的基础上应用的,这意味着偏移一个选择器而不是另一个选择器有效地解锁了将每个匹配时间序列的当前行为与过去行为进行比较的能力。要使用这个修饰符,我们需要在选择器之后指定它并添加偏移时间;例如:

读书笔记《hands-on-infrastructure-monitoring-with-prometheus》普罗米修斯查询语言--PromQL
Figure 7.8: Two minutes of samples of the http_requests_total metric of the past hour for the HTTP code 200
Despite not being directly related to PromQL, it's important to be aware of the moment feature of the Prometheus expression browser. This feature changes the query moment as if we went back to a specific date and time. The main difference between the moment picker and using offset is that the former is absolute while the latter is relative time shifting.

Subqueries

在 Prometheus 2.7.0 中引入子查询选择器之前,没有直接的方法将返回即时向量的函数的输出提供给范围向量函数。为此,您需要将生成所需即时向量的表达式记录为新的时间序列,也称为记录规则——我们将在 第 9 章定义警报和记录规则等待为了它有足够的数据,然后使用适当的范围矢量选择器将记录的系列输入范围矢量函数。子查询选择器通过允许评估随时间返回即时向量并将结果作为范围向量返回的函数来简化此过程,而无需等待记录规则来捕获足够的数据。子查询语法类似于范围向量,增加了能够指定应捕获样本的频率的细节,我们很快就会看到。

我们将使用以下查询示例来解释其语法:

max_over_time(rate(http_requests_total{handler="/health", instance="172.17.0.9:8000"}[5m])[1h:1m])

将查询拆分为其组件,我们可以看到以下内容:

组件

Description

rate(http_requests_total{handler="/health", instance="172.17.0.9:8000"}[5m])

The inner query to be run, which in this case is aggregating five minutes' worth of data into an instant vector.
[1h Just like a range vector selector, this defines the size of the range relative to the query evaluation time.
:1m] The resolution step to use. If not defined, it defaults to the global evaluation interval.
max_over_time The subquery returns a range vector, which is now able to become the argument of this aggregation operation over time.

这是一个常见的用例,因为在可能的情况下公开计数器是一种很好的做法(除了本质上是仪表的明显例外,例如当前内存占用),然后评估它们对失败的刮擦有弹性,但最有趣的是函数采用仪表范围。

评估子查询的成本相当高,因此强烈建议不要将它们用于仪表板,因为如果有足够的时间,记录规则会产生相同的结果。同样,出于同样的原因,它们不应用于记录规则。子查询最适合探索性查询,在这种情况下,事先不知道随着时间的推移需要查看哪些聚合。

Operators

PromQL 允许使用二进制、向量匹配和聚合运算符。在接下来的部分中,我们将逐一介绍,并提供有关如何以及何时使用它们的示例。

Binary operators

除了瞬时向量和范围向量,Prometheus 还支持标量类型的值,它由没有任何维度的单个数字组成。

在接下来的小节中,我们将探讨每个二元运算符:算术运算符和比较运算符。

Arithmetic

算术运算符提供两个操作数之间的基本数学运算。

有三种可用的操作数组合。最简单的是在两个标量之间,在应用所选算术运算符后将返回一个标量。我们还可以组合一个瞬时向量和一个标量,它将在标量和瞬时向量的每个样本之间应用选择的算术运算符,有效地返回具有更新样本的相同瞬时向量。我们可以拥有的最后一个组合是在两个即时向量之间。在这种情况下,在左侧的向量和右侧向量的匹配元素之间应用算术运算符,同时删除度量名称。如果不存在匹配项,则这些样本将不会成为结果的一部分。这种情况将在向量匹配部分中进一步解释。

作为参考,可用的算术运算符如下:

操作员

说明

+

添加

-

减法

*

乘法

/

分配

%

模数

^

力量

Comparison

如下表所示的比较运算符对于过滤结果很有用:

操作员

说明

==

平等的

!=

不相等

>

比...更棒

<

少于

>=

大于或等于

<=

小于或等于

比如说,我们有以下即时向量:

process_open_fds{instance="172.17.0.10:8000", job="hey-service"} 8
process_open_fds{instance="172.17.0.11:8000", job="hey-service"} 23

为此,我们应用如下比较运算符:

process_open_fds{job="hey-service"} > 10

结果如下:

process_open_fds{instance="172.17.0.11:8000", job="hey-service"} 23

这个操作表明我们已经有效地过滤了即时向量的结果,这是警报的基础,我们将在后面讨论,在 第 9 章定义警报和记录规则

此外,我们可以使用 bool 修饰符不仅返回所有匹配的时间序列,还可以将每个返回的样本修改为 1 或 0,具体取决于比较运算符是保留还是删除样本。

使用 bool 修饰符是比较标量的唯一方法;例如, 42 == bool 42

因此,我们可以将带有 bool 修饰符的相同查询应用于我们之前的示例:

process_open_fds{job="hey-service"} > bool 10

这将返回以下内容:

process_open_fds{instance="172.17.0.10:8000", job="hey-service"} 0
process_open_fds{instance="172.17.0.11:8000", job="hey-service"} 1

Vector matching

顾名思义,向量匹配是一种仅在向量之间可用的操作。到目前为止,我们已经了解到,当我们有一个标量和一个瞬时向量时,标量会应用于瞬时向量的每个样本。但是,当我们有两个即时向量时,我们如何匹配它们的样本呢?我们将在以下小节中解决这个问题。

One-to-one

由于二元运算符需要两个操作数,正如我们之前描述,当相同大小和标签集的向量位于一个运算符的每一侧时,即一对一时,具有精确的样本相同的标签/值对匹配在一起,而度量名称和所有不匹配的元素被删除。

让我们考虑一个例子。我们将从使用以下即时向量开始:

node_filesystem_avail_bytes{instance="172.17.0.13:9100", job="node-exporter-service", mountpoint="/Users"} 100397019136
node_filesystem_avail_bytes{instance="172.17.0.13:9100", job="node-exporter-service", mountpoint="/data"} 14120038400
node_filesystem_size_bytes{instance="172.17.0.13:9100", job="node-exporter-service", mountpoint="/Users"} 250685575168
node_filesystem_size_bytes{instance="172.17.0.13:9100", job="node-exporter-service", mountpoint="/data"} 17293533184

然后我们将应用以下操作:

node_filesystem_avail_bytes{} / node_filesystem_size_bytes{} * 100

这将返回生成的即时向量:

{instance="172.17.0.13:9100", job="node-exporter-service", mountpoint="/Users"} 40.0489813060515
{instance="172.17.0.13:9100", job="node-exporter-service", mountpoint="/data"} 81.64923991971679

聚合标签不匹配的向量可能很有用。在这些情况下,您可以在二元运算符之后应用 ignoring 关键字来忽略指定的标签。此外,还可以通过在二元运算符后使用 on 关键字来限制应使用双方的哪些标签进行匹配。

Many-to-one and one-to-many

有时,您需要执行其中一侧的元素与操作的另一侧的多个元素匹配的操作。发生这种情况时,您需要向 Prometheus 提供解释此类操作的方法。如果较高的基数在操作的左侧,您可以在 onignoring 之后使用 group_left 修饰符;如果它在右侧,则应应用 group_rightgroup_left 操作通常用于从表达式的右侧复制标签,这将在本章后面的一些实际示例中看到。

Logical operators

逻辑运算符最容易理解为它们的集合论对应物,如下表所示。这些运算符是 PromQL 中唯一可以多对多工作的运算符。表达式之间可以使用三种逻辑运算符:

操作员

说明

路口

联盟

除非

补充

and 逻辑运算符的工作原理是仅在右侧的表达式具有匹配标签键/值对的结果时从左侧返回匹配项。左侧没有匹配项的所有其他时间序列都将被丢弃。生成的时间序列将保留左侧操作数中的名称。这就是为什么它也被称为 intersection 运算符。 and 运算符通常像 if 语句一样使用:通过使用右侧的表达式作为返回左侧表达式的条件。

以下面的即时向量为例,我们将验证前面的语句:

node_filesystem_avail_bytes{instance="172.17.0.13:9100", job="node-exporter-service", mountpoint="/Users"} 1003970
node_filesystem_avail_bytes{instance="172.17.0.13:9100", job="node-exporter-service", mountpoint="/data"} 141200
node_filesystem_size_bytes{instance="172.17.0.13:9100", job="node-exporter-service", mountpoint="/Users"} 2506855
node_filesystem_size_bytes{instance="172.17.0.13:9100", job="node-exporter-service", mountpoint="/data"} 172935

我们将应用以下表达式:

node_filesystem_size_bytes > 100000 and node_filesystem_size_bytes < 200000

这将返回以下内容:

node_filesystem_avail_bytes{instance="172.17.0.13:9100", job="node-exporter-service", mountpoint="/data"} 141200
node_filesystem_size_bytes{instance="172.17.0.13:9100", job="node-exporter-service", mountpoint="/data"} 172935

联合逻辑运算符 通过从左侧返回元素来工作,除非没有匹配项,否则它将从右侧返回元素。同样,双方都需要有匹配的标签名称/值。

我们可以重用之前的数据样本并应用以下表达式:

node_filesystem_avail_bytes > 200000 or node_filesystem_avail_bytes < 2500000

结果如下:

node_filesystem_avail_bytes{instance="172.17.0.13:9100", job="node-exporter-service", mountpoint="/Users"} 1003970

最后,unless 逻辑运算符将返回第一个表达式中与第二个表达式中的标签名称/值对不匹配的元素。在集合论中,这称为补码。实际上,此运算符的工作方式与 and 相反,这意味着它也可以用作 if not 语句。

再一次,我们将使用我们之前在应用以下表达式时使用的相同样本数据:

node_filesystem_avail_bytes unless node_filesystem_avail_bytes < 200000

这反过来又为我们提供了以下结果:

node_filesystem_avail_bytes{instance="172.17.0.13:9100", job="node-exporter-service", mountpoint="/Users"} 1003970
node_filesystem_size_bytes{instance="172.17.0.13:9100", job="node-exporter-service", mountpoint="/Users"} 2506855

Aggregation operators

通过使用聚合运算符,我们可以获取一个即时向量并聚合其元素,从而产生一个新的即时向量,通常包含更少的元素。像这样的即时向量的每个聚合都按照我们在 第 4 章,Prometheus 度量基础

可用的聚合运算符如下:

操作员

说明

要求

总和

对元素求和

分钟

选择最小元素

最大

选择最大元素

平均

计算元素的平均值

stddev

计算元素的标准偏差

stdvar

计算元素的标准方差

计数

计算元素的数量

count_values

计算具有相同值的元素的数量

底部

样本中较低的 k 元素

需要元素的数量 (k) 作为标量

topk

样本值较高的 k 元素

需要元素的数量 (k) 作为标量

分位数

计算元素的分位数

需要将分位数 (0 ≤ φ ≤ 1) 定义为标量

需要参数的运算符(如count_valuesbottomktopkquantile)需要指定在向量表达式之前。有两个可用的修饰符与带有标签名称列表的聚合运算符一起使用: without 允许您定义要聚合的标签,有效地从结果向量中删除这些标签,而 by 正好相反;也就是说,它允许您指定要防止聚合的标签。每个聚合运算符只能使用一个修饰符。这些修饰符将影响运算符将聚合哪些维度。

例如,假设我们使用来自以下查询的一些示例数据:

rate(http_requests_total[5m])

这将生成类似于以下代码段的内容:

{code="200",endpoint="hey-port",handler="/",instance="172.17.0.10:8000",job="hey-service",method="get"} 5.891716069444445

{code="200",endpoint="hey-port",handler="/",instance="172.17.0.11:8000",job="hey-service",method="get"} 5.9669884444444445

{code="200",endpoint="hey-port",handler="/",instance="172.17.0.9:8000",job="hey-service",method="get"} 11.1336484826487

{code="200",endpoint="hey-port",handler="/health",instance="172.17.0.10:8000",job="hey-service",method="get"} 0.1

{code="200",endpoint="hey-port",handler="/health",instance="172.17.0.11:8000",job="hey-service",method="get"} 0.1

{code="200",endpoint="hey-port",handler="/health",instance="172.17.0.9:8000",job="hey-service",method="get"} 0.1000003703717421

如果我们想知道所有请求的总和,我们可以应用以下表达式:

sum(rate(http_requests_total[5m]))

这将返回以下内容:

{} 23.292353366909335

现在,如果我们添加 by 运算符,我们可以通过处理程序端点进行聚合:

sum by (handler) (rate(http_requests_total[5m]))

反过来,这将返回以下内容:

{handler="/"} 22.99235299653759
{handler="/health"} 0.3000003703717421

这个简单的示例演示了如何轻松聚合数据。

Binary operator precedence

评估 PromQL 查询时,应用二元运算符的顺序由运算符优先级决定。下表显示了优先顺序,从高到低:

Precedence Operator Description

1

^ Evaluated right to left, for example, 1 ^ 2 ^ 3 is evaluated as 1 ^ (2 ^ 3)
2 *, /, % Evaluated left to right, for example, 1 / 2 * 3 is evaluated as (1 / 2) * 3
3 +, - Evaluated left to right
4 ==, !=, <=, <, >=, > Evaluated left to right
5 and, unless Evaluated left to right
6 or Evaluated left to right

Functions

PromQL 有近 50 种不同的函数,适用于各种用例,例如数学;排序;计数器、仪表和直方图操作;标签转换;随着时间的推移聚合;类型转换;最后是日期和时间函数。在接下来的部分中,我们将介绍一些最常用的,并提供示例说明它们为何如此相关。

有关所有功能的全面概述,请访问 https://prometheus.io/docs/prometheus/latest/querying/functions/

absent()

absent() 函数将即时向量作为参数并返回以下内容:

  • An empty vector if the argument has results
  • 1-element vector with the sample value equal to 1, containing the labels from the specified argument in the case of non-conflicting equality matchers

顾名思义,这个功能对于提醒没有时间序列非常有用。

例如,假设存在即时向量,我们执行以下表达式:

absent(http_requests_total{method="get"})

这将返回以下内容:

no data

假设我们使用带有标签匹配器的表达式使用不存在的标签值,如下例所示:

absent(http_requests_total{method="nonexistent_dummy_label"})

这将产生一个带有不存在标签值的即时向量:

{method="nonexistent_dummy_label"} 1

让我们将 absent 应用于不存在的指标,如以下代码段所示:

absent(nonexistent_dummy_name)

这将转化为以下输出:

{} 1

最后,假设我们在不存在的指标和不存在的标签/值对上使用 absent,如以下代码段所示:

absent(nonexistent_dummy_name{method="nonexistent_dummy_label"})

结果可以在以下代码段中看到:

{method="nonexistent_dummy_label"} 1

label_join() and label_replace()

这些函数用于操作标签——它们允许您将标签连接到其他标签、提取标签值的一部分,甚至删除标签(尽管使用标准聚合运算符执行该特定操作更容易且更符合人体工程学)。在这两个函数中,如果定义的目标标签是一个新的,它将被添加到标签集中;如果它是现有标签,它将被替换。

使用 label_join 时,您需要提供即时向量、定义结果标签、识别结果连接的分隔符并建立要连接的标签,如以下语法所示:

label_join(<vector>, <resulting_label>, <separator>, source_label1, source_labelN)

例如,假设我们使用以下示例数据:

http_requests_total{code="200",endpoint="hey-port", handler="/",instance="172.17.0.10:8000",job="hey-service",method="get"} 1366
http_requests_total{code="200",endpoint="hey-port", handler="/health",instance="172.17.0.10:8000",job="hey-service",method="get"} 942

然后我们应用以下表达式:

label_join(http_requests_total{instance="172.17.0.10:8000"}, "url", "", "instance", "handler")

我们最终得到以下即时向量:

http_requests_total{code="200",endpoint="hey-port", handler="/",instance="172.17.0.10:8000",job="hey-service", method="get",url="172.17.0.10:8000/"} 1366
http_requests_total{code="200",endpoint="hey-port", handler="/health",instance="172.17.0.10:8000",job="hey-service", method="get",url="172.17.0.10:8000/health"} 942

当您需要任意操作标签时,label_replace 就是要使用的函数。它的工作方式是将正则表达式应用于所选源标签的值并将匹配的捕获组存储在目标标签上。源和目标都可以是同一个标签,有效地替换它的值。这听起来很复杂,但事实并非如此;我们来看看label_replace的语法:

label_replace(<vector>, <destination_label>, <regex_match_result>, <source_label>, <regex>)

假设我们采用前面的样本数据并应用以下表达式:

label_replace(http_requests_total{instance="172.17.0.10:8000"}, "port", "$1", "instance", ".*:(.*)")

结果将是带有新标签的匹配元素,称为 port:

http_requests_total{code="200",endpoint="hey-port",handler="/", instance="172.17.0.10:8000", job="hey-service",method="get",port="8000"} 1366
http_requests_total{code="200",endpoint="hey-port",handler="/health", instance="172.17.0.10:8000", job="hey-service",method="get",port="8000"} 942

使用 label_replace 时,如果正则表达式与标签值不匹配,则原始时间序列将原样返回。

predict_linear()

此函数接收范围向量和标量时间值作为参数。给定范围向量中数据的趋势,它将每个匹配时间序列的值从查询评估时间外推到未来指定的秒数。它使用线性回归来实现这样的预测,这意味着后台不会发生复杂的算法预测。它只能与仪表一起使用。

我们将应用以下表达式,它使用 predict_linear 使用一小时的数据范围,并推断未来四小时的样本值(60(秒)* 60(分钟)* 4) :

predict_linear(node_filesystem_free_bytes{mountpoint="/data"}[1h], 60 * 60 * 4)
{device="/dev/sda1", endpoint="node-exporter",fstype="ext4",instance="10.0.2.15:9100", job="node-exporter-service",mountpoint="/data", namespace="monitoring", pod="node-exporter-r88r6", service="node-exporter-service"} 15578514805.533087

rate() and irate()

这两个函数允许您计算给定计数器的增长率。两者都会自动调整计数器重置并将范围向量作为参数。

虽然 rate() 函数通过使用缩放范围内的第一个和最后一个值以适应范围窗口来提供指定时间间隔内的每秒平均变化率,但irate() 函数使用范围内的最后两个值进行计算,从而产生瞬时变化率。

重要的是要了解在什么情况下使用一个比另一个更合适。例如,在创建诸如仪表板之类的可视化时,我们可能希望提高对可能出现的峰值的认识; irate 符合这个标准。请注意,由于 irate() 使用范围内的最后两个值,因此降采样是明智的,因此只能在完全放大时使用。在构建警报表达式时,我们更感兴趣获得更平滑的趋势,因此虚假尖峰不会重置 for 计时器(我们将在 第 9 章定义警报和记录规则);在这种情况下,rate 是更适合应用的函数。始终确保范围向量中至少有四个样本,以便 rate() 可以可靠地工作。

为了显示这两个函数之间的差异,以下屏幕截图说明了相同的指标,在​​相同的时间范围内,一个使用 rate(),另一个使用 irate()

读书笔记《hands-on-infrastructure-monitoring-with-prometheus》普罗米修斯查询语言--PromQL
Figure 7.9: rate() of node_network_receive_bytes_total with 1m range
读书笔记《hands-on-infrastructure-monitoring-with-prometheus》普罗米修斯查询语言--PromQL
Figure 7.10: irate() of node_network_receive_bytes_total with 1m range

正如我们所见,irate 对底层计数器的变化更加敏感,而 rate 通常会产生更平滑的值。

histogram_quantile()

这个函数接受一个浮点数,它定义了所需的分位数 (0 ≤ φ ≤ 1),以及一个仪表类型的即时向量作为参数。每个时间序列必须有一个 le 标签(表示小于或等于)来表示存储桶的上限。该函数还期望一个选定的时间序列有一个名为 +Inf 的存储桶,它作为累积直方图的最后一个存储桶,即包罗万象。由于 Prometheus 客户端库生成的直方图对每个存储桶使用计数器,因此需要应用 rate() 将它们转换为量规,以便该函数完成工作。然后,为范围向量选择范围的时间将对应于分位数计算的窗口。虽然很少见,但第三方软件生成的一些直方图可能不使用计数器作为其存储桶,因此只要满足标签要求,就可以直接在 histogram_quantile 中使用。

例如,让我们执行以下表达式:

histogram_quantile(0.75, sum(rate(prometheus_http_request_duration_seconds_bucket[5m])) by (handler, le)) > 0

我们将看到类似于以下的结果:

{handler="/"} 0.07500000000000001
{handler="/api/v1/label/:name/values"} 0.07500000000000001
{handler="/static/*filepath"} 0.07500000000000001
{handler="/-/healthy"} 0.07500000000000001
{handler="/api/v1/query"} 0.07500000000000001
{handler="/graph"} 0.07500000000000001
{handler="/-/ready"} 0.07500000000000001
{handler="/targets"} 0.7028607713983935
{handler="/metrics"} 0.07500000000000001

这提供了一个内部 Prometheus 直方图的输出示例。

sort() and sort_desc()

正如他们的名字所暗示的那样,sort 接收一个向量并按样本值的升序对其进行排序,而 sort_desc 执行相同的功能,但按降序排列。

The topk and bottomk aggregation operators sort their results by default.

vector()

vector() 函数接收一个标量值作为参数,并返回一个没有标签的向量,该向量具有指定的标量参数的值。

例如,查询以下表达式:

vector(42)

它将返回以下内容:

{} 42

这通常用作确保表达式始终具有至少一个结果的一种方式,方法是将向量表达式与其结合,如以下代码所示:

http_requests_total{handler="/"} or vector(0)

由于 or 运算符返回两边,因此将始终存在值为 0 的样本。

Aggregation operations over time

我们之前讨论的聚合操作总是应用于即时向量。当我们需要对范围向量执行这些聚合时,PromQL 提供了 *_over_time 系列函数,它们的工作原理在 第 4 章Prometheus 度量基础。它们都采用一个范围向量并输出一个即时向量。以下是操作说明:

操作

说明

avg_over_time()

范围内所有样本的平均值。

count_over_time()

范围内所有样本的计数。

max_over_time()

范围内所有样本的最大值。

min_over_time()

范围内所有样本的最小值。

quantile_over_time()

范围内所有样本的分位数。它需要两个参数,将所需分位数 φ 定义为标量,其中 0 ≤ φ ≤ 1 作为第一个参数,然后是范围向量。

stddev_over_time()

样本值在范围内的标准偏差。

stdvar_over_time()

样本值在该范围内的标准方差。

sum_over_time()

范围内所有样本值的总和。

Time functions

Prometheus 提供了许多函数来帮助处理时间数据。这些对于几个场景很有用,例如检查最后一次看到进程或批处理作业是多久以前,仅在特定时间触发警报,或在特定日期根本不触发它们。 Prometheus 中的每个时间函数都采用通用协调时间 (UTC)。

time 函数以 UNIX 纪元格式(通常称为 UNIX 时间戳)返回具有当前时间的即时向量:自 1970 年 1 月 1 日 UTC 以来经过的秒数。< /跨度>

timestamp 函数返回一个即时向量,其中包含由提供的选择器返回的样本的 UNIX 时间戳。

minutehourmonthyear 函数都以相同的方式工作:它们接收带有一个的即时向量或多个时间戳,并返回一个即时向量及其代表的相应时间分量。这些函数的默认输入是 time 函数,因此,如果它们不带参数使用,它们将分别返回当前分钟、小时、月或年。

days_in_month 函数接收一个带有时间戳作为参数的即时向量,并返回每个时间戳在月中的天数。与前面的函数一样,默认输入参数是 time 函数。结果显然在 28 到 31 之间。

最后,与前面的函数类似,day_of_weekday_of_month 函数需要一个带有时间戳作为输入的即时向量,并返回对应的星期几(星期日为 0,星期一分别为 1 等)和月份中的某天(从 1 到 31)。默认输入是 time 函数。

Info and enum

有两种度量类型尚未提及,信息和枚举。它们是最近才出现的,但非常感谢它们带来的便利。 info 类型的指标的名称以 _info 结尾,并且是具有一个可能值 1 的常规量表。这种特殊类型的指标被设计为存储其值可能随时间变化的标签的地方,例如版本(例如,exporter 版本、语言版本和内核版本)、分配的角色或 VM 元数据信息;如果要在每个时间序列中导出这些标签,如果它们发生变化,则会发生连续性中断,因为指标标识(指标名称和标签集的组合)也会发生变化。这也会污染所有受影响的时间序列的标签,因为这些新标签会出现在每个指标中。要使用这种类型的度量,我们需要将它与我们希望使用乘法运算符增加的度量结合起来——因为 info 度量值是 1,所以乘法不会改变其他指标——以及 group_left/group_right 修饰符允许我们使用我们可能需要的标签来丰富结果向量。

以下是使用信息指标的查询示例:

node_uname_info{instance=”10.0.2.15:9100”}

我们可以在以下代码段中看到之前的查询结果:

node_uname_info{domainname="(none)",endpoint="node-exporter", instance="10.0.2.15:9100",job="node-exporter-service",machine="x86_64", namespace="monitoring",nodename="minikube",pod="node-exporter-r88r6", release="4.15.0",service="node-exporter-service",sysname="Linux", version="#1 SMP Fri Dec 21 23:51:58 UTC 2018"} 1

枚举度量类型也是一个量规,就像信息一样。它的目标是提供一种方法来公开任何可能需要状态跟踪的东西,例如状态机的当前状态。此类指标最常见的用例是公开守护进程的状态(启动、启动、停止、停止、失败等)。这种跟踪是通过维护标签上的状态信息、适当命名的状态,并将当前状态的度量值设置为 1 来完成的,否则为 0。

下面是一个使用枚举度量的即时向量选择器查询示例:

node_systemd_unit_state{name="kubelet.service"}

上一个查询产生以下代码段:

node_systemd_unit_state{endpoint="node-exporter",instance="10.0.2.15:9100",job="node-exporter-service",name="kubelet.service", namespace="monitoring",pod="node-exporter-jx2c2", state="activating"} 0
node_systemd_unit_state{endpoint="node-exporter",instance="10.0.2.15:9100",job="node-exporter-service",name="kubelet.service", namespace="monitoring",pod="node-exporter-jx2c2", state="active"} 1
node_systemd_unit_state{endpoint="node-exporter",instance="10.0.2.15:9100",job="node-exporter-service",name="kubelet.service", namespace="monitoring",pod="node-exporter-jx2c2", state="deactivating"} 0
node_systemd_unit_state{endpoint="node-exporter",instance="10.0.2.15:9100",job="node-exporter-service",name="kubelet.service", namespace="monitoring",pod="node-exporter-jx2c2", state="failed"} 0
node_systemd_unit_state{endpoint="node-exporter",instance="10.0.2.15:9100",job="node-exporter-service",name="kubelet.service", namespace="monitoring",pod="node-exporter-jx2c2"", state="inactive"} 0

现在我们了解了 PromQL 的基础知识,让我们深入研究一些常见的模式和编写表达式时可以避免的陷阱。

Common patterns and pitfalls

拥有如此强大的语言供您使用,您很容易被如此多的选择淹没。在接下来的部分中,我们将提供一些常见的模式和陷阱,以确保 PromQL 针对所描述的每种情况的预期用途,并通过这种方式进一步强化我们迄今为止为您提供的知识。跨度>

Patterns

虽然 PromQL 的强大功能和灵活性在信息提取方面提供了无限可能,但有一些查询模式可以使一组常见问题更容易理解,并有助于提高对受监控服务的洞察力。在以下主题中,我们将介绍一些我们的最爱,希望它们对您有用,就像它们目前对我们一样有用。

Service-level indicators

第 1 章中,监控基础 ,我们介绍了测量什么的概念,讨论了 Google 的四个黄金信号,以及 USE 和 RED 方法。基于这些知识,我们可以开始定义服务水平指标 (SLI),它反映了给定服务的性能和可用性。构造查询以生成 SLI 是 PromQL 使用的常见模式,也是最有用的模式之一。

让我们看一个 SLI 的例子:一个的典型定义是好事件的数量超过有效事件的数量;在这种情况下,我们想了解由 Prometheus 服务的请求的百分比是否等于或低于 100 毫秒,这使其成为延迟 SLI。首先,我们需要收集有关在该阈值下服务多少请求的信息;幸运的是,我们可以依赖一个已经可用的直方图类型度量,称为 prometheus_http_request_duration_seconds_bucket。正如我们已经知道的那样,这种类型的度量具有由 le 标签(小于或等于)表示的桶,因此我们只需匹配 100 毫秒(0.1 秒)以下的元素,如下所示:

prometheus_http_request_duration_seconds_bucket{le="0.1"} 

虽然比率通常是此类计算中的基本单位,但在本例中,我们需要一个百分比,因此我们必须将匹配的元素除以发出的请求总数 (prometheus_http_request_duration_seconds_count) 并将其相乘结果除以100。由于le标签的不匹配,这两个即时向量不能直接分割,所以我们必须忽略它,如下:

prometheus_http_request_duration_seconds_bucket{le="0.1"} / ignoring (le) prometheus_http_request_duration_seconds_count * 100

这为我们提供了一个即时向量,其中包含每个端点和每个实例的信息,将样本值设置为自服务在每个实例上启动以来响应时间低于 100 毫秒的请求百分比(请记住,_bucket 是一个计数器)。这是一个好的开始,但并不完全是我们所追求的,因为我们希望 SLI 用于整个服务,而不是每个实例或每个端点。在滚动窗口上计算它而不是对不确定的数据量进行平均也更有用;随着收集到更多数据,平均值变得更平滑且更难移动。因此,要解决这些问题,我们需要在一个时间窗口内对计数器进行评分以获得固定的滚动平均值,然后使用 sum() 聚合实例和端点。这样,我们也不需要忽略 le 标签,因为它在聚合过程中也会被丢弃。让我们把它们放在一起:

sum by (job, namespace, service) (
rate(prometheus_http_request_duration_seconds_bucket{le="0.1"}[5m])
) /
sum by (job, namespace, service) (
rate(prometheus_http_request_duration_seconds_count[5m])
) * 100

为我们的服务建立服务水平目标 (SLO) 现在变得很简单,因为我们只需要使用比较运算符指定我们要达到的百分比。这使得极好的条件被定义为警报。

Percentiles

我们刚刚学习了如何提取在给定延迟下提供服务的请求百分比,但是如果我们需要了解给定请求百分比的延迟怎么办?

例如,要获得第 95 个百分位数,我们可以通过定义分位数(在本例中为 0.95)来使用 histogram_quantile 函数,然后将查询表达式输入表示我们感兴趣的一组数据——在滚动时间窗口期间请求持续时间直方图中每个桶的平均增长率。如果我们想要服务的全局延迟,而不是每个实例/pod/handler,我们需要应用 sum() 聚合:

histogram_quantile(0.95, sum without (instance, pod, handler) (rate(prometheus_http_request_duration_seconds_bucket[5m])))

该表达式将产生一个值,表示 95% 的请求将等于或低于该值。

The health of scrape jobs

对于每个定义的抓取作业,Prometheus 将生成一个名为 up 的自动指标,它反映了相关作业的健康状况——1 表示成功抓取,0 表示失败。我们可以使用此指标快速可视化整个出口商和/或被抓取的应用程序基础设施的当前健康状态。

让我们概述一下所有被抓取的成功作业:

sum by (job) (up)
{job="hey-service"} 3
{job="cadvisor-service"} 1
{job="kube-state-metrics"} 2
{job="node-exporter-service"} 1
{job="prometheus-service"} 2

Pitfalls

PromQL 的强大功能和灵活性可以对时间序列数据进行一些令人印象深刻的切片和切块,但也可能成为挫折、意外结果甚至严重性能问题的根源。虽然最近发布的 Prometheus 一直在稳步引入功能来解决其中一些陷阱,但了解这些问题可以帮助您充分利用 PromQL,同时节省您的时间和计算资源。

Choosing the right functions for the data type

开始使用 PromQL 时最常见的陷阱是没有为数据类型(例如计数器、仪表或直方图)或向量类型选择正确的函数。即使在 Prometheus 文档中指出了此信息,仍然可能存在一些混淆,因为存在概念上相似的命名函数和聚合器:ratederivincrease deltaquantilehistogram_quantilesumsum_over_time、其中。幸运的是,在向量类型不匹配的情况下,表达式评估将失败并让您知道哪里出了问题;对于数据类型不匹配的情况,例如为需要量规的函数提供计数器,表达式可能会成功计算但返回不正确或具有欺骗性的结果。

Sum-of-rates versus rate-of-sums

前一点可能看起来很明显,但是当构建的查询的复杂性开始增加时,很容易出错。一个常见的例子是尝试对计数器的总和进行评分,而不是对速率求和。 rate 函数需要一个计数器,但计数器的总和实际上是一个计量器,因为当其中一个计数器重置时它会下降;这将在绘制图表时转化为看似随机的尖峰,因为 rate 会考虑任何减少计数器重置,但其他计数器的总和将被认为是零和当前值之间的巨大增量。在下图中,我们可以看到这一点:两个计数器(G1、G2),其中一个有一个重置(G2) ; G3 显示了通过对每个计数器的速率求和产生的预期聚合结果; G4 显示计数器 1 和 2 之和的样子; G5 表示速率函数如何将 G4 解释为计数器(突然增加是 0 与发生减少的点之间的差);最后,G6 显示了计数器总和的评分,错误的尖峰出现在 G2 的计数器重置发生的位置:

读书笔记《hands-on-infrastructure-monitoring-with-prometheus》普罗米修斯查询语言--PromQL
Figure 7.11: Approximation of what rate of sums and sum of rates look like

如何在 PromQL 中正确执行此操作的示例可能是:

sum(rate(http_requests_total[5m]))

在过去的 Prometheus 版本中犯这个错误有点困难,因为要给 rate 一个和的范围向量,我们要么需要一个记录规则,要么需要一个范围向量的手动总和。不幸的是,从 Prometheus 2.7.0 开始,现在可以在一个时间窗口内请求计数器的总和,从而有效地从该结果创建一个范围向量。这是一个错误,不应该这样做。因此,简而言之,始终在获取费率后应用聚合,而不是相反。

Having enough data to work with

函数的速率组(rateirateincreasederivdelta , 和 idelta) 需要在提供的范围向量中至少有两个样本才能正常工作。这意味着接近 scrape_interval 的时间范围可能无法产生所需的结果,因为单个失败的刮擦或窗口对齐问题(刮擦不会以确切的时间间隔发生并且可能会延迟)将使范围仅包含一个样本。因此建议使用 4(或更多)倍的 scrape_interval 以确保返回足够的样本以使计算工作。下图显示了在给定范围内改变样本趋势的失败抓取:

读书笔记《hands-on-infrastructure-monitoring-with-prometheus》普罗米修斯查询语言--PromQL
Figure 7.12: A failed scrape changing the trend of samples for a given range

Unexpected results when using increase

在一个相关的主题上,一个常见的混淆点是为什么像 increasedelta 这样的函数会产生非整数结果。这一点在文档中进行了简要说明,但值得扩展。由于 Prometheus 定期收集数据(在 scrape_interval 配置中定义),当查询要求提供一系列样本时,该范围的窗口限制通常不会与返回的数据。如果数据点与时间窗口匹配,这些函数会推断结果会是什么样子。他们通过使用提供的样本计算精确结果,然后将该结果乘以时间窗口在第一个和最后一个数据点之间的间隔上的比率来做到这一点,从而有效地将结果缩放到请求的范围。

Not using enough matchers to select a time series

编写 PromQL 表达式(无论是仪表板还是警报)时的另一个常见缺陷是没有使用足够的匹配器来确保返回的样本来自预期的时间序列。在 Prometheus 社区中,当所讨论的指标不是特定于特定软件时,将指标名称命名为应用程序被认为是一种反模式,即使是,也可能会发生命名冲突;这就是为什么在尝试提取有关特定软件的信息时始终明确选择job 的范围选择器是一种很好的做法。例如,我们可以查看由第一方 Prometheus Go 客户端库收集的 go_goroutines 指标:作为 Prometheus 生态系统的一个相当大的块是用 Go 编写的,并使用这个客户端库仪器,这个指标通常出现在许多抓取作业中。这意味着,如果我们要调查特定软件的聚合 go-routine 行为,如果使用的选择器对于我们感兴趣的实例来说不够窄,我们将得到不正确的结果。

Losing statistical significance

一个与 PromQL 没有特别相关但由于语言的灵活性而容易犯的错误是将转换应用于聚合值,从而失去其统计意义。例如,您可能很想对一组实例的摘​​要中预先计算的分位数进行平均,以了解集群,但从统计的角度来看,它们不能被进一步操纵——该操作的结果不会像整个集群的相应分位数。然而,这可以通过直方图来完成,因为在计算近似分位数之前,可以在集群范围内对来自每个实例的桶求和。另一个常见的例子是平均平均值。

Knowing what to expect when constructing complex queries

使用 PromQL 时需要注意的一个有趣细节是,在向量之间使用比较运算符时,返回的结果将来自比较的左侧。这意味着,在当前值和阈值之间进行比较时,您应该按该顺序进行比较(例如,current_value ),因为您可能希望返回的值是当前值而不是阈值:

node_filesystem_avail_bytes < node_filesystem_size_bytes * 0.2

此外,当使用 链接不同的比较时,结果仍然是第一次比较的左侧。以下示例返回文件系统中剩余空间的百分比,该百分比低于 20%,根据过去 6 小时的填充率,预计将在 4 小时内填满。请注意,它不是只读模式:

node_filesystem_avail_bytes/node_filesystem_size_bytes * 100 < 20
and
predict_linear(node_filesystem_avail_bytes[6h], 4*60*60) <= 0
and
node_filesystem_readonly == 0

将第一个比较与第二个比较会产生相同数量的结果,但也会显示 4 小时后可用的预测字节数。这个结果不太有用,因为准确地知道预测的负字节的数量只是传达了它们在现实中将为 0 的事实。但是,这两种表达式都可以用于警报,因为表达式的结果值不会在警报通知中发送。

The query of death

最后,在使用过于宽泛的选择器和内存密集型聚合来制作查询时应该小心。虽然 Prometheus 实现了默认检查和边界以避免无限的内存使用(如 第 5 章运行 Prometheus 服务器),仍然有可能遇到不够大的内存限制——容器限制甚至实际系统 RAM,这将使操作系统毫不客气地终止 Prometheus 服务器。使问题更加复杂的是,很难确定哪些查询使用的资源最多,尤其是在您几乎无法控制将哪些查询发送到服务器的环境中;没有内置慢查询日志功能,因为使其工作会涉及一些会影响性能和可管理性的权衡。然而,在实践中,对限制资源利用率的不断改进(特别是在内存方面)使得这个特定问题更难在尺寸良好的环境中发生。

Moving on to more complex queries

利用目前提供的信息,我们可以继续进行更复杂的查询,了解它们的构建方式以及对它们的期望。在接下来的部分中,我们将讨论一些需要使用 PromQL 来探索和巩固我们迄今为止所涵盖的概念的复杂场景。

In which node is Node Exporter running?

此方案旨在帮助您理解 info 指标和 group_left 修饰符等概念。

Scenario rationale

在 Kubernetes 上运行时,您可能需要对 Node Exporter pod 进行故障排除,为此您需要知道它在哪个主机上运行。节点导出器指标看到的是 pod 而不是主机,因此您在生成的指标中没有可用的主机名。在这种情况下,我们需要将缺失的信息添加到最初没有该标签的指标中。 此方案的另一种替代方法是通过重新标记在实例标签中提供所需的信息。

PromQL approach

以下查询允许我们使用另一个标签来扩充 node_exporter_build_info 指标,称为 nodename,其中包含有关运行 Node Exporter pod 的主机名的信息。

在此示例中,我们有以下即时向量:

node_exporter_build_info

这会产生以下结果:

node_exporter_build_info{branch="HEAD",endpoint="node-exporter",goversion="go1.11.2", instance="10.0.2.15:9100",job="node-exporter-service",namespace="monitoring", pod="node-exporter-r88r6",revision="f6f6194a436b9a63d0439abc585c76b19a206b21", service="node-exporter-service",version="0.17.0"} 1

我们还有 node_uname_info,它有 nodename 标签:

node_uname_info

这转化为以下代码:

node_uname_info{domainname="(none)",endpoint="node-exporter", instance="10.0.2.15:9100",job="node-exporter-service",machine="x86_64", namespace="monitoring",nodename="minikube",pod="node-exporter-r88r6", release="4.15.0",service="node-exporter-service",sysname="Linux", version="#1 SMP Fri Dec 21 23:51:58 UTC 2018"} 1

在信息指标的帮助下,如前所述,我们将使用以下表达式将 node_uname_info 信息类型指标中的 nodename 标签添加到 使用 group_left 的 node_exporter_build_info 指标:

node_exporter_build_info * on (pod, namespace) group_left (nodename) node_uname_info

可以在以下代码段中检查结果:


{branch="HEAD",endpoint="node-exporter",goversion="go1.11.2", instance="10.0.2.15:9100",job="node-exporter-service",namespace="monitoring", nodename="minikube",pod="node-exporter-r88r6", revision="f6f6194a436b9a63d0439abc585c76b19a206b21", service="node-exporter-service",version="0.17.0"} 1

Comparing CPU usage across different versions

此方案与前一个方案类似,但通过组合来自不同来源的指标并使它们一起工作,更进一步。

Scenario rationale

您可能想观察不同软件版本在吞吐量或资源使用方面的表现。通过清楚地绘制升级前后的模式,这可能更容易分析。在这个特定示例中,我们将查看升级前后 node_exporter 的容器 CPU 使用情况。

请记住几个注意事项:就本示例而言,node_exporter 作为容器运行,这在实际场景中是不明智的。此外,我们将使用来自 cAdvisor 的 container_cpu_usage_seconds_total,而不是直接从本地检测应用程序收集的 process_cpu_seconds_total,因此这种方法可以应用于任何类型的容器化进程,在此过程中巩固 cAdvisor 指标的使用。

PromQL approach

container_cpu_usage_seconds_total 指标为我们提供了运行每个容器所花费的 CPU 秒数,来自 cadvisor 导出器。 node_exporter 版本可以在 node_exporter_build_info 指标中找到。更麻烦的是,由于容器指标来自 cadvisor,因此在这些指标中注册的容器和 pod 是 cadvisor 的,而不是来自目标 pod;但是,我们可以分别在 container_label_io_kubernetes_container_namecontainer_label_io_kubernetes_pod_name 标签中找到原始容器名称和 pod 名称。

我们需要做的第一件事是获取每个 pod 在一分钟的滚动窗口上每秒使用的平均 CPU 秒数:

sum by (container_label_io_kubernetes_pod_name) (
rate(container_cpu_usage_seconds_total{container_label_io_kubernetes_container_name="node-exporter"}[1m])

接下来,我们需要在 node_exporter_build_info 中创建一个新标签,以便匹配按预期进行。对于这个用例,我们可以使用 label_joinlabel_replace,因为我们只是读取一个标签并将其内容逐字写入另一个标签:

label_join(node_exporter_build_info, "container_label_io_kubernetes_pod_name", "", "pod")

或者,我们可以使用以下代码:

label_replace(node_exporter_build_info, "container_label_io_kubernetes_pod_name", "$1", "pod", "(.+)")

最后,我们只需要通过它们的公共标签 container_label_io_kubernetes_pod_name 匹配两个指标,使用 on() 然后请求将版本标签加入 CPU 表达式的标签使用 group_left() 设置。让我们把它们放在一起:

sum by (container_label_io_kubernetes_pod_name) (
rate(container_cpu_usage_seconds_total{container_label_io_kubernetes_container_name="node-exporter"}[1m])
)
* on (container_label_io_kubernetes_pod_name)
group_left (version)
label_replace(node_exporter_build_info, "container_label_io_kubernetes_pod_name", "$1", "pod", "(.+)")
读书笔记《hands-on-infrastructure-monitoring-with-prometheus》普罗米修斯查询语言--PromQL
Figure 7.13: Node exporter version upgrade impact on CPU usage

所有这些一开始可能看起来很复杂,但在你自己尝试这些概念之后,它们很快就会变得更容易理解、应用和推理。

Summary

在本章中,我们了解了 PromQL 的基础知识,从选择器到函数,涵盖了二元运算符、向量匹配和聚合等概念。通过常见的模式和陷阱,我们了解到这种语言如何不仅仅支持简单的查询,以及它如何成为必不可少的基础设施工具,帮助设计和管理 SLI 和 SLO。我们还演示了几个 PromQL 大放异彩的场景,以及看似复杂的查询毕竟不是那么复杂。

在下一章中,第 8 章疑难解答和验证,我们将深入研究如何验证健康的 Prometheus 设置,并学习如何快速解决问题,确保监控堆栈的稳定性。

Questions

  1. What are the six available comparison operators in PromQL?
  2. When should a group_right modifier be used instead of a group_left one?
  3. Why shouldn't you use the sort() function when applying the topk aggregation operator?
  4. What is the major difference between rate() and irate()?
  5. Which type of metric has an _info suffix and what is its purpose?
  6. Should you sum and then rate or rate and then sum?
  7. How can you get the average CPU usage for the last five minutes in a percentage?