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

推荐订阅源

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 源码专题【左扬精讲】—— 架构演进:从 TSDB 到 MergeSet 的设计取舍 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 源码专题【左扬精讲】—— 整体数据流:一条监控数据的完整生命周期
左扬 · 2026-06-29 · via 博客园 - 左扬

VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 整体数据流:一条监控数据的完整生命周期

在监控系统中,一个监控数据点从 Prometheus 发出 Remote Write 请求,到最终写入磁盘成为可查询的时序数据,经历了怎样的旅程?本篇文章将从源码层面完整追踪一条监控数据的生命周期,包括协议解析、索引构建、数据压缩、存储合并等关键环节。

一、数据写入全链路概览

思考记忆提示理解数据全链路是理解 VM 架构的基础

  • 写入链路:Remote Write → HTTP → Parser → Storage.Add → Partition → InmemoryPart
  • 合并链路:InmemoryPart → Small Part → Big Part → 更大 Part
  • 查询链路:PromQL → Parser → Search → Merge → Results
  • 面试高频提问:一条数据从写入到可查询需要经历哪些步骤?

让我们先从宏观角度理解一条监控数据在 VictoriaMetrics 中的完整旅程:

  VictoriaMetrics 数据写入全链路
┌─────────────────────────────────────────────────────────────────────────────┐
│                                                                             │
│  ① 协议接入层                                                              │
│  ┌───────────────────────────────────────────────────────────────────────┐  │
│  │ Prometheus ──Remote Write──► :8428/api/v1/write                       │  │
│  │                                       │                                │  │
│  │                                       ▼                                │  │
│  │                          lib/protoparser/prometheus/                   │  │
│  │                          WriteRequestParser.Parse()                    │  │
│  └───────────────────────────────────────────────────────────────────────┘  │
│                                    │                                         │
│                                    ▼                                         │
│  ② 协议解析层                                                              │
│  ┌───────────────────────────────────────────────────────────────────────┐  │
│  │ TimeSeries → []RemoteWriteSample                                        │  │
│  │                                                                           │  │
│  │ 每个 Sample 包含:                                                       │  │
│  │   - MetricName: __name__="cpu_usage" job="prometheus"                   │  │
│  │   - Timestamp: 1704067200000 (毫秒)                                     │  │
│  │   - Value: 95.5                                                         │  │
│  └───────────────────────────────────────────────────────────────────────┘  │
│                                    │                                         │
│                                    ▼                                         │
│  ③ 索引构建层                                                              │
│  ┌───────────────────────────────────────────────────────────────────────┐  │
│  │ MetricName ──编码──► []byte ──查/创建──► MetricID (uint64)            │  │
│  │                                                                           │  │
│  │ lib/storage/metric_name.go: Marshal()                                   │  │
│  │ lib/storage/tsid.go: GetOrCreateTSID()                                  │  │
│  └───────────────────────────────────────────────────────────────────────┘  │
│                                    │                                         │
│                                    ▼                                         │
│  ④ 存储写入层                                                              │
│  ┌───────────────────────────────────────────────────────────────────────┐  │
│  │ RawRow{TSID, Timestamp, Value, PrecisionBits}                           │  │
│  │                                                                           │  │
│  │ lib/storage/partition.go: AddRawRows()                                 │  │
│  │ lib/storage/storage.go: Add()                                           │  │
│  └───────────────────────────────────────────────────────────────────────┘  │
│                                    │                                         │
│                                    ▼                                         │
│  ⑤ InmemoryPart 内存缓冲                                                    │
│  ┌───────────────────────────────────────────────────────────────────────┐  │
│  │ InmemoryPart.AddRow(RawRow)                                           │  │
│  │                                                                           │  │
│  │ 4 个 ChunkedBuffer 并行写入:                                          │  │
│  │   - metaindex buffer                                                   │  │
│  │   - index buffer                                                       │  │
│  │   - data buffer                                                        │  │
│  │   - lens buffer                                                        │  │
│  └───────────────────────────────────────────────────────────────────────┘  │
│                                    │                                         │
│                                    ▼ (1秒后)                                 │
│  ⑥ 刷盘生成 Part                                                          │
│  ┌───────────────────────────────────────────────────────────────────────┐  │
│  │ InmemoryPart.MustStoreToDisk()                                         │  │
│  │                                                                           │  │
│  │ 生成: small_001.tar (metaindex + index + data + lens)                 │  │
│  └───────────────────────────────────────────────────────────────────────┘  │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

