使用 Prometheus 搭建监控系统

本文更新于 {{ days }} 天前,内容可能已经过时。

Prometheus 是一个知名的开源监控和告警系统,它以多维时序数据模型和强大的 PromQL 查询语言著称,已成为云原生生态中事实上的监控标准。本文将介绍 Prometheus 的核心概念、使用方法、架构设计以及如何搭建一套完整的监控系统。

本文基于的软件版本如下,所涉及的内容可能会与后续版本有差异。

  • Prometheus 3.12.0
  • Alertmanager 0.33.1
  • Grafana 13.1.0

Prometheus 介绍#

Prometheus 由 SoundCloud 于 2012 年开发,并于 2016 年加入 CNCF,成为继 Kubernetes 之后 CNCF 的第二个毕业项目。Prometheus 之所以能在众多监控系统中脱颖而出,主要得益于以下几个特点。

  • 多维时序数据模型:数据以指标名称和键值对标签进行组织,天然支持多维度聚合和过滤。
  • PromQL 查询语言:功能强大的查询语言,支持对时序数据进行聚合、切片、计算等操作,可直接用于告警规则和仪表盘展示。
  • 运维简单:使用 Go 语言开发,静态链接的二进制文件易于在各种环境部署。
  • 生态丰富:社区提供了大量集成工具,覆盖数据库、中间件、硬件等各类监控场景,同时与 Grafana、Alertmanager 等工具无缝集成。

Prometheus 使用#

安装 Prometheus#

Prometheus 官网提供了多种安装方式,前面我们也提到了 Prometheus 运维简单的特点,它的安装也是比较简单的,直接前往 GitHub 下载对应版本的二进制文件即可。

下载好的压缩包解压后包含以下文件。

├── prometheus     # Prometheus 主程序
├── prometheus.yml # Prometheus 配置文件示例
└── promtool       # Prometheus 命令行工具,提供验证配置、测试规则、分析数据以及执行诊断任务等功能

通过运行以下命令判断下载的 Prometheus 的版本。

1
./prometheus --version

通过以下命令启动 Prometheus,并指定配置文件。启动后 Prometheus 默认监听 9090 端口,访问 http://localhost:9090 即可看到 Prometheus 的 Web UI。

1
./prometheus --config.file=prometheus.yml

接入 Prometheus#

我们实现一个简单的例子,模拟一个应用的指标上报。Prometheus 提供了多种编程语言的客户端库,其中 prometheus/client_golang 为 Go 应用程序提供接入 Prometheus 的功能。

Prometheus 的指标上报方式为拉模式,Go SDK 提供了一些通用的指标定义和封装,我们只需要实现一个 HTTP 接口 (通常默认为 /metrics),Prometheus 会定时拉取该接口返回的指标数据。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
	"net/http"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/collectors"
	"github.com/prometheus/client_golang/prometheus/promhttp"
)

func main() {
       // 注册 Go 和进程相关的指标
	reg := prometheus.NewRegistry()
	reg.MustRegister(
		collectors.NewGoCollector(),
		collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
	)
       // 实现接口 /metrics,返回指标数据
	http.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))
	http.ListenAndServe(":3000", nil)
}

编译并运行程序后,通过访问 http://localhost:3000/metrics 可以查看应用的指标数据。

接下来我们修改 Prometheus 的配置文件,增加一个定时拉取指标的配置。

1
2
3
4
5
6
7
scrape_configs:
  - job_name: "my-app"
    scrape_interval: 10s
    static_configs:
      - targets: ["localhost:3000"]
        labels:
          app: "my-app"

然后启动 Prometheus,并访问 http://localhost:9090 即可查看应用的 Go 和进程等指标数据。比如,我们想要查询 Go 的版本信息,只需要在查询框输入 go_info 即可。

1
2
go_info               # Prometheus 自身也有指标,因此这里会返回两个 app 结果
go_info{app="my-app"} # 只查询当前应用的指标

prom-go-info

这里 Table 展示的是查询时间点的值,切换到 Graph 可以查询一段时间的时序数据。

自定义指标上报#

