深入 Prometheus 内核:解析 Pull 采样模型与时序数据库底座原理
一、Pull模型的深度解析
1.1 一次完整的Scrape过程
当一个Scrape请求发生时,Prometheus内部是这样工作的:
flowchart TD N1["1. 服务发现 → 获取目标列表"] --> N2["2. 计算任务分发 → 每个ScrapeManager负责一组目标"] N2 --> N3["3. 发送HTTP GET → 请求目标的/metrics端点"] N3 --> N4["4. 解析响应 → 用TextParser解析Prometheus文本格式"] N4 --> N5["5. 样本处理 → 转换Label、时间戳、值"] N5 --> N6["6. Appender → 将样本写入TSDB"] N6 --> N7["7. 更新meta → 更新up指标、Scrape耗时等"]// prometheus/scrape/scrape.go — 简化的Scrape流程 func (sl *scrapeLoop) scrape(ctx context.Context) error { // 1. 发起HTTP请求 resp, err := sl.scraper.scrape(ctx, sl.target) if err != nil { sl.reportError(err) return err } // 2. 解析metrics文本 var totalSamples int sl.loopMut.Lock() // 3. 对每个样本调用Appender app := sl.appender(ctx) for _, series := range resp.series { ref, err := app.Append(series.labels, series.timestamp, series.value) if err != nil { // 跳过格式错误的样本 continue } totalSamples++ } // 4. 提交批量写入 err = app.Commit() sl.loopMut.Unlock() return nil }1.2 Pull模型的关键优势
通过看源码,我理解了为什么Prometheus坚持用Pull:
优势1:故障检测的即时性
当目标挂了,Pull模型能在下一个Scrape周期(默认15s)立即发现——up指标变成0。而Push模型必须等目标重新上线后才能上报,或者在Push端做心跳检测,增加了复杂度。
优势2:负载的可控性
Pull的节奏由Prometheus Server决定。如果Server负载高了,可以通过scrape_interval降低采集频率。Push的节奏由数据源决定,突发流量会直接冲击Server。
优势3:天然的服务发现对齐
# 服务发现自动生成的目标列表 scrape_configs: - job_name: 'kubernetes-pods' kubernetes_sd_configs: - role: pod relabel_configs: - source_labels: [__meta_kubernetes_pod_ready] # 只采集Ready状态的Pod regex: "true" action: keepPull模型可以结合服务发现的元数据做过滤——只有Ready的Pod才采集,这个能力Push模型很难实现。
1.3 一个被低估的设计:WAL与崩溃恢复
Prometheus的TSDB使用了WAL(Write-Ahead Log)来保证数据不丢失:
flowchart LR A["Scrape"] --> B["Head Appender"] B --> C["WAL (磁盘)"] C --> D["Head Chunk (内存)"] D --> E["压缩/合并"] E --> F["Block (磁盘)"]崩溃恢复策略:
- 启动时检查WAL目录
- 如果WAL存在,重放所有未压缩的样本
- 重建Head Chunk中的内存索引
- 继续正常采集
这个设计和ELK的Translog很像——都是先写日志再写数据。但TRDB的WAL是批量写入的(每秒一次),而ES的Translog默认是每次请求都刷盘。这就是为什么Prometheus的写入性能比ES好得多的原因之一。
二、TSDB的存储结构
2.1 目录结构
$ ls -la /data/prometheus/tsdb/ total 64 drwxr-xr-x wal/ # WAL目录 drwxr-xr-x 01GABCDEFG/ # Block目录 drwxr-xr-x 01GHIJKLMN/ -rw-r--r-- chunks_head/ # 当前内存chunk的映射文件 -rw-r--r-- index/ # 倒排索引 -rw-r--r-- meta.json # Block元数据 -rw-r--r-- tombstone/ # 删除标记 -rw-r--r-- lock # 文件锁2.2 Block结构
每个Block包含了2小时的数据,内部结构:
$ ls -la /data/prometheus/tsdb/01GABCDEFG/ total 32 drwxr-xr-x chunks/ # 存储压缩后的样本数据 drwxr-xr-x index/ # 倒排索引 -rw-r--r-- meta.json # 元数据:时间范围、stats等 -rw-r--r-- tombstone/ # 删除标记(逻辑删除)Prometheus将时间分成2小时一个的Block。每个Block是不可变的(immutable),只读不写。这样做的好处是:
- 不需要对Block加锁,查询可以安全并发
- 压缩合并时只需要创建新Block,删除旧Block
- 备份时可以无损复制Block文件
2.3 倒排索引:快速定位时间序列
Prometheus查询能这么快,倒排索引功不可没:
// 倒排索引结构(伪代码) type PostingsIndex struct { // 每个label对 → 对应的series ID列表 // 例如: service="payment" → [1, 5, 12, 45, 78] // env="prod" → [1, 2, 5, 12, 34, 45, 67, 78] mapping map[string][]uint64 } // 查询 service="payment", env="prod" // 取交集:Intersect([1,5,12,45,78], [1,2,5,12,34,45,67,78]) // = [1, 5, 12, 45, 78]这个倒排索引是内存映射(mmap)加载的,查询时不需要反序列化。这就是为什么Prometheus的label匹配查询能达到毫秒级响应。
三、Pull模型 + TSDB的协同设计
Pull模型和TSDB的设计是深度耦合的:
3.1 写入模式
Pull模型带来了稳定的写入节奏:
flowchart TD A["每15s一次Scrape"] --> B["每个目标产生5-20个时间序列"] B --> C["每个序列1个样本"] C --> D["每秒约1000-10000个样本写入"] D --> E["写入速率恒定"]恒定速率的写入对TSDB非常友好:
- WAL可以批量fsync(每秒一次)
- Head Chunk可以平稳增长(不会突然暴涨)
- 后台合并(Compaction)可以预测
3.2 压缩合并策略
// TSDB后台合并 — 将小Block合并成大Block func (db *DB) compaction() { // 1. 选择需要合并的Block(通常是最小的2-3个) blocks := selectBlocksForMerge() // 2. 创建新的Block newBlock := createMergedBlock(blocks) // 3. 原子替换:删除旧Block,写入新Block // 新Block包含了合并后的chunk和索引 // 这个过程不会阻塞查询 // 4. 清理WAL中已被合并的数据 }合并后的Block大小大约是原始数据的1/3(因为chunk压缩)。
四、性能优化实践
理解了原理后,我们在生产环境的调优:
# 1. 延长Block保留时间(默认15天对我们不够) --storage.tsdb.retention.time=30d # 2. 增大Block大小(减少Block数量,提升查询性能) --storage.tsdb.max-block-duration=4h # 3. 调整WAL大小(减少WAL清理频率) --storage.tsdb.wal-segment-size=256MB # 4. 内存限制(防止OOM) --storage.tsdb.max-chunks-to-persist=5000| 参数 | 默认值 | 优化值 | 效果 |
|---|---|---|---|
| retention.time | 15d | 30d | 保留更多历史数据 |
| max-block-duration | 2h | 4h | Block数量减半 |
| wal-segment-size | 128MB | 256MB | 减少WAL分段数量 |
| max-chunks-to-persist | 无限制 | 5000 | 防止内存暴涨 |
结语
理解Prometheus的Pull模型和TSDB原理后,再去看那些配置参数,就不只是"别人说这么配"了,而是知道每个参数背后的设计考量。
Pull模型为TSDB提供了稳定写入,TSDB为Pull模型提供了高效存储。这套设计不是一蹴而就的——它是经历了多年的生产实践和调优后才形成的。理解了它们的设计哲学,你就能在遇到性能问题时,做出合理的优化决策。