尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 架构演进:从 TSDB 到 MergeSet 的设计取舍

VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 架构演进:从 TSDB 到 MergeSet 的设计取舍
📅 发布时间:2026/7/4 14:59:30

一、TSDB 存储引擎演进史

思考记忆提示— 理解 TSDB 存储引擎的演进,才能理解 MergeSet 为什么会这样设计

  • 第一代 TSDB:基于 B-Tree(如 InfluxDB 1.x)
  • 第二代 TSDB:基于 LSM Tree(如 Prometheus 2.x、Cassandra)
  • 第三代 TSDB:MergeSet(VictoriaMetrics独创)
  • 面试高频提问:MergeSet 和 LSM Tree 的核心区别是什么?

1.1 传统 TSDB 的存储架构

在讨论 MergeSet 之前,我们需要了解传统 TSDB 的存储架构。主流的 TSDB(如 Prometheus 2.x)采用LSM Tree(Log-Structured Merge Tree)作为底层存储引擎。

LSM Tree 的核心思想是:

  1. 写入时:数据先写入内存中的 MemTable(类似 WAL),达到阈值后刷盘生成 SSTable
  2. 合并时:多个 SSTable 按层次合并,小表合并成大表(这就是"分层"的概念)
  3. 查询时:需要读取多个层次的 SSTable,可能影响查询性能
LSM Tree 架构 ┌─────────────────────────────────────────────────────────────────────────────┐ │ │ │ Level 0 (L0) │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ SSTable │ │ SSTable │ │ SSTable │ ← 新刷出的文件,小而多 │ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ │ │ │ │ └───────────┴───────────┘ │ │ │ │ │ ▼ │ │ Level 1 (L1) │ │ ┌───────────────────────────┐ │ │ │ SSTable │ ← 合并后的文件,较大 │ │ └─────────────┬─────────────┘ │ │ │ │ │ ▼ │ │ Level 2 (L2) │ │ ┌───────────────────────────┐ │ │ │ SSTable │ ← 更大 │ │ └─────────────┬─────────────┘ │ │ │ │ │ ▼ │ │ ... │ │ │ │ 问题:查询需要遍历所有层级,Level 越多,查询越慢 │ └─────────────────────────────────────────────────────────────────────────────┘

1.2 Prometheus TSDB 的局限性

Prometheus 2.x 的 TSDB 基于 LSM Tree 设计,虽然相比 1.x 版本有了巨大提升,但在超大规模场景下仍面临挑战:

问题描述影响
分层合并开销LSM Tree 需要多层合并,Level 越多 IO 越重写入放大、写放大问题严重
查询延迟不稳定查询需要遍历多个 Level,数据分散P99 延迟难以控制
内存占用高多层索引、BloomFilter 需要维护RAM 消耗大

注意

Prometheus 的 LSM Tree 实现与 Cassandra/RocksDB 有一定区别,但核心问题类似。对于超大规模场景(如 100 万+ series),LSM Tree 的分层合并策略会成为性能瓶颈。

二、MergeSet 核心设计:只合并不分层

思考记忆提示— MergeSet 的精髓在于"只合并不分层"——这是它与 LSM Tree 的本质区别

  • MergeSet 不分层,所有 Part 文件在同一层级
  • 合并策略:小型 Part 合并成大型 Part,永远变大的单向合并
  • 设计优势:查询只需扫描少量大文件,IO 更高效

2.1 MergeSet 的核心概念

MergeSet 是 VictoriaMetrics 独创的存储架构,其核心设计哲学可以用一句话概括:"只合并不分层"。这与 LSM Tree 的"分层合并"形成鲜明对比。

在 lib/mergeset/table.go 中,MergeSet 的设计理念被清晰定义:

// lib/mergeset/table.go // MergeSet 核心设计:只合并不分层 // MergeSet 与 LSM Tree 的本质区别: // - LSM Tree: 分层合并,Level N 合并到 Level N+1 // - MergeSet: 不分层,所有 Part 文件在同一目录,按大小合并 // Part 文件的生命周期: // InMemoryPart (新建) // ↓ (1秒后刷盘) // Small Part (小文件,KB级别) // ↓ (合并) // Big Part (大文件,MB级别) // ↓ (合并) // 更大的 Part // ↓ // 最终的超大 Part // 关键设计点: // 1. Part 文件永不删除,只合并成更大的文件 // 2. 查询时扫描所有 Part,但利用 BloomFilter 快速跳过无关 Part // 3. 后台任务持续合并小 Part 成大 Part,保持 Part 数量可控

2.2 MergeSet vs LSM Tree 对比