源码视角:数据写入的 6 个关键函数

理解数据写入链路,只需记住 6 个关键函数:

  • WriteRequestParser.Parse():解析 Remote Write 协议 → lib/protoparser/prometheus/
  • MetricName.Marshal():标签编码 → lib/storage/metric_name.go
  • GetOrCreateTSID():查/建 MetricID → lib/storage/tsid.go
  • Storage.Add():存储入口 → lib/storage/storage.go
  • InmemoryPart.AddRow():内存缓冲 → lib/mergeset/inmemory_part.go
  • MustStoreToDisk():刷盘 → lib/mergeset/inmemory_part.go

二、协议解析层:Remote Write 到 RawRow

思考记忆提示协议解析是数据的入口,理解它才能理解整个数据流

  • Remote Write 协议基于 Protobuf 编码
  • lib/protoparser 支持 12+ 种协议
  • 面试高频提问:VictoriaMetrics 支持哪些写入协议?

2.1 Remote Write 协议解析

当 Prometheus 发送 Remote Write 请求时,VictoriaMetrics 通过 lib/protoparser/prometheus/ 进行协议解析:

 // lib/protoparser/prometheus/parser.go
// Remote Write 协议解析入口

// WriteRequest 结构(来自 prompb):
// message WriteRequest {
//   repeated.TimeSeries timeseries = 1;
// }
// message TimeSeries {
//   labels  = 1;
//   samples = 2;
// }
// message Sample {
//   value       = 1;
//   timestamp   = 2;
// }

// 解析流程:
// 1. HTTP 请求到达 /api/v1/write
// 2. WriteRequestParser.Parse() 解析 Protobuf
// 3. 遍历每个 TimeSeries,提取 labels 和 samples
// 4. 将 labels 转换为 MetricName
// 5. 将 samples 转换为 RawRow

// MetricName 结构:
// 二进制格式:[__name__=cpu_usage\x00job=prometheus\x00instance=localhost:9090]
// 注意:标签之间用 \x00 分隔,键值对用 = 分隔

// RawRow 结构:
// type RawRow struct {
//     TSID          TSID           // 20 字节:MetricID + 其他
//     Timestamp     int64          // 毫秒时间戳
//     Value         float64        // 指标值
//     PrecisionBits int           // 精度位数
// }

2.2 MetricName 二进制编码

MetricName 的二进制编码在 lib/storage/metric_name.go 中实现:

 // lib/storage/metric_name.go
// MetricName 二进制编码格式

// 原始标签:
// __name__="cpu_usage"
// job="prometheus"
// instance="localhost:9090"

// 二进制编码过程:
// 1. 标签排序(黄金排序:__name__ → job → instance)
// 2. 拼接:__name__=cpu_usage\x00job=prometheus\x00instance=localhost:9090
// 3. 转义特殊字符(\x00 → \x00\xff, = → \x00\xfe)
// 4. 最终二进制:[__name__=cpu_usage][\x00][job=prometheus][\x00][instance=localhost:9090]

// 关键设计点:
// - \x00 作为分隔符,高概率不在标签中出现
// - 黄金排序确保相同标签组合的编码结果唯一
// - 转义机制处理边界情况

// MarshalBinary() 实现:
func (mn *MetricName) MarshalBinary() ([]byte, error) {
    // 1. 分配足够大的 buffer
    // 2. 按黄金顺序写入每个标签
    // 3. 写入分隔符和转义
    // 4. 返回编码后的字节数组
}

三、索引构建:MetricName 到 MetricID

思考记忆提示MetricID 是时序数据的唯一标识,理解它的创建过程很重要

  • MetricID 是 uint64,通过 TSID 结构关联到 MetricName
  • indexDB 存储 MetricName → MetricID 的映射
  • 面试高频提问:MetricID 是如何生成的?如何保证唯一性?

3.1 TSID 四字段体系

