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

推荐订阅源

Jina AI
Jina AI
Google DeepMind News
Google DeepMind News
C
Cybersecurity and Infrastructure Security Agency CISA
T
Tenable Blog
T
The Exploit Database - CXSecurity.com
Latest news
Latest news
G
GRAHAM CLULEY
Project Zero
Project Zero
L
Lohrmann on Cybersecurity
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Cyber Attacks, Cyber Crime and Cyber Security
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Webroot Blog
Webroot Blog
Help Net Security
Help Net Security
TaoSecurity Blog
TaoSecurity Blog
Hacker News: Ask HN
Hacker News: Ask HN
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
N
News and Events Feed by Topic
Cisco Talos Blog
Cisco Talos Blog
T
Tor Project blog
The Hacker News
The Hacker News
The Last Watchdog
The Last Watchdog
C
CXSECURITY Database RSS Feed - CXSecurity.com
V2EX - 技术
V2EX - 技术
S
Secure Thoughts
AWS News Blog
AWS News Blog
W
WeLiveSecurity
云风的 BLOG
云风的 BLOG
V
V2EX
Last Week in AI
Last Week in AI
雷峰网
雷峰网
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
G
Google Developers Blog
P
Palo Alto Networks Blog
A
Arctic Wolf
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
M
MIT News - Artificial intelligence
V
Visual Studio Blog
C
CERT Recently Published Vulnerability Notes
WordPress大学
WordPress大学
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Threatpost
Simon Willison's Weblog
Simon Willison's Weblog
PCI Perspectives
PCI Perspectives
量子位
K
Kaspersky official blog
腾讯CDC
Schneier on Security
Schneier on Security
F
Full Disclosure
S
Schneier on Security

博客园 - 左扬

VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 与其他 TSDB 对比:Prometheus/InfluxDB/Thanos/VM VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 写入吞吐/查询延迟/内存占用的数学模型 VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 模块依赖图——从 import 语句看组件关系 VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— Goroutine 池/atomic/零拷贝/sync.Pool VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 多租户架构——accountID/projectID 与 tenant 隔离 VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 版本演进:1.146.0 LTS 重大更新解析 VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 整体数据流:一条监控数据的完整生命周期 VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— Single-Node vs Cluster 模式本质区别 VictoriaMetrics 1.146.0 源码【左扬精讲】—— 开篇总览 Rust 专题【左扬精讲】—— 从语法到灵魂:Ownership、Borrowing 与多语言对比 kubernetes 源码【左扬精讲】—— kube-scheduler 启动流程源码分析 Rust 专题【左扬精讲】—— 选择控制语句、运算符与格式化输出 Rust 专题【左扬精讲】—— 所有权详解 Rust 专题【左扬精讲】—— 作用域详解 Rust 专题【左扬精讲】—— 变量、常量与标量数据类型 kubernetes 源码 / Operator 专题【左扬精讲】—— Deployment Controller 源码分析:从对象创建到滚动更新 kubernetes 源码 / Operator 专题【左扬精讲】—— Operator 开发中的 Webhook:从准入控制到生产部署 Kubernetes源码 / Operator 专题【左扬精讲】—— 实现 Application Controller:从零构建生产级控制器 Kubernetes 编程 / Operator 专题【左扬精讲】—— 定义 Application 资源 + 添加自定义新 API 完整指南 Kubernetes 源码【左扬精讲】—— kube-scheduler(调度专题 · 八):内部架构与核心组件 Kubernetes 源码【左扬精讲】—— kube-scheduler(调度专题 · 八): —— 从入口到调度的全链路源码剖析(k8s v1.36.1) DeepSeek-R1 多模态 R1 / VLM-GRPO【左扬精讲】—— Qwen2-VL 微调与视觉推理强化学习实战 DeepSeek-R1 工业 RAG + 微调混合系统【左扬精讲】—— R1 系列收官之作:从 Prompt → RAG → 微调 选型决策树 DeepSeek-R1 推理时扩展【左扬精讲】—— o1 / R1 慢思考机制:Self-Consistency + ToT + PRM 详解 DeepSeek-R1 端侧 LLM 工程【左扬精讲】—— llama.cpp 调参与 Apple Silicon / 国产 NPU / Android 端侧落地全攻略 DeepSeek-R1 vLLM + k8s 生产部署【左扬精讲】—— 从单卡 7B 到 100 卡 671B MoE 集群的工业化部署实战 DeepSeek-R1 评估与系统(Evaluation & Systems)【左扬精讲】—— 从 GSM8K/MMLU 到 LLM-as-Judge 的工业级评估方法论 DeepSeek-R1 模型训练与算法【左扬精讲】—— GRPO 进阶算法:DAPO / PRIME / RLVR / PRM 四大 2025 前沿改进 DeepSeek-R1 模型训练与算法【左扬精讲】—— 数据蒸馏:用 DeepSeek-R1-671B 生成 800K 高质量 CoT 样本的完整流水线 DeepSeek-R1 优化与微调实战【左扬精讲】—— 从 R1 强化学习新范式到 GRPO 微调一站式入门 Kubernetes 源码【左扬精讲】—— kube-scheduler(调度专题 · 七):自定义插件开发实战 —— 手写一个 Score 插件并注册到集群 Kubernetes 源码【左扬精讲】—— kube-scheduler(调度专题 · 六):Scheduler Profile 与多调度器 —— 如何配置多个 profile 实现多租户、Coordinated LeaderElection Kubernetes 源码【左扬精讲】—— kube-scheduler(调度专题 · 五):SchedulingQueue 与 QueueingHint —— 三段队列的细节、v1.36 新引入的 QueueingHint 工作机制 Kubernetes 源码【左扬精讲】—— kube-scheduler(调度专题 · 四):抢占(Preemption)算法剖析 —— DefaultPreemption 如何选 victim、PodDisruptionBudget 如何约束 Kubernetes 源码【左扬精讲】—— kube-scheduler(调度专题 · 二):内置插件逐个精读 — NodeResourcesFit / NodeAffinity / TaintToleration / PodTopologySpread / VolumeBinding / InterPodAffinity Kubernetes 源码 / Operator 专题【左扬精讲】——kube-scheduler(调度专题):调度器内置插件 逐个精读 k8s 源码级精讲(二十六):调度器内置插件逐个精读 Kubernetes 源码 / Operator 专题【左扬精讲】——kube-scheduler(调度专题):调度器内置插件精读 — NodeResourcesFit / NodeAffinity / TaintToleration / PodTopologySpread / VolumeBinding / InterPodAffinity Kubernetes 源码 / Operator 专题【左扬精讲】——kube-scheduler(调度专题):Scheduling Framework 扩展点逐个源码拆解 Kubernetes 源码 / Operator 专题【左扬精讲】——kube-scheduler(调度专题):初识调度模型、内部架构与事件驱动机制 Kubernetes 编程 / client-go 专题【左扬精讲】—— 四种客户端:为什么、怎么选、怎么用 Kubernetes 编程 / Operator 专题【左扬精讲】—— controller-runtime、kubebuilder、operator-sdk 三大框架深度对比 Kubernetes 编程 / Operator 专题【左扬精讲】—— 深入理解 ManagedFields 字段冲突协调机制 Kubernetes 编程 / Operator 专题【左扬精讲】—— k8s Finalizers 深度解析:对象的生命周期与删除控制 Kubernetes 编程 / Operator 专题【左扬精讲】—— OwnerReference 字段与级联删除机制 Kubernetes 编程 / Operator 专题【左扬精讲】—— 深入学习 Server-Side Apply:managedFields 替代 last-applied-configuration 的演进方向 Kubernetes 编程 / Operator 专题【左扬精讲】—— k8s Annotations 与元数据体系(Operator 专题) Kubernetes 编程 / Operator 专题【左扬精讲】—— RESTMapper:把 Group / Version / Kind / Resource 四元组翻译成 REST 路径的"查字典"大师 Kubernetes 编程 / Operator 专题【左扬精讲】—— Converter 资源版本转换器 Kubernetes 编程 / Operator 专题【左扬精讲】—— Application 业务扩展:从单 Deployment 到多 Workload 的复合 Operator 演进 Kubernetes 编程 / Operator 专题【左扬精讲】—— OwnerReference / Finalizer / 准入控制:k8s 资源生命周期的三大支柱 Kubernetes 编程 / Operator 专题【左扬精讲】—— controller-runtime 框架内幕:从 Manager 到 Reconcile 的全栈拆解 Kubernetes 编程 / Operator 专题【左扬精讲】—— 生产级 Operator 最佳实践:并发安全、资源清理与高可用设计 Kubernetes 编程 / Operator 专题【左扬精讲】—— application-operator Reconcile 循环源码精讲:从 client-go Informer 到 workqueue 的全链路解剖 Kubernetes 编程 / Operator 专题【左扬精讲】—— 从零搭建一个 application-operator 新项目:脚手架、API 设计与基于原生 DeploymentStatus/ServiceStatus 的状态建模 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:生产级 Controller 实践:并发安全、资源清理与高可用设计 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析: Controller 调试与诊断工具:从日志分析到问题定位 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:DynamicClient 操作 CRD:无需代码生成的动态操作 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:控制器与 APIServer 完整交互流程:从 Watch 到缓存同步 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:错误处理与重试机制:WorkQueue 限速器详解 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:Leader 选举机制:高可用控制器的必备技能 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:Controller 开发模式完整实战 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:SharedInformerFactory 与等待缓存同步 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:从认证配置到 Deployment 操作 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:版本对应、架构组件与组件关系 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:Informer 源码深度解析:从底层原理到实战应用 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:Reflector 源码深度解析 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:ListWatcher 源码深度解析 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:Indexer 与 ThreadSafeStore 核心原理与源码深度剖析 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:DeltaFIFO 核心原理与源码深度剖析 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:workqueue 核心原理与实战 Kubernetes 编程 / Operator 专题【左扬精讲】—— runtime.Codec 资源编解码:serializer 与 codec 差异、编解码数据结构、codec 核心调用链路 Kubernetes 编程 / Operator 专题【左扬精讲】—— Scheme 资源注册机制全解 Kubernetes 编程 / Operator 专题【左扬精讲】—— Kubernetes 自定义资源的内部版本与外部版本:从源码看版本定义机制 Kubernetes 编程 / Operator 专题【左扬精讲】—— Kubernetes 1.36.1 核心 API 数据结构全解 Kubernetes 编程 / Operator 专题【左扬精讲】—— Kubernetes 构建过程 【AIOPS】一文读懂LLM【左扬精讲】:从诞生到普及,解锁大语言模型的核心密码 【AIOPS】AI Agent 专题【左扬精讲】核心功能篇:MCP-VictoriaMetrics Hooks 源码精讲:Hooks 可观测性的无侵入式实现 【AIOPS】AI Agent 专题【左扬精讲】核心功能篇:MCP-VictoriaMetrics Golang 配置解析源码精讲 ——SRE 自定义 Agent 核心技巧 【AIOPS】AI Agent 专题【左扬精讲】核心功能篇:MCP-VictoriaMetrics Golang 并发模型解析 ——SRE 应对高并发采集的调优思路 【AIOPS】AI Agent 专题【左扬精讲】基础架构篇:MCP-VictoriaMetrics Golang 源码整体架构拆解 ——SRE 必懂的核心模块与数据流 OpenTelemetry 开发实战【左扬精讲】—— 云原生可观测体系构建与分布式追踪二次开发 Kubernetes 编程 / Operator 专题【左扬精讲】—— Operator 开发实战项目 7 —— 基于流量预测模型的智能弹性扩缩容 Operator 实战(AIOps 模型训练与智能扩容(下篇)—— 预测式弹性扩缩容 Operator 落地实现) Kubernetes 编程 / Operator 专题【左扬精讲】—— Operator 开发实战项目 7 —— 基于流量预测模型的智能弹性扩缩容 Operator 实战(AIOps 模型训练与智能扩容(上篇)—— 时序预测模型构建与离线训练) Kubernetes 编程 / Operator 专题【左扬精讲】—— Operator 开发实战项目 6 —— 基于运维专家知识库的智能故障诊断与排查 Operator 实战 Kubernetes 编程 / Operator 专题【左扬精讲】—— Operator 开发实战项目 5 —— 基于大语言模型(LLM)的实时日志流智能监测 Operator 实现 Kubernetes 编程 / Operator 专题【左扬精讲】—— Operator 开发实战项目 4 —— 基于 Operator 实现大模型私有化部署与管理 Kubernetes 编程 / Operator 专题【左扬精讲】—— Operator 开发实战项目 3(上篇)—— 面向 AI / 算力调度场景:GPU 竞价实例资源池统一调度管理 Operator 开发 Kubernetes编程 / Operator专题【左扬精讲】—— Operator 开发实战项目 2 —— 面向零售 / 电商潮汐流量难题:多云多集群数据中心级全链路弹性伸缩 DataCenter Scaler Operator 从 0 到 1 全链路开发 Kubernetes编程 / Operator专题【左扬精讲】—— 深入理解Kubebuilder注解:为什么Operator开发离不开这些特殊注释 Kubernetes编程 / Operator专题【左扬精讲】—— Operator 开发实战项目1 —— Applicaion Operator(通用应用生命周期管理 Operator 实战) Pod 镜像拉取失败?kubectl edit pods修改镜像地址的底层原理与实操 (该方法仅为临时应急方案,并非长期解决方案) Kubernetes编程/Operator专题精讲—— 理解控制器模式 —— 控制器模式的核心原理与实现逻辑(从原理到实践) 【AIOPS】AI Agent 专题【左扬精讲】模型微调实战:一站式平台 LLaMA-Factory 【AIOPS】AI Agent 专题【左扬精讲】基于 k8s+vLLM+Ray 分布式部署全指南:架构设计、资源调度与性能优化 【AIOPS】AI Agent专题【左扬精讲】非量化版DeepSeek分布式部署全指南:精度保障、显存规划与Ollama/vLLM选型 【AIOPS】AI Agent 专题【左扬精讲】零开发框架实现 ReAct Agent(Go SRE友好)
VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 架构演进:从 TSDB 到 MergeSet 的设计取舍
左扬 · 2026-06-29 · via 博客园 - 左扬

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