虽然 Prometheus 的 Go SDK 提供了一些通用的指标定义,但是大部分的指标应该是涉及到业务自身的,因此我们需要通过 Prometheus 提供的能力来注册和支持自定义指标。

在 Prometheus 里面有一个指标类型为 Counter,是一个递增的计数器。下面的例子中,我们举了一个 Counter 指标定义的例子。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
	"math/rand"
	"net/http"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/collectors"
	"github.com/prometheus/client_golang/prometheus/promauto"
	"github.com/prometheus/client_golang/prometheus/promhttp"
)

func main() {
	reg := prometheus.NewRegistry()
	counter := promauto.With(reg).NewCounter(prometheus.CounterOpts{
		Name: "my_app_counter",
		Help: "A test counter",
	})

	go func() {
		for {
            // 模拟指标递增,也可以使用 counter.Inc() 实现 +1
			counter.Add(float64(rand.Intn(100)))
			time.Sleep(2 * time.Second)
		}
	}()
	http.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))
	http.ListenAndServe(":3000", nil)
}

运行 Prometheus 并查询指标 my_app_counter 即可查询到应用的指标数据。

prom-my-app-counter

概念与语法#

数据模型#

Prometheus 采集和存储的数据本质上是时序数据,每一个时间序列都由其指标名称和称为标签的可选键值对组成唯一标识,比如我们需要统计接口的请求量,指标的定义如下:

  • 指标名称:http_requests_total
  • 标签:path、method、status_code

其中每一种 path、method、status_code 组合具备一个带时间戳的值流,即每个时间点上的值代表这个 path、method、status_code 组合下的这个时间的请求量。

同时,Prometheus 支持了多时间序列的聚合操作,使得我们可以统计任意维度下的指标数据。比如我们需要统计某个特定 path 的总请求量。

在 Prometheus 中,有两个特定的标签:job 和 instance。instance 是被抓取的目标实例,比如 <host>:<port>,而 job 是一组具备相同目的的实例集合,比如 my-app。当 Prometheus 抓取一个目标时,会自动为这些时间序列添加上这两个标签。

指标类型#

Prometheus 提供了四种核心指标类型:

  • Counter (计数器):单调递增的计数器,其值只能增加,或者在重启时重置为零,比如用于统计已处理的请求数、任务数或错误数。
  • Gauge (仪表盘):可以任意升降的数值,用于测量当前状态,比如温度、内存使用率、当前并发请求数等。
  • Histogram (直方图):分桶的计数器,通过可配置桶对观察结果进行计数,比如请求持续时间或响应大小的分布。
  • Summary (分位图/摘要):与直方图类似都是对观察值进行采样,但是是基于分位去统计而非桶,比如请求持续时间的分位数。

在前面的例子中,我们便是使用了自定义的 Counter 指标类型,通过 counter.Add() 和 counter.Inc() 来实现计数器递增。

PromQL 查询#

Prometheus 提供了一种称为 PromQL 的函数式查询语句,允许用户查询指标数据。PromQL 支持了时间范围查询、标签选择、聚合操作、时间戳操作等功能。

在 PromQL 中,一个表达式或者子表达式可以被求值为以下几种类型:

  • 标量 (Scalar):单个浮点数值,比如 1.0、2.5。
  • 即时向量 (Instant Vector):某一时刻的时间序列,通常用来看状态。
  • 范围向量 (Range Vector):某一时间范围内的时间序列,通常用来计算差值、速率。

即时向量选择器允许我们能直接查询到某个指标的在每个时间点的值,PromQL 表达式很简单,只需要指定指标名称即可。通过花括号可以筛选出符合指定标签的指标。

简单理解的话,即时向量查询就是查这个指标这个时间点的原始数据,比如我们上述的 go_info Table,实际查询到的就是当前时间的原始数据。而当我们将 Prometheus 查询切换到 Graph 模式后,则是查询一段时间内的 go_info 的所有即时向量,并将这些即时向量组成一条曲线。

以下语句可以查询到 http_request_total 在任意时刻的 Counter 指标值,但是值本身只能代表从计数创建开始的总请求量,没有其他含义。

1
2
3
4
5
# 查询所有 http_requests_total 指标
http_requests_total

