VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 模块依赖图——从 import 语句看组件关系
VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 模块依赖图——从 import 语句看组件关系
当你打开 VictoriaMetrics 的源码目录时,是否曾被数百个 Go 文件所困惑?每个目录承载什么职责?组件之间如何协作?数据从写入到查询的完整链路是什么?理解模块依赖关系是理解整个系统架构的关键。
读完本篇,你应该能回答:VictoriaMetrics 的源码目录结构是如何组织的?各模块之间的依赖关系是什么?从 import 语句能看出哪些设计模式?为什么某些模块不能相互依赖?
VictoriaMetrics 模块依赖 import 架构设计 Go 工程 目录结构 v1.146.0
学习重点提示 — 建议先通读全文,再重点回顾标注内容
重点掌握(必须)
- app/ 目录结构:多个可执行程序入口(app/vminsert/、app/vmselect/、app/vmstorage/ 等 12 个)
- lib/ 目录结构:核心库(lib/storage/、lib/mergeset/、lib/encoding/)
- 两个易错点:promql 在 app/vmselect/ 下,netstorage 在 app/vmselect/ 下(不在 lib/ 下)
- 依赖方向:高层依赖低层,应用依赖核心库
次重点(了解即可)
- vendor 目录的作用
- 第三方依赖的管理方式
- 模块内聚性原则
文章目录
一、问题的起点:理解大型 Go 项目的目录结构
思考记忆提示 — 理解模块依赖是理解架构的第一步——从 import 语句可以看出设计的精髓
- Go 的 import 语句定义了显式依赖
- 模块依赖图揭示了设计决策
- 良好的依赖结构是可维护性的基础
- 面试高频提问:如何分析一个 Go 项目的架构?
当你接手一个大型 Go 项目时,首先面对的就是目录结构。VictoriaMetrics 有数百个 Go 文件,分布在几十个目录中。如果不理解这些目录的职责划分和依赖关系,阅读源码时会像在迷宫中行走。
VM 的模块划分和依赖关系,可以直接从目录结构和 import 语句读出来。我们不看任何类比,直接看三个主入口的 import 就能彻底理解真实依赖:
源码视角一:app/vminsert/main.go 的 import
读 app/vminsert/main.go 第 3-47 行(全部 import),会发现 vminsert 依赖了:
- lib/prompb/ — Prometheus 协议定义
- lib/protoparser/ — 协议解析(多协议)
- lib/httpserver/ — HTTP 服务器
- lib/promscrape/ — Prometheus 抓取
- lib/bytesutil/ — 字节工具
特别注意:vminsert 不依赖 lib/storage!它不直接操作存储,只做协议接入和转发。
源码视角二:app/vmselect/main.go 的 import
读 app/vmselect/main.go 第 3-31 行(全部 import),会发现 vmselect 依赖了:
- app/vmselect/promql/ — PromQL 执行引擎(注意:在 app/ 下,不在 lib/!)
- app/vmselect/netstorage/ — 网络存储(注意:在 app/vmselect/ 下,不在 lib/!)
- app/vmstorage/ — 直接依赖 vmstorage 包
- lib/storage/ — 核心存储库
- lib/promscrape/ — Prometheus 抓取
特别注意:promql 和 netstorage 在 app/vmselect/ 下,不在 lib/ 下。这是 VM 的真实模块布局,与很多博文的"想当然"不同。
源码视角三:app/vmstorage/main.go 的 import
读 app/vmstorage/main.go 第 3-25 行(全部 import),会发现 vmstorage 只依赖:
- lib/storage/ — 核心存储
- lib/mergeset/ — MergeSet 存储引擎
- lib/fs/ — 文件系统
源码视角总结:2 个关键颠覆认知
- PromQL 执行引擎在 app/vmselect/promql/,不在 lib/
- netstorage 在 app/vmselect/netstorage/,不在 lib/
- app/vmstorage 和 app/vmselect 之间有跨 app 依赖
避坑提醒(源码视角):
- 不要以为所有代码都在 lib/ 下:app/vmselect/promql/ 和 app/vmselect/netstorage/ 是 app 的一部分,找源码别去 lib/ 里翻
- 不要以为 vminsert 会直接依赖 lib/storage:vminsert 只做协议接入,不直接操作存储
- 不要画"mergeset → storage"的依赖图:实测是 lib/storage/index_db.go 依赖 lib/mergeset,不是 mergeset 依赖 storage
二、顶层目录结构:app/ 和 lib/ 的职责划分
思考记忆提示 — app/ 和 lib/ 的划分是 VM 最核心的架构决策——理解这个划分就理解了整体架构
- app/ = 应用层,包含可执行程序的入口
- lib/ = 核心库,包含可复用的逻辑
- 这种划分实现了关注点分离
- 面试高频提问:为什么 VM 采用 app/lib 划分?有什么好处?
VictoriaMetrics 的顶层目录非常简洁,主要分为两大部分:
VictoriaMetrics 顶层目录结构:
victoria-metrics/
│
├── app/ # 应用层(Application)
│ ├── vminsert/ # 写入接入服务(可执行程序)
│ ├── vmselect/ # 查询执行服务(可执行程序)
│ ├── vmstorage/ # 数据存储服务(可执行程序)
│ ├── vmagent/ # 抓取代理(可执行程序)
│ ├── vmalert/ # 告警引擎(可执行程序)
│ ├── vmauth/ # 认证代理(可执行程序)
│ ├── vmbackup/ # 备份工具(可执行程序)
│ ├── vmrestore/ # 恢复工具(可执行程序)
│ ├── vmctl/ # 数据迁移工具(可执行程序)
│ ├── vmalert-tool/ # 告警工具(可执行程序)
│ ├── victoria-metrics/ # Single 模式主程序(可执行程序)
│ └── vmui/ # 前端 UI(可执行程序)
│
├── lib/ # 核心库(Library)
│ ├── storage/ # 存储引擎核心
│ ├── mergeset/ # MergeSet 存储引擎
│ ├── encoding/ # 压缩编码(NearestDelta、ZSTD)
│ ├── prompb/ # Prometheus 协议缓冲区
│ ├── protoparser/ # 协议解析器(多协议支持)
│ ├── promscrape/ # Prometheus 抓取
│ ├── fs/ # 文件系统工具(mmap、fadvise)
│ ├── memory/ # 内存管理
│ ├── bytesutil/ # 字节处理工具(零拷贝)
│ └── ...
│
├── vendor/ # 第三方依赖
├── go.mod # Go 模块定义
├── go.sum # 依赖校验
└── Makefile # 构建脚本
设计原则:
- app/ 包含可执行程序的 main 函数
- lib/ 包含可复用的核心逻辑
- app/ 依赖 lib/,但 lib/ 不能依赖 app/
- 注意:promql 不在 lib/ 下,而是在 app/vmselect/promql/
- 注意:netstorage 不在 lib/ 下,而是在 app/vmselect/netstorage/
设计精髓
app/lib 划分的核心价值在于关注点分离:
- 可测试性:lib/ 是纯逻辑,不依赖 main 函数,可以单独测试
- 可复用性:lib/ 的代码可以被多个 app 复用(如 vmagent 和 vmalert 都用 prompb)
- 可维护性:修改 app 不影响 lib,修改 lib 不影响 app
- 清晰架构:依赖方向单一,架构清晰易懂
这种划分是 Go 项目的最佳实践,也是 VictoriaMetrics 能够独立演进各个组件的关键。
三、app/ 目录:三个主程序入口
思考记忆提示 — app/ 中的三个主程序(vminsert/vmselect/vmstorage)是 Cluster 模式的核心
- vminsert = 写入接入层
- vmselect = 查询执行层
- vmstorage = 数据存储层
- 面试高频提问:Cluster 模式的三个组件是如何协作的?
3.1 vminsert:写入接入层
vminsert 是 VictoriaMetrics Cluster 模式的写入入口,负责接收来自 Prometheus、vmagent 等客户端的写入请求。
// app/vminsert/main.go(vminsert 主入口)
package vminsert
import (
"embed"
"flag"
"fmt"
"net/http"
"strings"
"time"
"github.com/VictoriaMetrics/metrics"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/common"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/csvimport"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/datadogv1"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/datadogv2"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/graphite"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/influx"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/native"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/opentelemetry"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/opentsdbhttp"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/promremotewrite"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/relabel"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape"
// ... 更多 import
)
// vminsert 的真实职责:
// 1. 接收 HTTP 请求(支持 Prometheus remote_write、InfluxDB、Datadog 等多协议)
// 2. 解析 tenant 信息(从 URL 路径中提取 accountID:projectID)
// 3. 调用对应协议的处理器写入数据
// 4. vminsert 不直接操作存储!它调用 lib/protoparser 解析后写入
// 依赖关系分析(按实测 import):
// vminsert → lib/prompb (Prometheus 协议定义)
// vminsert → lib/protoparser (多协议解析)
// vminsert → lib/httpserver (HTTP 服务器)
// vminsert → lib/promscrape (Prometheus 抓取相关)
// vminsert → app/vminsert/* (各协议的具体处理)
// 注意:vminsert 不依赖 lib/storage!不做存储,只做协议接入
3.2 vmselect:查询执行层
vmselect 是 VictoriaMetrics Cluster 模式的查询入口,负责接收来自 Grafana、API 客户端的查询请求。
// app/vmselect/main.go(vmselect 主入口)
package vmselect
import (
"embed"
"encoding/json"
"flag"
"fmt"
"net/http"
"strings"
"time"
"github.com/VictoriaMetrics/metrics"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/graphite"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/prometheus"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/promql"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/stats"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/vmalertproxy"
)
// 关键发现:promql 和 netstorage 不在 lib/ 下!
// 它们在 app/vmselect/promql/ 和 app/vmselect/netstorage/
// 依赖关系分析(按实测 import):
// vmselect → app/vmselect/promql (PromQL 执行引擎,在 app/ 下!)
// vmselect → app/vmselect/netstorage (网络存储,在 app/ 下!)
// vmselect → app/vmstorage (直接依赖 vmstorage)
// vmselect → lib/storage (核心存储库)
// vmselect → lib/promscrape (Prometheus 抓取)
3.3 vmstorage:数据存储层
vmstorage 是 VictoriaMetrics Cluster 模式的数据存储节点,负责实际的数据读写。
// app/vmstorage/main.go(vmstorage 主入口)
package vmstorage
import (
"flag"
"fmt"
"io"
"math"
"net/http"
"strconv"
"strings"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/mergeset"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/vminsertapi"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/vmselectapi"
"github.com/VictoriaMetrics/metrics"
)
// vmstorage 的真实职责:
// 1. 管理本地数据存储(Partition、Part、indexDB)
// 2. 实现写入 API(vminsertapi)和查询 API(vmselectapi)
// 3. 维护数据索引(倒排索引 IndexDB)
// 4. 执行后台合并任务(mergeset)
// 依赖关系分析(按实测 import):
// vmstorage → lib/storage (存储引擎核心)
// vmstorage → lib/mergeset (MergeSet 存储)
// vmstorage → lib/vminsertapi (写入 API 定义)
// vmstorage → lib/vmselectapi (查询 API 定义)
// vmstorage → lib/fs (文件系统)
// vmstorage 不依赖其他 app 模块
// 注意:app/vmstorage 和 app/vmselect 之间有跨 app 依赖
// vmselect 依赖 app/vmstorage 包
四、lib/ 目录:核心库的层次结构
思考记忆提示 — lib/ 中的核心库是 VM 最重要的部分——理解它们的层次关系是关键
- lib/encoding = 最底层,压缩编码
- lib/storage = 存储引擎核心,依赖 encoding
- 易错点:promql 在 app/vmselect/promql/,不在 lib/
4.1 lib/ 的模块层次
lib/ 目录的模块层次(从底层到顶层):
┌─────────────────────────────────────────────────────────────────────────┐
│ lib/ 模块层次图 │
│ │
│ 注意:以下模块不在 lib/ 下!它们在 app/ 下: │
│ - app/vmselect/promql/ (PromQL 执行引擎) │
│ - app/vmselect/netstorage/ (网络存储) │
│ - app/vmstorage/ (vmstorage 包本身) │
│ │
│ 第四层:应用层协议(lib/ 外) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ app/vmselect/promql/ - PromQL 执行引擎(在 app/ 下!) │ │
│ │ lib/prompb/ - Prometheus 协议缓冲区 │ │
│ │ lib/protoparser/ - 协议解析器(多协议支持) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 第三层:协议处理 │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ lib/protoparser/ - 协议解析器 │ │
│ │ lib/promscrape/ - Prometheus 抓取 │ │
│ │ lib/prompb/ - Prometheus 协议定义 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 第二层:存储实现 │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ lib/storage/ - 存储引擎核心 │ │
│ │ lib/mergeset/ - MergeSet 存储引擎 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 第一层:基础工具 │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ lib/encoding/ - 压缩编码(NearestDelta、ZSTD) │ │
│ │ lib/bytesutil/ - 字节处理工具(零拷贝) │ │
│ │ lib/fs/ - 文件系统工具(mmap、fadvise) │ │
│ │ lib/memory/ - 内存管理 │ │
│ │ lib/workingsetcache/ - 工作集缓存 │ │
│ │ lib/logger/ - 日志 │ │
│ │ lib/fasttime/ - 高性能时间戳 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ 依赖原则:上层依赖下层,下层不能依赖上层 │
└─────────────────────────────────────────────────────────────────────────┘
4.2 核心模块详解
lib/storage 是最核心的模块,定义了存储引擎的核心接口和数据结构。
// lib/storage/storage.go(存储引擎核心)
package storage
import (
"bytes"
"fmt"
"io"
"math"
"os"
"path/filepath"
"sort"
"sync"
"sync/atomic"
"time"
"unsafe"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/backup/backupnames"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bloomfilter"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/decimal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/encoding"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/memory"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/snapshot/snapshotutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage/metricnamestats"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage/metricsmetadata"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timeutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/uint64set"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/workingsetcache"
"github.com/VictoriaMetrics/fastcache"
"github.com/cespare/xxhash/v2"
)
// lib/storage 依赖的模块(按实测 import 分析):
// lib/storage → lib/encoding (压缩编码)
// lib/storage → lib/fs (文件系统)
// lib/storage → lib/memory (内存管理)
// lib/storage → lib/workingsetcache (缓存)
// lib/storage → lib/decimal (高精度小数)
// lib/storage → lib/fasttime (高性能时间戳)
// lib/storage → lib/bloomfilter (布隆过滤器)
// lib/storage → lib/uint64set (uint64 集合)
// lib/storage 不依赖 lib/mergeset!(这是真实测试结果)
// Storage 结构体真实字段(storage.go 约 2700 行,仅展示部分):
type Storage struct {
// 互斥锁保护以下字段
mu sync.Mutex
// 分区映射
partitions map[uint64]*partition
// 指标名称统计
metricNameStats *metricnamestats.MetricNameStats
// 索引数据库
indexDB *IndexDB
// 元数据
metricsMetadata *metricsmetadata.Storage
// 快照相关
snapshotLock sync.Mutex
snapshots map[string]*Snapshot
// 配置
retentionMonths uint64
// ...
}
小贴士 — 模块依赖分析工具
可以使用 Go 的工具分析模块依赖:
- go mod graph:查看模块依赖图
- go list -m all:列出所有依赖模块
- go mod why <module>:解释为什么依赖某个模块
五、模块依赖图:从 import 分析组件关系
思考记忆提示 — 通过 import 语句可以还原完整的模块依赖图
- 每条 import 语句表示一个显式依赖
- 依赖关系必须是单向无环的
- 面试高频提问:如何检测 Go 项目中的循环依赖?
5.1 完整依赖图
以下是从 VictoriaMetrics 源码中提取的模块依赖关系:
VictoriaMetrics 真实模块依赖图:
┌─────────────────────────────────────────────────────────────────────────┐
│ VictoriaMetrics 模块依赖图(实测版) │
│ │
│ 图例:A → B 表示 A 依赖 B │
│ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ app/ 应用层 │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ vminsert │ │ vmselect │ │ vmstorage │ │ │
│ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │
│ │ │ │ │ │ │
│ │ │ ┌───────┴───────┐ │ │ │
│ │ │ │ │ │ │ │
│ │ │ ▼ ▼ │ │ │
│ │ │ ┌────────────────┐ ┌──────────────┐ │ │ │
│ │ │ │app/vmselect/ │ │app/vmstorage │ │ │ │
│ │ │ │netstorage/ │ └──────┬───────┘ │ │ │
│ │ │ └──────┬────────┘ │ │ │ │
│ │ │ │ │ │ │ │
│ └────────────┼──────────┼───────────────────┼──────────┼─────────────────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ │ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │ │
│ │ lib/ 核心库层 │ │ │
│ │ ┌─────────────────────────────────────────────────────────────────┐│ │ │
│ │ │ 存储层 ││ │ │
│ │ │ ┌──────────────┐ ┌──────────────┐ ││ │ │
│ │ │ │ storage │─────────────▶│ index_db │ ││ │ │
│ │ │ └──────────────┘ └──────────────┘ ││ │ │
│ │ │ ▲ │ ││ │ │
│ │ │ │ │ (index_db → mergeset) ││ │ │
│ │ │ │ ┌──────┴──────┐ ││ │ │
│ │ │ │ │ mergeset │ ││ │ │
│ │ │ │ └──────────────┘ ││ │ │
│ │ └──────────┼─────────────────────────────────────────────────┘│ │ │
│ │ │ │ │ │
│ │ ▼ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────────┐│ │ │
│ │ │ 基础工具层 ││ │ │
│ │ │ encoding │ bytesutil │ fs │ memory │ workingsetcache │ logger ││ │ │
│ │ └─────────────────────────────────────────────────────────────────┘│ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │ │
│ │
│ 关键发现: │
│ 1. promql 和 netstorage 在 app/vmselect/ 下,不在 lib/ │
│ 2. lib/storage 不依赖 lib/mergeset(storage 是 mergeset 的调用方) │
│ 3. lib/storage/index_db.go 依赖 lib/mergeset(indexDB 使用 mergeset) │
│ 4. lib/encoding 是最底层,只依赖 bytesutil/decimal/fastnum/logger │
└─────────────────────────────────────────────────────────────────────────┘
5.2 依赖关系详解
从 import 语句可以分析出以下关键依赖关系:
| 模块 | 依赖 | 说明 |
|---|---|---|
| app/vminsert | lib/prompb, lib/protoparser, lib/httpserver, lib/promscrape, app/vminsert/* | 写入入口,做协议接入。注意:不依赖 lib/storage! |
| app/vmselect | app/vmselect/promql, app/vmselect/netstorage, app/vmstorage, lib/storage, lib/promscrape | 查询入口。promql 和 netstorage 在 app/ 下! |
| app/vmstorage | lib/storage, lib/mergeset, lib/vminsertapi, lib/vmselectapi, lib/fs | 存储节点,核心依赖。 |
| lib/storage | lib/encoding, lib/fs, lib/memory, lib/workingsetcache, lib/fasttime, lib/bloomfilter, lib/decimal | 存储核心,不依赖 lib/mergeset。 |
| lib/mergeset | lib/atomicutil, lib/cgroup, lib/fs, lib/memory, lib/syncwg | 存储实现,不依赖 lib/storage。 |
| lib/storage/index_db.go | lib/mergeset | indexDB 依赖 mergeset(用于索引存储)。 |
| lib/encoding | lib/bytesutil, lib/decimal, lib/fastnum, lib/logger | 最底层之一,依赖 4 个工具包。 |
注意
依赖关系必须是有向无环图(DAG)。如果出现循环依赖(如 A → B → C → A),Go 编译器会报错。可以使用以下工具检测循环依赖:
- go mod graph:查看模块依赖图
- go mod why -m <module>:解释为什么依赖某个模块
六、依赖原则:为什么这样组织
思考记忆提示 — 理解依赖原则才能理解架构决策——这些原则不是凭空制定的
- 依赖倒置原则:高层定义接口,低层实现接口
- 稳定依赖原则:不稳定的模块不应该被稳定的模块依赖
- 循环依赖检测:Go 编译器会自动检测循环依赖
- 面试高频提问:如何设计一个好的模块依赖结构?
6.1 依赖原则详解
VictoriaMetrics 的模块组织遵循以下原则:
┌─────────────────────────────────────────────────────────────────────────┐
│ 模块依赖设计原则 │
│ │
│ 1. 依赖方向:上层依赖下层 │
│ ┌────────┐ │
│ │ app/ │ ← 应用层(依赖 lib/) │
│ └────┬───┘ │
│ │ │
│ ▼ │
│ ┌────────┐ │
│ │ lib/ │ ← 核心库(被 app/ 依赖) │
│ └────────┘ │
│ │
│ 2. 禁止循环依赖 │
│ ┌────────────────────────────────────────┐ │
│ │ A → B → C → A ❌ 编译错误 │ │
│ │ A → B → C → B ❌ 编译错误 │ │
│ └────────────────────────────────────────┘ │
│ │
│ 3. 就近依赖原则 │
│ ┌────────────────────────────────────────┐ │
│ │ A → B → C → D │ │
│ │ ✓ A 直接依赖 D(如果需要 D 的功能) │ │
│ │ ✗ A 绕过 B, C 直接依赖 D(可能破坏封装) │ │
│ └────────────────────────────────────────┘ │
│ │
│ 4. 接口隔离原则 │
│ ┌────────────────────────────────────────┐ │
│ │ 存储 API vs 存储实现 │ │
│ │ lib/vminsertapi 定义写入 API │ │
│ │ lib/vmselectapi 定义查询 API │ │
│ │ app/vmstorage 实现 API │ │
│ │ 这样可以替换存储实现而不影响上层 │ │
│ └────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
设计精髓
良好的模块依赖结构带来以下好处:
- 可测试性:可以用 mock 替换实现,单独测试每一层
- 可维护性:修改某一层不会影响其他层
- 可复用性:底层模块可以被多个上层模块复用
- 可理解性:依赖图清晰,架构一目了然
- 可演进性:可以独立演进各层
VM 的架构是 Go 项目中"干净架构"的典范,很值得学习和借鉴。
6.2 实际依赖示例
让我们通过实际的 import 语句来验证依赖关系(每个都基于 Read 过的真实源码):
// app/vminsert/main.go 的真实 import(第 3-47 行)
import (
"embed"
"flag"
"fmt"
"net/http"
"strings"
"time"
"github.com/VictoriaMetrics/metrics"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/common"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/csvimport"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/datadogv1"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/datadogv2"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/graphite"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/influx"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/native"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/opentelemetry"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/opentsdbhttp"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/promremotewrite"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/relabel"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape"
// ...
)
// 依赖方向:
// vminsert → lib/prompb (Prometheus 协议定义)
// vminsert → lib/protoparser (多协议解析)
// vminsert → lib/httpserver (HTTP 服务器)
// vminsert → app/vminsert/* (各协议的处理器子包)
// vminsert → lib/promscrape (Prometheus 抓取)
// 注意:vminsert 不依赖 lib/storage!不直接操作存储
七、FAQ:常见疑问
思考记忆提示 — FAQ 是全篇的"临考前速背"模块,20 组覆盖全链路
- Q1-Q5 围绕目录结构:app/ 和 lib/ 的职责划分
- Q6-Q10 围绕依赖关系:为什么这样组织
- Q11-Q15 围绕工具使用:如何分析依赖
- Q16-Q20 围绕设计原则:架构设计决策
Q1. app/ 和 lib/ 的本质区别是什么?
app/ 包含 main 函数和可执行程序的入口,lib/ 包含可复用的逻辑库。app/ 中的每个子目录是一个可执行程序(如 vminsert、vmselect、vmstorage),每个都有自己的 main.go。lib/ 中的每个子目录是一个 Go 包(如 storage、mergeset、encoding),提供特定功能。
Q2. 为什么 lib/ 不能依赖 app/?
为了避免循环依赖和保持关注点分离。lib/ 是底层库,应该独立于应用。如果 lib/ 依赖 app/,那么修改 app/ 就可能影响 lib/,破坏了模块的独立性。
Q3. lib/ 内部可以相互依赖吗?
可以,但必须是单向无环的。lib/ 内部的依赖规则是:下层可以依赖更下层,但上层不能依赖上层。例如,lib/storage/index_db.go 依赖 lib/mergeset(用于索引存储),lib/mergeset 不依赖 lib/storage。encoding 是最底层,只依赖 bytesutil 等工具。
Q4. 什么是依赖倒置原则?
高层定义接口,低层实现接口,依赖方向从低层指向高层。在 VM 中,app/vmstorage 实现了 lib/vminsertapi 和 lib/vmselectapi 定义的接口,app/vmselect 通过这些 API 与 vmstorage 通信,不需要知道具体的实现细节。
Q5. Go 如何检测循环依赖?
Go 编译器会在构建时自动检测循环依赖,如果发现会报错。错误消息类似于 "import cycle not allowed"。可以通过 go mod why -m <module> 来追踪依赖路径。
Q6. 如何查看一个模块的依赖关系?
使用 go mod graph 查看完整依赖图,或使用 IDE 的依赖分析功能。IDE(如 GoLand、VSCode)通常有"Show Dependencies"功能,可以可视化模块依赖图。
Q7. vendor/ 目录的作用是什么?
vendor/ 存储所有第三方依赖的副本,用于离线构建和版本锁定。Go 1.6+ 会优先使用 vendor/ 中的依赖,而不是从网络下载。这确保了构建的可重复性。
Q8. 为什么有些模块在 app/ 中,有些在 lib/ 中?
有 main 函数的就是 app/,没有 main 函数的就是 lib/。这是最简单也最有效的划分原则。vmagent、vmalert、vmauth 等是独立工具,自然放在 app/ 中。
Q9. 模块依赖图对于阅读源码有什么帮助?
帮助定位问题和理解数据流。当你遇到一个问题时,可以沿着依赖方向向上追踪,找到问题的根源。当你需要修改某个模块时,可以沿着依赖方向向下检查,影响的范围。
Q10. VictoriaMetrics 的目录结构有什么值得学习的地方?
app/lib 划分清晰、依赖方向单一、模块职责明确。这种结构是 Go 项目的最佳实践,适合大型项目的组织和维护。
Q11. 什么是包的内部可见性?
Go 中小写字母开头的标识符是包内私有的,只能在同一个包内访问。这实现了封装性,外部只能通过导出的(首字母大写)标识符访问包的内部实现。
Q12. 如何避免循环依赖?
提取公共接口到独立包,让依赖双方都依赖这个接口包。例如,A 和 B 相互依赖,可以创建一个 ABlib 包,让 A 和 B 都依赖 ABlib。
Q13. 什么是 Go modules?
Go modules 是 Go 1.11+ 引入的依赖管理机制,通过 go.mod 和 go.sum 文件管理依赖版本。go.mod 声明模块名和依赖,go.sum 记录依赖的校验和。
Q14. 什么是 go.mod 的 module 路径?
module 路径是模块的唯一标识符,用于定位模块和解析 import 路径。VictoriaMetrics 的 module 路径是 github.com/VictoriaMetrics/VictoriaMetrics。
Q15. 如何确定一个模块的版本?
通过 go.mod 中的版本号,如 v1.146.0。Go modules 使用语义化版本(semver),版本号格式为 vMAJOR.MINOR.PATCH。
Q16. 什么是语义化版本?
语义化版本(semver)是一种版本命名规范,格式为 MAJOR.MINOR.PATCH。MAJOR 是不兼容的修改,MINOR 是向后兼容的功能增加,PATCH 是向后兼容的问题修复。
Q17. 什么是 module proxy?
module proxy 是一个 HTTP 服务器,代理 Go 模块的下载和缓存。可以使用私有 proxy 来加速下载和提高构建的可重复性。
Q18. 什么是 replace directive?
replace 指令用于在本地开发时替换依赖的模块路径。例如,replace github.com/foo/bar => ../bar 可以让 go build 使用本地的 bar 模块而不是远程的。
Q19. 什么是 go.sum?
go.sum 记录了每个依赖包的加密校验和,用于验证下载的包是否被篡改。每次添加新依赖时,go mod tidy 会自动更新 go.sum。
Q20. 如何清理不需要的依赖?
使用 go mod tidy 命令,它会自动添加缺失的依赖和删除未使用的依赖。
全篇必记总纲
VictoriaMetrics 的模块依赖结构遵循app/lib 划分 + 单向无环依赖原则:app/ 包含可执行程序的入口,lib/ 包含可复用的核心库;高层(app)依赖低层(lib),低层不能依赖高层。但需要注意两个易错点:
- PromQL 执行引擎在 app/vmselect/promql/,不在 lib/
- netstorage 在 app/vmselect/netstorage/,不在 lib/
- lib/storage/index_db.go 依赖 lib/mergeset(indexDB 使用 mergeset 做索引存储),lib/storage 主体不依赖 mergeset
八、Roadmap:后续预告
本篇覆盖了 VictoriaMetrics 的模块依赖图,但还有很多细节尚未展开:
- #09 性能模型:写入吞吐/查询延迟/内存占用的数学模型——理解 VM 的性能上限
- #10 与其他 TSDB 对比:Prometheus/InfluxDB/Thanos/VM——理解 VM 在竞品中的定位
- #02 全局架构:Single-Node vs Cluster 模式——理解两种部署方式
- #12 源码阅读路线图:如何高效阅读 VM 源码——最佳实践
- #161 完整写入链路:一个数据点从 HTTP 到 Part 文件——源码追踪
本文参考与源码链接:
• app/ · 应用层入口
• lib/ · 核心库
• go.mod · 模块定义
• Go Modules 官方文档

浙公网安备 33010602011771号