写入与查询全链路:vminsert → vmstorage → vmselect

📅 2026/7/7 6:02:36 👁️ 阅读次数 📝 编程学习
写入与查询全链路:vminsert → vmstorage → vmselect

VictoriaMetrics 的数据流分为写入链路查询链路两条完整路径。以下是详细的关键方法/函数追踪:

┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ VictoriaMetrics 数据流全景图 │ │ (写入 ←→ 查询) │ ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ │ │ ═══════════════════════════════════════════ 写 入 链 路 ═══════════════════════════════════════════ │ │ │ │ ┌─────────────┐ ┌─────────────────────────────────────────────────────┐ │ │ │ 数据来源 │ │ vminsert (写入接入层) │ │ │ │ (12+ 协议) │ │ app/vminsert/main.go │ │ │ └──────┬──────┘ │ │ │ │ │ │ RequestHandler() ─── HTTP 路由分发 │ │ │ │ │ │ │ │ │ ▼ │ ├── /api/v1/write ──► promremotewrite.InsertHandler() │ │ ┌─────────────┐ │ │ │ │ │ │ │ Prometheus │────► /api/v1/write │ ├── /influx/write ──► influx.InsertHandlerForHTTP() │ │ │ (remote_ │ │ │ │ │ │ │ │ write) │ │ ├── /datadog/api/v1/series ──► datadogv1.InsertHandlerForHTTP() │ │ └─────────────┘ │ │ │ │ │ │ ┌─────────────┐ │ ├── /datadog/api/v2/series ──► datadogv2.InsertHandlerForHTTP() │ │ │ InfluxDB │────► /influx/write │ │ │ │ │ │ │ (line │ │ ├── /opentelemetry/v1/metrics ──► opentelemetry.InsertHandler() │ │ │ protocol) │ │ │ │ │ │ │ └─────────────┘ │ ├── /api/v1/import ──► vmimport.InsertHandler() │ │ ┌─────────────┐ │ │ │ │ │ │ │ DataDog │────► /datadog/api/v1/series │ └── /zabbixconnector ──► zabbixconnector.InsertHandlerForHTTP() │ │ └─────────────┘ │ │ │ │ │ ┌─────────────┐ │ ▼ │ │ │ │ OpenTelemetry│────► /opentelemetry/v1/... │ ┌─────────────────────────────────────┐ │ │ │ └─────────────┘ │ │ lib/storage/storage.go │ │ │ │ ┌─────────────┐ │ │ │ │ │ │ │ Graphite │────► :2003 TCP/UDP │ │ Storage.AddRows() │ │ │ │ └─────────────┘ │ │ │ │ │ │ │ │ │ ├── TSIDCache 37% 查询/创建 │ │ │ │ │ │ │ (metricName → TSID) │ │ │ │ │ │ │ │ │ │ │ │ │ └── rawRowsShards.Cell.AddRow() │ │ │ │ │ │ │ (按 CPU 分片并行) │ │ │ │ │ └─────────────┬───────────────────────────┘ │ │ │ │ │ │ │ │ └──────────────────────────┼────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ vmstorage (存储层) │ │ │ │ lib/storage/ │ │ │ │ │ │ │ │ ┌───────────────────────────────────────────────┐ │ │ │ │ │ InMemoryPart (内存缓冲区) │ │ │ │ │ │ • pendingRowsFlushInterval 刷盘 │ │ │ │ │ │ • rawRowsShards 合并 │ │ │ │ │ │ • 转换为 Small Part │ │ │ │ │ └───────────────────────┬───────────────────────┘ │ │ │ │ │ (2秒强制刷盘) │ │ │ │ ▼ │ │ │ │ ┌───────────────────────────────────────────────┐ │ │ │ │ │ Part 文件 (四文件结构) │ │ │ │ │ │ • metaindex.bin (元数据索引) │ │ │ │ │ │ • index.bin (倒排索引) │ │ │ │ │ │ • items.bin (数据块) │ │ │ │ │ │ • lens.bin (块长度) │ │ │ │ │ └───────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ═══════════════════════════════════════════ 查 询 链 路 ═══════════════════════════════════════════ │ │ │ │ ┌─────────────┐ ┌─────────────────────────────────────────────────────┐ │ │ │ 查询客户端 │ │ vmselect (查询层) │ │ │ │ │ │ app/vmselect/main.go │ │ │ │ • Grafana │ │ │ │ │ │ • PromQL │ │ RequestHandler() ─── HTTP 路由分发 │ │ │ │ • API │ │ │ │ │ │ └──────┬──────┘ │ ├── /api/v1/query ──► prometheus.QueryHandler() │ │ │ │ │ │ (即时查询 /promQL eval) │ │ │ │ │ │ ▼ │ │ │ │ │ ├── /api/v1/query_range ──► prometheus.QueryRangeHandler() │ │ │ │ │ │ (范围查询) │ │ │ │ │ │ ▼ │ │ │ ▼ │ ├── /api/v1/series ──► prometheus.SeriesHandler() │ │ ┌─────────────┐ │ │ │ (获取序列列表) │ │ │ │ /api/v1/ │ │ │ ▼ │ │ │ │ query │────► 即时查询 │ ├── /api/v1/labels ──► prometheus.LabelsHandler() │ │ │ │ │ │ │ (获取标签列表) │ │ │ │ 场景: │ │ │ ▼ │ │ │ │ PromQL │ │ ├── /api/v1/label/{name}/values ──► prometheus.LabelValuesHandler() │ │ │ 即时求值 │ │ │ │ (获取标签值) │ │ │ └─────────────┘ │ │ ▼ │ │ │ ┌─────────────┐ │ ├── /api/v1/export ──► prometheus.ExportHandler() │ │ │ /api/v1/ │ │ │ │ (导出原始数据) │ │ │ │ query_range │────► 范围查询 │ │ ▼ │ │ │ │ │ │ ├── /render ──► graphite.RenderHandler() │ │ │ 场景: │ │ │ │ (Graphite Render API) │ │ │ │ 仪表盘 │ │ │ ▼ │ │ │ │ 时间范围 │ │ └── /vmui/* ──► vmui 文件服务 │ │ └─────────────┘ │ │ (Web UI) │ │ ┌─────────────┐ │ ▼ │ │ │ /api/v1/ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ series │────► 系列查询 │ │ lib/promql/ │ │ │ │ │ │ │ │ │ │ │ 场景: │ │ │ PromQL.Parse() ──── PromQL 语法解析 │ │ │ │ 查找指标 │ │ │ │ │ │ │ └─────────────┘ │ │ ▼ │ │ │ ┌─────────────┐ │ │ PromQL.Exec() ──── 查询执行 │ │ │ │ /api/v1/ │ │ │ │ │ │ │ │ labels │────► 标签查询 │ │ ├── 索引查询 (indexDB.Search) │ │ │ └─────────────┘ │ │ │ │ (根据 Tag 找 TSID) │ │ │ │ │ │ ▼ │ │ │ │ │ ├── 数据获取 (netstorage.Search) │ │ │ │ │ │ │ (从 Parts 读取数据块) │ │ │ │ │ │ ▼ │ │ │ │ │ └── k-way Merge ──── 并行归并排序 │ │ │ │ │ │ │ │ │ │ app/vmselect/netstorage/ │ │ │ │ │ • netstorage.Search() ── 查询入口 │ │ │ │ │ • netstorage.SearchTagKeys() ── Tag Key 查询 │ │ │ │ │ • netstorage.SearchTagValues() ── Tag Value 查询 │ │ │ │ └─────────────────────────────────────────────────────┘ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

我理解源码的意思是说

数据流可以想象成现代化物流分拣中心(以集群模式为例)。想象你在逛一个超大型的"时间序列数据物流园",里面有接待处(vminsert)、仓库(vmstorage)、取货员(vmselect)。

  • vminsert = 前台接待 + 分拣台:多个 vminsert 节点就像接待处,多个接待员同时工作,接收来自四面八方的"快递"(Prometheus remote_write、InfluxDB line protocol、DataDog push、OpenTelemetry 等 12+ 种协议)。接待员不是一个人干所有活,而是按 CPU 核心分成多个分拣台(rawRowsShards),每个分拣台独立处理一部分数据,实现写入并行化。
  • vmstorage = 仓库管理:多个 vmstorage 节点就像多个仓库,每个仓库管理自己的数据分区。InMemoryPart 是"临时货架"——新到的快递先放在这里,按 pendingRowsFlushInterval(默认 2 秒)打包一次搬到正式货架上(Small Part)。多个 Small Part 合并成 Big Part(大箱子)。数据分片,分布式存储,每个仓库各管各的。
  • vmselect = 取货员:多个 vmselect 节点就像多个取货员,同时在各个仓库里找货。Grafana/PromQL 发出"取货单"(查询请求),取货员先看目录找在哪(indexDB.Search),再从多个仓库并行取货,最后把所有货物归并到一起送出去。查询并行,结果归并,效率翻倍。
  • 负载均衡器(LB):在 vminsert 和 vmselect 前端,通常部署 Nginx/vmauth 做流量分发。统一入口,透明扩容,前端无感知后端有几台机器。

数据的一生(从写入到查询):

  • 到达:HTTP 请求带着数据到达 vminsert 的 /api/v1/write
  • 分拣:RequestHandler 把请求分发给对应协议的 InsertHandler(Prometheus 用 promremotewrite,InfluxDB 用 influx)
  • 编目:先查 TSIDCache,有记录直接用 TSID,无则创建新 TSID 并建立 metricName → TSID 映射
  • 缓存:数据暂存到 rawRowsShards,按 CPU 核心分片
  • 入库:InmemoryPart 按 pendingRowsFlushInterval 刷盘变成 Small Part,多个 Small Part 合并成 Big Part
  • 索引:倒排索引(indexDB)记录每条数据存在哪个 Part 文件的哪个块
  • 查询:Grafana 发起 PromQL 查询 → vmselect 解析 → indexDB 找数据位置 → 从 Part 文件读取数据块 → k-way 归并排序 → 返回结果

关键设计哲学:

  • 写入:CPU 分片并行(rawRowsShards)+ 节点级扩展(多 vminsert)= 写入无瓶颈
  • 存储:数据分片(按 metric 路由到不同 vmstorage)+ 副本/一致性 = 数据不丢失
  • 查询:k-way 归并(多个 Part 并行读取后归并)+ 节点级扩展(多 vmselect)= 查询高吞吐

设计精髓:为什么这样分层?

VictoriaMetrics 的三层架构(vminsert/vmstorage/vmselect)设计有以下考量:

  • 协议解耦:vminsert 负责适配 12+ 种协议,统一转换为内部格式
  • 存储抽象:vmstorage 屏蔽了 MergeSet 存储引擎的复杂性,对上提供统一接口
  • 查询优化:vmselect 独立出来后可以单独扩展,应对查询密集型场景
  • Cluster 支持:三层分离后可以独立扩缩容,Cluster 模式下分别部署

必记闭环逻辑(核心考点)

VictoriaMetrics 的三层架构(vminsert/vmstorage/vmselect)可以分离部署(Cluster 模式)也可以合一(Single-Node 模式)。写入链路:HTTP → RequestHandler() 路由 → 各协议 InsertHandler() → Storage.AddRows() → rawRowsShards 分片 → InMemoryPart 按 pendingRowsFlushInterval 刷盘 → Part 四文件。查询链路:HTTP → QueryHandler() → PromQL.Parse() 解析 → indexDB.Search() 索引查询 → netstorage.Search() 数据获取 → k-way Merge 归并排序。

四、核心性能优势:为什么 VM 能做到省 7x RAM?

思考记忆提示— 本节是专题的"亮点"——理解 VM 的核心优势,为后续深入理解存储引擎做铺垫

  • 省 RAM 的三个关键设计:MergeSet(LSM-less)+ TSIDCache 37% + blockCache 分层
  • 这些设计是相互配合的,不是孤立的优化
  • 面试高频提问:VictoriaMetrics 为什么能比 Prometheus 省这么多内存?TSIDCache 37% 是怎么来的?

"比 Prometheus 省 7x RAM"是 VictoriaMetrics 最广为人知的优势。但这不是单一优化点,而是多个设计决策协同工作的结果。

4.1 三大核心设计

设计精髓

VictoriaMetrics 省 RAM 的核心哲学是:数据按需加载,而非全量常驻。Prometheus 把所有数据块加载到内存(mmap),而 VM 只把热点数据(通过 TSIDCache 和 blockCache)保留在内存,冷数据放在磁盘按需读取。这是典型的"内存-磁盘分层"设计,比 Prometheus 的"内存优先"更节省资源。

设计一:MergeSet vs LSM Tree(LSM-less 设计)

传统的 LSM Tree(如 RocksDB)采用分层合并策略:L0 → L1 → L2 → ... 每层大小呈指数增长,合并时需要同时读取多层数据,内存压力大。VictoriaMetrics 的 MergeSet 采用了只合并不分层的 LSM-less 设计:

  • 新数据先写入内存(InMemoryPart,按 pendingRowsFlushInterval 默认 2 秒刷新到磁盘成为 Small Part)
  • 多个 Small Part 合并成 Big Part(但不会合并到更大的层)
  • 查询时只需读取最新的 Big Part + 最近的 Small Part,不需要遍历所有层级

4.2 TSID 与 TSIDCache 核心概念详解

前置概念

在理解 TSIDCache 之前,必须先理解TSID(Time Series ID)和metricID的区别:TSID 是 VM 内部给每条时序分配的唯一数字 ID,用于存储层;metricID 是 TSID 的别名,两者指向同一个概念。

4.2.1 TSID(Time Series ID)

VictoriaMetrics 兼容 Prometheus 协议,但底层存储不是直接存 metric{label="xxx"} 完整字符串,而是做一层映射:

  1. 收到 Prometheus 格式指标 http_requests{job="api",instance="127.0.0.1"},先把标签按字典序排序,生成标准唯一字符串(Canonical MetricName)
  2. 给这个唯一时序分配一个内部数字编号TSID(metricID),全程内部使用,对外不暴露;
  3. 磁盘存储时:
    • 原始样本(时间戳+数值)只按 TSID 分组压缩存放;
    • 倒排索引只存「标签→TSID」映射,查询时先通过标签找到一堆 TSID,再读取对应数据块。

核心结论:Prometheus 原生存储每条数据都携带完整指标+标签字符串,开销大;VictoriaMetrics 用短小数字 TSID 替代超长标签字符串,大幅压缩存储、提速读写。

4.2.2 TSIDCache(Time Series ID Cache)

全称:Time Series ID Cache,内存哈希缓存,存储「标准化指标字符串(metricName)→ TSID(即 metricID)」的映射关系。

监控路径说明
vmstorage_tsids_created_total新建 TSID 计数器,暴涨说明新时序大量涌现
vm_slow_row_inserts_total慢写入计数器,未命中 TSIDCache 时飙升

写入流程(决定写入性能):

  1. 每条写入样本进来,先查 TSIDCache:
    • 缓存命中(hit):直接拿到 TSID,快速写入内存缓冲,快路径,几乎无磁盘 IO;
    • 缓存未命中(miss):必须去磁盘 IndexDB 索引文件里查找该时序是否存在;磁盘随机读、解压索引、重建映射,慢路径,CPU/磁盘 IO 暴涨。
  2. 查到/新建 TSID 后,把映射写入 TSIDCache,后续同一条时序直接命中。
4.2.3 为什么 TSIDCache 占 37%?

TSIDCache 直接决定写入吞吐

  • 活跃时序越多,需要缓存的映射条目越多;
  • 缓存不够大 → 大量 miss → 写入变慢、磁盘压力大;
  • 官方默认分配 -memory.allowedDataPointers 的37%给它,专门承载所有活跃时序的「metricName→metricID」映射表。

容量规划参考:

series 规模建议 TSIDCache 内存说明
100 万4GB+保证绝大多数活跃时序命中缓存
500 万16GB+支撑中等规模监控场景
1000 万32GB+大规模生产环境
4.2.4 内存分配全景

memory.allowedDataPointers默认按以下比例分配:

缓存项默认占比存放内容
TSIDCache37%存放「metricName→metricID」映射,保障写入速度
data blockCache (ibCache)25%data blocks(解压后的时序样本块),提升查询速度
index blockCache (idxbCache)10%index blocks(索引块解压后的元信息)
ibSparseCache5%data blocks 的稀疏访问优化层
metricNameCache10%「metricID→metricName」映射
tagFiltersCache~3%标签过滤结果缓存
日期/小时级元数据
(dateMetricIDCache / prevHourMetricIDs / currHourMetricIDs 等)
可动态扩展按日期和小时桶缓存 metricID 集合

上述百分比仅适用于各缓存未通过SetXxxCacheSize()显式覆写的默认情形。各缓存的具体取值由memory.Allowed()在启动时统一核算。

4.2.5 运维判断 TSIDCache 是否够用

Grafana VM 面板查看 vm_slow_row_inserts_total 增长率:

  • 长期 >5%:TSIDCache 内存不足,大量时序查磁盘索引,写入性能衰减;
  • 优化方案:扩容机器总内存(提升 37% 对应的 TSIDCache 容量),或拆分多 vmstorage 分片承载 series。
4.2.6 与 Prometheus/OpenTSDB 的关系

Prometheus 原生存储的短板:每条数据都完整携带指标+标签,没有统一的全局时序 ID 缓存,频繁解析字符串,标签多、时序量大时内存占用、磁盘体积、查询解析开销爆炸。

VictoriaMetrics 的设计:VM 只是接收 Prometheus 协议数据,内部存储引擎完全自研,和 Prometheus TSDB、OpenTSDB 都无关。上层兼容 Prometheus remote write、PromQL;底层自研 IndexDB + MergeSet 存储,引入 TSID 数字 ID 做轻量化索引。OpenTSDB 用 UID 做标签映射(类似 TSID 思路),VM 借鉴了这种「数字 ID 替代字符串」的高效思路。

TSIDCache 核心结论

  • TSID(Time Series ID)是 VM 内部给每条 Prometheus 时序分配的唯一数字 ID,替代冗长标签字符串;
  • TSIDCache 全称 Time Series ID Cache,是「metricName→metricID」映射缓存,决定写入性能,占总可用内存 37%;
  • VM 只是兼容 Prometheus 协议,底层自研存储引入 TSID 机制,和原生 Prometheus TSDB 架构完全不同。

设计三:blockCache 三层设计

blockCache 分为三层,每层缓存不同类型的数据:

层级缓存内容内存占比
ibCache数据块内容(items.bin)25%
idxbCache索引块内容(index.bin)10%
ibSparseCacheibCache 的稀疏访问优化5%