MergeSet 架构(VictoriaMetrics) ┌─────────────────────────────────────────────────────────────────────────────┐ │ │ │ /data/ │ │ ├── 2024_01/ │ │ │ ├── small_001.tar / small_002.tar / small_003.tar ← 小文件,合并中 │ │ │ ├── big_001.tar ← 大文件,已稳定 │ │ │ ├── big_002.tar │ │ │ └── super_001.tar / super_002.tar ← 更大文件 │ │ │ │ │ ├── 2024_02/ ... │ │ └── 2024_03/ ... │ │ │ │ 特点: │ │ - 所有 Part 文件在同一目录层级 │ │ - 小文件持续合并成大文件(单向合并) │ │ - 查询扫描所有 Part,但用 BloomFilter 过滤 │ │ - IO 模式:顺序读大文件,而非随机读多层小文件 │ │ │ └─────────────────────────────────────────────────────────────────────────────┘
维度LSM Tree (Prometheus)MergeSet (VictoriaMetrics)
文件层级多层(L0, L1, L2...)单层(所有 Part 在同级目录)
合并方向逐层向上合并小 Part → 大 Part(单向)
查询方式遍历所有层级扫描所有 Part + BloomFilter
IO 模式大量小文件随机读少量大文件顺序读
写放大严重(多层重复写)轻量(只写一次)
查询延迟不稳定(P99 难控制)稳定(可预测)

源码视角:MergeSet 合并调度

MergeSet 的合并调度逻辑在 lib/mergeset/table.go 的 scheduleMerges() 函数中实现:

  • 默认配置:defaultPartsToMerge=15,每次合并最多 15 个小 Part
  • 合并策略:优先合并"最老"的小 Part,避免大量小文件堆积
  • 并行合并:通过 rawItemsShards 实现 CPU 级别的并行合并
  • ZSTD 压缩:合并时自动选择压缩级别,getCompressLevel() 根据数据量动态选择

三、源码解析:MergeSet vs LSM Tree

思考记忆提示— 源码是理解 MergeSet 设计取舍的最佳途径

  • lib/mergeset/ 是 MergeSet 的核心实现
  • lib/storage/ 中的 Table/Partition 对接 MergeSet
  • 面试高频提问:MergeSet 为什么不需要 WAL?

3.1 InmemoryPart:1秒刷盘的原子性保证

MergeSet 不使用 WAL(Write-Ahead Log),而是通过InmemoryPart的原子性刷盘实现数据可靠性。这在 lib/mergeset/inmemory_part.go 中实现:

// lib/mergeset/inmemory_part.go // InmemoryPart 核心设计:原子性刷盘 // 刷盘流程: // 1. 内存中构建完整的 Part 数据(4 个 buffer 并行写入) // 2. 调用 MustStoreToDisk() 原子性刷盘 // 3. 刷盘成功后才更新目录索引 // MustStoreToDisk 的关键点: // - 先写临时文件(如 small_001.tar.tmp) // - 刷盘成功后,原子性 rename 到正式文件名 // - 如果进程崩溃,临时文件会被忽略,不会污染数据 // 这就是为什么 MergeSet 不需要 WAL: // - InmemoryPart 每秒刷盘,数据最多丢失 1 秒 // - 刷盘后的数据已经是完整可用的 Part 文件 // - 重启时扫描目录即可恢复所有 Part

3.2 Part 文件结构:四文件合一

MergeSet 的 Part 文件采用独特的四文件结构,这在 lib/mergeset/part.go 中定义:

MergeSet Part 文件结构 ┌─────────────────────────────────────────────────────────────────────────────┐ │ │ │ Part.tar 文件内部结构: │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ metaindex.bin │ │ │ │ ├── [MetaIndexRow 1] ← Block 1 的元信息(offset, size, min/max) │ │ │ │ ├── [MetaIndexRow 2] ← Block 2 的元信息 │ │ │ │ └── [MetaIndexRow N] ← Block N 的元信息 │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ index.bin │ │ │ │ ├── [IndexRow 1] ← MetricName → BlockID 映射 │ │ │ │ ├── [IndexRow 2] │ │ │ │ └── [IndexRow N] │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ items.bin │ │ │ │ ├── [Item 1] ← 时序数据点(Timestamp + Value) │ │ │ │ ├── [Item 2] │ │ │ │ └── [Item N] │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ lens.bin │ │ │ │ └── 每行的长度信息(用于快速随机访问) │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ │ 关键设计点: │ │ - metaindex.bin:Block 的索引,用于快速定位数据 │ │ - index.bin:MetricName 倒排索引,用于标签查询 │ │ - items.bin:实际数据,commonPrefix 压缩 │ │ - lens.bin:行长度,用于随机访问 │ │ │ └─────────────────────────────────────────────────────────────────────────────┘

小贴士— 为什么 Part 文件是 .tar 格式?

