当前位置: 首页 > news >正文

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

一、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 查询延迟 │ ⚠️ │ ⚠️ │ ✅ │ │ 低内存占用 │ ⚠️ │ ⚠️ │ ✅ │ │ 数据零丢失要求 │ ✅ │ ✅ │ ⚠️ │ │ 运维简单优先 │ ⚠️ │ ⚠️ │ ✅ │ │ 开源生态成熟 │ ✅ │ ⚠️ │ ⚠️ │ │ │ │ ✅ 强烈推荐 ⚠️ 可用但非最优 ❌ 不推荐 │ │ │ └─────────────────────────────────────────────────────────────────────────────┘

五、面试高频提问

http://www.jsqmd.com/news/1122353/

相关文章:

  • NHANES数据库研究:从数据清洗到顶刊发表的实战解析
  • GLM5.1与DeepSeek V4编程实战对比:长上下文理解与代码生成精度的工程权衡
  • SQL注入实战:基于PHPStudy与SQLi-Labs的本地靶场搭建与手工注入全解析
  • MyComputerManager:彻底掌控你的Windows文件管理器,告别顽固图标困扰
  • 基于CBAM-YOLOv7的交通信号灯识别系统设计与实现
  • 基于YOLOv10的电子元器件自动识别系统开发
  • 提示词工程实战指南:从核心原则到高级模式,构建高效LLM应用
  • KMR221与PIC18LF45K50在嵌入式电压监测中的高精度应用
  • OpenClaw.NET 率先原生支持 MCP Apps
  • AI生产力工具实践指南:从需求到落地
  • 2026 卡点音乐素材下载网站 TOP5 评测 版权合规商用卡点 BGM 平台推荐
  • AI智能体运行时正走向“水电化”:从Managed Agents看Runtime层的价值迁移
  • 基于YOLOv11的风力叶片缺陷智能检测系统开发
  • MCP架构实战:模块化AI投研代理的原理与落地
  • 监督学习还是无监督学习?建模前必须通过的业务对齐三分钟检查
  • MiMo-V2.5-Pro:面向中文开发者的Claude级本地代码模型
  • VLA高效化陷阱:模型压缩不是万能解,数据管道才是真瓶颈
  • 3步恢复B站旧版界面:Bilibili-Old功能增强解决方案
  • Postman便携版实战指南:原理、配置与高级应用场景
  • 大模型后Scaling Law时代:8个关键技术拐点解析
  • Hugging Face Hub大文件上传实战指南
  • 微信小程序自动化渗透测试工具e0e1-wx实战指南
  • ARM Cache 一致性:DMA 数据错了,先别骂外设
  • QModMaster:开源免费的ModBus调试工具终极指南
  • Prompt与Finetune如何选:基于任务结构强度的工程决策指南
  • STM32与EEPROM硬件设计及I2C驱动优化实践
  • 机器学习项目成败关键:精准问题定义四步法
  • 基于PyQt与VGG16的肺部结节智能检测系统开发
  • STM32F429与13DOF传感器融合实现高精度定位
  • AI自动化UI开发:从PSD到UGUI的工程化实践与工具选型