Files

384 lines
21 KiB
Markdown
Raw Permalink Normal View History

2023-11-06 22:51:17 +08:00
# PromQL 介绍
**PromQL** 是 Prometheus 监控系统内置的一种查询语言,PromQL 允许你以灵活的方式选择、聚合等其他方式转换和计算时间序列数据,该语言仅用于读取数据。可以说 PromQL 是我们学习 Prometheus 最困难也是最重要的部分,本章节我们将介绍 PromQL 的基础知识、理论基础,然后会深入了解更加高级的查询模式。
## 目标
通过对本章节 PromQL 的学习你将能够有效地构建、分享和理解 PromQL 查询,可以帮助我们从容应对报警规则、仪表盘可视化等需求,还能够避免一些在使用 PromQL 表达式的时候遇到的一些陷进。
## 执行
前面基础章节我们介绍了 Prometheus 整体的架构:
![Prometheus 架构中的 PromQL](../img/20210408121947.png)
当 Prometheus 从系统和服务收集指标数据时,它会把数据存储在内置的时序数据库(TSDB)中,要对收集到的数据进行任何处理,我们都可以使用 PromQL 从 TSDB 中读取数据,同时可以对所选的数据执行过滤、聚合以及其他转换操作。
PromQL 的执行可以通过两种方式来触发:
- 在 Prometheus 服务器中,记录规则和警报规则会定期运行,并执行查询操作来计算规则结果(例如触发报警)。该执行在 Prometheus 服务内部进行,并在配置规则时自动发生。
- 外部用户和 UI 界面可以使用 Prometheus 服务提供的 [HTTP API](https://prometheus.io/docs/prometheus/latest/querying/api/) 来执行 PromQL 查询。这就是仪表盘软件(例如 [Grafana](https://grafana.com/grafana/)、[PromLens](https://promlens.com/) 以及 Prometheus 内置 Web UI)访问 PromQL 的方式。
## 场景
PromQL 可以用于许多监控场景,下面简单介绍几个相关案例。
**临时查询**
我们可以用 PromQL 来对收集的数据进行实时查询,这有助于我们去调试和诊断遇到的一些问题,我们一般也是直接使用内置的表达式查询界面来执行这类查询:
![PromQL 查询](../img/20210408142609.png)
**仪表盘**
同样我们也可以基于 PromQL 查询来创建可视化的图形、表格等面板,当然一般我们都会使用 Grafana:
![grafana dashboard](../img/20210408142805.png)
Grafana 原生支持 Prometheus 作为数据源,并内置支持了 PromQL 表达式的查询。
**报警**
Prometheus 可以直接使用基于 PromQL 对收集的数据进行的查询结果来生成报警,一个完整的报警规则如下所示:
```yaml
groups:
- name: demo-service-alerts
rules:
- alert: Many5xxErrors
expr: |
(
sum by(path, instance, job) (
rate(demo_api_request_duration_seconds_count{status=~"5..",job="demo"}[1m])
)
/
sum by(path, instance, job) (
rate(demo_api_request_duration_seconds_count{job="demo"}[1m])
) * 100 > 0.5
)
for: 30s
labels:
severity: critical
annotations:
title: '{{$labels.instance}} high 5xx rate on {{$labels.path}}'
description: 'The 5xx error rate for path {{$labels.path}} on {{$labels.instance}} is {{$value}}%.'
```
除了构成报警规则核心的 PromQL 表达式(上面 YAML 文件中的 `expr` 属性),报警规则还包含其他的一些元数据字段,后面在具体讲解报警的章节中会详细和大家讲解。
然后,Prometheus 可以通过一个名为 `Alertmanager` 的组件来发送报警通知,可以配置一些接收器来接收这些报警,比如用钉钉来接收报警:
![钉钉](../img/20211003163143.png)
**自动化**
此外我们还可以构建自动化流程,针对 PromQL 执行的查询结果来做出决策,比如 Kubernetes 中基于自定义指标的 HPA。
# 数据模型
在开始学习 PromQL 的知识之前,我们先重新来熟悉下 Prometheus 的数据模型
![Prometheus 数据模型](../img/20210409165112.png)
## 时间序列
Prometheus 会将所有采集到的样本数据以时间序列的方式保存在**内存数据库**中,并且定时保存到硬盘上。时间序列是按照时间戳和值的序列顺序存放的,我们称之为向量(vector),每条时间序列通过指标名称(metrics name)和一组标签集(labelset)命名。如下所示,可以将时间序列理解为一个以时间为 X 轴的数字矩阵:
```text
^
│ . . . . . . . . . . . . . . . . . . . node_cpu_seconds_total{cpu="cpu0",mode="idle"}
│ . . . . . . . . . . . . . . . . . . . node_cpu_seconds_total{cpu="cpu0",mode="system"}
│ . . . . . . . . . . . . . . . . . . node_load1{}
│ . . . . . . . . . . . . . . . . . .
v
<------------------ 时间 ---------------->
```
在时间序列中的每一个点称为一个样本(sample),样本由以下三部分组成:
- 指标(metric):指标名和描述当前样本特征的标签集合
- 时间戳(timestamp):一个精确到毫秒的时间戳
- 样本值(value): 一个 float64 的浮点型数据表示当前样本的值
如下所示:
```text
<--------------- metric ---------------------><-timestamp -><-value->
http_request_total{status="200", method="GET"}@1434417560938 => 94355
http_request_total{status="200", method="GET"}@1434417561287 => 94334
http_request_total{status="404", method="GET"}@1434417560938 => 38473
http_request_total{status="404", method="GET"}@1434417561287 => 38544
http_request_total{status="200", method="POST"}@1434417560938 => 4748
http_request_total{status="200", method="POST"}@1434417561287 => 4785
```
在形式上,所有的指标都通过如下格式表示:
```text
<metric name>{<label name> = <label value>, ...}
```
- 指标的名称可以反映被监控样本的含义(比如,http*request_total 表示当前系统接收到的 HTTP 请求总量)。指标名称只能由 ASCII 字符、数字、下划线以及冒号组成并必须符合正则表达式`[a-zA-Z*:]a-zA-Z0-9*:]*`。
- 标签(label)反映了当前样本的特征维度,通过这些维度 Prometheus 可以对样本数据进行过滤,聚合等。标签的名称只能由 ASCII 字符、数字以及下划线组成并满足正则表达式 `[a-zA-Z_][a-zA-Z0-9_]*`。
指标名称
在 TSDB 内部,指标名称也只是一个特殊的标签,标签名为 `__name__`,由于这个标签在 PromQL 中随时都会使用,所以在使用 PromQL 查询的时候就被单独写在了标签列表前面了。另外像 `method=""` 这样的空标签在 Prometheus 种相当于一个不存在的标签,在 Prometheus 代码里面是明确地剥离了空标签的,并不会存储它们。
## 类型
每个不同的 `metric_name`和 `label` 组合都称为**时间序列**,在 Prometheus 的表达式语言中,表达式或子表达式包括以下四种类型之一:
- `瞬时向量(Instant vector)`:一组时间序列,每个时间序列包含单个样本,它们共享相同的时间戳。也就是说,表达式的返回值中只会包含该时间序列中的最新的一个样本值。而相应的这样的表达式称之为瞬时向量表达式。
- `区间向量(Range vector)`:一组时间序列,每个时间序列包含一段时间范围内的样本数据,这些是通过将时间选择器附加到方括号中的瞬时向量(例如[5m]5 分钟)而生成的。
- `标量(Scalar)`:一个简单的数字浮点值。
- `字符串(String)`:一个简单的字符串值。
所有这些指标都是 Prometheus 定期从 metrics 接口那里采集过来的。采集的间隔时间的设置由 `prometheus.yml` 配置中的 `scrape_interval` 指定。最多抓取间隔为 30 秒,这意味着至少每 30 秒就会有一个带有新时间戳记录的新数据点,这个值可能会更改,也可能不会更改,但是每隔 `scrape_interval` 都会产生一个新的数据点。
# 指标类型
从存储上来讲所有的监控指标都是相同的,但是在不同的场景下这些指标又有一些细微的差异。 例如,在 Node Exporter 返回的样本中指标 `node_load1` 反应的是当前系统的负载状态,随着时间的变化这个指标返回的样本数据是在不断变化的。而指标 `node_cpu_seconds_total` 所获取到的样本数据却不同,它是一个持续增大的值,因为其反应的是 CPU 的累计使用时间,从理论上讲只要系统不关机,这个值是会一直变大。
为了能够帮助用户理解和区分这些不同监控指标之间的差异,Prometheus 定义了 4 种不同的指标类型:`Counter(计数器)`、`Gauge(仪表盘)`、`Histogram(直方图)`、`Summary(摘要)`。
在 node-exporter(后面会详细讲解)返回的样本数据中,其注释中也包含了该样本的类型。例如:
```text
# HELP node_cpu_seconds_total Seconds the cpus spent in each mode.
# TYPE node_cpu_seconds_total counter
node_cpu_seconds_total{cpu="cpu0",mode="idle"} 362812.7890625
```
## Counter
`Counter` (只增不减的计数器) 类型的指标其工作方式和计数器一样,只增不减,所以它对于存储诸如服务的 HTTP 请求数量或使用的 CPU 时间之类的信息非常有用。常见的监控指标,如 `http_requests_total`、`node_cpu_seconds_total` 都是 `Counter` 类型的监控指标。
![Counter](../img/prometheus-metric-type-counter.jpeg)
可能你会觉得一直增加的数据没什么用处,了解服务从开始有多少请求有什么价值吗?但是需要记住,每个指标都存储了时间戳的,所有你的 HTTP 请求数现在可能是 1000 万,但是 Prometheus 也会记录之前某个时间点的值,我们可以去查询过去一个小时内的请求数,当然更多的时候我们想要看到的是请求数增加或减少的速度有多快,因此通常情况对于 `Counter` 指标我们都是去查看变化率而不是本身的数字。`PromQL` 内置的聚合操作和函数可以让用户对这些数据进行进一步的分析,例如,通过 `rate()` 函数获取 HTTP 请求的增长率:
```promql
rate(http_requests_total[5m])
```
## Gauge
与 `Counter` 不同,`Gauge`(可增可减的仪表盘)类型的指标侧重于反应系统的当前状态,因此这类指标的样本数据可增可减。常见指标如 `node_memory_MemFree_bytes`(当前主机空闲的内存大小)、`node_memory_MemAvailable_bytes`(可用内存大小)都是 `Gauge` 类型的监控指标。由于 Gauge 指标仍然带有时间戳存储,所有我们可以看到随时间变化的值,通常可以直接把它们绘制出来,这样就可以看到值本身而不是变化率了,通过 `Gauge` 指标,用户可以直接查看系统的当前状态。
![Gauge](../img/prometheus-metric-type-gauge.jpeg)
这些简单的指标类型都只是为每个样本获取一个数字,但 Prometheus 的强大之处在于如何让你跟踪它们,比如我们绘制了两张图,一个是 HTTP 请求的变化率,另一个是分配的 gauge 类型的实际内存,直接从图上就可以看出这两个之间有一种关联性,当请求出现峰值的时候,内存的使用也会出现峰值,但是我们仔细观察也会发现在峰值过后,内存使用量并没有恢复到峰值前的水平,整体上它在逐渐增加,这表明很可能应用程序中存在内存泄露的问题,通过这些简单的指标就可以帮助我们找到这些可能存在的问题。
![关联性](../img/20211004141744.png)
对于 `Gauge` 类型的监控指标,通过 `PromQL` 内置函数 `delta()` 可以获取样本在一段时间范围内的变化情况。例如,计算 CPU 温度在两个小时内的差异:
```promql
delta(cpu_temp_celsius{host="zeus"}[2h])
```
还可以直接使用 `predict_linear()` 对数据的变化趋势进行预测。例如,预测系统磁盘空间在 4 个小时之后的剩余情况:
```promql
predict_linear(node_filesystem_free_bytes[1h], 4 * 3600)
```
## Histogram 和 Summary
除了 `Counter` 和 `Gauge` 类型的监控指标以外,Prometheus 还定义了 `Histogram` 和 `Summary` 的指标类型。`Histogram` 和 `Summary` 主用用于统计和分析样本的分布情况。
在大多数情况下人们都倾向于使用某些量化指标的平均值,例如 CPU 的平均使用率、页面的平均响应时间,这种方式也有很明显的问题,以系统 API 调用的平均响应时间为例:如果大多数 API 请求都维持在 100ms 的响应时间范围内,而个别请求的响应时间需要 5s,那么就会导致某些 WEB 页面的响应时间落到**中位数**上,而这种现象被称为**长尾问题**。
为了区分是平均的慢还是长尾的慢,最简单的方式就是按照请求延迟的范围进行分组。例如,统计延迟在 `0~10ms` 之间的请求数有多少而 `10~20ms` 之间的请求数又有多少。通过这种方式可以快速分析系统慢的原因。`Histogram` 和 `Summary` 都是为了能够解决这样的问题存在的,通过 `Histogram` 和`Summary` 类型的监控指标,我们可以快速了解监控样本的分布情况。
**Summary**
摘要用于记录某些东西的平均大小,可能是计算所需的时间或处理的文件大小,摘要显示两个相关的信息:`count`(事件发生的次数)和 `sum`(所有事件的总大小),如下图计算摘要指标可以返回次数为 3 和总和 15,也就意味着 3 次计算总共需要 15s 来处理,平均每次计算需要花费 5s。下一个样本的次数为 10,总和为 113,那么平均值为 11.3,因为两组指标都记录有时间戳,所以我们可以使用摘要来构建一个图表,显示平均值的变化率,比如图上的语句表示的是 5 分钟时间段内的平均速率。
![Summary](../img/prometheus-metric-type-summary.jpeg)
例如,指标 `prometheus_tsdb_wal_fsync_duration_seconds` 的指标类型为 `Summary`,它记录了 Prometheus Server 中 `wal_fsync` 的处理时间,通过访问 Prometheus Server 的 `/metrics` 地址,可以获取到以下监控样本数据:
```text
# 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。其中中位数(quantile=0.5)的耗时为 0.012352463,9 分位数(quantile=0.9)的耗时为 0.014458005s。
**Histogram**
摘要非常有用,但是平均值会隐藏一些细节,上图中 10 与 113 的总和包含非常广的范围,如果我们想查看时间花在什么地方了,那么我们就需要直方图了。直方图以 bucket 桶的形式记录数据,所以我们可能有一个桶用于需要 1s 或更少的计算,另一个桶用于 5 秒或更少、10 秒或更少、20 秒或更少、60 秒或更少。该指标返回每个存储桶的计数,其中 3 个在 5 秒或更短的时间内完成,6 个在 10 秒或更短的时间内完成。Prometheus 中的直方图是**累积**的,因此所有 10 次计算都属于 60 秒或更少的时间段,而在这 10 次中,有 9 次的处理时间为 20 秒或更少,这显示了数据的分布。所以可以看到我们的大部分计算都在 10 秒以下,只有一个超过 20 秒,这对于计算百分位数很有用。
![Histogram](../img/prometheus-metric-type-histogram.jpeg)
在 Prometheus Server 自身返回的样本数据中,我们也能找到类型为 Histogram 的监控指标`prometheus_tsdb_compaction_chunk_range_seconds_bucket`:
```text
# HELP prometheus_tsdb_compaction_chunk_range_seconds Final time range of chunks on their first compaction
# TYPE prometheus_tsdb_compaction_chunk_range_seconds histogram
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="100"} 71
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="400"} 71
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="1600"} 71
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="6400"} 71
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="25600"} 405
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="102400"} 25690
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="409600"} 71863
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="1.6384e+06"} 115928
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="6.5536e+06"} 2.5687892e+07
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="2.62144e+07"} 2.5687896e+07
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="+Inf"} 2.5687896e+07
prometheus_tsdb_compaction_chunk_range_seconds_sum 4.7728699529576e+13
prometheus_tsdb_compaction_chunk_range_seconds_count 2.5687896e+07
```
与 `Summary` 类型的指标相似之处在于 `Histogram` 类型的样本同样会反应当前指标的记录的总数(以 `_count` 作为后缀)以及其值的总量(以 `_sum` 作为后缀)。不同在于 `Histogram` 指标直接反应了在不同区间内样本的个数,区间通过标签 `le` 进行定义。
# 演示服务
为了尽可能详细地给大家演示 PromQL 指标查询,这里我们将 Fork 一个开源的 Prometheus 演示服务来进行查询,这样可以让我们更加灵活地对指标数据进行控制,项目仓库地址:https://github.com/cnych/prometheus_demo_service,这是一个 Go 语言开发的服务,我们可以自己构建应用。
## 构建
首先准备 golang 环境:
```shell
☸ ➜ wget https://golang.org/dl/go1.16.3.linux-amd64.tar.gz
☸ ➜ rm -rf /usr/local/go && tar -C /usr/local -xzf go1.16.3.linux-amd64.tar.gz
# 配置环境变量,可以将下面命令添加到 /etc/profile 中
☸ ➜ export PATH=$PATH:/usr/local/go/bin
# 执行go命令验证
☸ ➜ go version
go version go1.16.3 linux/amd64
```
然后 clone 代码构建:
```shell
# 首先clone代码(建议使用ssh模式,你懂的~)
☸ ➜ git clone https://github.com/cnych/prometheus_demo_service
☸ ➜ cd prometheus_demo_service
# 配置 GOPROXY 代理
☸ ➜ export GOPROXY=https://goproxy.cn
# 构建
☸ ➜ env GOOS=linux GOARCH=amd64 go build -o prometheus_demo_service
```
构建完成后启动 3 个服务,分别监听 10000、10001、10002 端口:
```shell
☸ ➜ ps -aux |grep demo
root 15224 2.9 0.1 834120 14836 pts/0 Sl 10:39 0:00 ./prometheus_demo_service --listen-address=:10000
root 15333 3.0 0.2 899656 16888 pts/0 Sl 10:39 0:00 ./prometheus_demo_service --listen-address=:10001
root 15353 2.7 0.1 907596 14896 pts/0 Sl 10:39 0:00 ./prometheus_demo_service --listen-address=:10002
```
上面 3 个服务都在 `/metrics` 端点暴露了一些指标数据,我们可以把这 3 个服务配置到 Prometheus 抓取任务中,这样后续就可以使用这几个服务来进行 PromQL 查询说明了。
## 配置
完整的 `prometheus.yml` 配置文件如下所示:
```yaml
global:
scrape_interval: 5s # 抓取频率
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
# 配置demo抓取任务
- job_name: demo
scrape_interval: 15s
scrape_timeout: 10s
static_configs:
- targets:
- demo-service-0:10000
- demo-service-1:10001
- demo-service-2:10002
```
这里我们将 3 个服务配置到名为 demo 的抓取任务中,为了看上去更加清晰,这里我们使用 `demo-service-<index>` 来代替服务地址,直接在 Prometheus 所在节点的 `/etc/hosts` 文件中添加上对应服务的映射:
```shell
☸ ➜ cat /etc/hosts
......
192.168.31.46 demo-service-0
192.168.31.46 demo-service-1
192.168.31.46 demo-service-2
```
![Prometheus 演示服务](../img/20210410191428.png)
配置完成后直接启动 Prometheus 服务即可(可以参考前面的[安装配置章节](https://docs.youdianzhishi.com/prometheus/promql/basic/install/)):
```shell
☸ ➜ ./prometheus
```
启动后可以在 `/targets` 页面查看是否在正确抓取监控指标:
![Prometheus Targets](../img/20210412105544.png)
该演示服务**模拟**了一些用于我们测试的监控指标,包括:
- 暴露请求计数和响应时间(以 path、method 和响应状态码为标签 key)的 HTTP API 服务
- 一个定期的批处理任务,它暴露了最后一次成功运行的时间戳和处理的字节数
- 有关 CPU 数量及其使用情况的综合指标
- 有关内存使用情况的综合指标
- 有关磁盘总大小及其使用情况的综合指标
- 其他指标......
我们将在后面查询各个指标时了解更多关于它们的信息。