时序数据库内核:TSM、TSI、倒排索引与 Gorilla 压缩
选定了 Metrics 技术栈之后,下一个必须理解的问题是:数据究竟怎么被写进去的,查询时从哪里读,压缩算法是否真的有效。这些细节直接影响写入吞吐、查询延迟、磁盘占用和 OOM 风险。本文从时序数据的写入特征出发,逐一拆解 Prometheus TSDB、InfluxDB TSM/TSI、VictoriaMetrics mergeset 的内部机制,重点讲 Gorilla 压缩的数学原理,最后对比 ClickHouse 作为 Metrics 存储的适用边界。
一、时序数据的写入特征
1.1 基本特征
时间序列数据(Time-Series Data)与 OLTP 场景的数据有本质区别:
| 特征 | 说明 | 对存储的影响 |
|---|---|---|
| 追加写(Append-Only) | 永远写新时间戳,不修改历史 | 可以用 WAL+内存缓冲,顺序写优化 |
| 高 QPS 写入 | 每秒写入百万级样本 | 需要批量写入、压缩、L0→L1 合并 |
| 时间局部性 | 查询通常只看最近 1-7 天 | 冷数据可以压缩存档,热数据保持快速访问 |
| 低基数查询 | 按标签聚合,不按点查询 | 需要倒排索引:标签 → 序列列表 |
| 规律变化 | 相邻采样点差值小 | Delta 编码可以极大压缩 |
| 浮点值 | 64 位 IEEE 754 | XOR 编码相邻值能获得 12:1 压缩比 |
1.2 核心设计权衡
写入友好 查询友好
────────────────────────────────────────────────────
顺序追加 vs 随机访问支持
大批量 flush vs 实时可查
全量写入 vs 稀疏存储(空值)
磁盘占用小 vs 解压速度快
几乎所有的时序数据库都在这个权衡上做出不同选择。Prometheus TSDB 偏向写入友好(WAL + 批量块);ClickHouse MergeTree 偏向查询友好(宽列存储 + 稀疏索引)。
二、Prometheus TSDB 内部
2.1 总体写入路径
Scrape 样本
↓
内存 Appender(追加器)
↓ 同步写
WAL(Write-Ahead Log) ← 持久化保障
↓ 异步
Head Block(内存,约 2h) ← 最近数据,支持实时查询
↓ flush(约 2h 一次)
只读磁盘 Block ← 按 2h 窗口保存
↓ Compaction
合并 Block(6h / 24h / 1w) ← 后台合并,节省磁盘
↓ Retention
删除超期数据
2.2 WAL 细节
WAL(Write-Ahead Log)是 Prometheus 崩溃恢复的基础。位于
{data_dir}/wal/ 目录下。
WAL 记录类型(Record Type):
| 类型 | 内容 |
|---|---|
Series |
序列定义:标签集 → Series ID(uint64) |
Samples |
样本:(Series ID, timestamp, float64) 三元组 |
Tombstones |
删除标记(admin/delete API 调用后) |
Exemplars |
Exemplar 样本(OpenMetrics 规范) |
Metadata |
指标元数据(type、help、unit) |
WAL 段(Segment)文件结构:
WAL/
00000001 ← 128 MiB 段文件
00000002
00000003 ← 当前写入段
checkpoint.0001234/ ← 检查点(包含 Series 记录快照)
检查点(Checkpoint):WAL 不断滚动,但不能无限增长。Prometheus 定期将 Series 记录压缩为检查点,删除已对应到磁盘块的旧 WAL 段。恢复时从最新检查点 + 后续 WAL 段重放。
// WAL 记录编码(伪代码,参考 tsdb/record/record.go)
type RefSample struct {
Ref uint64 // Series 引用 ID
T int64 // 毫秒时间戳
V float64 // 样本值
}
// 批量编码为 varint + LittleEndian float64
func encodeSamples(samples []RefSample, b []byte) []byte {
for _, s := range samples {
b = binary.AppendUvarint(b, s.Ref)
b = binary.AppendVarint(b, s.T)
b = binary.LittleEndian.AppendUint64(b, math.Float64bits(s.V))
}
return b
}2.3 Chunk 格式
一个 Chunk 存储一条序列约 120 个样本(约 2h),使用 XOR 压缩。
Chunk 头部(8 字节):
| 样本数量 (2B) | 起始时间戳 (4B) | 结束时间戳 (4B) | 编码类型 (1B) |
Chunk 数据(变长):
bit-stream:Gorilla XOR 编码的时间戳 + 值
Block 目录结构:
blocks/
01J2K3M4N5P6Q7R8S9/ ← ULID(Universally Unique Lexicographically Sortable Identifier)
meta.json ← 元信息(时间范围、序列数、样本数)
chunks/
000001 ← 128 MiB chunk 文件
index ← 倒排索引(标签 → posting list)
tombstones ← 删除标记
meta.json 示例:
{
"ulid": "01J2K3M4N5P6Q7R8S9",
"minTime": 1704067200000,
"maxTime": 1704074400000,
"stats": {
"numSamples": 12453021,
"numSeries": 89234,
"numChunks": 178468
},
"compaction": {
"level": 1,
"sources": ["01J2K..."]
},
"version": 1
}2.4 TSDB 倒排索引
Prometheus TSDB
的索引文件(index)存储标签到序列 ID
的映射,支持快速按标签查询:
查询:{job="api-server", status="200"}
步骤 1:查找 Posting List(倒排列表)
job="api-server" → [1, 4, 7, 12, 19, ...]
status="200" → [1, 3, 7, 10, 19, ...]
步骤 2:求交集(AND)
[1, 4, 7, 12, 19, ...] ∩ [1, 3, 7, 10, 19, ...] = [1, 7, 19, ...]
步骤 3:通过 Series ID 找到 Chunk offset,读取数据
Posting List 按 Series ID 排序,支持高效求交(类似倒排索引布尔检索)。
三、InfluxDB TSM 引擎
3.1 TSM 是什么
TSM(Time-Structured Merge Tree)是 InfluxDB 1.x/2.x 的核心存储引擎,由 Paul Dix 等人设计,灵感来自 LSM-Tree(Log-Structured Merge Tree),但专门针对时序数据做了优化。
3.2 TSM 写入路径
写入请求(Line Protocol)
↓
WAL(预写日志,.wal 文件)
↓
内存 Cache(按 {measurement,tag set,field,时间范围} 组织)
↓ flush(Cache 达到阈值 / 定期)
TSM 文件(.tsm) ← 不可变,追加写
↓ Compaction
合并小 TSM 为大 TSM
↓
最终 TSM(每个序列的完整历史)
TSM 文件内部结构:
┌─────────────────────────────────────────────────────┐
│ Magic Number (4B) │
│ Version (1B) │
│ ───────────────────────────────────────────────── │
│ Block₁: [Series Key][Encoding][Data (compressed)] │
│ Block₂: ... │
│ ... │
│ ───────────────────────────────────────────────── │
│ Index: │
│ Key Entry: [key_len][key_bytes][type][count] │
│ Index Entry: [min_time][max_time][offset][sz] │
│ ───────────────────────────────────────────────── │
│ Footer: [index_offset (8B)] │
└─────────────────────────────────────────────────────┘
- Block 键(Key) =
measurement,tag1=v1,tag2=v2#!~#fieldKey(InfluxDB 特有格式) - 每个 Block 存储一个序列的一段时间窗口,使用 snappy 压缩
- Index 存储键到 Block 的 B-Tree 结构,支持按键+时间范围快速定位
3.3 TSM Compaction 层次
Level 1(L1):Cache flush 生成,小文件(< 2 MiB)
Level 2(L2):L1 合并,中等文件(< 25 MiB)
Level 3(L3):L2 合并(< 1 GiB)
Level 4(L4):最大文件(> 1 GiB,长期数据)
Compaction 策略:优先合并有重叠时间范围的文件,消除数据重叠提升查询性能。
四、InfluxDB TSI 倒排索引
4.1 为什么需要 TSI
InfluxDB 1.x 早期使用内存倒排索引(inmem),当序列数量(Series Cardinality)超过数百万时,内存占用无法控制,成为 OOM 的主要原因。
TSI(Time-Series Index)是 InfluxDB 1.5 引入的磁盘倒排索引,解决高基数问题。
4.2 TSI 文件结构
tsi1/
L0-00000001.tsl ← WAL 式日志,记录新增/删除的序列
L1-00000001.tsi ← 压缩后的磁盘倒排索引文件
L2-00000001.tsi
...
MANIFEST ← 当前活跃文件列表
TSI 索引文件结构:
TSI 文件由多个块组成:
Measurement Block:measurement 名称列表 + 每个 measurement 的 tag 索引偏移
Tag Block(每个 measurement 一个):
tag key 列表 → tag value 列表 → Posting List(Series Key 列表)
Series Block:所有 Series Key(tag set)的存储 + ID 映射
Hash Index Block:Series Key 的哈希加速查找
与 Prometheus 不同,InfluxDB TSI 用 Roaring Bitmap 来表示 Posting List,支持高效布尔运算(AND、OR、NOT)。
五、Gorilla 压缩算法
5.1 背景
Gorilla 是 Facebook(现 Meta)于 2015 年在论文 Gorilla: A Fast, Scalable, In-Memory Time Series Database 中提出的压缩算法,专为时序浮点数设计。论文中实测压缩率 12:1,每个样本平均存储 1.37 字节(相比原始 16 字节节省了 88%)。
5.2 时间戳压缩:Delta-of-Delta
核心思路:时序数据的采样间隔通常固定(如每 15s 一个),相邻时间戳的差值(Delta)接近常数,因此对 Delta 再求差(Delta-of-Delta,ΔΔ)结果接近零。
原始时间戳:1704067200, 1704067215, 1704067230, 1704067245, ...
Delta : 15, 15, 15, ...
Delta-of-Delta: 0, 0, ...
编码方式(变长 bit 编码):
ΔΔ = 0 → 存储 '0' (1 bit)
ΔΔ ∈ [-63, 64] → 存储 '10' + 7bit 有符号值 (9 bits)
ΔΔ ∈ [-255, 256] → 存储 '110' + 9bit 有符号值 (12 bits)
ΔΔ ∈ [-2047, 2048]→ 存储 '1110' + 12bit 有符号值(16 bits)
其他 → 存储 '1111' + 32bit 完整值 (36 bits)
固定间隔的时序数据:几乎所有 ΔΔ 都是 0,每个时间戳只需 1 bit!
5.3 浮点值压缩:XOR
核心思路:相邻样本值通常接近(如 CPU 使用率在短时间内变化不大),XOR 结果中会有大量前导零(leading zeros)和尾随零(trailing zeros),只需存储有效部分(meaningful bits)。
# XOR 压缩伪代码
def encode_value(prev_val: float, curr_val: float) -> bits:
xor = float_to_bits(prev_val) ^ float_to_bits(curr_val)
if xor == 0:
return bits('0') # 1 bit,值不变
leading = count_leading_zeros(xor)
trailing = count_trailing_zeros(xor)
meaningful = 64 - leading - trailing
if leading == prev_leading and trailing >= prev_trailing:
# 可以复用上次的 leading/trailing 信息
return bits('10') + bits(xor >> trailing, meaningful) # 2 + meaningful bits
else:
# 需要存储新的 leading/trailing
return bits('11') + bits(leading, 5) + bits(meaningful - 1, 6) + bits(xor >> trailing, meaningful)
# 2 + 5 + 6 + meaningful bits实测数据(来自 Gorilla 论文):
典型监控数据(CPU、内存等):
- 约 51% 的样本 XOR = 0(值完全相同),只需 1 bit
- 约 36% 的样本可复用 leading/trailing,平均 ~26.6 bits
- 约 13% 需要完整编码,平均 ~40 bits
加权平均:~1.37 字节/样本
原始大小:16 字节/样本(8B timestamp + 8B float64)
压缩比:约 11.7:1
5.4 Go 实现参考(Prometheus tsdb/chunkenc)
// 参考 prometheus/tsdb/chunkenc/xor.go
// XORChunk 实现 Gorilla 压缩
type xorAppender struct {
b *bstream // bit stream
t int64 // 上一个时间戳
v float64 // 上一个值
tDelta uint64 // 上一个 delta
leading uint8 // 上一个 leading zeros
trailing uint8 // 上一个 trailing zeros
}
func (a *xorAppender) Append(t int64, v float64) {
// 时间戳:delta-of-delta 编码
tDelta := uint64(t - a.t)
dod := int64(tDelta - a.tDelta)
encodeTimestamp(a.b, dod)
a.tDelta = tDelta
a.t = t
// 值:XOR 编码
xorVal := math.Float64bits(v) ^ math.Float64bits(a.v)
encodeValue(a.b, xorVal, &a.leading, &a.trailing)
a.v = v
}
func encodeTimestamp(b *bstream, dod int64) {
switch {
case dod == 0:
b.writeBit(zero)
case -63 <= dod && dod <= 64:
b.writeBits(0b10, 2)
b.writeBits(uint64(dod), 7)
case -255 <= dod && dod <= 256:
b.writeBits(0b110, 3)
b.writeBits(uint64(dod), 9)
case -2047 <= dod && dod <= 2048:
b.writeBits(0b1110, 4)
b.writeBits(uint64(dod), 12)
default:
b.writeBits(0b1111, 4)
b.writeBits(uint64(dod), 32)
}
}5.5 其他压缩变种
| 算法 | 用途 | 特点 |
|---|---|---|
| Gorilla(原版) | Facebook 内存 TSDB | bit-level XOR,论文 1.37 B/样本 |
| Prometheus XOR | Prometheus TSDB chunks | 基于 Gorilla,改进 ΔΔ bit 分配 |
| VictoriaMetrics codec | VM 存储层 | 自研,时间戳 varint,值 zstd+xor |
| InfluxDB TSM | InfluxDB | per-type 编码(integer/float/bool/string),snappy 外层 |
| Zstd | ClickHouse | 块级 zstd 压缩,不感知时序特征但通用性强 |
六、VictoriaMetrics 存储引擎(mergeset)
6.1 架构概览
VictoriaMetrics 的存储引擎不是基于 TSDB 的 Chunk 模型,而是基于自研的 mergeset——一种面向时序数据优化的 LSM-Tree 变体。
写入(vmstorage)
↓
内存 rawRows 批次(~1s 缓冲)
↓ 按 MetricID + 时间戳排序
小 Part(inmemory)
↓ background merge
磁盘 Part(按月分区)
↓ compaction
更大的 Part
6.2 Part 内部结构
part/
metaindex.bin ← 索引的索引(每 8192 行一条元索引)
index.bin ← 块索引:(MetricID, MinTimestamp, MaxTimestamp, offset)
data.bin ← 压缩数据块(每块存储一个 MetricID 的一段时间序列)
timestamps.bin ← 时间戳数组(delta-of-delta + varint)
values.bin ← 值数组(XOR + zstd)
三层索引(减少 I/O):
查询 {job="api"} 在时间窗口 [T1, T2]
1. 从 inverted index(独立 Part)找 MetricID 列表
2. 从 metaindex 定位 index.bin 中的粗粒度区间
3. 从 index.bin 找具体数据块 offset
4. 读取 data.bin 中的压缩块,解压返回
6.3 按月分区(Partition)
VictoriaMetrics 将数据按月分区(YYYY_MM
目录),每个分区独立合并:
storageDataPath/
data/
big/
2024_01/ ← 2024年1月的已合并数据
part_abc/
2024_02/
small/
2024_01/ ← 近期小 Part,待合并
按月分区的优势:删除历史数据(TTL)只需删除整个月份目录,无需合并或碎片整理。
6.4 索引分区(inverted index)
VictoriaMetrics 的倒排索引也是 mergeset 结构,存储:
labelName=labelValue → [MetricID, MetricID, ...]
MetricID → MetricName(完整标签集的 JSON 编码)
查询 {job="api", env="prod"} 时: 1.
从索引找 job="api" 的 MetricID 集合 2. 从索引找
env="prod" 的 MetricID 集合 3. 求交集得目标
MetricID 列表 4. 从数据层读取时序数据
七、ClickHouse 作为 Metrics 存储
7.1 适用场景
ClickHouse 本身是 OLAP 列式数据库,但通过 MergeTree 的适当配置,可以高效存储 Metrics 数据,特别适合:
- 需要 SQL 灵活查询的场景(ad-hoc 分析)
- 同时存储 Metrics 和 Logs 的统一平台
- 高写入吞吐(MergeTree 顺序写,SSD 可达 100 MB/s+)
7.2 MergeTree 表设计
-- Metrics 存储表(适配 Prometheus Remote Write)
CREATE TABLE metrics (
timestamp DateTime64(3, 'UTC'),
metric_name LowCardinality(String),
labels Map(String, String), -- 标签 KV
value Float64
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(timestamp)
ORDER BY (metric_name, labels, timestamp)
TTL timestamp + INTERVAL 90 DAY DELETE
SETTINGS
index_granularity = 8192,
min_bytes_for_wide_part = 0;
-- 跳数索引加速标签查询
ALTER TABLE metrics ADD INDEX idx_labels labels TYPE bloom_filter GRANULARITY 4;7.3 AggregatingMergeTree 实现下采样
-- 1分钟粒度聚合视图
CREATE TABLE metrics_1m (
timestamp DateTime64(3, 'UTC'),
metric_name LowCardinality(String),
labels Map(String, String),
avg_value AggregateFunction(avg, Float64),
max_value AggregateFunction(max, Float64),
min_value AggregateFunction(min, Float64)
)
ENGINE = AggregatingMergeTree()
PARTITION BY toYYYYMM(timestamp)
ORDER BY (metric_name, labels, toStartOfMinute(timestamp));
-- 物化视图自动聚合
CREATE MATERIALIZED VIEW metrics_1m_mv TO metrics_1m AS
SELECT
toStartOfMinute(timestamp) AS timestamp,
metric_name,
labels,
avgState(value) AS avg_value,
maxState(value) AS max_value,
minState(value) AS min_value
FROM metrics
GROUP BY timestamp, metric_name, labels;7.4 查询示例
-- 按时间查询 p99 延迟(结合分位数函数)
SELECT
toStartOfMinute(timestamp) AS t,
quantile(0.99)(value) AS p99
FROM metrics
WHERE metric_name = 'http_request_duration_seconds'
AND labels['job'] = 'api-server'
AND timestamp BETWEEN '2024-01-01' AND '2024-01-02'
GROUP BY t
ORDER BY t;
-- Skip Index 验证(EXPLAIN 查看是否跳过分区)
EXPLAIN indexes = 1
SELECT count() FROM metrics
WHERE labels['env'] = 'production';7.5 ClickHouse 与专用 TSDB 对比
| 维度 | ClickHouse | VictoriaMetrics |
|---|---|---|
| PromQL 支持 | ✗(需 chproxy 等适配) | ✓ 原生 MetricsQL |
| 写入吞吐 | ~10M samples/s(集群) | ~5-10M samples/s(集群) |
| 磁盘占用 | 中(zstd,无时序特化压缩) | 低(专用压缩 codec) |
| SQL 灵活性 | ✓ 完整 SQL | ✗ 只有 MetricsQL |
| 运维复杂度 | 高 | 低 |
| 最佳场景 | Logs+Metrics 统一存储 | 纯 Metrics |
八、索引设计与查询加速
8.1 倒排索引(Inverted Index)
倒排索引是 Metrics 查询加速的核心。本质是标签到序列 ID 列表的映射:
job=api-server → [1, 4, 7, 19, 234, 501, ...]
status=200 → [1, 7, 19, 45, 501, ...]
env=production → [4, 7, 101, 234, ...]
查询 {job="api-server", status="200"}:
取 AND:{1,4,7,19,234,501,...} ∩ {1,7,19,45,501,...} = {1,7,19,501,...}
8.2 Roaring Bitmap
InfluxDB TSI、Apache Druid 等使用 Roaring Bitmap 存储 Posting List。
Roaring Bitmap 将 32 位整数按高 16 位分桶(最多 65536 个桶),每个桶内: - 若元素数量 ≤ 4096,用 16 位整数数组 - 若元素数量 > 4096,用 Bitset(位图)
# Roaring Bitmap AND 操作示意
def and_roaring(bm1: RoaringBitmap, bm2: RoaringBitmap) -> RoaringBitmap:
result = RoaringBitmap()
for bucket in bm1.buckets & bm2.buckets: # 只处理共同桶
a = bm1.buckets[bucket]
b = bm2.buckets[bucket]
if isinstance(a, ArrayContainer) and isinstance(b, ArrayContainer):
result.add_bucket(bucket, array_and(a, b))
elif isinstance(a, BitmapContainer) and isinstance(b, BitmapContainer):
result.add_bucket(bucket, bitmap_and(a, b))
# ...
return result8.3 Bloom Filter 加速
对于稀疏标签(如 trace ID 级别的高基数标签),倒排索引反而会占用大量内存。此时可以用 Bloom Filter 做近似过滤:
-- ClickHouse Skip Index(Bloom Filter)
ALTER TABLE metrics
ADD INDEX idx_label_val
(mapValues(labels))
TYPE bloom_filter(0.01) -- 误判率 1%
GRANULARITY 4;Bloom Filter 保证:不存在的标签值 99% 的情况下可以跳过整个 granule,避免无谓的解压+扫描。
九、Benchmark 方法与对比
9.1 基准测试框架
测试 Metrics 存储性能的标准基准:
# Victoria Metrics 官方 benchmark 工具
git clone https://github.com/valyala/prometheus-benchmark
cd prometheus-benchmark
# 配置:500 万 active series,15s 抓取间隔
cat > config.yaml << 'EOF'
scrapeInterval: 15s
seriesCount: 5000000
uniqueLabelValuesCount: 100
scrapeConfigsCount: 1
EOF
# 运行,指向目标存储
./prometheus-benchmark \
--remote-write-url=http://victoriametrics:8428/api/v1/write \
--series-count=5000000 \
--duration=1h9.2 公开 Benchmark 数据
来自 VictoriaMetrics 官方博客(2023 年,对比测试,结果需结合实际环境验证):
| 存储 | 写入吞吐(samples/s) | 磁盘占用(10亿样本) | 查询 P99 |
|---|---|---|---|
| Prometheus | ~240K | 128 GiB | < 100ms(本地) |
| VictoriaMetrics 单机 | ~1.2M | 18 GiB | < 100ms |
| VictoriaMetrics 集群 | ~8M(6 节点) | 18 GiB/节点 | < 200ms |
| InfluxDB OSS | ~300K | 80 GiB | ~200ms |
| ClickHouse | ~2M(适当配置) | 40 GiB | ~500ms(复杂查询) |
注意:Benchmark 结果高度依赖数据模式、硬件配置和查询复杂度,上表仅供量级参考。
十、国内场景:大厂时序存储实践
10.1 滴滴的时序存储实践
滴滴出行在公开技术博客(2022 年)中描述了其 Metrics 存储演进:
- 第一阶段:单机 Prometheus,活跃序列超过 1000 万时 OOM 成为常态。
- 第二阶段:引入 Thanos 解决跨集群查询,但 Compactor 在处理数千个 2h Block 时频繁超时。
- 第三阶段:迁移至自研 TSDB(基于
Prometheus TSDB 改造),核心改动:
- WAL 分组(按序列哈希分片),减少 WAL 重放时间
- Head Block 写入路径并发优化(减少锁竞争)
- 使用 Zstd 替代 Snappy 压缩磁盘块,节省 ~30% 磁盘
10.2 字节跳动的 Series Churn 问题
字节跳动在 KubeCon China 2023 分享中提到:
“K8s 滚动发布导致每次发布产生数十万条新序列(Pod IP 变化),旧序列 2 小时后才被 Head Block flush 出内存,发布高峰期活跃序列膨胀 3-5 倍,Prometheus 内存从 4 GiB 飙升至 20 GiB。”
解决方案:
# Prometheus 配置优化(减少序列留存时间)
storage:
tsdb:
# 减少 Head Block 保留时间(代价:降低新鲜数据可查窗口)
retention.time: 15d
# 更激进的 WAL Truncation(减少恢复时间)
wal-compression: true
# 缩短 OOO(Out-of-Order)窗口
out-of-order-time-window: 10m同时通过 relabel 丢弃 Pod IP 等高基数标签:
relabel_configs:
- source_labels: [__meta_kubernetes_pod_ip]
action: labeldrop
- source_labels: [__meta_kubernetes_pod_uid]
action: labeldrop十一、工程坑点
11.1 Series Churn(序列流失)导致 OOM
现象:Prometheus 内存持续增长,无法下降,最终被 OOM Killer 杀死。
根因:Kubernetes 滚动部署、HPA 伸缩、Canary 发布等操作不断创建新的 Pod(新 IP、新 UID),每个新 Pod 产生新的时间序列标签组合,即使旧 Pod 已消亡,其序列仍保留在 Head Block 中约 2 小时。
监控:
# 监控当前活跃序列数(告警阈值根据机器内存设定)
prometheus_tsdb_head_series
# 监控序列创建速率(过高说明有 churn)
rate(prometheus_tsdb_head_series_created_total[5m])
# 监控被删序列(正常发布后应上升)
rate(prometheus_tsdb_head_series_removed_total[5m])
解决方案:
# 1. 通过 relabel 删除高基数标签
relabel_configs:
- regex: 'pod_ip|container_id|uid'
action: labeldrop
# 2. 减少不必要的标签维度
# 3. 对于短生命周期 Job,使用 Pushgateway 代替直接 scrape
# 4. 升级到 VictoriaMetrics(序列创建开销更低)11.2 Compaction 风暴
现象:Prometheus 在某个时间点 CPU 飙升,查询延迟增大,磁盘 I/O 满载。
根因:大量 2h Block 同时触发 Compaction(如 Prometheus 重启后重建了大量碎片块),Compactor 与查询竞争 I/O 和 CPU。
解决方案:
# Prometheus 2.43+ 支持 Compaction 并发限制
# 通过 --storage.tsdb.max-concurrent-queries 间接减轻 Compaction 影响
# 更直接:在 Prometheus 内存充足时尽量减少 Compaction 触发频率
# 或切换到 VictoriaMetrics(Compaction 在后台小批量进行,不产生风暴)11.3 WAL 重放慢导致 Prometheus 长时间不可用
现象:Prometheus 重启后需要等待 5-10 分钟才能响应查询(WAL 重放期间)。
根因:WAL 过大(长时间 flush 失败、scrape interval 过短)导致重放耗时。
解决方案:
# 启用 WAL 压缩(Prometheus 2.20+ 默认开启)
--storage.tsdb.wal-compression
# 定期检查 WAL 大小
du -sh /data/prometheus/wal/
# 正常:< 1 GiB
# 异常(> 5 GiB):可能有写入异常
# 手动触发 WAL 截断(谨慎操作,需停止 Prometheus)
# 或使用 tsdb 工具检查
tsdb analyze /data/prometheus十二、选型建议
12.1 存储引擎选择决策
Q:主要查询模式是什么?
PromQL / MetricsQL 为主 → Prometheus TSDB / VictoriaMetrics
SQL 为主 / 与日志共存 → ClickHouse MergeTree
InfluxDB 生态 → TSM + TSI
Q:最大活跃序列数量级?
< 500 万 → Prometheus TSDB(默认)或 VictoriaMetrics 单机
> 500 万 → VictoriaMetrics 集群(磁盘和内存效率更优)
Q:历史数据保留时间?
< 30 天 → 本地磁盘即可
> 90 天 → 对象存储(Thanos/Mimir)或 ClickHouse 冷热分层
12.2 压缩算法选择
| 场景 | 推荐压缩 |
|---|---|
| 高频时序(< 1s 间隔) | Gorilla / XOR,ΔΔ 压缩率高 |
| 低频时序(> 1min 间隔) | Zstd 块压缩,简单有效 |
| 混合场景 | per-type 编码(InfluxDB TSM 方式) |
| 高基数低频 | 先 delta 编码时间戳,值用 snappy/zstd |
参考资料
- Fabian Reinartz, “Writing a Time Series Database from Scratch” (Prometheus blog, 2017) — https://prometheus.io/blog/2017/04/10/
- Pelkonen et al., “Gorilla: A Fast, Scalable, In-Memory Time Series Database” (VLDB 2015) — http://www.vldb.org/pvldb/vol8/p1816-teller.pdf
- InfluxDB TSM Engine Documentation — https://docs.influxdata.com/influxdb/v1/concepts/storage_engine/
- InfluxDB TSI Documentation — https://docs.influxdata.com/influxdb/v1/concepts/tsi-details/
- VictoriaMetrics Storage Engine Blog — https://victoriametrics.com/blog/
- 滴滴技术博客,“滴滴监控系统演进实践”(2022 年)
- KubeCon China 2023,“字节跳动大规模 Prometheus 实践”
- ClickHouse Documentation, “MergeTree” — https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/
- Roaring Bitmap Paper — https://arxiv.org/abs/1402.6407
- Björn Rabenstein, “Prometheus Storage: Technical Terms for SREs” (PromCon EU 2019)
上一篇:Metrics:Prometheus、VictoriaMetrics、Thanos、Mimir、M3
下一篇:Logs:Loki、ClickHouse、Elasticsearch、OpenObserve 的取舍
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【可观测性工程】Metrics:Prometheus、VictoriaMetrics、Thanos、Mimir、M3
从 Prometheus 架构与数据模型出发,系统梳理 Remote Write、PromQL 进阶、Thanos 全局聚合、Mimir 多租户、VictoriaMetrics 性能、M3DB 原理,以及五者在大规模生产场景下的对比矩阵与迁移实践。
【可观测性工程】可观测性全景:Metrics、Logs、Traces、Profiles、Events 五大支柱
从控制论到云原生:拆解可观测性的五大信号支柱,对比监控与可观测性的本质区别,梳理开源/商业/SaaS 分类,以及国内互联网公司三大支柱落地现状与典型工程坑点。
【可观测性工程】指标体系设计:USE、RED、Golden Signals 与业务 KPI
USE 方法论适用于资源,RED 方法论适用于请求,Golden Signals 适用于服务——三套方法论各有其适用对象。本文从 Brendan Gregg、Tom Wilkie、Google SRE 的原始定义出发,构建覆盖资源→服务→业务的完整指标体系,并给出 Prometheus 命名规范、基数治理策略与可抄的指标清单。
可观测性工程
从 Metrics、Logs、Traces 到 Profiling、eBPF、OpenTelemetry 与 SLO 治理,面向中国工程团队的可观测性系统化手册。