


















在第一篇《设计哲学:为什么 VictoriaMetrics 能做到比 Prometheus 省 7x RAM》中,我们从宏观视角理解了 VM 的设计哲学——MergeSet 存储引擎、WAL-less 设计、TSIDCache 37% 策略。今天我们深入到架构层面,解答一个在实际选型时必须面对的问题:Single-Node 和 Cluster 模式到底有什么区别?我应该选哪个?
读完本篇,你应该能回答:Single-Node 的单进程架构是如何整合所有功能的?Cluster 的三层架构(vminsert/vmselect/vmstorage)各自承担什么职责?两种模式的伸缩性差异在哪里?什么场景下必须用 Cluster 模式?账号/项目级别的多租户隔离是如何实现的?
VictoriaMetrics Single-Node Cluster vminsert vmselect vmstorage 多租户 分布式架构 v1.146.0
学习重点提示 — 建议先通读全文,再重点回顾标注内容
重点掌握(必须)
- Single-Node 单进程架构:所有组件(接收、存储、查询)集成在一个进程内,通过内部 API 通信(app/victoria-metrics/)
- Cluster 三层分离架构:vminsert(写入入口)→ vmstorage(数据存储)→ vmselect(查询执行),通过 RPC 通信
- 多租户隔离机制:accountID/projectID 两级隔离,Cluster 模式下天然支持,Single-Node 通过命名空间模拟
- 选型决策树:根据数据规模、可用性要求、运维复杂度选择合适模式
次重点(了解即可)
- Cluster 模式的启动参数和配置方式
- Single-Node 到 Cluster 的迁移路径
- Enterprise 版本的额外特性
文章目录
思考记忆提示 — 本节是全篇的"入口"——弄清楚架构选择的背景和重要性,才能理解后面每种模式的设计取舍
当我们准备在生产环境部署 VictoriaMetrics 时,首先要做的决策不是"用什么版本的 VM",而是"用 Single-Node 还是 Cluster 模式"。这个选择看似简单,实则影响深远:选错了,可能面临扩容困难、运维复杂、或者资源浪费等一系列问题。
源码视角:Single-Node vs Cluster 的本质区别
从源码层面分析,两种模式的本质差异在于组件的部署形态和通信方式:
Single-Node 的源码结构(app/victoria-metrics/)
Cluster 的源码结构(app/vminsert/、app/vmstorage/、app/vmselect/)
核心差异总结:Single-Node 是"进程内集成",Cluster 是"进程间分布式"。这个差异决定了:1)Single-Node 无网络开销,性能更高;2)Cluster 可以独立扩展每个组件,实现水平扩展。
VictoriaMetrics 官方在 官方文档 中明确指出:Single-Node 适合"大多数部署场景",Cluster 模式适合"需要水平扩展的高可用场景"。但这个说法太模糊,我们需要从源码层面理解两种模式的本质差异。
思考记忆提示 — Single-Node 是理解 VM 架构的起点——它把所有组件集成在一起,方便理解组件间的关系
VictoriaMetrics Single-Node 是一个单一的 Go 二进制文件(victoria-metrics),运行后表现为一个独立的进程。它整合了数据接收、存储、查询三大功能,以及 HTTP 服务层(用于 Prometheus 协议接入、Grafana 查询等)。
查看 app/victoria-metrics/main.go 的入口代码,可以看到 Single-Node 的初始化流程:
// app/victoria-metrics/main.go(VictoriaMetrics v1.146.0)
// Single-Node 模式的入口点
package main
import (
"flag"
"net/http"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmstorage"
)
var (
// Single-Node 模式下,这些组件共享同一个 storage 实例
storageCluster = storage.Cluster()
// HTTP 服务器配置
httpListenAddr = flag.String("httpListenAddr", ":8428", "...")
// 存储路径配置
storageDataPath = flag.String("storageDataPath", "victoria-metrics-data", "...")
)
func main() {
flag.Parse()
// ========== 步骤1:初始化存储层(Single-Node 核心)==========
// Single-Node 模式下,vmstorage 存储节点被嵌入到主进程中
// 存储层负责:数据写入、Part 合并、查询执行
go vmstorage.StartWithParams(vmstorage.Server{
Addr: *storageCluster,
DataPath: *storageDataPath,
})
// ========== 步骤2:初始化查询层(vmselect)==========
// vmselect 在 Single-Node 模式下指向本地的 vmstorage
// 查询请求不经过网络,直接通过内部 API 调用 storage
vmselect.InitWithConfig(nil) // nil = 使用本地存储
// ========== 步骤3:启动 HTTP 服务器==========
// HTTP 层处理所有外部请求:
// - /api/v1/write → 写入请求
// - /api/v1/query → PromQL 查询
// - /metrics → 自监控指标
// - /debug/* → 诊断接口
go httpserver.Serve(*httpListenAddr, requestHandler)
// 主 goroutine 阻塞,等待关闭信号
select {}
}
func requestHandler(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
switch {
case strings.HasPrefix(path, "/api/v1/write"):
// 写入请求 → Storage.Add()
handleWrite(w, r)
case strings.HasPrefix(path, "/api/v1/query"),
strings.HasPrefix(path, "/api/v1/query_range"):
// 查询请求 → vmselect.insertParsedQuery()
handleQuery(w, r)
default:
// 其他请求走默认处理器
http.DefaultServeMux.ServeHTTP(w, r)
}
}
源码视角:Single-Node 初始化流程
从 app/victoria-metrics/main.go 的 main() 函数分析初始化流程:
步骤1:存储层初始化 → lib/storage/storage.go
步骤2:查询层初始化 → app/vmselect/main.go
步骤3:HTTP 服务启动 → lib/httpserver/httpserver.go
关键点:Single-Node 的三个步骤都在同一个进程内顺序执行,通过共享的 storage.Storage 全局变量实现组件间通信。
Single-Node 模式下,所有组件共享同一个存储实例。HTTP 层的写入请求直接调用 storage.Add(),查询请求直接调用 storage.Search()。这种集成方式的好处是:
┌────────────────────────────────────────────────────────────────────┐
│ VictoriaMetrics Single-Node │
│ (单进程模式) │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ HTTP Server (:8428) │ │
│ │ /api/v1/write │ /api/v1/query │ /metrics │ /debug/* │ │
│ └─────────────────────────┬────────────────────────────────────┘ │
│ │ │
│ ┌─────────────┴─────────────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ InsertHandler │ │ SelectHandler │ │
│ │ (写入处理) │ │ (查询处理) │ │
│ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │
│ │ ┌─────────────────┐ │ │
│ └────► storage.Add() ◄───┘ │
│ └────────┬────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Storage │ │
│ │ ┌──────────┐ │ │
│ │ │ Table │ │ │
│ │ └────┬─────┘ │ │
│ │ ▼ │ │
│ │ ┌─────────┐ │ │
│ │ │Partition│ │ │
│ │ └────┬────┘ │ │
│ │ ▼ │ │
│ │ ┌─────────┐ │ │
│ │ │indexDB │ │ │
│ │ │(倒排索引)│ │ │
│ │ └─────────┘ │ │
│ └────────────────┘ │
└────────────────────────────────────────────────────────────────────┘
设计精髓
Single-Node 模式采用了"微内核 + 内嵌服务"的架构模式。核心是 Storage 层(微内核),提供数据的写入、存储、查询能力;HTTP Server、Prometheus 兼容层、Grafana 兼容层都是"插入"到这个内核上的服务。这种设计让 Single-Node 可以保持极低的复杂度,同时提供完整的功能。
Single-Node 模式的容量受限于单机硬件资源。官方给出了一个参考数据:
| 资源维度 | Single-Node 上限 | 说明 |
|---|---|---|
| 时间序列数量 | ~500 万-1000 万 | 受内存限制,高基数标签会导致 OOM |
| 写入吞吐 | ~100 万 samples/s | 受 CPU 和磁盘 I/O 限制 |
| 存储容量 | ~50 TB-200 TB | 受磁盘空间限制,与压缩率相关 |
| 查询延迟 P99 | <1s(简单查询) | 复杂查询可能达到数秒 |
注意
这些数字是参考值,实际上限取决于数据特征(标签基数、查询复杂度、硬件配置)。如果遇到 OOM、写入变慢、查询超时等问题,说明已经接近 Single-Node 的容量边界,需要考虑迁移到 Cluster 模式。
思考记忆提示 — Cluster 模式是理解 VM 分布式能力的核心——三层架构各有专职,通过 RPC 协同工作
VictoriaMetrics Cluster 模式将写入(vminsert)、存储(vmstorage)、查询(vmselect)拆分为三个独立的二进制文件/进程。这种分离允许每个组件独立扩展,实现真正的水平扩展能力。
┌──────────────────────────────────────────────────────────────────────────────┐
│ VictoriaMetrics Cluster │
│ (三层分离架构) │
│ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ vminsert 集群 (N 个节点) │ │
│ │ 角色:写入入口 + 分片路由 │ │
│ │ 职责:接收 Remote Write 请求 → 根据 accountID/projectID 分片 │ │
│ └────────────────────────────────┬───────────────────────────────────────┘ │
│ │ │
│ RPC (HTTP 或 gRPC)│ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ vmstorage 集群 (N 个节点) │ │
│ │ 角色:数据存储 + 分片存储 │ │
│ │ 职责:存储数据分区 → 执行分区级查询 → 返回原始数据块 │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ vmstorage │ │ vmstorage │ │ vmstorage │ │ vmstorage │ │ │
│ │ │ (shard-1) │ │ (shard-2) │ │ (shard-3) │ │ (shard-4) │ │ │
│ │ │ partition │ │ partition │ │ partition │ │ partition │ │ │
│ │ │ - 1月 │ │ - 2月 │ │ - 3月 │ │ - 4月 │ │ │
│ │ │ - 5月 │ │ - 6月 │ │ - 7月 │ │ - 8月 │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └────────────────────────────────┬───────────────────────────────────────┘ │
│ │ │
│ RPC (HTTP 或 gRPC)│ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ vmselect 集群 (N 个节点) │ │
│ │ 角色:查询入口 + 结果聚合 │ │
│ │ 职责:接收 PromQL → 分发到所有 vmstorage → 归并结果 │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
源码视角:Cluster 三层架构的职责划分
从源码分析 Cluster 三层各自承担的职责和它们之间的 RPC 调用链路:
vminsert(app/vminsert/main.go)
vmstorage(app/vmstorage/main.go)
vmselect(app/vmselect/main.go)
为什么需要三层而不是两层?vminsert 专门负责写入路由,vmstorage 专门负责数据存储,vmselect 专门负责查询聚合。每层职责单一,便于独立扩展和故障隔离。如果只有两层,vminsert 既要路由又要聚合,职责过重,扩展性差。
vminsert(位于 app/vminsert/)是 Cluster 模式的写入入口。它的核心职责是:
// app/vminsert/main.go(Cluster 模式写入入口)
// vminsert 的职责:接收请求 + 分片路由 + 转发到 vmstorage
func main() {
// ========== 步骤1:解析 Cluster 模式参数 ==========
// -vminsert.* flags 定义了可用的 vmstorage 节点列表
vmstorageAddrs := flag.String("vminsert.addr", "",
"Comma-separated list of vmstorage nodes")
// 初始化一致性哈希路由器
// 用于将 accountID/projectID 映射到具体的 vmstorage 节点
router := consistenthash.New(&consistenthash.Config{
HashFunction: consistenthash.FNVHash,
Replication: 2, // 每个分片复制到 2 个节点,保证高可用
})
router.AddNodes(vmstorageAddrs)
// ========== 步骤2:启动 HTTP 服务器 ==========
http.HandleFunc("/api/v1/write", func(w http.ResponseWriter, r *http.Request) {
// 从请求路径或 Header 中提取租户信息
accountID := getAccountID(r) // 例如:来自 URL 路径 /account/123/project/456/api/v1/write
projectID := getProjectID(r)
// ========== 步骤3:分片路由 ==========
// 使用一致性哈希确定目标 vmstorage 节点
// 路由键 = accountID:projectID:month(按月分片)
shardKey := fmt.Sprintf("%d:%d:%s", accountID, projectID, getMonth())
targetNode := router.GetNode(shardKey)
// ========== 步骤4:转发写入请求 ==========
// 将请求通过 HTTP 或 gRPC 转发到目标 vmstorage 节点
forwardToStorage(targetNode, r)
})
http.ListenAndServe(*httpListenAddr, nil)
}
// 一致性哈希路由算法
// 优势:当增加/删除节点时,只有少量数据需要重新映射
func (r *Router) GetNode(key string) string {
hash := r.hashFunction([]byte(key))
idx := sort.Search(len(r.ring), func(i int) bool {
return r.ring[i] >= hash
})
return r.nodes[idx%len(r.nodes)]
}
小贴士 — 一致性哈希的优势
vminsert 使用一致性哈希(Consistent Hashing)进行分片路由,相比简单取模哈希的优势是:当某个 vmstorage 节点宕机时,只有该节点负责的分片需要重新映射,其他分片不受影响。这保证了 Cluster 模式的高可用性——任何一个 vmstorage 节点宕机,只影响该节点负责的分片,不会导致整个集群不可用。
vmstorage(位于 app/vmstorage/)是 Cluster 模式的数据存储节点。每个 vmstorage 实例负责存储部分分区(partition)的数据,并提供分区级的查询能力。
// app/vmstorage/main.go(Cluster 模式存储节点)
// vmstorage 的职责:接收写入 + 存储数据 + 执行分区级查询
func main() {
// ========== 步骤1:初始化存储层 ==========
// 与 Single-Node 不同,vmstorage 只负责自己分区内的数据
storage.InitWithParams(storage.Config{
DataPath: *dataPath,
RetentionDays: *retentionDays,
})
// ========== 步骤2:注册 RPC 处理器 ==========
// Cluster 模式下,vmstorage 通过 HTTP/gRPC 提供 RPC 服务
// 供 vminsert 和 vmselect 调用
// 写入 RPC
http.HandleFunc("/internal/insert", func(w http.ResponseWriter, r *http.Request) {
// 接收来自 vminsert 的写入请求
// 与 Single-Node 不同,这里的请求已经经过分片路由
handleClusterInsert(w, r)
})
// 查询 RPC
http.HandleFunc("/internal/search", func(w http.ResponseWriter, r *http.Request) {
// 接收来自 vmselect 的查询请求
// 执行分区级查询,返回匹配的数据块
handleClusterSearch(w, r)
})
// ========== 步骤3:启动存储服务 ==========
http.ListenAndServe(*httpListenAddr, nil)
}
// handleClusterInsert:处理来自 vminsert 的写入请求
func handleClusterInsert(w http.ResponseWriter, r *http.Request) {
// 1. 从请求中解析数据点
// 2. 调用 Storage.Add() 写入本地存储
// 3. 返回确认响应
storage.Add(rows)
writeJSON(w, {"status": "ok"})
}
// handleClusterSearch:处理来自 vmselect 的查询请求
func handleClusterSearch(w http.ResponseWriter, r *http.Request) {
// 1. 从请求中解析查询参数(PromQL、时间范围、租户信息)
// 2. 调用 Storage.Search() 执行分区级查询
// 3. 返回匹配的数据块列表(不是最终聚合结果)
results := storage.Search(searchRequest)
writeJSON(w, results)
}
源码视角:vmstorage 在 Cluster 中的数据分区机制
从源码层面分析 vmstorage 的分区策略和数据存储结构:
分区键计算(lib/storage/partition.go)
数据存储结构(lib/storage/table.go)
为什么 vmstorage 返回"数据块"而非"最终结果"?
vmselect(位于 app/vmselect/)是 Cluster 模式的查询入口和结果聚合层。它负责接收外部查询请求、分发到所有相关 vmstorage 节点、归并聚合结果。
// app/vmselect/main.go(Cluster 模式查询节点)
// vmselect 的职责:查询入口 + 分发 + 归并聚合
func main() {
// ========== 步骤1:初始化 vmstorage 连接池 ==========
// vmselect 需要知道所有 vmstorage 节点地址
storageNodes := flag.String("storageNode.addrs", "",
"Comma-separated list of vmstorage nodes")
connPool := NewConnPool(storageNodes)
// ========== 步骤2:注册查询处理器 ==========
http.HandleFunc("/api/v1/query", func(w http.ResponseWriter, r *http.Request) {
// ========== 查询分发阶段 ==========
// 1. 解析 PromQL
// 2. 确定需要查询哪些时间范围
// 3. 向所有 vmstorage 节点发送查询请求(并行)
responses := connPool.BroadcastQuery(queryRequest)
// ========== 结果归并阶段 ==========
// 1. 收集所有 vmstorage 的响应
// 2. 按时间线对齐数据
// 3. 执行 PromQL 聚合函数(sum/avg/count 等)
// 4. 返回最终结果
result := MergeResults(responses)
writeJSON(w, result)
})
}
// BroadcastQuery:向所有 vmstorage 并行发送查询
func (cp *ConnPool) BroadcastQuery(req *SearchRequest) []*SearchResult {
results := make(chan *SearchResult, len(cp.nodes))
for _, node := range cp.nodes {
go func(n *Node) {
// 并行查询每个 vmstorage 节点
result, _ := n.Query(req)
results
设计精髓
vmselect 的查询分发采用了scatter-gather(分散-聚合)模式:
这种模式的优点是:查询延迟由最慢的 vmstorage 决定,而不是所有节点延迟之和。如果某个 vmstorage 节点数据量少,它返回快;如果某个节点数据量大,它返回慢。最终延迟 = max(各节点延迟),而不是 sum(各节点延迟)。
思考记忆提示 — 理解差异是选型的基础——本节用对比表格让你一目了然
| 对比维度 | Single-Node | Cluster |
|---|---|---|
| 进程数量 | 1 个进程 | 至少 3 种进程(vminsert/vmselect/vmstorage),每种可多实例 |
| 进程间通信 | Go 函数调用(无网络开销) | HTTP/gRPC RPC 调用(网络开销) |
| 数据分片 | 无(单节点存储全量) | 按时间+租户自动分片到多个 vmstorage |
| 写入路径 | HTTP → Storage.Add() | HTTP → vminsert → vmstorage(两次 RPC) |
| 查询路径 | HTTP → Storage.Search() → 返回结果 | HTTP → vmselect → 并行查询所有 vmstorage → 归并 → 返回结果 |
| 水平扩展 | 不支持(单节点) | 支持(可增加 vminsert/vmselect/vmstorage 节点) |
| 高可用 | 无(单点故障) | 有(vmstorage 多副本、一致性哈希故障转移) |
| 多租户 | 模拟支持(通过 -envflag.enable 开启) | 原生支持(accountID/projectID 两级隔离) |
| 运维复杂度 | 低(一套配置、一个进程) | 高(需要协调多个进程、负载均衡、健康检查) |
| 适用规模 | < 1000 万 series | 1000 万 ~ 10 亿+ series |
| 写入延迟 | 更低(无 RPC 开销) | 略高(两次 RPC) |
| 查询延迟 | 低(直接本地查询) | P99 略高(需归并多节点结果) |
| 资源利用率 | 高(所有组件共享资源) | 中等(各进程资源隔离,可能利用率不均) |
源码视角:Single-Node 与 Cluster 的核心差异
从源码层面总结两种模式的核心差异:
进程模型对比
源码入口对比(app/ 目录)
通信方式对比
适用场景
思考记忆提示 — 多租户是 Cluster 模式的核心特性——理解两级隔离才能理解 Cluster 的分片逻辑
VictoriaMetrics 的多租户隔离采用两级体系:accountID(账户级)和 projectID(项目级)。这种设计源自其前身——VictoriaMetrics 最初是给大规模多租户 SaaS 服务设计的,因此多租户是核心特性。
在 Cluster 模式下,租户标识通过 URL 路径传递:
# Cluster 模式下的写入 URL
http://vminsert-cluster:8400/account/{accountID}/project/{projectID}/api/v1/write
# Cluster 模式下的查询 URL
http://vmselect-cluster:8481/account/{accountID}/project/{projectID}/api/v1/query
# 示例
http://vminsert-cluster:8400/account/123/project/456/api/v1/write
# accountID = 123, projectID = 456
小贴士 — URL 路径 vs Header
租户标识可以通过 URL 路径(/account/{id}/project/{id}/)或 Header(X-Account-ID、X-Project-ID)传递。URL 路径更直观,Header 更灵活。VictoriaMetrics 两种方式都支持。
vminsert 根据租户标识计算路由键(routing key),将数据分发到对应的 vmstorage 节点:
// 租户路由键计算逻辑
// 路由键 = accountID:projectID:月份(按月分片)
func ComputeRoutingKey(accountID, projectID uint32, timestamp int64) string {
month := getMonth(timestamp) // 例如 "2024-01"
// 路由键格式:accountID:projectID:month
return fmt.Sprintf("%d:%d:%s", accountID, projectID, month)
}
// 一致性哈希路由
// 相同路由键永远路由到相同的 vmstorage 节点
func (r *Router) Route(key string) *StorageNode {
hash := r.hashFunction([]byte(key))
return r.ring.GetNode(hash)
}
// 示例:
// - accountID=1, projectID=1, 时间戳=2024-01-15 → 路由键="1:1:2024-01"
// - accountID=1, projectID=2, 时间戳=2024-01-15 → 路由键="1:2:2024-01"
// - accountID=2, projectID=1, 时间戳=2024-01-15 → 路由键="2:1:2024-01"
//
// 相同 accountID/projectID 的数据会路由到同一个 vmstorage 节点
// 不同租户的数据天然隔离
源码视角:多租户隔离的两级路由机制
从源码层面分析 accountID/projectID 两级租户隔离的实现:
租户标识解析(lib/storage/tenant.go)
路由键计算(lib/consistenthash/consistenthash.go)
数据隔离原理
Single-Node 模式原生不支持多租户隔离,但可以通过 -promscrape.config 或外部代理(如 vmauth)模拟多租户效果:
// Single-Node 模式下,多租户通常通过 vmauth 实现
// vmauth = 认证代理,根据 API Key 路由到不同的后端
// vmauth 配置示例
// accounts:
// - name: tenant-1
// url_prefix:
// - http://victoria-metrics:8428
// api_keys:
// - key: "token-tenant1-xxx"
// - name: tenant-2
// url_prefix:
// - http://victoria-metrics:8428
// api_keys:
// - key: "token-tenant2-xxx"
// 使用方式
// Tenant-1 的 Prometheus:
// remote_write:
// - url: "http://vmauth:8427/api/v1/write"
// bearer_token: "token-tenant1-xxx"
//
// Tenant-2 的 Prometheus:
// remote_write:
// - url: "http://vmauth:8427/api/v1/write"
// bearer_token: "token-tenant2-xxx"
注意
Single-Node + vmauth 的方案可以实现"逻辑隔离"(不同租户的数据在同一个数据库里,通过标签区分),但无法实现"物理隔离"(不同租户的数据存在不同的存储节点)。如果需要严格的物理隔离(如数据主权合规),必须使用 Cluster 模式。
思考记忆提示 — 选型是生产部署的核心决策——本节给出清晰的决策框架
┌─────────────────────────────────────────────────────────────────────────────┐
│ VictoriaMetrics 选型决策树 │
│ │
│ 开始选型 │
│ │ │
│ ▼ │
│ ┌───────────────────────────────┐ │
│ │ Q1: 预计时间序列数量是多少? │ │
│ └───────────────────────────────┘ │
│ │ │
│ ┌──────────────────┼──────────────────┐ │
│ ▼ ▼ ▼ │
│ < 100 万 100 万-1000 万 > 1000 万 │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 小规模 │ │ 中规模 │ │ 大规模 │ │
│ │建议选 │ │建议选 │ │必须选 │ │
│ │Single- │ │Single- │ │Cluster │ │
│ │Node │ │Node │ │模式 │ │
│ └─────────┘ └──────────┘ └──────────┘ │
│ │ │ │ │
│ └──────────────────┼──────────────────┘ │
│ ▼ │
│ ┌───────────────────────────────┐ │
│ │ Q2: 需要高可用吗? │ │
│ └───────────────────────────────┘ │
│ │ │
│ ┌─────────┴─────────┐ │
│ ▼ ▼ │
│ 是 否 │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │Cluster │ │继续评估 │ │
│ │(多副本) │ │下一题 │ │
│ └──────────┘ └──────────┘ │
│ │ │
│ ┌─────────┴─────────┐ │
│ ▼ ▼ │
│ 是 否 │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │需要严格 │ │继续评估 │ │
│ │物理隔离? │ │下一题 │ │
│ └──────────┘ └──────────┘ │
│ │ │
│ ┌─────────┴─────────┐ │
│ ▼ ▼ │
│ 是 否 │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │Cluster │ │Single- │ │
│ │(唯一选择)│ │Node + │ │
│ │ │ │vmauth │ │
│ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
选型决策口诀
思考记忆提示 — 源码是理解架构的最佳途径——本节带你深入 main.go 看初始化流程
// app/victoria-metrics/main.go(Single-Node 入口)
package main
import (
"flag"
"log"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
)
var (
// 存储路径
storageDataPath = flag.String("storageDataPath", "victoria-metrics-data",
"Path to storage data")
// HTTP 监听地址
httpListenAddr = flag.String("httpListenAddr", ":8428",
"HTTP listen address")
// 日志级别
loggerLevel = flag.String("loggerLevel", "INFO",
"Logger level: INFO/WARN/ERROR/DEBUG")
)
func main() {
// ========== 步骤1:初始化日志 ==========
logger.InitWithLevel(*loggerLevel)
// ========== 步骤2:解析命令行参数 ==========
flag.Parse()
// ========== 步骤3:验证参数 ==========
if *storageDataPath == "" {
log.Fatal("-storageDataPath must be set")
}
// ========== 步骤4:初始化存储 ==========
// storage.Init() 会创建存储目录、加载已有数据、启动后台合并任务
if err := storage.Init(*storageDataPath); err != nil {
log.Fatalf("storage.Init failed: %s", err)
}
// ========== 步骤5:启动 HTTP 服务器 ==========
// HTTP 服务器处理所有外部请求
go func() {
if err := httpserver.Serve(*httpListenAddr, requestHandler); err != nil {
log.Fatalf("httpserver.Serve failed: %s", err)
}
}()
// ========== 步骤6:注册优雅关闭 ==========
// 收到 SIGTERM/SIGINT 时,先停止接收请求,再等待后台任务完成
setupGracefulShutdown()
// ========== 步骤7:主 goroutine 阻塞 ==========
<-make(chan struct{})
}
// Cluster 模式各组件的典型启动参数
// vminsert 启动参数
// vminsert -storageNode=vmstorage-1:8400,vmstorage-2:8400,vmstorage-3:8400
type vminsertFlags struct {
storageNodeAddrs = flag.String("storageNode", "",
"Comma-separated list of vmstorage addresses")
maxQueueSize = flag.Int("maxQueueSize", 100_000,
"Max number of rows queued before dropping")
insertTimeout = flag.Duration("insertTimeout", 30*time.Second,
"Timeout for inserting data")
}
// vmstorage 启动参数
// vmstorage -storageDataPath=/data/vmstorage -retentionDays=90
type vmstorageFlags struct {
storageDataPath = flag.String("storageDataPath", "vmstorage-data",
"Path to storage data")
retentionDays = flag.Int("retentionDays", 0,
"Data retention period in days")
convergeTimeout = flag.Duration("convergeTimeout", 5*time.Second,
"Timeout for converge")
}
// vmselect 启动参数
// vmselect -storageNode=vmstorage-1:8401,vmstorage-2:8401,vmstorage-3:8401
type vmselectFlags struct {
storageNodeAddrs = flag.String("storageNode", "",
"Comma-separated list of vmstorage addresses")
maxConcurrent = flag.Int("maxConcurrent", 8,
"Max concurrent requests")
searchTimeout = flag.Duration("searchTimeout", 60*time.Second,
"Query timeout")
}
源码视角:三种模式的启动参数对比
从 lib/flag.go 和各组件的 main.go 分析启动参数差异:
Single-Node(app/victoria-metrics/main.go)
vminsert(app/vminsert/main.go)
vmstorage(app/vmstorage/main.go)
vmselect(app/vmselect/main.go)
思考记忆提示 — 纸上得来终觉浅——本节给出可直接使用的部署模板
# docker-compose.yml(Single-Node 部署)
version: '3.8'
services:
victoria-metrics:
image: victoriametrics/victoria-metrics:v1.146.0
container_name: victoria-metrics
restart: unless-stopped
ports:
- "8428:8428" # HTTP API
- "2003:2003" # Graphite
- "4242:4242" # OpenTSDB
volumes:
- vm-data:/victoria-metrics-data
command:
- "-storageDataPath=/victoria-metrics-data"
- "-retentionPeriod=3month"
- "-httpListenAddr=0.0.0.0:8428"
- "-loggerLevel=INFO"
resources:
limits:
memory: 8Gi
cpus: '4'
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8428/health"]
interval: 30s
timeout: 10s
retries: 3
volumes:
vm-data:
# Kubernetes 部署(Cluster 模式)
# 包含 vminsert、vmstorage、vmselect 三个 StatefulSet
---
# vminsert Deployment(写入入口)
apiVersion: apps/v1
kind: Deployment
metadata:
name: vminsert
namespace: monitoring
spec:
replicas: 2
selector:
matchLabels:
app: vminsert
template:
metadata:
labels:
app: vminsert
spec:
containers:
- name: vminsert
image: victoriametrics/vminsert:v1.146.0
args:
- "-storageNode=vmstorage-0.vmstorage.monitoring.svc:8400,
vmstorage-1.vmstorage.monitoring.svc:8400,
vmstorage-2.vmstorage.monitoring.svc:8400"
- "-httpListenAddr=:8400"
ports:
- containerPort: 8400
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
---
# vmstorage StatefulSet(存储节点)
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: vmstorage
namespace: monitoring
spec:
serviceName: vmstorage
replicas: 3
selector:
matchLabels:
app: vmstorage
template:
metadata:
labels:
app: vmstorage
spec:
containers:
- name: vmstorage
image: victoriametrics/vmstorage:v1.146.0
args:
- "-storageDataPath=/storage"
- "-httpListenAddr=:8400"
- "-retentionPeriod=3month"
volumeMounts:
- name: storage
mountPath: /storage
resources:
requests:
memory: "4Gi"
cpu: "1000m"
limits:
memory: "16Gi"
cpu: "4000m"
volumeClaimTemplates:
- metadata:
name: storage
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 100Gi
---
# vmselect Deployment(查询入口)
apiVersion: apps/v1
kind: Deployment
metadata:
name: vmselect
namespace: monitoring
spec:
replicas: 2
selector:
matchLabels:
app: vmselect
template:
metadata:
labels:
app: vmselect
spec:
containers:
- name: vmselect
image: victoriametrics/vmselect:v1.146.0
args:
- "-storageNode=vmstorage-0.vmstorage.monitoring.svc:8401,
vmstorage-1.vmstorage.monitoring.svc:8401,
vmstorage-2.vmstorage.monitoring.svc:8401"
- "-httpListenAddr=:8481"
ports:
- containerPort: 8481
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "4Gi"
cpu: "2000m"
注意
Cluster 模式部署时,vmstorage 节点之间的数据同步需要通过 -replicationFactor 参数配置。建议设置为 2(每个分片复制到 2 个节点),在可用性和存储成本之间取得平衡。
思考记忆提示 — FAQ 是全篇的"临考前速背"模块,20 组覆盖全链路
理论上只受磁盘空间限制,实际建议控制在 1000 万 series 以内。VictoriaMetrics 的压缩率很高(通常 10x-50x),100GB 原始数据压缩后可能只有 5-10GB。但真正的瓶颈是内存:TSIDCache、blockCache 等缓存需要占用大量内存。建议使用 memory.Allowed 参数限制 VM 可用内存,并监控 vm_cache_entries_*/size_bytes 指标。
查询 API 完全兼容,写入 API 在 Cluster 模式下需要额外指定租户路径。PromQL 查询(/api/v1/query、/api/v1/query_range)在两种模式下完全兼容。写入 API 在 Cluster 模式下需要在 URL 中添加 /account/{id}/project/{id} 前缀,例如 /account/1/project/1/api/v1/write。
一致性哈希机制会自动将请求路由到其他节点,该节点负责的分片数据暂时不可用。如果配置了 -replicationFactor=2,每个分片有 2 份副本,宕机一个节点不影响可用性(另一份副本继续服务)。如果未配置副本,宕机节点的数据在恢复前无法写入或查询。建议生产环境至少设置 -replicationFactor=2。
通过 vmauth 认证代理实现逻辑隔离,所有租户共享同一个数据库。在 Single-Node 模式下,无法实现物理隔离。只能用 vmauth 做 API Key 认证,不同租户通过不同的 Bearer Token 写入,数据通过标签(tenant_id)区分。这是一种"软隔离",无法防止恶意租户通过高基数标签破坏系统。
可以,但会增加单点故障风险。理论上可以用 vminsert 同时作为查询入口(直接指向 vmstorage),但这样所有查询都会经过同一个 vminsert 节点,成为瓶颈。标准做法是分离部署:vminsert 专注写入,vmselect 专注查询,各自独立扩展。
因为查询需要经过 vmselect → vmstorage 两跳,以及结果归并开销。Single-Node 的查询是:HTTP → Storage.Search() → 返回结果(一跳)。Cluster 的查询是:HTTP → vmselect → 并行查询多个 vmstorage → 归并结果(两跳 + 归并)。网络延迟和归并计算都会增加 P99 延迟。如果 vmstorage 节点之间网络延迟高(如跨机房部署),影响会更明显。
当遇到 OOM、写入吞吐量瓶颈、查询超时、或需要高可用时。具体指标:1)频繁遭遇 Too many tsIDs currentStorageSizeBytes > Allowed 错误;2)写入吞吐量无法满足需求(< 50 万 samples/s);3)复杂查询 P99 延迟 > 5s;4)需要多副本高可用。这些信号表明 Single-Node 已接近容量边界。
每个组件都暴露 /metrics 端点,Cluster 模式下有专门的集群监控指标。vminsert 监控:vminsert_requests_total、vminsert_request_duration_seconds。vmstorage 监控:vmstorage_rows_queued、vmstorage_parts_being_merged。vmselect 监控:vmselect_search_duration_seconds、vmselect_search_results_count。建议在 Grafana 中导入官方 Cluster Dashboard。
可以,使用 vmctl 工具进行在线迁移。迁移步骤:1)在 Cluster 模式部署新的 VictoriaMetrics;2)使用 vmctl 将 Single-Node 的数据导出为快照;3)使用 vmrestore 将快照导入 Cluster;4)切换 Prometheus remote_write 指向新集群。官方推荐使用 vmctl prometheus 命令进行增量迁移。
建议至少 3 个节点,配合 replicationFactor=2 实现高可用。节点数量规划原则:1)数据量:每个 vmstorage 节点建议存储不超过 50TB 压缩数据;2)吞吐量:每个节点建议处理不超过 50 万 samples/s;3)副本数:replicationFactor=2 时,需要 2 倍存储空间;4)最小节点:生产环境至少 3 个,测试环境可用 2 个。
accountID 和 projectID 由应用自行定义,VM 只做路由隔离。常见做法:accountID = 组织/租户 ID(从 SSO/LDAP 获取),projectID = 产品线/环境 ID(dev/staging/prod)。例如:accountID=1001, projectID=1 表示"租户 1001 的生产环境"。这两个 ID 是无符号 32 位整数,范围 0-4294967295。
Enterprise 版本支持账号级存储配额,开源版本需要外部配额控制。Enterprise 版本可以通过 -limits.numTenant 参数限制租户数量,以及通过 VMSingleTenantQuota 资源设置存储上限。开源版本没有内置配额,需要通过 Prometheus 侧(如 metric_relabel_configs)或 API 网关(vmauth)限制写入速率。
Single-Node 在启动参数指定,Cluster 模式在每个 vmstorage 节点指定。Single-Node:-retentionPeriod=3month(全局)。Cluster 模式:每个 vmstorage 节点独立配置 -retentionPeriod,建议所有节点保持一致。如果不一致,可能导致某些分区数据已删除但查询仍在请求这些分区。
通过 vmauth 或 vmgateway 在查询请求前添加租户 Header/路径。vmauth 配置示例:在 vmauth 中配置多个后端,每个后端对应不同的 accountID/projectID。请求到达 vmauth 时,根据 API Key 选择对应的后端,后端在转发请求时自动添加租户路径。
无法完全模拟,但可以用 relabel 和标签实现逻辑隔离。一种实践是:在 Prometheus 端使用 metric_relabel_configs 为每个租户的数据添加不同的 tenant_id 标签,然后通过 vmauth 或 Grafana 的租户筛选实现逻辑隔离。但这只是"软隔离",无法防止恶意租户写入高基数标签。
数据量 > 1000 万 series、需要高可用、需要严格多租户隔离时。具体场景:1)多租户 SaaS 服务,每个租户需要独立存储隔离;2)超过 1000 万 series 的超大规模部署;3)需要 99.9%+ 可用性的生产系统;4)需要跨机房容灾的部署。
需要管理多个进程/容器、配置负载均衡、实现健康检查、处理节点扩缩容。具体开销:1)部署复杂度增加 3 倍(需要部署 3 种组件);2)监控维度增加(每个组件都需要单独监控);3)故障排查更复杂(需要定位是 vminsert/vmselect/vmstorage 哪一层的问题);4)升级时需要滚动升级多个组件。
技术上可以,但不推荐,会导致职责混乱。vminsert 的职责是写入,如果同时做查询,会导致资源竞争(写入请求和查询请求争抢 CPU/内存/网络)。标准架构是:写入走 vminsert,查询走 vmselect,各自独立扩展。
公式:存储容量 = 写入速率 × 保留时间 × 压缩率 × 副本数。例如:每秒写入 10 万 samples,保留 90 天,压缩率 10x,副本书 2,则存储容量 = 100000 × 86400 × 90 × 10 ÷ 2 ÷ 1e12 ≈ 38.9 TB。建议预留 30% 余量,实际需要约 50TB。
可以,用 vmgateway 作为统一入口,后端混合部署。一种架构是:轻量级数据走 Single-Node(低延迟),重量级数据走 Cluster(高吞吐)。vmgateway 根据请求特征(租户 ID、数据量级别)动态路由到不同的后端。这是架构演进的中间态,随着业务增长,最终会全部迁移到 Cluster。
全篇必记总纲
VictoriaMetrics 的 Single-Node 和 Cluster 模式是同一套存储引擎的两种部署形态:Single-Node 将所有组件集成在单进程内,适合小规模高效部署;Cluster 模式将写入(vminsert)、存储(vmstorage)、查询(vmselect)拆分为独立进程,通过一致性哈希实现数据分片和水平扩展,适合大规模高可用场景。多租户隔离通过 accountID/projectID 两级体系实现,Cluster 模式原生支持,Single-Node 模式通过外部代理模拟。选型的核心原则是:小规模选 Single-Node 省心省力,大规模选 Cluster 可扩展性强。
本篇覆盖了 Single-Node 和 Cluster 模式的核心架构差异,但还有几条主线尚未展开:
本文参考与源码链接:
• app/victoria-metrics/main.go · Single-Node 入口
• app/vminsert/ · Cluster 写入入口
• app/vmstorage/ · Cluster 存储节点
• app/vmselect/ · Cluster 查询节点
• lib/storage/ · 存储核心层
• lib/consistenthash/ · 一致性哈希路由
• VictoriaMetrics Cluster 官方文档
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。