TSID(TimeSeries ID)是 VictoriaMetrics 中时序数据的核心标识,在 lib/storage/tsid.go 中定义:

 // lib/storage/tsid.go
// TSID 结构:20 字节定长标识符

type TSID struct {
    // 字段 1:MetricGroupID (uint32, 4 字节)
    // 用于同批次写入的快速分组
    MetricGroupID uint32
    
    // 字段 2:JobID (uint32, 4 字节)
    // 标签中 job 的索引 ID
    
    // 字段 3:InstanceID (uint32, 4 字节)
    // 标签中 instance 的索引 ID
    
    // 字段 4:MetricID (uint64, 8 字节)
    // 全局唯一递增的时序 ID
}

// TSID 生成流程(GetOrCreateTSID):
// 1. 接收 MetricName(二进制编码后的标签)
// 2. 查询 indexDB:MetricName → TSID
// 3. 如果找到,返回现有 TSID
// 4. 如果未找到,生成新 TSID:
//    - atomic.AddUint64(&globalMetricIDCounter, 1) 获取新 MetricID
//    - 写入 indexDB:MetricName → TSID
//    - 返回新 TSID

// 为什么是 20 字节?
// - 4 + 4 + 4 + 8 = 20 字节
// - 对齐到 8 字节边界,访问效率高
// - 20 字节比 24 字节(3 个 uint64)更节省空间

3.2 indexDB 倒排索引

indexDB 是 VictoriaMetrics 的倒排索引引擎,在 lib/storage/index_db.go 中实现:

 // lib/storage/index_db.go
// indexDB 8 种索引前缀

// indexDB 是一个 LSM-like 的键值存储,专门存储索引数据
// 索引前缀定义了不同类型的索引:

const (
    // 前缀 0x00:MetricName → MetricID
    // 用于 TSID 查询
    nsPrefixMetricID = 0x00
    
    // 前缀 0x01:TagKey → TagValue → MetricID[]
    // 用于标签查询(如 job=prometheus)
    nsPrefixTagMetricID = 0x01
    
    // 前缀 0x02:TagValue → MetricID[]
    // 用于无键标签查询(如 =prometheus)
    nsPrefixMetricIDTagValue = 0x02
    
    // 前缀 0x03:Date → MetricID[]
    // 用于按日期查询
    nsPrefixDateMetricID = 0x03
    
    // 前缀 0x04-0x07:其他专用索引
    nsPrefix4 = 0x04
    nsPrefix5 = 0x05
    nsPrefix6 = 0x06
    nsPrefix7 = 0x07
)

// 写入索引数据:
// AddMetricName(tsid TSID, mn *MetricName)
// 1. 编码 MetricName 为二进制
// 2. 写入 nsPrefixMetricID: MetricName → TSID
// 3. 遍历每个标签,写入 nsPrefixTagMetricID
// 4. 写入 nsPrefixDateMetricID

// 查询索引数据:
// SearchTagKeys() / SearchTagValues() / GetTSIDByMetricName()

四、存储层:Partition 到 Part 文件

思考记忆提示Partition 是数据存储的核心,理解它才能理解查询

  • Partition 按月分区,不同月份的数据在不同 Partition
  • 数据先写入 InmemoryPart,1秒后刷盘生成 Part
  • 面试高频提问:数据是如何按时间分区存储的?

4.1 Partition 分区策略

VictoriaMetrics 按月分区存储数据,在 lib/storage/partition.go 中实现:

 // lib/storage/partition.go
// Partition 分区策略:按月分区

// Partition 目录结构:
// /data/2024_01/     ← 2024 年 1 月的分区
// ├── small_001.tar
// ├── small_002.tar
// ├── big_001.tar
// └── ...
// /data/2024_02/     ← 2024 年 2 月的分区
// └── ...

// 分区键计算:
func getPartitionKey(timestamp int64) string {
    // 时间戳转月份
    t := time.Unix(timestamp/1000, 0)  // 毫秒转秒
    return fmt.Sprintf("%d_%02d", t.Year(), t.Month())
    // 例如:2024-01-15 10:00:00 → "2024_01"
}

