写入与查询全链路: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 把请求分发给对应协议的 InsertHandlerPrometheus 用 promremotewriteInfluxDB 用 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 的三个关键设计MergeSetLSM-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 TreeLSM-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 之前必须先理解TSIDTime Series ID和metricID的区别TSID 是 VM 内部给每条时序分配的唯一数字 ID用于存储层metricID 是 TSID 的别名两者指向同一个概念。4.2.1 TSIDTime Series IDVictoriaMetrics 兼容 Prometheus 协议但底层存储不是直接存 metric{labelxxx} 完整字符串而是做一层映射收到 Prometheus 格式指标 http_requests{jobapi,instance127.0.0.1}先把标签按字典序排序生成标准唯一字符串Canonical MetricName给这个唯一时序分配一个内部数字编号TSIDmetricID全程内部使用对外不暴露磁盘存储时原始样本时间戳数值只按 TSID 分组压缩存放倒排索引只存「标签→TSID」映射查询时先通过标签找到一堆 TSID再读取对应数据块。核心结论Prometheus 原生存储每条数据都携带完整指标标签字符串开销大VictoriaMetrics 用短小数字 TSID 替代超长标签字符串大幅压缩存储、提速读写。4.2.2 TSIDCacheTime Series ID Cache全称Time Series ID Cache内存哈希缓存存储「标准化指标字符串metricName→ TSID即 metricID」的映射关系。监控路径说明vmstorage_tsids_created_total新建 TSID 计数器暴涨说明新时序大量涌现vm_slow_row_inserts_total慢写入计数器未命中 TSIDCache 时飙升写入流程决定写入性能每条写入样本进来先查 TSIDCache缓存命中hit直接拿到 TSID快速写入内存缓冲快路径几乎无磁盘 IO缓存未命中miss必须去磁盘 IndexDB 索引文件里查找该时序是否存在磁盘随机读、解压索引、重建映射慢路径CPU/磁盘 IO 暴涨。查到/新建 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 核心结论TSIDTime Series ID是 VM 内部给每条 Prometheus 时序分配的唯一数字 ID替代冗长标签字符串TSIDCache 全称 Time Series ID Cache是「metricName→metricID」映射缓存决定写入性能占总可用内存 37%VM 只是兼容 Prometheus 协议底层自研存储引入 TSID 机制和原生 Prometheus TSDB 架构完全不同。设计三blockCache 三层设计blockCache 分为三层每层缓存不同类型的数据层级缓存内容内存占比ibCache数据块内容items.bin25%idxbCache索引块内容index.bin10%ibSparseCacheibCache 的稀疏访问优化5%