在时序数据库(TSDB)的世界里,存储引擎的设计直接决定了写入性能、查询效率和资源利用率。VictoriaMetrics 采用了独特的 MergeSet 存储架构,与传统 TSDB(如 Prometheus、InfluxDB)的 LSM Tree 或 B-Tree 有着本质区别。本篇文章将从源码层面深入剖析 MergeSet 的设计哲学,以及它相比传统 TSDB 的优势与取舍。

一、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.goscheduleMerges() 函数中实现:

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

五、面试高频提问

面试问答本节精选面试高频问题,直接命中面试官想听到的答案

Q1:MergeSet 和 LSM Tree 的核心区别是什么?

核心区别在于"是否分层"。LSM Tree 将数据分成多层(L0, L1, L2...),每层大小递增,合并时需要逐层向上合并。MergeSet 不分层,所有 Part 文件在同一目录,小 Part 持续合并成大 Part,合并方向是单向的(Small → Big → Bigger)。这种设计让 MergeSet 的查询更稳定(只需扫描少量大文件),而 LSM Tree 的查询需要遍历多个层级。

Q2:MergeSet 为什么不需要 WAL?

因为 MergeSet 使用 InmemoryPart 的原子性刷盘代替 WAL。lib/mergeset/inmemory_part.go 中,MustStoreToDisk() 函数先写临时文件,刷盘成功后原子性 rename 到正式文件名。进程崩溃时,临时文件会被忽略,不会污染数据。代价是最多丢失 1 秒数据。

Q3:MergeSet 的 Part 文件为什么是 .tar 格式?

.tar 用于打包 metaindex、index、items、lens 四个文件。每个 Part.tar 内部包含 4 个二进制文件:metaindex.bin(Block 索引)、index.bin(标签倒排索引)、items.bin(实际时序数据)、lens.bin(行长度)。.tar 本身不压缩,压缩发生在 items.bin 内部,通过 commonPrefix + ZSTD 实现 30-50% 的存储节省。

Q4:MergeSet 如何保证查询性能?

通过 BloomFilter + 顺序读大文件 + commonPrefix 压缩。查询时先通过 BloomFilter 快速判断某个 Part 是否可能包含目标数据,跳过不相关的 Part。对于可能包含数据的 Part,顺序读取大文件比 LSM Tree 的随机读多层小文件更高效。commonPrefix 压缩减少了解码数据量,进一步提升查询速度。

全篇必记口诀

MergeSet 的精髓是"只合并不分层":InmemoryPart 原子性刷盘替代 WAL,BloomFilter 过滤减少 IO,commonPrefix + ZSTD 双重压缩节省空间。记住这个口诀:"合并不分层,刷盘无 WAL,大文件顺序读"