# 通过标签过滤
http_requests_total{path="/", status_code="200"}

当我们需要计算 http_request_total 的速率和增量的时候,我们需要引入范围向量选择器。通过指定一个时间范围,使用方括号语法 [duration],可以查询这个时间段内的所有数据点。

在 Graph 模式下,范围向量选择器的结果不能直接展示,通常需要配合聚合函数使用,因为范围向量选择器的结果本身就已经是一个范围向量,而查询一段时间的范围向量相当于每个时间点都具备一段连续采样点,不能直接组成一条曲线。为了让结果可展示,我们需要将连续采样点聚合成 1 个点,因此需要使用聚合函数。比如通过 rate 函数可以求速率,increase 函数求增量/差值。

1
2
3
4
5
# 通过每 1m 的采样点,计算 http_requests_total 的速率 (QPS)
rate(http_requests_total[1m])

# 通过每 1m 的采样点,计算 http_requests_total 的增量 (请求量)
increase(http_requests_total[1m])

我们可以在指标后面通过花括号来过滤特定标签的数据,但是我们查到的数据都是按标签分组的多条曲线,为了做曲线聚合,可以通过 sum 和 by、without 做聚合和分组操作。

1
2
3
4
5
6
7
8
# 查询 /ping 下所有 path/method/status_code 组合的 QPS,能查到到多条 path/method/status_code 不同的指标曲线
rate(http_requests_total{path="/ping"}[1m])

# 查询 /ping 的总 QPS,只能查到一条指标曲线
sum(rate(http_requests_total{path="/ping"}[1m]))

# 查询 /ping 的总 QPS,然后按 status_code 进行分组,能查到多条 status_code 不同的指标曲线
sum by(status_code) (rate(http_requests_total{path="/ping"}[1m]))

PromQL 还支持 max、avg、min、count、topk、bottomk 等聚合函数,标签过滤中可以使用 =!==~ (正则匹配)、!~ (正则不匹配) 等匹配操作符。

PromQL 同时支持复杂的运算逻辑,以支持指标与指标间的计算。比如,计算 HTTP 服务的成功率。

1
1 - sum(increase(http_requests_total{status_code=~"(4|5).*"}[1m])) / sum(increase(http_requests_total[1m]))

Web应用示例#

接下来我们实现一个 Web 应用示例,来展示 Counter、Gauge、Histogram、Summary 四种核心指标类型的用法。在这个例子中,我们定义了四个自定义指标:http_requests_in_progress、http_requests_total、http_requests_latency_us、http_requests_latency_us_histogram。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package main

import (
	"fmt"
	"math/rand/v2"
	"net/http"
	"net/http/httptest"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/collectors"
	"github.com/prometheus/client_golang/prometheus/promauto"
	"github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
	reg                      *prometheus.Registry
	requestsInProgress       *prometheus.GaugeVec
	requestsTotal            *prometheus.CounterVec
	requestsLatency          *prometheus.SummaryVec
	requestsLatencyHistogram *prometheus.HistogramVec
)

var labels = []string{"method", "path", "status_code"}

func init() {
	reg = prometheus.NewRegistry()
	reg.MustRegister(
		collectors.NewGoCollector(),
		collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
	)
	requestsInProgress = promauto.With(reg).NewGaugeVec(prometheus.GaugeOpts{Name: "http_requests_in_progress"}, labels[:2])
	requestsTotal = promauto.With(reg).NewCounterVec(prometheus.CounterOpts{Name: "http_requests_total"}, labels)
	requestsLatency = promauto.With(reg).NewSummaryVec(prometheus.SummaryOpts{
		Name:       "http_requests_latency_us",
		Objectives: map[float64]float64{0.5: 0.01, 0.9: 0.01, 0.95: 0.01, 0.99: 0.01},
	}, labels)
	requestsLatencyHistogram = promauto.With(reg).NewHistogramVec(prometheus.HistogramOpts{
		Name:    "http_requests_latency_us_histogram",
		Buckets: []float64{100000, 300000, 500000, 700000, 1000000},
	}, labels)
}

