VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— Single-Node vs Cluster 模式本质区别
VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— Single-Node vs Cluster 模式本质区别
在第一篇《设计哲学:为什么 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 版本的额外特性
文章目录
一、为什么需要了解 Single-Node 和 Cluster 的区别?——架构选择是生产部署的第一步
思考记忆提示 — 本节是全篇的"入口"——弄清楚架构选择的背景和重要性,才能理解后面每种模式的设计取舍
- 架构选择直接影响运维复杂度、数据容量、可用性三大关键指标
- Single-Node 和 Cluster 不是"新旧版本"的关系,而是两种不同的部署形态
- 面试高频提问:什么场景下 Single-Node 会遇到瓶颈?Cluster 模式的额外开销是什么?
当我们准备在生产环境部署 VictoriaMetrics 时,首先要做的决策不是"用什么版本的 VM",而是"用 Single-Node 还是 Cluster 模式"。这个选择看似简单,实则影响深远:选错了,可能面临扩容困难、运维复杂、或者资源浪费等一系列问题。
从源码层面分析,两种模式的本质差异在于组件的部署形态和通信方式:
Single-Node 的源码结构(app/victoria-metrics/)
- 入口文件:app/victoria-metrics/main.go 的 main()
- 进程模型:单一二进制文件 victoria-metrics,在一个进程内同时提供写入、存储、查询能力
- 通信方式:HTTP 请求先进入 requestHandler(),再由 vminsert.RequestHandler()、vmselect.RequestHandler()、vmstorage.RequestHandler() 分发给对应组件
Cluster 的源码结构(app/vminsert/、app/vmstorage/、app/vmselect/)
- 入口文件:app/vminsert/main.go、app/vmstorage/main.go、app/vmselect/main.go 各自的 main()
- 通信方式:HTTP/gRPC RPC 调用,通过 app/vminsert/、app/vmstorage/、app/vmselect/ 中的 RPC 处理器分发请求
- 路由机制:一致性哈希(lib/consistenthash/),路由键为 accountID:projectID:partition
核心差异总结:Single-Node 是"进程内集成",Cluster 是"进程间分布式"。这个差异决定了:1)Single-Node 无网络开销,性能更高;2)Cluster 可以独立扩展每个组件,实现水平扩展。
VictoriaMetrics 官方在 官方文档 中明确指出:Single-Node 适合"大多数部署场景",Cluster 模式适合"需要水平扩展的高可用场景"。但这个说法太模糊,我们需要从源码层面理解两种模式的本质差异。
二、Single-Node 单进程架构——All-in-One 的工程哲学
思考记忆提示 — Single-Node 是理解 VM 架构的起点——它把所有组件集成在一起,方便理解组件间的关系
- Single-Node 的核心是单进程 + 内部 API:写入、存储、查询都在同一个进程内完成
- main.go 是入口,app/victoria-metrics/ 是 Single-Node 的核心代码
- 面试高频提问:Single-Node 模式下,HTTP 请求是如何被处理的?存储层和查询层如何通信?
2.1 Single-Node 的进程结构
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 模式入口:同一个进程内启动 storage/vmselect/vminsert
package main
import (
"flag"
"os"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert"
vminsertcommon "github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/common"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/buildinfo"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/cgroup"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/envflag"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/procutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/pushmetrics"
)
var (
httpListenAddrs = flagutil.NewArrayString("httpListenAddr", ":8428")
vmselectMaxConcurrentRequests = flag.Int("search.maxConcurrentRequests", getDefaultMaxConcurrentRequests(), ...)
vmselectMaxQueueDuration = flag.Duration("search.maxQueueDuration", 10*time.Second, ...)
)
func getDefaultMaxConcurrentRequests() int {
n := min(cgroup.AvailableCPUs()*2, 16)
return n
}
func main() {
cgroup.SetGOGC(30)
flag.CommandLine.SetOutput(os.Stdout)
envflag.Parse()
buildinfo.Init()
logger.Init()
vmstorage.Init(*vmselectMaxConcurrentRequests, promql.ResetRollupResultCacheIfNeeded)
vmselect.Init(*vmselectMaxConcurrentRequests, *vmselectMaxQueueDuration)
vminsertcommon.StartIngestionRateLimiter(*maxIngestionRate)
vminsert.Init()
startSelfScraper()
go httpserver.Serve(*httpListenAddrs, requestHandler, httpserver.ServeOptions{
UseProxyProtocol: *useProxyProtocol,
})
sig := procutil.WaitForSigterm()
vminsert.Stop()
vmstorage.Stop()
vmselect.Stop()
_ = sig
}
func requestHandler(w http.ResponseWriter, r *http.Request) bool {
if vminsert.RequestHandler(w, r) {
return true
}
if vmselect.RequestHandler(w, r) {
return true
}
if vmstorage.RequestHandler(w, r) {
return true
}
return false
}
从 app/victoria-metrics/main.go 的 main() 函数分析初始化流程:
步骤1:存储层初始化 → lib/storage/storage.go
- vmstorage.Init() 初始化存储引擎
- 创建 Table 实例(管理 Partition)和 indexDB 实例(倒排索引)
- 初始化 TSIDCache 和 blockCache 等查询缓存
步骤2:查询层初始化 → app/vmselect/main.go
- vmselect.Init() 初始化查询层并发限制与 rollup 结果缓存
- 与 Cluster 模式不同,这里不需要跨进程通信
步骤3:HTTP 服务启动 → lib/httpserver/httpserver.go
- httpserver.Serve() 监听 :8428 端口
- 注册路由:/api/v1/write → 写入,/api/v1/query → 查询
关键点:Single-Node 的三个步骤都在同一个进程内顺序执行,通过共享的 storage.Storage 全局变量实现组件间通信。
2.2 Single-Node 的组件集成关系
Single-Node 模式下,所有组件共享同一个存储实例。HTTP 层的写入请求进入 requestHandler() 后,由 vminsert.RequestHandler()、vmselect.RequestHandler()、vmstorage.RequestHandler() 分发给对应组件;查询请求同理。这种集成方式的好处是:
- 零网络开销:进程内函数调用比网络 RPC 快几个数量级
- 资源利用率高:所有组件共享 CPU、内存、磁盘 I/O
- 运维简单:一个进程、一个端口、一套配置
┌────────────────────────────────────────────────────────────────────┐
│ 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 可以保持极低的复杂度,同时提供完整的功能。
2.3 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 三层分离架构——水平扩展的艺术
思考记忆提示 — Cluster 模式是理解 VM 分布式能力的核心——三层架构各有专职,通过 RPC 协同工作
- vminsert = 写入入口层(接收请求 + 分片路由)
- vmstorage = 数据存储层(数据分片 + 查询执行)
- vmselect = 查询聚合层(查询分发 + 结果归并)
- 面试高频提问:Cluster 模式下,写入请求是如何分片的?查询请求如何跨节点聚合结果?
3.1 Cluster 模式的三层架构
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 三层各自承担的职责和它们之间的 RPC 调用链路:
vminsert(app/vminsert/main.go)
- 职责:接收外部写入请求,执行 relabel,通过 vminsertapi 写入远端 vmstorage
- 关键类型/模块:common.IngestionRateLimiter、promremotewrite.RequestHandler
- 扩展点:支持 Graphite/InfluxDB/OpenTSDB/OpenTelemetry 等多种协议
vmstorage(app/vmstorage/main.go)
- 职责:存储分配给自己的分区数据,提供 /internal/insert 与 /internal/search 等 RPC 服务
- 关键入口:requestHandler() 负责内部请求分发
- 存储结构:table 管理多个 partition,每个 partition 内再按 inmemory/small/big 组织数据
vmselect(app/vmselect/main.go)
- 职责:提供 PromQL 查询入口,读取 vmstorage 暴露的查询接口,并发控制与结果缓存
- 关键入口:RequestHandler() 处理查询请求,内部依赖 promql 与 netstorage 模块
- 并行策略:通过 concurrencyLimitCh 控制最大并发查询数
为什么需要三层而不是两层?vminsert 专门负责写入路由与协议兼容,vmstorage 专门负责数据存储与分区管理,vmselect 专门负责查询入口与 PromQL 执行。每层职责单一,便于独立扩展和故障隔离。如果只有两层,vminsert 既要路由又要查询聚合,职责过重,扩展性差。
3.2 vminsert 写入入口层
vminsert(位于 app/vminsert/)是 Cluster 模式的写入入口。它的核心职责是:
- 接收外部写入请求(Prometheus Remote Write、InfluxDB、OpenTelemetry 等)
- 根据请求中的 accountID/projectID 进行分片路由
- 将数据转发到对应的 vmstorage 节点
// app/vminsert/main.go(VictoriaMetrics v1.146.0)
// Cluster 模式写入入口:接收外部请求,执行租户解析与分片路由
package main
import (
"embed"
"flag"
"net/http"
"strings"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/common"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/promremotewrite"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/relabel"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/procutil"
)
var (
graphiteListenAddr = flag.String("graphiteListenAddr", "", "...")
influxListenAddr = flag.String("influxListenAddr", "", "...")
opentsdbListenAddr = flag.String("opentsdbListenAddr", "", "...")
opentsdbHTTPListenAddr = flag.String("opentsdbHTTPListenAddr", "", "...")
maxLabelsPerTimeseries = flag.Int("maxLabelsPerTimeseries", 40, "...")
)
func main() {
logger.Init()
// 初始化多种协议监听器(Graphite/InfluxDB/OpenTSDB/OTel 等)
// 通过 InsertHandler/relabel 等模块把数据写入 vmstorage
_ = common.StartIngestionRateLimiter
_ = promremotewrite.RequestHandler
_ = relabel.RequestHandler
_ = procutil.WaitForSigterm
}
小贴士 — 一致性哈希的优势
vminsert 使用一致性哈希(Consistent Hashing)进行分片路由,相比简单取模哈希的优势是:当某个 vmstorage 节点宕机时,只有该节点负责的分片需要重新映射,其他分片不受影响。这保证了 Cluster 模式的高可用性——任何一个 vmstorage 节点宕机,只影响该节点负责的分片,不会导致整个集群不可用。
3.3 vmstorage 数据存储层
vmstorage(位于 app/vmstorage/)是 Cluster 模式的数据存储节点。每个 vmstorage 实例负责存储部分分区(partition)的数据,并提供分区级的查询能力。
// app/vmstorage/main.go(VictoriaMetrics v1.146.0)
// Cluster 模式存储节点:初始化 storage,并通过 /internal/* 提供 RPC 服务
package main
import (
"flag"
"log"
"net/http"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
)
var (
storageDataPath = flag.String("storageDataPath", "victoria-metrics-data", "...")
retentionPeriod = flagutil.NewRetentionDuration("retentionPeriod", "1M", "...")
httpListenAddr = flag.String("httpListenAddr", ":8400", "...")
)
func main() {
logger.Init()
flag.Parse()
opts := storage.OpenOptions{
Retention: *retentionPeriod,
FutureRetention: 2 * 24 * time.Hour,
}
strg := storage.MustOpenStorage(*storageDataPath, opts)
http.HandleFunc("/internal/insert", vmStorage.requestHandler)
http.HandleFunc("/internal/search", vmStorage.requestHandler)
http.ListenAndServe(*httpListenAddr, nil)
}
从源码层面分析 vmstorage 的分区策略和数据存储结构:
分区键计算(lib/storage/partition.go)
- 分区维度:时间(按月)+ 租户(accountID:projectID)
- 路由键格式:fmt.Sprintf("%d:%d:%s", accountID, projectID, partitionName)
- 月份分区:lib/storage/partition.go 中的 partition.name 采用 YYYY_MM 格式
- 一致性哈希确保相同路由键永远路由到相同的 vmstorage 节点
数据存储结构(lib/storage/table.go)
- 每个 vmstorage 节点存储多个 Partition,每个 Partition 对应不同的月份
- 每个 Partition 内部包含:内存缓冲 part、small parts、big parts;具体字段和刷盘逻辑在 lib/storage/partition.go 与 lib/storage/table.go
- 倒排索引通过 indexdb 提供标签查询能力
为什么 vmstorage 返回"数据块"而非"最终结果"?
- vmstorage 通过 requestHandler() 暴露 /internal/insert 与 /internal/search,供 vmselect 读取原始分区数据
- vmselect 的 RequestHandler() 与 promql 模块负责解析查询、并发读取多个 vmstorage,并做最终聚合
- 设计原因:vmstorage 只返回分区级原始数据,保持存储层简单;查询语义和聚合计算集中在 vmselect
3.4 vmselect 查询聚合层
vmselect(位于 app/vmselect/)是 Cluster 模式的查询入口和结果聚合层。它负责接收外部查询请求、分发到所有相关 vmstorage 节点、归并聚合结果。
// app/vmselect/main.go(VictoriaMetrics v1.146.0)
// Cluster 模式查询节点:处理 PromQL 查询,并通过 vmstorage API 读取数据
package main
import (
"flag"
"net/http"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/promql"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
)
var (
searchMaxConcurrentRequests = flag.Int("search.maxConcurrentRequests", 8, ...)
searchMaxQueueDuration = flag.Duration("search.maxQueueDuration", 10*time.Second, ...)
httpListenAddr = flag.String("httpListenAddr", ":8481", "...")
)
func main() {
logger.Init()
flag.Parse()
vmstorage.Init(*searchMaxConcurrentRequests, promql.ResetRollupResultCacheIfNeeded)
// vmselect 通过 vmstorage 提供的查询接口读取数据
// 实际查询执行路径在 app/vmselect/promql/ 和 netstorage 中
_ = vmstorage.GetSearch
_ = vmstorage.PutSearch
_ = http.ListenAndServe
}
设计精髓
vmselect 的查询分发采用了scatter-gather(分散-聚合)模式:
- 分散(Scatter):将查询请求并行发送到所有 vmstorage 节点
- 聚合(Gather):收集所有响应后,使用 k-way 归并算法合并结果
这种模式的优点是:查询延迟由最慢的 vmstorage 决定,而不是所有节点延迟之和。如果某个 vmstorage 节点数据量少,它返回快;如果某个节点数据量大,它返回慢。最终延迟 = max(各节点延迟),而不是 sum(各节点延迟)。
四、两种模式的核心差异对比
思考记忆提示 — 理解差异是选型的基础——本节用对比表格让你一目了然
- Single-Node = 单进程 + 内部调用,适合小规模
- Cluster = 多进程 + RPC 调用,适合大规模高可用
- 面试高频提问:两种模式的性能差异?迁移成本?
| 对比维度 | 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:单一进程,三个组件(vminsert/vmstorage/vmselect)通过函数调用通信
- Cluster:三个独立进程,通过 HTTP/gRPC RPC 调用通信
源码入口对比(app/ 目录)
- Single-Node:app/victoria-metrics/main.go → 一个二进制文件
- Cluster:app/vminsert/main.go、app/vmstorage/main.go、app/vmselect/main.go → 三个二进制文件
通信方式对比
- Single-Node:直接函数调用 storage.Add()、storage.Search()(无网络开销)
- Cluster:一致性哈希路由 + RPC 调用(lib/consistenthash/)
适用场景
- Single-Node:≤ 1000 万 series,写入 ≤ 100 万 samples/s,运维简单优先
- Cluster:> 1000 万 series,高可用,多租户,水平扩展优先
五、多租户隔离机制——accountID/projectID 两级体系
思考记忆提示 — 多租户是 Cluster 模式的核心特性——理解两级隔离才能理解 Cluster 的分片逻辑
- accountID = 一级租户(通常对应一个组织/团队)
- projectID = 二级租户(通常对应一个项目/产品线)
- 路由键 = accountID:projectID:partition
- 面试高频提问:Single-Node 模式下如何实现多租户?
VictoriaMetrics 的多租户隔离采用两级体系:accountID(账户级)和 projectID(项目级)。这种设计源自其前身——VictoriaMetrics 最初是给大规模多租户 SaaS 服务设计的,因此多租户是核心特性。
5.1 租户标识的传递方式
在 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 两种方式都支持。
5.2 分片路由与租户隔离
vminsert 根据租户标识计算路由键(routing key),将数据分发到对应的 vmstorage 节点:
// lib/storage/partition.go(VictoriaMetrics v1.146.0)
// 分区命名采用 "YYYY_MM" 形式,例如 "2024_01"
// Name is the name of the partition in the form YYYY_MM.
// The time range for the partition. Usually this is a whole month.
type partition struct {
name string
tr TimeRange
// ...
}
// mustCreatePartition creates new partition for the given timestamp
func mustCreatePartition(timestamp int64, smallPartitionsPath, bigPartitionsPath, indexDBPath string, s *Storage) *partition {
// partition name 由时间戳推导为 YYYY_MM
// ...
}
源码里并没有一个单独的 `getMonth()` 函数,而是直接把时间戳映射成 `YYYY_MM` 的分区名;Cluster 路由键的构造在 vminsert/relabel 与写入协议解析中完成,核心目的是让相同租户+月份的数据落到同一个 partition。
从源码层面分析 accountID/projectID 两级租户隔离的实现:
分区与租户隔离(lib/storage/partition.go、lib/storage/table.go)
- 租户 ID 格式:accountID:projectID(uint32:uint32)
- 数据按 partition.name(YYYY_MM)组织;不同租户数据通过路由落到不同 vmstorage
- vmstorage 的 table 管理多个 partition,每个 partition 内再按 inmemory/small/big 组织
路由键与一致性哈希(lib/consistenthash/consistenthash.go)
- 路由键格式:fmt.Sprintf("%d:%d:%s", accountID, projectID, partitionName)
- 月份分区:lib/storage/partition.go 中的 partition.name 采用 YYYY_MM 格式
- 一致性哈希:相同路由键 → 相同 vmstorage 节点(通过 FNVHash 实现)
数据隔离原理
- 租户隔离在 vminsert 层通过路由实现:不同租户的数据自动路由到不同的 vmstorage 节点
- 物理隔离:每个 vmstorage 节点只存储分配给自己的分区数据
- 副本与容错:-replicationFactor 控制写入副本数,vmselect 查询时按配置容忍节点丢失
5.3 Single-Node 的多租户模拟
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 模式。
六、选型决策树——我应该用哪个模式?
思考记忆提示 — 选型是生产部署的核心决策——本节给出清晰的决策框架
- 选 Single-Node 的条件:规模小 + 运维简单
- 选 Cluster 的条件:规模大 + 高可用 + 多租户
- 面试高频提问:从 Single-Node 迁移到 Cluster 需要注意什么?
┌─────────────────────────────────────────────────────────────────────────────┐
│ VictoriaMetrics 选型决策树 │
│ │
│ 开始选型 │
│ │ │
│ ▼ │
│ ┌───────────────────────────────┐ │
│ │ Q1: 预计时间序列数量是多少? │ │
│ └───────────────────────────────┘ │
│ │ │
│ ┌──────────────────┼──────────────────┐ │
│ ▼ ▼ ▼ │
│ < 100 万 100 万-1000 万 > 1000 万 │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 小规模 │ │ 中规模 │ │ 大规模 │ │
│ │建议选 │ │建议选 │ │必须选 │ │
│ │Single- │ │Single- │ │Cluster │ │
│ │Node │ │Node │ │模式 │ │
│ └─────────┘ └──────────┘ └──────────┘ │
│ │ │ │ │
│ └──────────────────┼──────────────────┘ │
│ ▼ │
│ ┌───────────────────────────────┐ │
│ │ Q2: 需要高可用吗? │ │
│ └───────────────────────────────┘ │
│ │ │
│ ┌─────────┴─────────┐ │
│ ▼ ▼ │
│ 是 否 │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │Cluster │ │继续评估 │ │
│ │(多副本) │ │下一题 │ │
│ └──────────┘ └──────────┘ │
│ │ │
│ ┌─────────┴─────────┐ │
│ ▼ ▼ │
│ 是 否 │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │需要严格 │ │继续评估 │ │
│ │物理隔离? │ │下一题 │ │
│ └──────────┘ └──────────┘ │
│ │ │
│ ┌─────────┴─────────┐ │
│ ▼ ▼ │
│ 是 否 │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │Cluster │ │Single- │ │
│ │(唯一选择)│ │Node + │ │
│ │ │ │vmauth │ │
│ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
选型决策口诀
- 小规模单机足够:100 万 series 以下,单节点够用
- 中规模单点风险可接受:100 万-1000 万 series,可以继续用单节点
- 大规模必须集群:1000 万 series 以上,Cluster 是唯一选择
- 高可用必须集群:任何规模,只要需要高可用就用 Cluster
- 多租户物理隔离必须集群:合规要求下只能用 Cluster
七、源码解析——从 main.go 看架构初始化
思考记忆提示 — 源码是理解架构的最佳途径——本节带你深入 main.go 看初始化流程
- Single-Node 和 Cluster 的启动参数不同,入口点也不同
- lib/logger 是统一的日志框架
- 面试高频提问:VictoriaMetrics 如何根据参数决定运行 Single-Node 还是 Cluster 模式?
7.1 Single-Node 的 main.go 入口
// app/victoria-metrics/main.go(VictoriaMetrics v1.146.0)
// Single-Node 入口:同进程内组装 storage/vmselect/vminsert
package main
import (
"flag"
"os"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert"
vminsertcommon "github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/common"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/promql"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/buildinfo"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/cgroup"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/envflag"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/procutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/pushmetrics"
)
var (
httpListenAddrs = flagutil.NewArrayString("httpListenAddr", ":8428")
vmselectMaxConcurrentRequests = flag.Int("search.maxConcurrentRequests", getDefaultMaxConcurrentRequests(), ...)
vmselectMaxQueueDuration = flag.Duration("search.maxQueueDuration", 10*time.Second, ...)
)
func getDefaultMaxConcurrentRequests() int {
n := min(cgroup.AvailableCPUs()*2, 16)
return n
}
func main() {
cgroup.SetGOGC(30)
flag.CommandLine.SetOutput(os.Stdout)
envflag.Parse()
buildinfo.Init()
logger.Init()
vmstorage.Init(*vmselectMaxConcurrentRequests, promql.ResetRollupResultCacheIfNeeded)
vmselect.Init(*vmselectMaxConcurrentRequests, *vmselectMaxQueueDuration)
vminsertcommon.StartIngestionRateLimiter(*maxIngestionRate)
vminsert.Init()
startSelfScraper()
go httpserver.Serve(*httpListenAddrs, requestHandler, httpserver.ServeOptions{
UseProxyProtocol: *useProxyProtocol,
})
sig := procutil.WaitForSigterm()
vminsert.Stop()
vmstorage.Stop()
vmselect.Stop()
_ = sig
}
func requestHandler(w http.ResponseWriter, r *http.Request) bool {
if vminsert.RequestHandler(w, r) {
return true
}
if vmselect.RequestHandler(w, r) {
return true
}
if vmstorage.RequestHandler(w, r) {
return true
}
return false
}
7.2 Cluster 各组件的启动参数
// 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")
}
从 app/victoria-metrics/main.go、app/vminsert/main.go、app/vmselect/main.go 和 app/vmstorage/main.go 分析启动参数差异:
Single-Node(app/victoria-metrics/main.go)
- -storageDataPath:数据存储路径
- -httpListenAddr=:8428:HTTP 监听地址
- -retentionDays:数据保留天数
- 参数最少,配置最简单
vminsert(app/vminsert/main.go)
- -vmstorageAddrs:vmstorage 节点列表(逗号分隔)
- -replicationFactor:数据副本数
- 必须知道 vmstorage 节点地址才能路由
vmstorage(app/vmstorage/main.go)
- 参数与 Single-Node 类似,但监听不同端口(默认 :8400)
- 提供 /internal/insert 和 /internal/search RPC 端点
vmselect(app/vmselect/main.go)
- -storageNodeAddrs:vmstorage 节点列表
- 监听 :8481 提供查询入口
- -searchTimeout:查询超时时间
八、实战配置——两种模式的部署模板
思考记忆提示 — 纸上得来终觉浅——本节给出可直接使用的部署模板
- Single-Node:Docker 单容器部署
- Cluster:Kubernetes StatefulSet 部署
- 面试高频提问:生产环境有哪些必须配置的安全参数?
8.1 Single-Node 部署模板
# 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:
8.2 Cluster 模式 Kubernetes 部署模板
# 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:常见疑问
思考记忆提示 — FAQ 是全篇的"临考前速背"模块,20 组覆盖全链路
- Q1-Q5 围绕 Single-Node 特性:容量、配置、限制
- Q6-Q10 围绕 Cluster 架构:三层职责、分片策略
- Q11-Q15 围绕多租户:accountID/projectID、隔离机制
- Q16-Q20 围绕选型和迁移:什么时候选什么、如何迁移
Q1. Single-Node 模式下最大能存储多少数据?
理论上只受磁盘空间限制,实际建议控制在 1000 万 series 以内。VictoriaMetrics 的压缩率很高(通常 10x-50x),100GB 原始数据压缩后可能只有 5-10GB。但真正的瓶颈是内存:TSIDCache、blockCache 等缓存需要占用大量内存。建议使用 memory.Allowed 参数限制 VM 可用内存,并监控 vm_cache_entries_*/size_bytes 指标。
Q2. Single-Node 和 Cluster 模式的 HTTP API 兼容吗?
查询 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。
Q3. Cluster 模式下,如果某个 vmstorage 节点宕机会怎样?
一致性哈希机制会自动将请求路由到其他节点,该节点负责的分片数据暂时不可用。如果配置了 -replicationFactor=2,每个分片有 2 份副本,宕机一个节点不影响可用性(另一份副本继续服务)。如果未配置副本,宕机节点的数据在恢复前无法写入或查询。建议生产环境至少设置 -replicationFactor=2。
Q4. Single-Node 模式下如何实现多租户?
通过 vmauth 认证代理实现逻辑隔离,所有租户共享同一个数据库。在 Single-Node 模式下,无法实现物理隔离。只能用 vmauth 做 API Key 认证,不同租户通过不同的 Bearer Token 写入,数据通过标签(tenant_id)区分。这是一种"软隔离",无法防止恶意租户通过高基数标签破坏系统。
Q5. Cluster 模式下,vminsert 和 vmselect 可以只部署一个吗?
可以,但会增加单点故障风险。理论上可以用 vminsert 同时作为查询入口(直接指向 vmstorage),但这样所有查询都会经过同一个 vminsert 节点,成为瓶颈。标准做法是分离部署:vminsert 专注写入,vmselect 专注查询,各自独立扩展。
Q6. Cluster 模式下,查询延迟为什么比 Single-Node 高?
因为查询需要经过 vmselect → vmstorage 两跳,以及结果归并开销。Single-Node 的查询是:HTTP → Storage.Search() → 返回结果(一跳)。Cluster 的查询是:HTTP → vmselect → 并行查询多个 vmstorage → 归并结果(两跳 + 归并)。网络延迟和归并计算都会增加 P99 延迟。如果 vmstorage 节点之间网络延迟高(如跨机房部署),影响会更明显。
Q7. 什么情况下应该从 Single-Node 迁移到 Cluster?
当遇到 OOM、写入吞吐量瓶颈、查询超时、或需要高可用时。具体指标:1)频繁遭遇 Too many tsIDs currentStorageSizeBytes > Allowed 错误;2)写入吞吐量无法满足需求(< 50 万 samples/s);3)复杂查询 P99 延迟 > 5s;4)需要多副本高可用。这些信号表明 Single-Node 已接近容量边界。
Q8. Cluster 模式下,如何监控各组件健康状态?
每个组件都暴露 /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。
Q9. Single-Node 的数据可以迁移到 Cluster 吗?
可以,使用 vmctl 工具进行在线迁移。迁移步骤:1)在 Cluster 模式部署新的 VictoriaMetrics;2)使用 vmctl 将 Single-Node 的数据导出为快照;3)使用 vmrestore 将快照导入 Cluster;4)切换 Prometheus remote_write 指向新集群。官方推荐使用 vmctl prometheus 命令进行增量迁移。
Q10. Cluster 模式下,vmstorage 节点的数量如何规划?
建议至少 3 个节点,配合 replicationFactor=2 实现高可用。节点数量规划原则:1)数据量:每个 vmstorage 节点建议存储不超过 50TB 压缩数据;2)吞吐量:每个节点建议处理不超过 50 万 samples/s;3)副本数:replicationFactor=2 时,需要 2 倍存储空间;4)最小节点:生产环境至少 3 个,测试环境可用 2 个。
Q11. Cluster 模式下,如何确定 accountID 和 projectID?
accountID 和 projectID 由应用自行定义,VM 只做路由隔离。常见做法:accountID = 组织/租户 ID(从 SSO/LDAP 获取),projectID = 产品线/环境 ID(dev/staging/prod)。例如:accountID=1001, projectID=1 表示"租户 1001 的生产环境"。这两个 ID 是无符号 32 位整数,范围 0-4294967295。
Q12. Cluster 模式支持账号配额限制吗?
Enterprise 版本支持账号级存储配额,开源版本需要外部配额控制。Enterprise 版本可以通过 -limits.numTenant 参数限制租户数量,以及通过 VMSingleTenantQuota 资源设置存储上限。开源版本没有内置配额,需要通过 Prometheus 侧(如 metric_relabel_configs)或 API 网关(vmauth)限制写入速率。
Q13. 两种模式下的数据保留策略(retention)有什么区别?
Single-Node 在启动参数指定,Cluster 模式在每个 vmstorage 节点指定。Single-Node:-retentionPeriod=3month(全局)。Cluster 模式:每个 vmstorage 节点独立配置 -retentionPeriod,建议所有节点保持一致。如果不一致,可能导致某些分区数据已删除但查询仍在请求这些分区。
Q14. Cluster 模式下,如何实现查询路由到特定租户?
通过 vmauth 或 vmgateway 在查询请求前添加租户 Header/路径。vmauth 配置示例:在 vmauth 中配置多个后端,每个后端对应不同的 accountID/projectID。请求到达 vmauth 时,根据 API Key 选择对应的后端,后端在转发请求时自动添加租户路径。
Q15. Single-Node 模式下可以模拟 Cluster 的多租户路由吗?
无法完全模拟,但可以用 relabel 和标签实现逻辑隔离。一种实践是:在 Prometheus 端使用 metric_relabel_configs 为每个租户的数据添加不同的 tenant_id 标签,然后通过 vmauth 或 Grafana 的租户筛选实现逻辑隔离。但这只是"软隔离",无法防止恶意租户写入高基数标签。
Q16. 什么场景下必须使用 Cluster 模式?
数据量 > 1000 万 series、需要高可用、需要严格多租户隔离时。具体场景:1)多租户 SaaS 服务,每个租户需要独立存储隔离;2)超过 1000 万 series 的超大规模部署;3)需要 99.9%+ 可用性的生产系统;4)需要跨机房容灾的部署。
Q17. Cluster 模式的额外运维开销有哪些?
需要管理多个进程/容器、配置负载均衡、实现健康检查、处理节点扩缩容。具体开销:1)部署复杂度增加 3 倍(需要部署 3 种组件);2)监控维度增加(每个组件都需要单独监控);3)故障排查更复杂(需要定位是 vminsert/vmselect/vmstorage 哪一层的问题);4)升级时需要滚动升级多个组件。
Q18. Cluster 模式下,vminsert 能否直接查询 vmstorage?
技术上可以,但不推荐,会导致职责混乱。vminsert 的职责是写入,如果同时做查询,会导致资源竞争(写入请求和查询请求争抢 CPU/内存/网络)。标准架构是:写入走 vminsert,查询走 vmselect,各自独立扩展。
Q19. 如何估算 Cluster 模式的存储容量?
公式:存储容量 = 写入速率 × 保留时间 × 压缩率 × 副本数。例如:每秒写入 10 万 samples,保留 90 天,压缩率 10x,副本书 2,则存储容量 = 100000 × 86400 × 90 × 10 ÷ 2 ÷ 1e12 ≈ 38.9 TB。建议预留 30% 余量,实际需要约 50TB。
Q20. Single-Node 和 Cluster 模式可以混合使用吗?
可以,用 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 可扩展性强。
十、Roadmap:后续预告
本篇覆盖了 Single-Node 和 Cluster 模式的核心架构差异,但还有几条主线尚未展开:
- #03 架构演进:从 TSDB 到 MergeSet 的设计取舍——理解 MergeSet 相比 LSM Tree 的优势
- #04 整体数据流:一条监控数据的完整生命周期——从 HTTP 请求到 Part 文件的完整链路
- #16 协议接入层:12 种数据协议的统一入口——Remote Write、InfluxDB、OpenTelemetry 如何被解析
- #20 写入核心链路:Storage.Add() 的三段式处理——数据是如何被写入存储的
- #201 Cluster 架构:vminsert/vmstorage/vmselect 分布式协作——Cluster 模式的完整源码分析
本文参考与源码链接:
• app/victoria-metrics/main.go · Single-Node 入口
• app/vminsert/ · Cluster 写入入口
• app/vmstorage/ · Cluster 存储节点
• app/vmselect/ · Cluster 查询节点
• lib/storage/ · 存储核心层
• lib/consistenthash/ · 一致性哈希路由
• VictoriaMetrics Cluster 官方文档

浙公网安备 33010602011771号