// GetOrCreatePartition:
// 1. 根据时间戳计算分区键
// 2. 在 partitions map 中查找
// 3. 如果不存在,创建新 Partition
// 4. 返回 Partition 实例

// Partition 内部结构:
type Partition struct {
    // 分区键,如 "2024_01"
    path string
    
    // 该分区的 InmemoryPart
    ip *InmemoryPart
    
    // 该分区已刷盘的 Part 列表
    parts []*Part
    
    // 后台合并任务
    merger *Merger
}

4.2 InmemoryPart 刷盘机制

InmemoryPart 是内存缓冲层,在 lib/mergeset/inmemory_part.go 中实现:

 // lib/mergeset/inmemory_part.go
// InmemoryPart:内存缓冲 + 1秒刷盘

// 4 个并行 buffer:
type InmemoryPart struct {
    // metaindex buffer:存储 Block 元信息
    metaindexBuf *bytes.Buffer
    
    // index buffer:存储 MetricName → BlockID 映射
    indexBuf *bytes.Buffer
    
    // data buffer:存储时序数据点
    dataBuf *bytes.Buffer
    
    // lens buffer:存储每行数据的长度
    lensBuf *bytes.Buffer
    
    // 写入锁
    mu sync.Mutex
    
    // 写入计数器
    rowsCount uint64
}

// AddRow 流程:
// 1. 加锁
// 2. 将 RawRow 写入 4 个 buffer(可能并行)
// 3. 解锁
// 4. 如果达到刷盘阈值,触发后台刷盘

// 刷盘流程(MustStoreToDisk):
// 1. 创建临时文件 small_XXX.tar.tmp
// 2. 将 4 个 buffer 的内容打包写入 .tar 文件
// 3. 调用 tar.Close() 关闭
// 4. os.Rename() 原子性重命名为 small_XXX.tar
// 5. 更新 partition.parts 列表

// 刷盘触发条件:
// - 定时器:每 1 秒强制刷盘
// - 大小:buffer 超过阈值(默认 8MB)

五、查询层:PromQL 到结果集

思考记忆提示查询是写入的逆过程,理解它才能理解数据如何被消费

  • 查询链路:PromQL → Parser → Search → Merge → Results
  • 涉及多分区并行查询和 k-way 归并
  • 面试高频提问:查询是如何跨分区执行的?

5.1 PromQL 执行引擎

PromQL 查询执行在 lib/promql/exec.go 中实现:

 // lib/promql/exec.go
// PromQL 执行引擎

// 查询流程:
// 1. 解析 PromQL → AST(抽象语法树)
//   parseWithCache(q.PromQL)
//
// 2. 执行 AST → 时序数据
//   evalExpr(ast, q)
//
// 3. 返回结果
//   return results

// AST 节点类型:
// - *parser.MatrixSelector:瞬时向量选择器(如 cpu_usage{job="prometheus"})
// - *parser.VectorSelector:向量选择器
// - *parser.AggregateExpr:聚合表达式(如 sum()、avg())
// - *parser.Call:函数调用(如 rate()、irate())
// - *parser.BinaryExpr:二元运算
// - *parser.SubqueryExpr:子查询

// 执行顺序:
// 1. 从叶子节点(VectorSelector)开始
// 2. 调用 Search() 在存储层查询数据
// 3. 逐层向上执行算子(聚合、函数、二元运算)
// 4. 返回最终结果

// VectorSelector 执行:
// func (vs *VectorSelector) eval(q *Query) []Sequence {
//     // 1. 解析标签选择器
//     tf := NewTagFilters(vs.LabelMatchers)
//     
//     // 2. 调用存储层搜索
//     searchResult := storage.Search(tf, vs.Start, vs.End)
//     
//     // 3. 应用时间范围过滤
//     return filterByTimeRange(searchResult, vs.Start, vs.End)
// }

5.2 多分区并行查询

当查询跨越多个月份时,VictoriaMetrics 并行查询所有相关分区:

  多分区并行查询流程