func handler(w http.ResponseWriter, r *http.Request) {
	// 上报 Metrics
	var code int
	requestsInProgress.With(prometheus.Labels{"method": r.Method, "path": r.URL.Path}).Inc()
	defer func(start time.Time) {
		labels := prometheus.Labels{"method": r.Method, "path": r.URL.Path, "status_code": fmt.Sprint(code)}
		requestsInProgress.With(prometheus.Labels{"method": r.Method, "path": r.URL.Path}).Dec()
		requestsTotal.With(labels).Inc()
		requestsLatency.With(labels).Observe(float64(time.Since(start).Microseconds()))
		requestsLatencyHistogram.With(labels).Observe(float64(time.Since(start).Microseconds()))
	}(time.Now())

	// 模拟处理耗时
	time.Sleep(time.Duration(rand.IntN(1000)) * time.Millisecond)

	// 模拟成功或者失败
	if v := rand.IntN(1000); v < 900 {
		code = http.StatusOK
	} else if v >= 900 && v < 950 {
		code = http.StatusBadRequest
	} else {
		code = http.StatusGatewayTimeout
	}
	w.WriteHeader(code)
}

func main() {
	http.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))
	http.HandleFunc("/", handler)
	http.HandleFunc("/ping", handler)
	http.ListenAndServe(":3000", nil)
}

请求量、QPS 和成功率#

在上面的例子中,http_requests_total 是一个 Counter 计数器,每当处理一个请求则加一。在前面我们提到过,Counter 指标直接查询到的结果是查询时间的瞬时向量,无法描述请求量和 QPS。因此,我们需要使用范围向量选择器和聚合函数来查询数据。

我们先是通过范围向量选择器查询每 1 分钟的采样点,然后通过 increase 聚合函数获取这些采样点的增量,即 1 分钟内的请求量。由于指标具备多个标签,这样查询的结果会有多条曲线,因此通过 sum 聚合函数来获取多条曲线的总和,最终得到 Web 应用的请求量。

1
sum(increase(http_requests_total[1m]))

prom-web-requests

查询 QPS 同理,只不过我们不是通过 increase 来获取采样点的增量,而是通过 rate 获取采样点的速率。

1
sum(rate(http_requests_total[1m]))

prom-web-qps

为了获取成功率,我们需要在请求量的基础上拆分出成功请求和失败请求。

1
2
1 - sum(increase(http_requests_total{status_code=~"(4|5).*"}[1m])) /
	sum(increase(http_requests_total[1m]))

prom-web-succ-rate

分位时延和分桶时延#

上述例子中的 http_requests_latency_us 是一个 Summary 分位图,用于描述请求处理耗时的分位数。Summary 需要提前定义分位值,在上面的例子中,我们通过 Objectives 定义了 P50、P90、P95、P99 耗时分位,因此查询的时候也只能查询这些分位。

Summary 本身就是一个即时向量,因此可以直接查询,无需通过范围向量选择器查询。不过如果需要聚合多个曲线,还是需要用 avg 聚合或者其他聚合函数。

1
http_requests_latency_us{quantile="0.9",status_code="200"}

prom-web-latency

分位时延计算天然就具备了误差,因为分位数是基于样本的统计值,只会存储指定的分位数据,而不会存储所有原始数据。

如果我们不希望拿到的分位时延,而是期望统计某一段时延范围的请求量级,我们可以使用 Histogram 直方图指标。

1
2
3
4
5
6
7
8
var (
    requestsLatencyHistogram *prometheus.HistogramVec
)

requestsLatencyHistogram = promauto.With(reg).NewHistogramVec(prometheus.HistogramOpts{
    Name:    "http_requests_latency_us_histogram",
    Buckets: []float64{100000, 300000, 500000, 700000, 1000000},
}, labels)

Histogram 需要提前定义好桶,然后上报指标时会修改对应的桶的数据。Prometheus 为 Histogram 自动创建了三个指标。

  • _bucket:Counter 指标,具备特殊标签 le,用于统计小于 le 值的桶的请求量级
  • _sum:Counter 指标,存储所有值的总和
  • _count:Counter 指标,存储所有值的总数

