惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
V2EX
C
Check Point Blog
博客园_首页
B
Blog
D
Docker
U
Unit 42
量子位
I
InfoQ
有赞技术团队
有赞技术团队
Martin Fowler
Martin Fowler
GbyAI
GbyAI
L
LangChain Blog
云风的 BLOG
云风的 BLOG
博客园 - Franky
美团技术团队
T
The Blog of Author Tim Ferriss
阮一峰的网络日志
阮一峰的网络日志
月光博客
月光博客
Vercel News
Vercel News
Recent Announcements
Recent Announcements
雷峰网
雷峰网
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
Google DeepMind News
Google DeepMind News

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

国密算法与国密 TLS 系列索引 【系统架构设计】架构质量属性:不只是"高可用高性能" 【系统架构设计百科】告警策略:如何避免"狼来了" 【系统架构设计】CQRS:读写分离的架构哲学 【系统架构设计】空间架构:极端扩展场景的解法 【系统架构设计】微服务架构深度审视:优势、代价与适用边界 【系统架构设计】扩展性原理:水平、垂直与对角扩展 【系统架构设计】无状态设计:扩展的第一步也是最难的一步 【系统架构设计】缓存架构:从本地到分布式的多级缓存体系 【系统架构设计】管道与过滤器:Unix 哲学的架构表达 【系统架构设计】复杂性管理:架构的核心战场 【系统架构设计】消息队列架构:异步解耦的设计与陷阱 【系统架构设计】CDN 架构:全球加速的设计原理 【系统架构设计】连接池设计:被忽视的性能杀手 【系统架构设计】弹性设计模式:熔断器、舱壁与超时 【系统架构设计】高可用设计模式:冗余、故障转移与仲裁 【系统架构设计】容量规划:从拍脑袋到数据驱动 【系统架构设计】数据库扩展:分库分表的工程实践与替代方案 【系统架构设计】SLO 工程:可靠性的量化管理 【系统架构设计】性能建模:用数学思维分析系统瓶颈 【系统架构设计】混沌工程:主动验证系统的韧性 【系统架构设计】零拷贝与内存映射:数据搬运的极致优化 【系统架构设计】线程模型:从 thread-per-request 到协程 【系统架构设计】容灾架构:多活与灾备设计 【系统架构设计】数据库性能模式:索引、查询与连接管理 【系统架构设计】数据建模:从关系范式到文档模型的真实权衡 【系统架构设计】吞吐量优化:批处理、流水线与并发模型 【系统架构设计】流处理架构:从批处理到实时的范式迁移 【系统架构设计】搜索引擎架构:倒排索引之上的系统设计 【系统架构设计】时序数据架构:监控与 IoT 的存储设计
【可观测性工程】时序数据库内核:TSM、TSI、倒排索引与 Gori...
2026-04-22 · via 土法炼钢兴趣小组的算法知识备份

选定了 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 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 存储演进:

  • 第一阶段:单机 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

参考资料

  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 治理,面向中国工程团队的可观测性系统化手册。