┌─────────────────────────────────────────────────────────────────────────────┐
│                                                                             │
│  PromQL: cpu_usage{job="prometheus"}[1h]                                   │
│                                                                             │
│  ┌───────────────────────────────────────────────────────────────────────┐  │
│  │ 查询时间范围:2024-01-15 09:00 ~ 10:00                                 │  │
│  │ 需要查询的分区:2024_01                                                  │  │
│  └───────────────────────────────────────────────────────────────────────┘  │
│                                    │                                         │
│                                    ▼                                         │
│  ┌───────────────────────────────────────────────────────────────────────┐  │
│  │ Table.Search()                                                        │  │
│  │                                                                       │  │
│  │ 1. 根据时间范围确定需要查询的 Partition 列表                           │  │
│  │ 2. 并行向每个 Partition 发送 Search 请求                               │  │
│  │ 3. 收集所有 Partition 的结果                                           │  │
│  │ 4. k-way 归并排序                                                     │  │
│  └───────────────────────────────────────────────────────────────────────┘  │
│                                    │                                         │
│              ┌─────────────────────┼─────────────────────┐                  │
│              ▼                     ▼                     ▼                  │
│  ┌───────────────────┐  ┌───────────────────┐  ┌───────────────────┐      │
│  │ Partition 2024_01 │  │ Partition 2024_02 │  │ Partition 2024_03 │      │
│  │                   │  │                   │  │                   │      │
│  │ Part 1: hits      │  │ Part 1: hits      │  │ Part 1: hits      │      │
│  │ Part 2: hits      │  │ Part 2: no hit    │  │ Part 2: hits      │      │
│  │ Part 3: no hit    │  │ Part 3: hits      │  │ Part 3: no hit    │      │
│  └───────────────────┘  └───────────────────┘  └───────────────────┘      │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

源码视角:查询链路的 5 个关键函数

理解查询链路,只需记住 5 个关键函数:

  • parseWithCache():解析 PromQL → AST → lib/promql/
  • evalExpr():执行 AST → 时序数据 → lib/promql/exec.go
  • Table.Search():多分区并行查询 → lib/storage/table_search.go
  • Partition.Search():分区级搜索 → lib/storage/partition_search.go
  • Merge():k-way 归并排序 → lib/storage/merge.go

六、面试高频提问

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

Q1:一条数据从写入到可查询需要经历哪些步骤?

经历 6 个关键步骤:协议解析 → 索引构建 → 存储写入 → 内存缓冲 → 刷盘 → 合并。具体来说:1)Remote Write 请求到达 lib/protoparser/prometheus/ 被解析为 TimeSeries;2)MetricName 被编码为二进制;3)通过 GetOrCreateTSID() 获取或创建 MetricID;4)数据写入 Partition.AddRawRows();5)InmemoryPart 每秒刷盘生成 Part;6)后台合并任务将小 Part 合并成大 Part。

Q2:MetricID 是如何生成的?如何保证唯一性?

通过原子操作全局递增计数器生成,通过 indexDB 去重保证唯一性。lib/storage/tsid.go 中,GetOrCreateTSID() 使用 atomic.AddUint64() 原子递增全局计数器获取新 MetricID。写入 indexDB 前先查询是否已存在,已存在则直接返回,避免重复。

Q3:数据是如何按时间分区存储的?

按月分区,通过时间戳计算分区键。lib/storage/partition.go 中,getPartitionKey() 将毫秒时间戳转换为 "YYYY_MM" 格式的分区键。不同月份的数据存储在不同目录(如 /data/2024_01//data/2024_02/),查询时根据时间范围只扫描相关分区。

Q4:InmemoryPart 为什么每秒刷盘而不是等内存满?

平衡数据可靠性和写入性能。如果只依赖内存阈值刷盘,在低写入场景下数据可能在内存中停留很久,进程崩溃会丢失大量数据。1 秒刷盘保证最多丢失 1 秒数据,同时保持高写入吞吐。对于超低写入场景(如每分钟只有几条数据),可以配置 -retentionPeriod 控制数据保留时间。

全篇必记口诀

数据全链路 = 写入链路 + 查询链路。写入链路:Remote Write → Parser → Marshal → GetOrCreateTSID → Add → MustStoreToDisk。查询链路:PromQL → parseWithCache → evalExpr → Table.Search → Partition.Search → Merge。记住这个口诀:"解析建索引,缓冲后刷盘,查询并归并"