一般来说我们要查询某个耗时范围的请求量,只需要使用 _bucket 后缀指标即可。比如要查询 500ms 耗时以上的请求量。

1
2
sum(increase(http_requests_latency_us_histogram_bucket{le="+Inf"}[1m])) -
sum(increase(http_requests_latency_us_histogram_bucket{le="500000.0"}[1m]))

prom-web-latency-histogram

并发量#

在上面的例子中,http_requests_in_progress 是一个 Gauge 指标,用于描述当前正在处理的并发量,在请求开始的时候给这个指标加一,请求结束的时候减一。

Gauge 指标本身就是即时向量,直接查询的结果就是当前正在处理的并发量。不过如果为了聚合不同标签的并发量,我们还是需要使用 sum 聚合函数。

1
sum(http_requests_in_progress)

prom-web-concurrency

生产可用的 Prometheus#

Basic Auth 鉴权#

当我们将 Prometheus 部署到生产环境时,期望从外部访问 Prometheus Web UI 界面,方便查询线上指标数据排查问题。由于 Prometheus 默认没有做任何鉴权判断,直接暴露到互联网可能存在数据泄露风险,因此我们需要添加鉴权策略。

Prometheus 支持 Basic Auth 鉴权,我们可以添加一个 web.yml 的配置文件,来指定用户名和密码。

1
2
basic_auth_users:
  admin: $2a$12$UJCGsQ5fhVmOg.6KtttqNOSNll/Yzmr3K5inzCno5zl0Yn29nYevq

上面指定了用户名 admin 和密码 123456,在配置文件中的密码是 123456 经过 bcrypt 加盐哈希的结果。

通过以下命令启动 Prometheus,访问 Web UI 的时候需要输入用户名和密码。

1
./prometheus --config.file=prometheus.yml --web.config.file=web.yml

Alertmanager 告警系统#

Prometheus 的告警系统分为两部分:Prometheus Server 负责评估告警规则并将触发的告警发送给 Alertmanager,Alertmanager 负责告警的去重、分组、抑制和路由到不同的通知渠道。

首先,我们需要在 Prometheus 的配置文件中指定告警规则文件的路径,然后添加告警规则配置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# prometheus.yml
rule_files:
  - my_app_rules.yml

# my_app_rules.yml
groups:
  - name: my_app
    rules:
      - alert: 成功率低于90%
        expr: 1 - sum(increase(http_requests_total{status_code=~"(4|5).*"}[1m])) / sum(increase(http_requests_total[1m])) < 0.9
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "服务成功率已经只有 {{ $value }} 了,请及时跟进"

上述规则的含义是,如果服务在 1 分钟内请求失败率超过 10%,则触发名为 成功率低于90% 的告警。

  • alert:告警名称,用于标识告警。
  • expr:PromQL 表达式,用于判断是否触发告警。
  • for:告警持续时间,只有当条件持续满足指定时间后才会触发告警,避免因瞬时波动产生误报。
  • labels:附加到告警的标签,可用于路由或分类。
  • annotations:告警的注释信息,支持模板变量,用于生成告警通知的摘要和描述。

重新启动 Prometheus,可以在 Alerts 页面查看已经配置生效的告警规则和相应的告警状态,告警存在以下三种状态:

  • Inactive:没有达到告警阈值
  • Pending:达到了告警阈值,但是还没有持续满足指定时间,即 for 的值
  • Firing:达到了告警阈值,且满足告警的持续时间

prom-alert

为了接收告警通知,我们需要安装和配置 Alertmanager。Alertmanager 同样可以从 GitHub 下载二进制文件,解压后的目录结构类似。通过运行以下命令启动 Alertmanager,并指定配置文件,启动后 Alertmanager 会监听 9093 端口,也有一个类似 Prometheus Web UI 的 Web 界面用于查看、操作、屏蔽告警。

1
./alertmanager --config.file=alertmanager.yml

接下来我们试着将告警通知发送到邮箱中。首先需要修改 Prometheus 的配置文件,添加 Alertmanager 的地址。然后需要在 Alertmanager 的配置文件中添加邮箱配置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# prometheus.yml
alerting:
  alertmanagers:
    - static_configs:
        - targets:
          - localhost:9093

