Prometheus 监控体系原理:Pull 模式与 TSDB 时序数据库

深入剖析 Prometheus 核心架构,从 Pull 拉取模式到 TSDB 时序数据库的内部实现,结合源码解析与实践案例

目录


一、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):

// 文件: 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 抓取循环详解

// 文件: 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 作为缓存层:

# 客户端主动推送指标到 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):

// 文件: model/labels.go
// Labels 是标签集合,用于唯一标识时序
type Labels []Label

type Label struct {
    Name  string
    Value string
}

// Series 代表单个时序
type Series interface {
    // 返回时序的标签
    Labels() Labels
    
    // 返回时序的样本迭代器
    Iterator() ChunkIterator
}

3.3 存储目录结构

/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):

{
  "ulid": "01JE9TB3K4W7JZY8MVP5X2",
  "minTime": 1704067200000,
  "maxTime": 1704080000000,
  "stats": {
    "numSeries": 15234,
    "numChunks": 45678,
    "numSamples": 891012
  },
  "compaction": {
    "level": 1,
    "sources": ["wal"]
  }
}

3.4 TSDB vs 其他时序数据库

特性Prometheus TSDBInfluxDBTimescaleDB
存储引擎自定义 LSM-TreeTSM (基于 LSM)基于 PostgreSQL
数据模型多维标签Tag + Field关系型扩展
查询语言PromQLInfluxQL / FluxSQL
分布式需联邦方案原生集群原生集群
压缩率极高中等
适用场景指标监控IoT/时序分析复杂查询

四、Head Block 内存存储机制

4.1 Head Block 架构

Head Block 是 TSDB 的内存部分,存储最近写入的时序数据:

索引

存储

写入请求

MemSeries
时序元数据

MemChunk
样本数据块

Series Hash Map
快速查找

Chunk Ring Buffer
循环缓冲区

核心数据结构(v2.55.0):

// 文件: 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 写入流程源码解析

// 文件: 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 开源算法):

// 文件: 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):

// 文件: 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,将已固化的数据清除:

// 文件: 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 恢复内存状态:

// 文件: 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-size128MB单个 WAL 文件大小
--storage.tsdb.retention.time15d数据保留时间
--storage.tsdb.retention.size0 (无限制)最大磁盘占用

优化建议

# 高频写入场景增大 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):

// 文件: 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):

// 文件: 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

查询结果

谓词下推

索引选择

剪枝

优化示例

# 原始查询
rate(http_requests_total[5m])

# 优化后的执行计划
1. 从索引中找到所有 http_requests_total 的 Series
2. 按 time 索引定位最近 5 分钟的 Chunks
3. 仅加载相关的 Chunks(避免全表扫描)
4. 计算 rate

6.4 查询性能对比

查询类型优化前优化后提升
单标签查询50ms5ms10x
多标签 AND200ms15ms13x
范围查询500ms30ms16x
正则匹配1s100ms10x

七、性能优化最佳实践

7.1 抓取配置优化

# 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 使用查询延迟
5s4x3x-10%
15s1x1x基准
60s0.25x0.3x+30%

7.2 存储优化配置

# 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 高基数问题处理

什么是高基数

# 好的标签(低基数)
status_code: "200", "404", "500"  # 3 个值
method: "GET", "POST"              # 2 个值

# 坏的标签(高基数)
user_id: "123456"                  # 可能百万级值
request_id: "abc-xyz-123"          # 每次请求不同

解决方案

// 使用 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 分联邦集群方案

对于超大规模监控,可使用联邦架构:

# 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 自身

# 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 监控体系的核心原理:

  1. Pull 模式:通过服务端主动拉取实现简单可靠的监控架构,配合 Pushgateway 支持短生命周期任务
  2. TSDB 时序数据库:基于 LSM-Tree 的存储引擎,结合 Head Block 内存存储与 WAL 预写日志保证性能与持久性
  3. Block 压缩机制:定期将内存数据压缩为不可变 Block,配合倒排索引实现高效查询
  4. 性能优化:合理配置抓取间隔、控制标签基数、使用联邦架构应对大规模监控场景

Prometheus 的成功证明了"简单设计"的力量——通过 Pull 模式、内置 TSDB、PromQL 查询语言三大核心设计,构建了一套强大且易用的云原生监控体系。


参考资料

标签Prometheus 监控 Pull模式 TSDB 时序数据库

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