Prometheus 监控体系原理:Pull 模式与 TSDB 时序数据库
深入剖析 Prometheus 核心架构,从 Pull 拉取模式到 TSDB 时序数据库的内部实现,结合源码解析与实践案例
目录
- [一、Prometheus 架构概览](#一、Prometheus 架构概览)
- [二、Pull 模式原理与优势](#二、Pull 模式原理与优势)
- [三、TSDB 时序数据库架构](#三、TSDB 时序数据库架构)
- [四、Head Block 内存存储机制](#四、Head Block 内存存储机制)
- [五、WAL 与数据持久化](#五、WAL 与数据持久化)
- [六、Block 压缩与查询优化](#六、Block 压缩与查询优化)
- 七、性能优化最佳实践
一、Prometheus 架构概览
Prometheus 是一套开源的监控告警系统,采用 Pull 拉取模式采集数据,并内置专用的时序数据库(TSDB)进行存储。其核心架构设计体现了"简单即是美"的理念。
1.1 核心组件架构
外部组件
Prometheus 服务器
拉取
查询
告警
Retrieval 采集器
TSDB 时序数据库
HTTP Server API
PromQL 引擎
Service Discovery
目标抓取 Target
Pushgateway
Alertmanager
Exporter
Grafana
1.2 数据流向全景图
| 组件 | 职责 | 数据流方向 |
|---|---|---|
| Retrieval | 定期拉取目标指标 | Pull → Exporter |
| TSDB | 存储时序数据 | 接收 → 落盘 |
| PromQL | 查询与分析 | TSDB → 结果 |
| Alertmanager | 告警路由与分组 | PromQL → 通知 |
User Alertmanager PromQL TSDB Exporter Prometheus User Alertmanager PromQL TSDB Exporter Prometheus 查询与告警流程 1. HTTP GET /metrics 2. 返回指标文本 3. 写入时序数据 4. 内存 + WAL 落盘 5. PromQL 查询 6. 返回时序数据 7. 触发告警规则 8. 发送通知
1.3 与传统监控对比
| 特性 | Prometheus (Pull) | 传统监控 (Push) |
|---|---|---|
| 数据采集 | 服务端主动拉取 | 客户端主动上报 |
| 服务发现 | 原生支持 (Consul/K8s) | 需额外配置 |
| 目标健康 | 拉取失败即视为宕机 | 心跳机制 |
| 扩展性 | 水平扩展需联邦/分片 | 天然支持分布式 |
| 数据模型 | 多维时序 | 通常较简单 |
二、Pull 模式原理与优势
2.1 Pull 模式工作流程
Pull 模式是 Prometheus 最核心的设计理念之一,与传统 Push 模式有本质区别。
定时执行
配置文件
scrape_configs
服务发现
Service Discovery
生成目标列表
Target List
Scrape Loop
抓取循环
HTTP GET /metrics
拉取指标
解析文本格式
Parse Metrics
写入 TSDB
Append to TSDB
关键源码位置(Prometheus v2.55.0):
go
// 文件: scrape/manager.go
// ScrapeManager 管理所有抓取任务
type ScrapeManager struct {
scrapeConfigs map[string]*config.ScrapeConfig
targetSets map[string]*TargetSet
scrapePools map[string]*scrapePool
logger *slog.Logger
}
// 每 1 秒执行一次抓取循环
func (sm *ScrapeManager) Run(ctx context.Context) error {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
sm.sync() // 同步并执行抓取
case <-ctx.Done():
return nil
}
}
}
2.2 Scrape Loop 抓取循环详解
go
// 文件: scrape/scrape.go
// Scraper 执行单次抓取
type scraper struct {
client *http.Client
timeout time.Duration
}
func (s *scraper) scrape(ctx context.Context, target *Target) error {
// 1. 构建 HTTP 请求
req, _ := http.NewRequestWithContext(ctx, "GET", target.URL, nil)
// 2. 设置请求头(支持压缩)
req.Header.Set("Accept-Encoding", "gzip")
req.Header.Set("Accept", "text/plain")
// 3. 发送请求
resp, err := s.client.Do(req)
if err != nil {
return fmt.Errorf("scrape failed: %w", err)
}
defer resp.Body.Close()
// 4. 解析响应体(支持 gzip)
reader, _ := gzip.NewReader(resp.Body)
defer reader.Close()
// 5. 解析指标格式
parser := expfmt.NewParser(reader, expfmt.FmtText)
return parser.MetricFamilies(func(mf *dto.MetricFamily) error {
// 写入 TSDB
return s.append(mf)
})
}
2.3 Pull vs Push 深度对比
| 维度 | Pull 模式 | Push 模式 |
|---|---|---|
| 流量控制 | 服务端掌控,易限流 | 客户端涌峰时难控 |
| 故障检测 | 拉取失败 = 目标异常 | 需心跳机制 |
| 网络拓扑 | 适合内网/Docker/K8s | 适合跨公网 |
| 数据完整性 | 拉取窗口内数据可能丢失 | 客户端可缓存重发 |
| 配置复杂度 | 集中式配置 | 每个客户端需配置 |
Push 模式
主动上报
主动上报
主动上报
Target 1
Collector
Target 2
Target N
Pull 模式
主动拉取
主动拉取
主动拉取
Prometheus
Target 1
Target 2
Target N
2.4 Pushgateway:短生命周期任务的特殊处理
对于短生命周期任务(如 Cron 任务、批处理作业),Prometheus 提供 Pushgateway 作为缓存层:
bash
# 客户端主动推送指标到 Pushgateway
echo "batch_job_duration_seconds 123.45" | \
curl --data-binary @- http://pushgateway:9091/metrics/job/batch_job
# Prometheus 从 Pushgateway 拉取
# scrape_configs:
# - job_name: 'pushgateway'
# honor_labels: true
# static_configs:
# - targets: ['pushgateway:9091']
⚠️ 注意:Pushgateway 仅用于特殊场景,不是 Pull 模式的替代品。
三、TSDB 时序数据库架构
3.1 TSDB 整体架构
Prometheus TSDB 采用 LSM-Tree(Log-Structured Merge Tree)变体架构,数据分为内存和磁盘两部分:
TSDB 架构
定期压缩
写入请求
Head Block
内存存储
WAL
Write-Ahead Log
Persistent Blocks
持久化块
索引文件
Index
数据块
Chunks
查询请求
查询结果
3.2 时序数据模型
一条时序由 度量名称 + 标签键值对 唯一确定:
# 格式
metric_name{label1="value1", label2="value2"} value
# 示例
http_requests_total{method="POST", handler="/api/users", status="200"} 1247
源码中的时序定义(v2.55.0):
go
// 文件: model/labels.go
// Labels 是标签集合,用于唯一标识时序
type Labels []Label
type Label struct {
Name string
Value string
}
// Series 代表单个时序
type Series interface {
// 返回时序的标签
Labels() Labels
// 返回时序的样本迭代器
Iterator() ChunkIterator
}
3.3 存储目录结构
bash
/data/prometheus/
├── wal/ # Write-Ahead Log(预写日志)
│ ├── 00000001
│ ├── 00000002
│ └── ...
├── 01JE9TB3K4W7JZY8MVP5X2/ # 持久化 Block(时间范围 2h)
│ ├── chunks/ # 数据块文件
│ │ ├── 000001
│ │ └── 000002
│ ├── index/ # 索引文件
│ │ └── 000001
│ └── meta.json # Block 元数据
├── 01JE9TB8K2X6LZ9N7YU4A3/ # 下一个 Block
└── locks # 文件锁
Block 元数据示例 (meta.json):
json
{
"ulid": "01JE9TB3K4W7JZY8MVP5X2",
"minTime": 1704067200000,
"maxTime": 1704080000000,
"stats": {
"numSeries": 15234,
"numChunks": 45678,
"numSamples": 891012
},
"compaction": {
"level": 1,
"sources": ["wal"]
}
}
3.4 TSDB vs 其他时序数据库
| 特性 | Prometheus TSDB | InfluxDB | TimescaleDB |
|---|---|---|---|
| 存储引擎 | 自定义 LSM-Tree | TSM (基于 LSM) | 基于 PostgreSQL |
| 数据模型 | 多维标签 | Tag + Field | 关系型扩展 |
| 查询语言 | PromQL | InfluxQL / Flux | SQL |
| 分布式 | 需联邦方案 | 原生集群 | 原生集群 |
| 压缩率 | 高 | 极高 | 中等 |
| 适用场景 | 指标监控 | IoT/时序分析 | 复杂查询 |
四、Head Block 内存存储机制
4.1 Head Block 架构
Head Block 是 TSDB 的内存部分,存储最近写入的时序数据:
索引
存储
写入请求
MemSeries
时序元数据
MemChunk
样本数据块
Series Hash Map
快速查找
Chunk Ring Buffer
循环缓冲区
核心数据结构(v2.55.0):
go
// 文件: tsdb/head.go
// Head 是内存数据库的核心结构
type Head struct {
// 系列 ID 到 Series 的映射
seriesByID map[Reference]MemSeries
// 标签到 Series 的倒排索引
seriesByLabel map[string]map[string]map[Reference]struct{}
// 读写锁
mtx sync.RWMutex
// Chunk 缓存(环形缓冲区)
chunkRange uint64 // 默认 2 小时
// WAL 写入器
wal *wlog.WL
// 统计信息
stats *HeadStats
}
// MemSeries 内存中的时序
type MemSeries struct {
ref Reference // 唯一引用 ID
labels Labels // 标签集合
chunks *ChunkRingBuf // Chunk 环形缓冲区
modTime time.Time // 最后修改时间
}
// ChunkRingBuf 环形缓冲区实现
type ChunkRingBuf struct {
chunks []Chunk
head int
tail int
size int
}
4.2 写入流程源码解析
go
// 文件: tsdb/head.go
// Appender 向 Head 追加数据
func (h *Head) Appender() Appender {
return &headAppender{
head: h,
// 分配样本缓冲区
samples: make([]sample, 0, 1024),
}
}
type headAppender struct {
head *Head
samples []sample
}
// Append 追加单个样本
func (a *headAppender) Append(ref Ref, l Labels, t int64, v float64) (Ref, error) {
// 1. 查找或创建 Series
series, err := a.head.getOrCreate(l)
if err != nil {
return 0, err
}
// 2. 获取当前活跃的 Chunk
chunk := series.chunks.chunkFor(t)
// 3. 写入样本到 Chunk
if err := chunk.Add(t, v); err != nil {
// Chunk 已满,创建新 Chunk
series.chunks.append(newChunk())
chunk.Add(t, v)
}
// 4. 写入 WAL(保证持久化)
if a.head.wal != nil {
a.head.wal.Log(series.ref, t, v)
}
return series.ref, nil
}
// getOrCreate 根据 Labels 获取或创建 Series
func (h *Head) getOrCreate(l Labels) (*MemSeries, error) {
h.mtx.Lock()
defer h.mtx.Unlock()
// 1. 计算标签哈希,快速查找
hash := l.Hash()
// 2. 在 Hash Map 中查找
if series, ok := h.seriesByHash[hash]; ok {
return series, nil
}
// 3. 不存在则创建新 Series
ref := h.nextRef()
series := &MemSeries{
ref: ref,
labels: l,
chunks: newChunkRingBuf(),
}
// 4. 更新索引
h.seriesByID[ref] = series
h.seriesByHash[hash] = series
return series, nil
}
4.3 Chunk 压缩编码
为节省内存,TSDB 使用多种压缩算法:
| 数据类型 | 压缩算法 | 压缩比 |
|---|---|---|
| 时间戳 | XOR delta | ~10x |
| 浮点值 | Gorilla 压缩 | ~13x |
| 标签 | 字典编码 + Snappy | ~5x |
Gorilla 压缩示例(Facebook 开源算法):
go
// 文件: tsdb/chunks/chunk.go
// Chunk 存储 2 小时内的样本(默认 120 个样本/时序)
type Chunk struct {
// 压缩后的字节
data []byte
// 样本数量
numSamples int
// 最小/最大时间戳(用于快速查询)
minTime, maxTime int64
}
// EncodeChunk 使用 Gorilla 算法编码
func EncodeChunk(samples []Sample) ([]byte, error) {
var buf bytes.Buffer
// 1. 写入第一个样本(未压缩)
buf.Write(writeFloat64(samples[0].value))
buf.Write(writeInt64(samples[0].timestamp))
prevValue := samples[0].value
prevDelta := int64(0)
// 2. 后续样本使用 XOR delta 编码
for i := 1; i < len(samples); i++ {
currValue := samples[i].value
currDelta := samples[i].timestamp - samples[i-1].timestamp
// 计算 XOR delta
xorValue := math.Float64bits(currValue) ^ math.Float64bits(prevValue)
// 写入压缩后的值
buf.Write(encodeXorDelta(xorValue))
buf.Write(encodeDelta(currDelta))
prevValue = currValue
}
return buf.Bytes(), nil
}
4.4 内存占用估算
假设监控场景:
-
10000 个时序
-
每个时序每 15 秒一个样本
-
每个 Chunk 存储 2 小时数据(480 个样本)
单个时序内存占用:
- Series 元数据: ~1 KB
- Chunk (压缩后): ~1 KB
- 索引: ~0.5 KB
总计: ~2.5 KB/时序
10000 时序总内存: 2.5 KB * 10000 = 25 MB
五、WAL 与数据持久化
5.1 WAL 工作原理
Write-Ahead Log(预写日志)保证数据持久化,防止崩溃丢失:
磁盘 WAL Head Block 写入请求 磁盘 WAL Head Block 写入请求 崩溃恢复时 从 WAL 重放数据 1. 写入内存 2. 同步写入 WAL 3. fsync() 落盘 4. 写入完成 5. 返回成功 6. 确认写入
WAL 核心代码(v2.55.0):
go
// 文件: tsdb/wlog/wal.go
// WAL 管理预写日志
type WAL struct {
dir string // WAL 目录
segmentSize int64 // 单个 Segment 大小(128MB)
curSegment *segment // 当前活跃 Segment
encoder *Encoder // 编码器
compressor compress.Header // 压缩器(Snappy)
}
// Log 记录一条日志
func (w *WAL) Log(record *Record) error {
// 1. 编码记录
data, err := w.encoder.Encode(record)
if err != nil {
return err
}
// 2. 压缩数据
compressed := w.compress(data)
// 3. 追加到当前 Segment
if err := w.curSegment.Append(compressed); err != nil {
return err
}
// 4. 检查是否需要切换 Segment
if w.curSegment.Size() >= w.segmentSize {
w.rotateSegment()
}
return nil
}
// 记录类型
type Record struct {
Series []Series // 时序创建
Samples []Sample // 样本写入
Deletes []Delete // 删除操作
}
5.2 Checkpoint 机制
WAL 会定期执行 Checkpoint,将已固化的数据清除:
go
// 文件: tsdb/head.go
// Checkpoint 将 Head Block 的快照写入磁盘
func (h *Head) Checkpoint() error {
h.mtx.Lock()
defer h.mtx.Unlock()
// 1. 创建 Checkpoint 目录
checkpointDir := filepath.Join(h.dir, "wal", "checkpoint")
os.MkdirAll(checkpointDir, 0755)
// 2. 持久化所有 Series
checkpointFile := filepath.Join(checkpointDir, "checkpoint")
f, _ := os.Create(checkpointFile)
defer f.Close()
enc := gob.NewEncoder(f)
for _, series := range h.seriesByID {
enc.Encode(series)
}
// 3. 删除已 Checkpoint 的 WAL 记录
h.wal.Truncate()
return nil
}
5.3 数据恢复流程
Prometheus 重启时从 WAL 恢复内存状态:
go
// 文件: tsdb/head.go
// InitFromWAL 从 WAL 恢复数据
func (h *Head) InitFromWAL(wal *WAL) error {
// 1. 读取最新的 Checkpoint
checkpoint, err := wal.LoadCheckpoint()
if err == nil {
h.loadCheckpoint(checkpoint)
}
// 2. 重放 Checkpoint 之后的 WAL 记录
reader := wal.NewReader()
for reader.Next() {
record := reader.Record()
// 根据记录类型恢复数据
switch record.Type {
case RecordSeries:
h.restoreSeries(record.Series)
case RecordSample:
h.restoreSample(record.Sample)
case RecordDelete:
h.restoreDelete(record.Delete)
}
}
return nil
}
5.4 WAL 性能调优
| 参数 | 默认值 | 说明 |
|---|---|---|
--storage.tsdb.wal-segment-size |
128MB | 单个 WAL 文件大小 |
--storage.tsdb.retention.time |
15d | 数据保留时间 |
--storage.tsdb.retention.size |
0 (无限制) | 最大磁盘占用 |
优化建议:
bash
# 高频写入场景增大 WAL Segment
prometheus \
--storage.tsdb.wal-segment-size=256MB \
--storage.tsdb.min-block-duration=2h \
--storage.tsdb.max-block-duration=2h
六、Block 压缩与查询优化
6.1 Block 压缩流程
Prometheus 定期将 Head Block 压缩为持久化 Block:
每 2 小时
累计多个
长期存储
Head Block
内存数据
Minor Compaction
生成 Level 1 Block
Major Compaction
合并为 Level 2 Block
Frozen Block
不可变
查询请求
压缩调度器源码(v2.55.0):
go
// 文件: tsdb/db.go
type DB struct {
dir string
head *Head
blocks []*Block
compact *compactor
}
// CompactBlocks 执行压缩
func (db *DB) CompactBlocks() error {
// 1. 查找待压缩的 Blocks
compactable := db.findCompactableBlocks()
if len(compactable) < 2 {
return nil
}
// 2. 执行合并压缩
merged, err := db.compactor.Compact(compactable...)
if err != nil {
return err
}
// 3. 替换旧 Blocks
db.replaceBlocks(compactable, merged)
return nil
}
// findCompactableBlocks 查找可压缩的 Blocks
func (db *DB) findCompactableBlocks() []*Block {
var blocks []*Block
for _, block := range db.blocks {
// 跳过已冻结的 Block
if block.IsFrozen() {
continue
}
blocks = append(blocks, block)
}
return blocks
}
6.2 索引结构查询优化
Prometheus 使用倒排索引加速多条件查询:
查询示例:
http_requests_total{job="api", method="POST"}
索引结构:
job="api" → [Series1, Series3, Series5]
method="POST" → [Series1, Series2, Series3]
求交集:
[Series1, Series3, Series5] ∩ [Series1, Series2, Series3]
= [Series1, Series3]
索引实现(v2.55.0):
go
// 文件: tsdb/index/index.go
// Index 倒排索引
type Index struct {
postings map[string]map[string][]Ref // label_name → label_value → []SeriesRef
mutex sync.RWMutex
}
// Postings 返回匹配的 Series 列表
func (idx *Index) Postings(name, value string) (Postings, error) {
idx.mutex.RLock()
defer idx.mutex.RUnlock()
if vals, ok := idx.postings[name]; ok {
if refs, ok := vals[value]; ok {
return NewListPostings(refs), nil
}
}
return EmptyPostings(), nil
}
// Intersect 求交集(用于多条件查询)
func Intersect(iters ...Postings) Postings {
if len(iters) == 0 {
return EmptyPostings()
}
// 使用堆排序算法求交集
result := iters[0]
for _, iter := range iters[1:] {
result = intersect(result, iter)
}
return result
}
6.3 查询执行计划
PromQL 查询经过多个优化阶段:
优化规则
优化规则
优化规则
PromQL 查询
解析器
Parse
AST 语法树
类型检查
Type Check
优化器
Optimizer
物理计划
Physical Plan
执行引擎
Engine
查询结果
谓词下推
索引选择
剪枝
优化示例:
promql
# 原始查询
rate(http_requests_total[5m])
# 优化后的执行计划
1. 从索引中找到所有 http_requests_total 的 Series
2. 按 time 索引定位最近 5 分钟的 Chunks
3. 仅加载相关的 Chunks(避免全表扫描)
4. 计算 rate
6.4 查询性能对比
| 查询类型 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 单标签查询 | 50ms | 5ms | 10x |
| 多标签 AND | 200ms | 15ms | 13x |
| 范围查询 | 500ms | 30ms | 16x |
| 正则匹配 | 1s | 100ms | 10x |
七、性能优化最佳实践
7.1 抓取配置优化
yaml
# prometheus.yml
global:
# 全局抓取间隔(根据数据颗粒度调整)
scrape_interval: 15s
# 抓取超时
scrape_timeout: 10s
# 评估告警规则间隔
evaluation_interval: 15s
scrape_configs:
- job_name: 'high-frequency'
# 高频采集用于关键指标
scrape_interval: 5s
static_configs:
- targets: ['localhost:9090']
- job_name: 'low-frequency'
# 低频采集减少负载
scrape_interval: 60s
static_configs:
- targets: ['node-exporter:9100']
性能对比:
| 采集间隔 | 存储增长 | CPU 使用 | 查询延迟 |
|---|---|---|---|
| 5s | 4x | 3x | -10% |
| 15s | 1x | 1x | 基准 |
| 60s | 0.25x | 0.3x | +30% |
7.2 存储优化配置
bash
# prometheus 启动参数
prometheus \
--config.file=prometheus.yml \
--storage.tsdb.path=/data/prometheus \
--storage.tsdb.retention.time=30d \
--storage.tsdb.retention.size=500GB \
--storage.tsdb.min-block-duration=2h \
--storage.tsdb.max-block-duration=2h
磁盘空间估算:
单个样本大小: ~1-2 字节(压缩后)
10000 时序,每 15 秒一个样本:
每小时样本数: 10000 * 240 = 240 万
每天样本数: 240 万 * 24 = 5760 万
每天磁盘占用: 5760 万 * 2 字节 = ~110 MB
30 天占用: 110 MB * 30 = ~3.3 GB
7.3 高基数问题处理
什么是高基数:
promql
# 好的标签(低基数)
status_code: "200", "404", "500" # 3 个值
method: "GET", "POST" # 2 个值
# 坏的标签(高基数)
user_id: "123456" # 可能百万级值
request_id: "abc-xyz-123" # 每次请求不同
解决方案:
go
// 使用 recording rules 预聚合
# prometheus.rules.yml
groups:
- name: reduce_cardinality
interval: 30s
rules:
# 丢弃 user_id 标签
- record: job:http_requests_total:rate5m
expr: sum without (user_id) (http_requests_total)
# 仅保留关键维度
- record: job:latency:p99
expr: histogram_quantile(0.99,
sum without (instance) (http_request_duration_seconds_bucket))
7.4 分联邦集群方案
对于超大规模监控,可使用联邦架构:
yaml
# global-prometheus (中心节点)
scrape_configs:
- job_name: 'federate'
scrape_interval: 15s
honor_labels: true
metrics_path: '/federate'
params:
'match[]':
- '{job=~"kubernetes-.*"}'
- '{__name__=~"job:.*"}'
static_configs:
- targets:
- 'prometheus-1:9090'
- 'prometheus-2:9090'
- 'prometheus-3:9090'
中心 Prometheus
边缘 Prometheus
拉取
Prometheus 1
业务 A
Federate Endpoint
Prometheus 2
业务 B
Prometheus 3
业务 C
Global Prometheus
Alertmanager
7.5 监控 Prometheus 自身
promql
# TSDB 性能指标
# 写入速率
rate(prometheus_tsdb_head_samples_appended_total[5m])
# 查询性能
histogram_quantile(0.99, rate(prometheus_http_request_duration_seconds_bucket[5m]))
# 内存使用
process_resident_memory_bytes{job="prometheus"}
# 磁盘 I/O
rate(node_disk_io_time_seconds_total[5m])
# WAL 大小
prometheus_tsdb_wal_size_bytes
总结
本文深入剖析了 Prometheus 监控体系的核心原理:
- Pull 模式:通过服务端主动拉取实现简单可靠的监控架构,配合 Pushgateway 支持短生命周期任务
- TSDB 时序数据库:基于 LSM-Tree 的存储引擎,结合 Head Block 内存存储与 WAL 预写日志保证性能与持久性
- Block 压缩机制:定期将内存数据压缩为不可变 Block,配合倒排索引实现高效查询
- 性能优化:合理配置抓取间隔、控制标签基数、使用联邦架构应对大规模监控场景
Prometheus 的成功证明了"简单设计"的力量------通过 Pull 模式、内置 TSDB、PromQL 查询语言三大核心设计,构建了一套强大且易用的云原生监控体系。
参考资料:
标签 :Prometheus 监控 Pull模式 TSDB 时序数据库