# alertmanager.yml
global:
  smtp_smarthost: "smtp.example.com:465"
  smtp_from: "noreply@example.com"
  smtp_auth_username: "noreply@example.com"
  smtp_auth_password: "your_password"
  smtp_require_tls: false

route:
  group_by: ["alertname"] # 按 alertname 分组,相同分组的告警会合并为一条通知
  group_wait: 30s # 等待 30 秒以收集同一组的告警,然后一起发送
  group_interval: 5m # 已发送的组如果有新的其他告警,等待 5 分钟后再次发送
  repeat_interval: 1h # 已发送的告警如果仍在触发,等待 1 小时后重复发送
  receiver: "email"

receivers:
  - name: "email"
    email_configs:
      - to: "test@example.com" # 告警发送邮箱地址

然后重启 Prometheus 和 Alertmanager,如果服务成功率低于 90% 的时候,邮箱会收到对应的告警邮件。

alert-mail

Grafana 监控大盘#

在前面我们通过 Prometheus Web UI 查询和展示了指标数据,虽然 PromQL 功能强大,但 Prometheus 自带的 Web UI 在可视化方面比较简陋,只支持基本的图表展示,无法满足生产环境的监控需求。比如,我们希望能将多个指标组合到一个仪表盘中、支持丰富的图表类型 (时序图、热力图、仪表盘等)、支持变量和模板、支持告警状态展示等。

Grafana 是一个开源的数据可视化平台,支持 Prometheus、InfluxDB、Elasticsearch、MySQL 等多种数据源。它提供了丰富的面板类型和灵活的布局能力,可以将 Prometheus 的指标数据以直观、美观的方式展示出来,是 Prometheus 生态中最常用的可视化搭档。

我们需要前往 Grafana 官方网站 下载安装包并安装,也可以直接下载 Standalone 版本直接开箱即用,也可以通过 Docker 使用。安装好 Grafana 后,通过命令运行 Grafana。

1
2
# Windows 运行命令
grafana server

Grafana 启动后将在 3000 端口启动 Web UI 界面,默认用户名和密码均为 admin。首次登录会提示修改密码,修改后即可进入 Grafana 的主页。

在使用 Grafana 展示 Prometheus 的指标数据之前,需要先将 Prometheus 配置为 Grafana 的数据源。进入 Grafana 后,依次点击 “Connections” -> “Data Sources” -> “Add data source”,选择 “Prometheus”,填写 Prometheus 的访问地址。然后通过 “Save & test” 测试连接是否成功。

数据源配置完成后,我们可以开始创建仪表盘。点击 “Dashboards” -> “New Dashboard”,然后添加面板,选择刚才配置的 Prometheus 数据源,即可开始添加面板。在前面 Web 应用示例中,我们定义了四个自定义指标,接下来我们将这些指标逐一展示到 Grafana 仪表盘中,最终效果如下图。

grafana

TSDB 时序数据库#

存储引擎#

Prometheus 自身实现了一个 TSDB 存储引擎,用于存储指标数据。接下来,我们将简单研究下 Prometheus 的时序数据库的架构和原理。

首先我们分析下 Prometheus 的本地文件。

data/
├── 01KX41.../          # Block (2h)
│   ├── meta.json       # Block 元信息
│   ├── chunks/         # 时序数据
│   │   └── 000001
│   ├── index           # 索引文件
│   └── tombstones      # 删除标记
├── 01KXAN.../          # Block (2h)
├── ...
├── chunks_head/        # 当前 Head Block 的 Chunk 文件
│   └── 00000001
└── wal/                # Write-Ahead Log
    ├── 00000002
    └── 00000003

默认 Prometheus 的存储都是在 data 目录下面,里面存放了 Head Chunks、WAL、Block 三种类型的数据。

  • Block: 持久化存储,一个 Block (2 小时数据) 可能包含多个 Chunk 文件,指标数据存储在 Chunk 中;
  • Head Chunks: 当前 Head Block (内存数据) 的 Chunk 文件,用于崩溃时重建内存状态;
  • WAL: Write-Ahead Log,操作日志记录 (包含元数据和指标数据), 用于崩溃时重建内存状态。

