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

推荐订阅源

cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
SecWiki News
SecWiki News
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Forbes - Security
Forbes - Security
Schneier on Security
Schneier on Security
W
WeLiveSecurity
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Google Online Security Blog
Google Online Security Blog
O
OpenAI News
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
S
Secure Thoughts
PCI Perspectives
PCI Perspectives
人人都是产品经理
人人都是产品经理
Blog — PlanetScale
Blog — PlanetScale
S
SegmentFault 最新的问题
Help Net Security
Help Net Security
G
GRAHAM CLULEY
Latest news
Latest news
V
Visual Studio Blog
The Cloudflare Blog
T
Troy Hunt's Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Stack Overflow Blog
Stack Overflow Blog
GbyAI
GbyAI
I
InfoQ
Know Your Adversary
Know Your Adversary
B
Blog RSS Feed
V2EX - 技术
V2EX - 技术
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
H
Heimdal Security Blog
Y
Y Combinator Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
The GitHub Blog
The GitHub Blog
P
Palo Alto Networks Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
T
Tor Project blog
T
Threat Research - Cisco Blogs
博客园 - 三生石上(FineUI控件)
Cloudbric
Cloudbric
博客园 - Franky
博客园 - 叶小钗
S
Security @ Cisco Blogs
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
阮一峰的网络日志
阮一峰的网络日志
WordPress大学
WordPress大学
T
Threatpost
MongoDB | Blog
MongoDB | Blog
V
Vulnerabilities – Threatpost
Martin Fowler
Martin Fowler

博客园 - 左扬

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,大文件顺序读"