深入 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使用了WALWrite-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列表 // 例如: servicepayment → [1, 5, 12, 45, 78] // envprod → [1, 2, 5, 12, 34, 45, 67, 78] mapping map[string][]uint64 } // 查询 servicepayment, envprod // 取交集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.time30d # 2. 增大Block大小减少Block数量提升查询性能 --storage.tsdb.max-block-duration4h # 3. 调整WAL大小减少WAL清理频率 --storage.tsdb.wal-segment-size256MB # 4. 内存限制防止OOM --storage.tsdb.max-chunks-to-persist5000参数默认值优化值效果retention.time15d30d保留更多历史数据max-block-duration2h4hBlock数量减半wal-segment-size128MB256MB减少WAL分段数量max-chunks-to-persist无限制5000防止内存暴涨结语理解Prometheus的Pull模型和TSDB原理后再去看那些配置参数就不只是别人说这么配了而是知道每个参数背后的设计考量。Pull模型为TSDB提供了稳定写入TSDB为Pull模型提供了高效存储。这套设计不是一蹴而就的——它是经历了多年的生产实践和调优后才形成的。理解了它们的设计哲学你就能在遇到性能问题时做出合理的优化决策。
深入 Prometheus 内核:解析 Pull 采样模型与时序数据库底座原理
发布时间:2026/6/4 14:08:55
深入 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使用了WALWrite-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列表 // 例如: servicepayment → [1, 5, 12, 45, 78] // envprod → [1, 2, 5, 12, 34, 45, 67, 78] mapping map[string][]uint64 } // 查询 servicepayment, envprod // 取交集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.time30d # 2. 增大Block大小减少Block数量提升查询性能 --storage.tsdb.max-block-duration4h # 3. 调整WAL大小减少WAL清理频率 --storage.tsdb.wal-segment-size256MB # 4. 内存限制防止OOM --storage.tsdb.max-chunks-to-persist5000参数默认值优化值效果retention.time15d30d保留更多历史数据max-block-duration2h4hBlock数量减半wal-segment-size128MB256MB减少WAL分段数量max-chunks-to-persist无限制5000防止内存暴涨结语理解Prometheus的Pull模型和TSDB原理后再去看那些配置参数就不只是别人说这么配了而是知道每个参数背后的设计考量。Pull模型为TSDB提供了稳定写入TSDB为Pull模型提供了高效存储。这套设计不是一蹴而就的——它是经历了多年的生产实践和调优后才形成的。理解了它们的设计哲学你就能在遇到性能问题时做出合理的优化决策。