除此之外,Prometheus 在内存中维护了 Head Block,用于存储最新的 2 小时数据。

为了提高数据查询效率,Prometheus 给每个 Block 都维护了一个倒排索引,用于快速定位到对应的时间序列。倒排索引的文件格式如下:

  • Symbol Table: 符号表,用于存储指标标签的名称和值。
  • Series: 指标序列,用于存储指标的标签引用和 Chunk 引用,不存储指标数据本身。
┌────────────────────────────┬─────────────────────┐
│ magic(0xBAAAD700) <4b>     │ version(1) <1 byte> │
├────────────────────────────┴─────────────────────┤
│ ┌──────────────────────────────────────────────┐ │
│ │                 Symbol Table                 │ │
│ ├──────────────────────────────────────────────┤ │
│ │                    Series                    │ │
│ ├──────────────────────────────────────────────┤ │
│ │                 Label Index 1                │ │
│ ├──────────────────────────────────────────────┤ │
│ │                      ...                     │ │
│ ├──────────────────────────────────────────────┤ │
│ │                 Label Index N                │ │
│ ├──────────────────────────────────────────────┤ │
│ │                   Postings 1                 │ │
│ ├──────────────────────────────────────────────┤ │
│ │                      ...                     │ │
│ ├──────────────────────────────────────────────┤ │
│ │                   Postings N                 │ │
│ ├──────────────────────────────────────────────┤ │
│ │               Label Offset Table             │ │
│ ├──────────────────────────────────────────────┤ │
│ │             Postings Offset Table            │ │
│ ├──────────────────────────────────────────────┤ │
│ │                      TOC                     │ │
│ └──────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘

指标数据本身将存储在 Chunks 文件中,具体文件格式如下。每个 Chunk 文件中记录了 Chunk 的长度、编码方式、数据、CRC 校验和等,而数据本身将通过 XOR 等编码方式进行压缩编码,以减少存储空间占用。

┌──────────────────────────────┐
│  magic(0x85BD40DD) <4 byte>  │
├──────────────────────────────┤
│    version(1) <1 byte>       │
├──────────────────────────────┤
│    padding(0) <3 byte>       │
├──────────────────────────────┤
│ ┌──────────────────────────┐ │
│ │         Chunk 1          │ │
│ ├──────────────────────────┤ │
│ │          ...             │ │
│ ├──────────────────────────┤ │
│ │         Chunk N          │ │
│ └──────────────────────────┘ │
└──────────────────────────────┘

┌───────────────┬───────────────────┬─────────────┬───────────────────┐
│ len <uvarint> │ encoding <1 byte> │ data <data> │ checksum <4 byte> │
└───────────────┴───────────────────┴─────────────┴───────────────────┘

写入和查询#

Prometheus 的数据写入路径如下:

  1. 内存缓冲:新采集的样本首先写入 WAL 并追加到内存中的 Head Block,并将数据 mmap 到 Head Chunks 中;
  2. 批量压缩:当 Head Block 积累到 2 小时的数据后,会被压缩成一个持久化的 Block;
  3. 数据重建:当 Prometheus 进程崩溃时,会从 WAL 中恢复数据并重建 Head Block。

Prometheus 的数据查询路径如下:

  1. 倒排索引:首先解析 PromQL 表达式,通过 index 倒排索引定位到需要查询的 Chunk 引用。
  2. 读取数据:根据 Chunk 引用从 Chunks 文件中读取数据,并解码还原为原始的采样点;
  3. 合并结果:查询跨越多个 Block 时将查询结果按时间顺序合并,最终返回给 PromQL 引擎进行聚合计算。

结语#

Prometheus 作为云原生生态的事实标准监控方案,凭借其强大和丰富的功能,已经成为微服务和容器化环境不可或缺的运维工具。当然,本文只是介绍了 Prometheus 的基础用法,也是本人学习 Prometheus 的记录,它还有更多高级特性和原理值得探索,这里就不再继续展开了,希望本文能帮助到你快速上手 Prometheus,并在实际项目中搭建起一套可靠的监控体系。