土法炼钢兴趣小组的算法知识备份

【可观测性工程】时序数据库内核:TSM、TSI、倒排索引与 Gorilla 压缩

文章导航

分类入口
architectureobservability
标签入口
#tsdb#prometheus#influxdb#tsm#tsi#gorilla#victoriametrics#clickhouse#compression#metrics

目录

时序数据库内核: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)]                       │
└─────────────────────────────────────────────────────┘

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 数据,特别适合:

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 result

8.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=1h

9.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 存储演进:

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

参考资料

  1. Fabian Reinartz, “Writing a Time Series Database from Scratch” (Prometheus blog, 2017) — https://prometheus.io/blog/2017/04/10/
  2. Pelkonen et al., “Gorilla: A Fast, Scalable, In-Memory Time Series Database” (VLDB 2015) — http://www.vldb.org/pvldb/vol8/p1816-teller.pdf
  3. InfluxDB TSM Engine Documentation — https://docs.influxdata.com/influxdb/v1/concepts/storage_engine/
  4. InfluxDB TSI Documentation — https://docs.influxdata.com/influxdb/v1/concepts/tsi-details/
  5. VictoriaMetrics Storage Engine Blog — https://victoriametrics.com/blog/
  6. 滴滴技术博客,“滴滴监控系统演进实践”(2022 年)
  7. KubeCon China 2023,“字节跳动大规模 Prometheus 实践”
  8. ClickHouse Documentation, “MergeTree” — https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/
  9. Roaring Bitmap Paper — https://arxiv.org/abs/1402.6407
  10. Björn Rabenstein, “Prometheus Storage: Technical Terms for SREs” (PromCon EU 2019)

上一篇Metrics:Prometheus、VictoriaMetrics、Thanos、Mimir、M3

下一篇Logs:Loki、ClickHouse、Elasticsearch、OpenObserve 的取舍

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-22 · architecture / observability

【可观测性工程】指标体系设计:USE、RED、Golden Signals 与业务 KPI

USE 方法论适用于资源,RED 方法论适用于请求,Golden Signals 适用于服务——三套方法论各有其适用对象。本文从 Brendan Gregg、Tom Wilkie、Google SRE 的原始定义出发,构建覆盖资源→服务→业务的完整指标体系,并给出 Prometheus 命名规范、基数治理策略与可抄的指标清单。

2026-04-22 · architecture / observability

可观测性工程

从 Metrics、Logs、Traces 到 Profiling、eBPF、OpenTelemetry 与 SLO 治理,面向中国工程团队的可观测性系统化手册。


By .