.tar 格式最初用于将多个文件打包成一个便于传输。在 MergeSet 中,.tar 格式用于将 metaindex、index、items、lens 四个文件打包成一个 Part。.tar 本身不压缩,压缩发生在 items.bin 内部的 ZSTD 压缩。

3.3 commonPrefix 压缩:存储空间减少 30-50%

MergeSet 的另一大优化是commonPrefix 压缩,在 lib/mergeset/block_header.go 中实现:

// lib/mergeset/block_header.go // commonPrefix 压缩原理 // BlockHeader 结构: type BlockHeader struct { // commonPrefix 长度:当前 Block 与前一个 Block 的公共前缀长度 CommonPrefixLen uint64 // 第一个 Item 的元信息 FirstItemMeta uint64 // 最后一个 Item 的元信息 LastItemMeta uint64 // Items 数量 ItemsCount uint64 // 压缩类型(NearestDelta / ZSTD / None) CompressionType uint64 } // 压缩示例: // 未压缩:[2024-01-01 10:00:00] cpu_usage{job="prometheus",instance="localhost:9090"} 95.5 // 压缩后:[2024-01-01 10:00:00] cpu_usage{job="prometheus",instance="localhost:9090"} 95.5 // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 全部存储 // ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ // 只存一次,后面的 Block 只存差异 // 实际效果: // - 时序数据通常有很长的共同前缀(标签名+标签值模式固定) // - commonPrefix 压缩可以将存储空间减少 30-50% // - 同时保持解码速度(不需要解压,只需提取差异部分)

四、设计取舍与适用场景

设计精髓

MergeSet 的设计哲学是"用空间换时间,用简单换性能"。放弃 WAL 换来的是写入的极致简单;只合并不分层换来的是查询的可预测性。

4.1 MergeSet 的优势

优势原因实际效果
写入简单不需要 WAL,不需要复杂的两阶段写入写入延迟极低
查询稳定扫描大文件而非多层小文件P99 延迟可控
资源高效commonPrefix + ZSTD 双重压缩存储空间减少 50%+
运维简单无分层,无复杂合并策略调参少,易理解

4.2 MergeSet 的取舍

取舍描述影响
无 WAL进程崩溃可能丢失最多 1 秒数据不适用于数据零丢失的金融场景
Part 数量膨胀高写入场景下,小 Part 产生速度快于合并需要足够的 CPU 进行后台合并
查询全扫描查询需要遍历所有 Part(虽然有 BloomFilter)超多 Part 时查询变慢

4.3 适用场景对比

VictoriaMetrics MergeSet vs Prometheus LSM Tree vs InfluxDB TSM ┌─────────────────────────────────────────────────────────────────────────────┐ │ │ │ 场景 │ Prometheus │ InfluxDB │ VM │ │ ─────────────────────────────────┼─────────────┼────────────┼────────────│ │ 超大规模 series (1000万+) │ ⚠️ │ ⚠️ │ ✅ │ │ 高写入吞吐 (100万 samples/s) │ ⚠️ │ ⚠️ │ ✅ │ │ 稳定 P99 查询延迟 │ ⚠️ │ ⚠️ │ ✅ │ │ 低内存占用 │ ⚠️ │ ⚠️ │ ✅ │ │ 数据零丢失要求 │ ✅ │ ✅ │ ⚠️ │ │ 运维简单优先 │ ⚠️ │ ⚠️ │ ✅ │ │ 开源生态成熟 │ ✅ │ ⚠️ │ ⚠️ │ │ │ │ ✅ 强烈推荐 ⚠️ 可用但非最优 ❌ 不推荐 │ │ │ └─────────────────────────────────────────────────────────────────────────────┘

五、面试高频提问

相关新闻

  • AI工具链加速学术论文写作:30天高效完成
  • OpenClaw.NET 率先原生支持 MCP Apps
  • MyComputerManager:彻底掌控你的Windows文件管理器,告别顽固图标困扰

最新新闻

  • DV、OV、EV证书全解析:从验证原理到云服务商选购实战
  • 零基础打造百元级智能热敏打印机:ESP32终极方案完整攻略
  • 遗传算法工程化实战:破解早熟、多样性坍塌与多目标优化
  • 17种AI智能体架构实战:从基础到高级应用
  • LC709204V与PIC18F8722实现高精度电池电量监测方案
  • Ryujinx Switch模拟器:从零开始到畅玩游戏的完整指南

日新闻

  • STM32F745VG与MC6470 IMU的高性能姿态控制系统设计
  • 机器不消费,人何以生存
  • AI项目操作手册编写规范与最佳实践

周新闻

  • Windows字体自定义终极方案:No!! MeiryoUI完全指南
  • Deepin Boot Maker:告别命令行,3分钟制作Linux启动盘的智能解决方案
  • Plain Craft Launcher 2:重新定义你的Minecraft游戏体验

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号