03 - 架构原理
03 · 架构原理
本章目标
- 理解 VictoriaMetrics 的存储引擎设计
- 了解数据压缩策略及其优势
- 掌握集群版三大组件的职责与协作
- 认识数据分片(sharding)和复制(replication)机制
- 为性能调优和容量规划打下理论基础
3.1 整体架构概览
写入路径 (Write Path)
┌──────────┐ ┌──────────┐ ┌──────────┐
│ vmagent │ │Prometheus│ │ 应用程序 │
│ scrape │ │remote_w │ │ SDK │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────┐
│ vminsert │
│ (路由 + 分片) │
└─────────────────┬───────────────────────┘
│ consistent hashing
▼
┌──────────┬──────────┬──────────┐
│vmstorage1│vmstorage2│vmstorage3│
│ │ │ │
│ Part │ Part │ Part │ ← 分区(Partitions)
│ Index │ Index │ Index │ ← 倒排索引
│ Cache │ Cache │ Cache │ ← 内存缓存
└──────────┴──────────┴──────────┘
▲
│
┌─────────────────────────────────────────┐
│ vmselect │
│ (查询 + 聚合 + 缓存) │
└─────────────────┬───────────────────────┘
│
▼
┌──────────┐
│ Grafana │
│ vmui │
└──────────┘
查询路径 (Query Path)
3.2 存储引擎
3.2.1 核心设计原则
VictoriaMetrics 的存储引擎基于以下原则设计:
| 原则 | 实现方式 |
|---|---|
| 追加写入 | 所有数据以 append-only 方式追加到数据文件 |
| 不可变分区 | 数据文件一旦写入就不再修改 |
| 后台合并 | 小分区(part)后台合并为大分区(类似 LSM-Tree) |
| 列式存储 | 时间戳和值分别存储,提升压缩效率 |
| 倒排索引 | metric_name → time_series_id 的快速查找 |
3.2.2 数据写入流程
写入请求 (metric_name{label=value} timestamp value)
│
▼
┌─────────────────┐
│ 1. 内存缓冲区 │ ← 先写入内存中的活跃 block
│ (Active Block) │
└────────┬────────┘
│ 达到阈值(如 64MB)或定时 flush
▼
┌─────────────────┐
│ 2. 写入磁盘 │ ← 生成一个 Part(分区)
│ (Immutable │ 包含:data.bin + index.bin + timestamps.bin
│ Part) │
└────────┬────────┘
│ 后台合并
▼
┌─────────────────┐
│ 3. Part 合并 │ ← 小 Part 合并为大 Part
│ (Merge) │ 减少文件数量,提升查询效率
└─────────────────┘
3.2.3 Part(分区)结构
每个 Part 包含以下文件:
<part_dir>/
├── metadata.json # 分区元数据
├── timestamps.bin # 时间戳列(列式存储)
├── values.bin # 数据值列(列式存储)
├── index.bin # 倒排索引
└── metaindex.bin # 元索引(index 的索引)
分区的生命周期:
小 Part (memory flush)
│
▼
中 Part (merge level 1)
│
▼
大 Part (merge level 2)
│
▼
超大 Part (merge level N)
│
▼ retention 过期
删除
3.2.4 倒排索引
倒排索引结构:
┌──────────────┬─────────────────────┐
│ Search Tag │ Time Series IDs │
├──────────────┼─────────────────────┤
│ __name__=cpu │ 1, 3, 7, 12, 45 │
│ host=web01 │ 1, 5, 12 │
│ host=web02 │ 3, 7, 45 │
│ region=cn │ 1, 3, 5, 7 │
└──────────────┴─────────────────────┘
查询 cpu{host="web01"} 时:
→ 查找 __name__=cpu → {1, 3, 7, 12, 45}
→ 查找 host=web01 → {1, 5, 12}
→ 取交集 → {1, 12}
→ 读取时间序列 1 和 12 的数据
3.3 数据压缩
3.3.1 压缩策略概览
| 数据类型 | 压缩算法 | 说明 |
|---|---|---|
| 时间戳 | delta-of-delta + varbit | 相邻时间戳差值通常恒定,压缩率极高 |
| 浮点值 | XOR 压缩 | 类 Facebook Gorilla,相似值差异小 |
| 字符串(label) | 字典编码 + 前缀压缩 | label 值重复率高 |
| 索引 | 前缀压缩 | metric name 共享前缀 |
3.3.2 时间戳压缩(Delta-of-Delta)
原始时间戳序列(每 15 秒采集):
1000, 1015, 1030, 1045, 1060
Delta(一阶差值):
-, 15, 15, 15, 15
Delta-of-Delta(二阶差值):
-, -, 0, 0, 0
编码后:只需存储第一个值(1000)、第一个delta(15)、然后一串 0
用 varbit 编码 0 只需 1 bit,压缩率极高!
3.3.3 值压缩(XOR)
原始浮点值:
95.20, 95.15, 95.18, 95.22
XOR 编码:
存储完整第一个值: 95.20
XOR(95.20, 95.15) = 微小差异 → 几个有效位 → 极少 bit
XOR(95.15, 95.18) = 微小差异 → 几个有效位 → 极少 bit
...
3.3.4 压缩率对比
| 场景 | Prometheus TSDB | VictoriaMetrics | 压缩比提升 |
|---|---|---|---|
| CPU 指标(15s 间隔) | 1.2 bytes/sample | 0.15 bytes/sample | 8x |
| 内存指标(60s 间隔) | 1.5 bytes/sample | 0.2 bytes/sample | 7.5x |
| 网络计数器(高精度) | 1.8 bytes/sample | 0.3 bytes/sample | 6x |
| 混合工作负载 | ~1.5 bytes/sample | ~0.2 bytes/sample | 7-10x |
3.4 集群组件详解
3.4.1 vminsert(写入层)
职责:
- 接收客户端写入请求
- 按 metric name + labels 进行一致性哈希分片
- 将数据路由到对应的 vmstorage 节点
客户端请求
│
▼
┌─────────────────────────┐
│ vminsert │
│ │
│ 1. 解析写入协议 │
│ 2. 计算 hash(series) │
│ 3. hash % N = 节点索引 │
│ 4. 转发到对应 vmstorage │
└──────────────────────────┘
│ │ │
▼ ▼ ▼
vmstorage0 vmstorage1 vmstorage2
关键参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
-storageNode | 无 | vmstorage 地址列表 |
-replicationFactor | 1 | 副本数(1 = 无副本) |
-maxLabelsPerTimeseries | 30 | 每个时间序列最大 label 数 |
-dedup.minScrapeInterval | 0s | 去重间隔 |
3.4.2 vmstorage(存储层)
职责:
- 接收并持久化时序数据
- 维护倒排索引
- 响应 vmselect 的数据查询
- 处理数据保留(retention)和合并(merge)
vmstorage 内部结构:
┌──────────────────────────────┐
│ vmstorage │
│ │
│ ┌────────────────────────┐ │
│ │ 内存缓存 (Cache) │ │
│ │ - 最近写入的数据 │ │
│ │ - 热数据索引 │ │
│ └────────────────────────┘ │
│ │
│ ┌────────────────────────┐ │
│ │ 活跃 Part │ │
│ │ - 接收新数据 │ │
│ │ - 定期 flush 到磁盘 │ │
│ └────────────────────────┘ │
│ │
│ ┌────────────────────────┐ │
│ │ 存储层 │ │
│ │ - Part 0 (最旧) │ │
│ │ - Part 1 │ │
│ │ - Part 2 │ │
│ │ - Part N (最新) │ │
│ └────────────────────────┘ │
│ │
│ ┌────────────────────────┐ │
│ │ 倒排索引 │ │
│ │ - metric name 索引 │ │
│ │ - label 索引 │ │
│ └────────────────────────┘ │
└──────────────────────────────┘
关键参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
-storageDataPath | victoria-metrics-data | 数据存储路径 |
-retentionPeriod | 1 个月 | 数据保留期 |
-memory.allowedPercent | 60 | 可用内存百分比 |
-dedup.minScrapeInterval | 0s | 去重间隔 |
-minScrapeInterval | 0s | 最小采集间隔 |
3.4.3 vmselect(查询层)
职责:
- 接收查询请求(MetricsQL / PromQL)
- 将查询扇出(fan-out)到所有 vmstorage 节点
- 合并各节点的中间结果
- 返回最终结果
- 管理查询结果缓存
查询请求:cpu_usage{host="web01"}
│
▼
┌──────────┐
│ vmselect │
└─────┬────┘
│ 并行发送到所有 vmstorage
┌────────┼────────┐
▼ ▼ ▼
storage0 storage1 storage2
(部分数据) (部分数据) (部分数据)
│ │ │
└────────┼────────┘
│ merge 结果
▼
┌──────────┐
│ vmselect │ 排序、去重、返回
└─────┬────┘
│
▼
最终结果
关键参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
-storageNode | 无 | vmstorage 地址列表 |
-cacheDataPath | 内存缓存 | 查询缓存存储路径 |
-dedup.minScrapeInterval | 0s | 去重间隔 |
-search.maxQueryDuration | 30s | 最大查询耗时 |
-search.maxConcurrentRequests | 16 | 最大并发查询数 |
3.5 数据分片机制
3.5.1 一致性哈希
vminsert 使用一致性哈希将时间序列分配到 vmstorage 节点:
哈希环 (Hash Ring)
0
│
┌────┴────┐
│ │
节点0 节点1
(120°) (240°)
│ │
└────┬────┘
│
节点2
(360°=0°)
series = {__name__="cpu_usage", host="web01"}
hash(series) = 180° → 分配到 节点1
3.5.2 分片策略
# vminsert 将数据分片到 3 个 vmstorage
vminsert \
-storageNode=vmstorage1:8400,vmstorage2:8400,vmstorage3:8400
# 每个时间序列始终写入同一个 vmstorage
# 这保证了单个 vmstorage 对某个 series 的数据完整性
3.5.3 扩缩容影响
| 操作 | 影响 | 是否需要数据迁移 |
|---|---|---|
| 增加 vmstorage | 新 series 分配到新节点 | 否(旧数据仍在原节点) |
| 减少 vmstorage | 对应 series 查询可能失败 | 是(需要数据迁移) |
| 增加 vmselect | 查询并发能力增加 | 否 |
| 增加 vminsert | 写入并发能力增加 | 否 |
注意:减少 vmstorage 节点会导致部分数据不可查询。生产环境扩容容易、缩容需要规划数据迁移。
3.6 复制机制
3.6.1 配置副本
# vminsert 设置副本因子为 2
vminsert \
-storageNode=vmstorage1:8400,vmstorage2:8400,vmstorage3:8400 \
-replicationFactor=2
3.6.2 副本写入流程
replicationFactor=2 时:
写入请求
│
▼
vminsert
│
├──── hash(series) → 主节点 vmstorage0
│
└──── hash(series) + 1 → 副本节点 vmstorage1
任意一个节点宕机,数据仍可查询
3.6.3 副本数选择建议
| 节点数 | 建议副本数 | 容错能力 | 存储开销 |
|---|---|---|---|
| 1 | 1(无副本) | 0 节点 | 1x |
| 2 | 2 | 1 节点 | 2x |
| 3 | 2 | 1 节点 | 2x |
| 5+ | 2 或 3 | 1-2 节点 | 2-3x |
提示:副本数不能大于 vmstorage 节点数,否则 vminsert 会报错。
3.7 数据保留与过期
3.7.1 保留策略
# 保留 90 天数据
vmstorage -retentionPeriod=90d
# 保留 2 年数据
vmstorage -retentionPeriod=730d
# 保留 6 个月
vmstorage -retentionPeriod=6M
3.7.2 过期删除机制
数据过期流程:
1. 后台任务每小时检查一次
2. 识别包含过期数据的 Part
3. 如果 Part 全部过期 → 直接删除整个 Part
4. 如果 Part 部分过期 → 重写 Part(保留未过期数据)
5. 释放磁盘空间
时间线:
├── Part A (全过期) ──▶ 直接删除 ✅
├── Part B (部分过期) ──▶ 重写为 Part B' ✅
└── Part C (未过期) ──▶ 保留 ⏭️
注意:磁盘空间不会立即释放,需要等待文件系统回收。使用 SSD 时,建议预留 20-30% 额外空间。
3.8 与 Prometheus TSDB 架构对比
| 维度 | Prometheus TSDB | VictoriaMetrics |
|---|---|---|
| 数据结构 | WAL + Block | Part + Merge |
| 索引 | 正排 + 倒排 | 倒排索引 |
| 压缩 | Gorilla | 自研压缩(更强) |
| 分块大小 | 2 小时 | 动态合并 |
| 内存模式 | mmap | 自管理内存 |
| 多租户 | 不支持 | 支持(集群版) |
| 水平扩展 | 不支持 | 原生支持 |
| 数据更新 | 不支持 | 不支持(追加写入) |
3.9 内部通信协议
集群组件间通信
vminsert ──(net.InsertProtobuf)──▶ vmstorage
写入协议:protobuf 序列化,HTTP/gRPC
vmselect ──(net.SelectProtobuf)──▶ vmstorage
查询协议:protobuf 序列化,HTTP/gRPC
端口分配(默认):
┌─────────────┬──────────┬──────────┬──────────┐
│ 组件 │ HTTP API │ 写入端口 │ 查询端口 │
├─────────────┼──────────┼──────────┼──────────┤
│ vminsert │ 8480 │ - │ - │
│ vmselect │ 8481 │ - │ - │
│ vmstorage │ 8482 │ 8400 │ 8401 │
└─────────────┴──────────┴──────────┴──────────┘
本章小结
| 要点 | 内容 |
|---|---|
| 存储引擎 | 追加写入 + 不可变 Part + 后台合并 |
| 压缩策略 | 时间戳 delta-of-delta,值 XOR 编码,7-10x 压缩率提升 |
| 集群组件 | vminsert(写入路由)、vmstorage(存储)、vmselect(查询聚合) |
| 分片 | 一致性哈希,扩容容易缩容难 |
| 复制 | 支持多副本,副本数 ≤ 节点数 |
| 保留 | Part 级别管理,后台定期清理过期数据 |