VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 多租户架构——accountID/projectID 与 tenant 隔离
VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 多租户架构:accountID/projectID 与 tenant 隔离
当你需要在同一个 VictoriaMetrics 实例中为多个客户或团队提供监控服务时,如何保证数据隔离?当某个租户的写入量突然飙升,如何防止它影响其他租户的查询性能?当你需要按租户进行资源配额管理时,VM 是如何实现的?
读完本篇,你应该能回答:VictoriaMetrics 的多租户模型是如何设计的?accountID 和 projectID 的关系是什么?Cluster 模式下如何实现 tenant 级别的资源隔离?写入和查询路径上 tenant 信息是如何传递的?
lib/auth/auth.go ← Token 结构体(AccountID/ProjectID)+ NewToken 解析
lib/tenantmetrics/counter_map.go ← TenantID 结构体 + per-tenant 指标统计
app/vminsert/main.go ← Cluster 写入入口 RequestHandler
app/vmselect/main.go ← Cluster 查询入口 Init
docs/victoriametrics/Cluster-VictoriaMetrics.md ← 官方多租户文档
VictoriaMetrics Multi-Tenant accountID projectID Tenant隔离 Cluster模式 资源配额 v1.146.0
学习重点提示 — 建议先通读全文,再重点回顾标注内容
重点掌握(必须)
- TenantID 结构:lib/auth/auth.go 中 Token 的两字段设计
- 写入路径 tenant 传递:app/vminsert/main.go 的 RequestHandler 如何处理 tenant
- 查询路径 tenant 隔离:app/vmselect/main.go 的 Init 如何注册路由
- 存储层 tenant 路由:数据在各 vmstorage 节点间均匀分布
次重点(了解即可)
- Enterprise 版本的配额管理特性
- Single-Node 模式下的多租户支持
- TenantID 的 URL 编码格式
文章目录
一、问题的起点:为什么需要多租户?
思考记忆提示 — 多租户是云服务和企业级部署的基础——理解需求才能理解设计
- 多租户可以降低运维成本:一个实例服务多个客户
- 多租户可以实现资源隔离:防止某个租户影响其他租户
- 多租户支持计费和配额管理:按租户统计资源使用
- 面试高频提问:VM 的多租户是如何实现的?和 Prometheus 的 federation 有什么区别?
在 SaaS 监控服务、企业内部监控平台等场景中,多租户是刚需。一个监控平台需要为多个客户或团队提供服务,每个客户的数据必须严格隔离,防止"串租"现象。同时,平台运营方需要按租户进行资源配额管理和计费。
多租户在 VM 中不是什么额外的"附加功能",而是从数据结构层面就设计好的基础模型。我们直接从 lib/auth/auth.go 出发,剖析两字段设计的真实实现:
源码视角一:Token 结构体只有两个 uint32 字段
读 lib/auth/auth.go 第 10-13 行,Token 结构体的定义极简:
type Token struct {
AccountID uint32
ProjectID uint32
}
没有第三个字段,没有 version,没有 metadata。VM 的多租户模型从数据结构上就是两个字段,非常克制。为什么要用两个 uint32 而不是一个大整数?答案在 lib/auth/auth.go 第 23 行的 String() 方法:
func (t *Token) String() string {
if t.ProjectID == 0 {
return fmt.Sprintf("%d", t.AccountID)
}
return fmt.Sprintf("%d:%d", t.AccountID, t.ProjectID)
}
当 ProjectID == 0 时,只输出 accountID。这就是 Cluster URL 格式 /insert/{accountID}/... 而非 /insert/{accountID}:{projectID}/... 的根本原因——projectID 为 0 时可以省略。
源码视角二:Init 方法的解析逻辑决定了 URL 格式
读 lib/auth/auth.go 第 46-66 行的 Init 方法:
func (t *Token) Init(authToken string) error {
tmp := strings.Split(authToken, ":")
if len(tmp) > 2 {
return fmt.Errorf("unexpected number of items...")
}
n, err := strconv.ParseUint(tmp[0], 10, 32)
accountID := uint32(n)
projectID := uint32(0)
if len(tmp) > 1 {
n, err := strconv.ParseUint(tmp[1], 10, 32)
projectID = uint32(n)
}
t.Set(accountID, projectID)
return nil
}
关键点:最多 Split 出 2 段(禁止 3 段);如果只有 1 段,projectID 默认为 0。这就是为什么 Single-Node 用 ?tenant=123:456 而 Cluster 用 URL 路径中的 accountID(projectID 放 URL 参数或省略)。
源码视角三:multitenant 特殊值
读 lib/auth/auth.go 第 35-43 行:
func NewTokenPossibleMultitenant(authToken string) (*Token, error) {
if authToken == "multitenant" {
return nil, nil
}
return NewToken(authToken)
}
当传入 "multitenant" 时返回 nil Token。这是 Cluster 多租户端点(如 /insert/multitenant/prometheus/...)的实现基础——内部用 nil Token 区分"未指定具体租户"的请求。
源码视角总结:VM 多租户的 3 个设计原则
- 1 个 Token 两个字段:AccountID uint32 + ProjectID uint32,范围 [0, 2^32)
- 1 个省略规则:ProjectID == 0 时可省略,体现在 String() 和 URL 格式
- 1 个特殊值:"multitenant" 返回 nil Token,支持多租户端点
1.1 多租户 vs 单租户
首先需要明确多租户和单租户的区别:
| 特性 | 单租户模式 | 多租户模式 |
|---|---|---|
| 部署架构 | 每个客户独立部署一个 VM 实例 | 多个客户共享一个 VM 实例 |
| 数据隔离 | 物理隔离(不同服务器) | 逻辑隔离(tenant 命名空间) |
| 运维成本 | 高(每个实例单独维护) | 低(统一管理) |
| 资源利用率 | 低(资源可能闲置) | 高(资源共享) |
| 适用场景 | 大型企业、关键业务 | SaaS 服务、中小型客户 |
1.2 VM 多租户 vs Prometheus Federation
Prometheus 通过 Federation 实现多实例聚合,但这是"伪多租户"——每个 Prometheus 实例是独立的,数据聚合只是视图层面的,真正的数据隔离靠的是物理分离。
注意
Prometheus Federation 不是真正的多租户。每个 Prometheus 实例是独立的物理部署,只是通过 Federation 将数据聚合到中心节点。数据写入路径上没有 tenant 标识,无法实现真正的资源隔离和配额管理。VictoriaMetrics 的多租户是在数据写入路径上内嵌 tenant 信息,存储层天然支持 tenant 级别的隔离。
二、TenantID 架构:accountID/projectID 两段式设计
思考记忆提示 — TenantID 是 VM 多租户的核心——理解它的结构就理解了整个多租户模型
- TenantID = accountID + projectID,两段式设计
- accountID 是"账户",projectID 是"项目",可以理解为"公司 + 部门"
- 面试高频提问:accountID 和 projectID 的关系是什么?为什么不用单一段式?
2.1 Token 的数据结构
VictoriaMetrics 的 Token 采用两段式设计:accountID/projectID。这个设计参考了 Google Cloud 的项目模型,提供了两层隔离能力。注意:真实源码中没有名为 TenantID 的结构体,实际类型名是 Token(定义在 lib/auth/auth.go)。
// lib/auth/auth.go(Token 数据结构)
// VictoriaMetrics v1.146.0
type Token struct {
// AccountID:账户 ID,范围 [0, 2^32)
// 同一账户下的所有项目共享账户级别的配额
AccountID uint32
// ProjectID:项目 ID,范围 [0, 2^32)
// 同一项目下的数据属于同一个业务线或服务
ProjectID uint32
}
// Token 的字符串表示
// 注意:当 ProjectID == 0 时只输出 AccountID
func (t *Token) String() string {
if t.ProjectID == 0 {
return fmt.Sprintf("%d", t.AccountID)
}
return fmt.Sprintf("%d:%d", t.AccountID, t.ProjectID)
}
// 解析字符串为 Token
// 格式:accountID:projectID 或只有 accountID
func (t *Token) Init(authToken string) error {
tmp := strings.Split(authToken, ":")
if len(tmp) > 2 {
return fmt.Errorf("unexpected number of items...")
}
accountID := uint32(0)
projectID := uint32(0)
n, _ := strconv.ParseUint(tmp[0], 10, 32)
accountID = uint32(n)
if len(tmp) > 1 {
n, _ := strconv.ParseUint(tmp[1], 10, 32)
projectID = uint32(n)
}
t.Set(accountID, projectID)
return nil
}
设计精髓
两段式设计的优势在于提供了灵活的多级隔离能力:
- 同项目隔离:相同 projectID、不同 accountID 的数据完全隔离
- 账户级配额:可以按 accountID 设置资源配额,所有项目共享
- 项目级配额:可以按 projectID 设置更细粒度的配额
- 审计方便:可以按 accountID 聚合项目,生成账户级别的报表
如果使用单一段式(如只用一个 tenantID),就无法实现账户级别的聚合和配额管理。
2.2 TenantID 的 URL 编码
在 HTTP API 中,TenantID 通过 URL 路径传递,Cluster 模式只用 accountID 作为路径段,projectID 可省略。
┌─────────────────────────────────────────────────────────────────────────┐
│ TenantID URL 编码格式 │
│ │
│ Single-Node 模式(URL 参数): │
│ /api/v1/write?tenant=123:456 │
│ /api/v1/query?tenant=123:456 │
│ │
│ Cluster 模式(URL 路径,★ 关键点): │
│ /insert/{accountID}/prometheus/api/v1/write │
│ /select/{accountID}/prometheus/api/v1/query │
│ │
│ 说明: │
│ - 路径中只用 accountID,projectID 省略时为 0 │
│ - 如果需要精确指定 projectID,通过 HTTP 头指定: │
│ AccountID: {accountID} │
│ ProjectID: {projectID} │
│ - 多租户端点:/insert/multitenant/...(通过 vm_account_id 标签指定) │
└─────────────────────────────────────────────────────────────────────────┘
注意:Cluster URL 中不存在 /insert/123:456/... 格式。accountID 和 projectID 不能同时出现在 URL 路径中,projectID 只能通过 HTTP 头或标签传递。这与 Single-Node 的 ?tenant=123:456 格式不同。
三、写入路径:vminsert 如何处理 tenant
思考记忆提示 — vminsert 是 Cluster 模式的写入入口——理解它如何处理 tenant 是理解多租户写入的关键
- vminsert 接收各协议数据,统一写入路径
- 数据在各 vmstorage 节点间均匀分布,不是按 tenant 路由到特定节点
- 面试高频提问:Cluster 模式下 tenant 数据是如何分布的?
3.1 vminsert 的 HTTP 处理
vminsert 是 VictoriaMetrics Cluster 模式的写入入口,接收来自 Prometheus、vmagent 等客户端的写入请求。读 app/vminsert/main.go,其核心是 RequestHandler 函数,通过 switch 语句分发到各协议的 InsertHandler。
// app/vminsert/main.go(vminsert 主入口)
// VictoriaMetrics v1.146.0
// RequestHandler 是 vminsert 的核心 HTTP 处理函数
func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
path := strings.ReplaceAll(r.URL.Path, "//", "/")
// 处理静态资源
if strings.HasPrefix(path, "/static") {
staticServer.ServeHTTP(w, r)
return true
}
switch path {
case "/prometheus/api/v1/write", "/api/v1/write", "/api/v1/push":
// Prometheus Remote Write 协议入口
prometheusWriteRequests.Inc()
if err := promremotewrite.InsertHandler(r); err != nil {
httpserver.Errorf(w, r, "%s", err)
return true
}
w.WriteHeader(http.StatusNoContent)
return true
// ... 其他协议分支
}
return false
}
3.2 Cluster 数据分布机制
根据 docs/victoriametrics/Cluster-VictoriaMetrics.md 官方文档:数据在各 vmstorage 节点间均匀分布(evenly spread),不是按 tenant 哈希到特定节点。这意味着:
- 同一个 tenant 的数据可能分布在多个 vmstorage 节点上
- 查询时 vmselect 需要 scatter-gather 跨节点聚合
- 这与"一致性哈希按 tenant 路由"的设计完全不同
Cluster 数据分布模型:
┌────────────┐ ┌──────────────────────────────────────────────┐
│ vminsert │────▶│ vmstorage 节点池 │
│ (写入) │ │ │
└────────────┘ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ storage1 │ │ storage2 │ │ storage3 │ │
│ │ 均匀分布 │ │ 均匀分布 │ │ 均匀分布 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└──────────────────────────────────────────────┘
关键点:
- 数据按时间或 ID 范围均匀分片
- 同一个 tenant 的数据可能分布在多个 storage 节点
- 查询时需要跨节点聚合(scatter-gather)
小贴士 — vs Prometheus 的 Federation
Prometheus Federation 是"每个 Prometheus 实例独立存储"的聚合,物理隔离但资源利用率低。VictoriaMetrics Cluster 是"数据分片均匀分布"的共享存储,逻辑隔离且资源利用率高。
四、查询路径:vmselect 如何按 tenant 过滤
思考记忆提示 — vmselect 是 Cluster 模式的查询入口——理解它如何处理 tenant 过滤是理解多租户查询的关键
- vmselect 解析 URL 路径中的 accountID 信息
- 将查询请求分发到多个 vmstorage 节点并聚合结果
- 面试高频提问:查询时如何保证只能访问自己有权限的 tenant 数据?
4.1 vmselect 的初始化
vmselect 是 VictoriaMetrics Cluster 模式的查询入口,读 app/vmselect/main.go 第 49-60 行的 Init 函数,可以看到它初始化了网络存储、Rollup 缓存等组件。
// app/vmselect/main.go(vmselect 主入口)
// VictoriaMetrics v1.146.0
// Init 初始化 vmselect
func Init(vmselectMaxConcurrentRequests int, vmselectMaxQueueDuration time.Duration) {
tmpDirPath := vmstorage.DataPath() + "/tmp"
fs.MustRemoveDirContents(tmpDirPath)
netstorage.InitTmpBlocksDir(tmpDirPath)
promql.InitRollupResultCache(vmstorage.DataPath() + "/cache/rollupResult")
maxConcurrentRequests = vmselectMaxConcurrentRequests
maxQueueDuration = vmselectMaxQueueDuration
concurrencyLimitCh = make(chan struct{}, maxConcurrentRequests)
initVMUIConfig()
// ... 更多初始化
}
4.2 多租户查询
根据官方文档,vmselect 支持三种 tenant 指定方式:
- URL 路径:/select/{accountID}/prometheus/api/v1/query
- HTTP 头:AccountID 和 ProjectID 头
- 多租户端点:/select/multitenant/...(通过 vm_account_id 标签过滤)
设计精髓
VM 多租户隔离的核心是数据路径隔离:
- 存储层隔离:每个 tenant 的数据通过目录或分片键隔离
- 查询层隔离:查询时只扫描对应 tenant 的数据分片
- 权限验证:通过 vmauth/vmgateway 在入口处做 API Key 和 JWT 验证
五、存储路径:vmstorage 如何按 tenant 分区
思考记忆提示 — vmstorage 是数据存储的核心——理解它的分区机制是理解多租户存储的关键
- 数据在 vmstorage 节点内按分片键分区
- 不同 tenant 的数据在同一 vmstorage 节点内通过分区键隔离
- 面试高频提问:不同 tenant 的数据会混在一起吗?如何保证隔离?
5.1 数据目录结构
vmstorage 节点上的数据按分区存储,读 lib/storage/filenames.go,核心目录结构如下:
vmstorage 数据目录结构:
storageDataPath/
│
├── data/ # 数据根目录(见 filenames.go dataDirname)
│ ├── 20230601/ # 按月分区(date-based partitioning)
│ │ ├── small/ # 小 Part(未合并)
│ │ └── big/ # 大 Part(合并后)
│ ├── 20230602/
│ └── ...
│
├── indexdb/ # 索引数据库(见 filenames.go indexdbDirname)
│ └── ...
│
├── cache/ # 缓存目录(见 filenames.go cacheDirname)
│ └── ...
│
└── metadata/ # 元数据目录
└── ...
关键点:
- 按时间分区(date-based partitioning),不是按 tenant 分区
- 数据在各 vmstorage 节点间均匀分布(evenly spread)
- 同一 tenant 的数据可能跨多个日期分区
注意:vmstorage 数据按时间分区(date-based partitioning),不是按 tenant 目录隔离。不存在 data/{accountID}/{projectID}/ 嵌套目录结构。Tenant 隔离是通过数据分片键和查询时过滤实现的,不是通过文件系统目录实现的。
5.2 Tenant 级别的资源统计
vmstorage 维护每个 tenant 的资源使用统计,读 lib/tenantmetrics/counter_map.go,TenantID 结构体和 CounterMap 用于 per-tenant 指标追踪:
// lib/tenantmetrics/counter_map.go(per-tenant 指标统计)
// VictoriaMetrics v1.146.0
// TenantID 定义了指标的 tenant 维度
type TenantID struct {
AccountID uint32
ProjectID uint32
}
// CounterMap 是按 tenant 分组的计数器 map
type CounterMap struct {
metric string
m sync.Map
mt atomic.Value // multitenant 的聚合计数器
}
// GetByTenant 返回指定 tenant 的计数器
func (cm *CounterMap) GetByTenant(key *TenantID) *metrics.Counter {
if key == nil {
// multitenant:返回聚合计数器
return cm.getMultitenantCounter()
}
// ... 按 tenant key 返回或创建计数器
}
Per-tenant 指标通过 /metrics 端点暴露,文档见 docs/victoriametrics/PerTenantStatistic.md。
六、Single-Node 模式下的多租户支持
思考记忆提示 — Single-Node 模式也支持多租户——只是没有 Cluster 模式的分布式能力
- Single-Node 模式下多租户通过URL 参数 ?tenant= 传递
- 数据在本地按 tenant 目录隔离存储
- 面试高频提问:Single-Node 模式支持多少个租户?有没有限制?
6.1 Single-Node 的多租户 API
Single-Node 模式支持通过 tenant 参数指定租户,数据在本地按 tenant 隔离存储。
Single-Node 模式多租户 API:
写入:
POST /api/v1/write?tenant=123:456
Body: metric_name{label="value"} value timestamp
查询:
GET /api/v1/query?tenant=123:456&query=up
GET /api/v1/query_range?tenant=123:456&query=up&start=...&end=...
说明:
- tenant 参数格式:accountID:projectID
- 如果不指定 tenant,默认为 0:0(匿名租户)
- accountID 和 projectID 都是 uint32,范围 [0, 2^32)
- 性能取决于总 active series 数量,非 tenant 数量
七、Enterprise 版本的配额管理
思考记忆提示 — Enterprise 版本提供更强大的多租户管理能力——包括资源配额和访问控制
- 资源配额:限制每个租户的写入速率、存储大小、时间序列数量
- 访问控制:基于 API Key 的细粒度权限管理
- 配额预检:写入前检查配额,超限则拒绝
- 面试高频提问:Enterprise 版本的配额管理是如何实现的?
7.1 资源配额类型
VictoriaMetrics Enterprise 版本支持多种类型的资源配额:
| 配额类型 | 说明 | 作用 |
|---|---|---|
| ingestionRate | 写入速率(samples/s) | 防止租户写入量过大影响其他租户 |
| storageSize | 存储大小(字节) | 限制租户使用的磁盘空间 |
| seriesCount | 时间序列数量 | 限制租户创建的指标数量,防止高基数 |
| queriesPerSecond | 查询速率(queries/s) | 限制租户的查询频率 |
| queryResolution | 查询分辨率(最大时间范围) | 防止租户查询过大时间范围 |
八、FAQ:常见疑问
思考记忆提示 — FAQ 是全篇的"临考前速背"模块,20 组覆盖全链路
- Q1-Q5 围绕 TenantID 结构:accountID/projectID 设计原理
- Q6-Q10 围绕写入路径:vminsert tenant 解析和数据分布
- Q11-Q15 围绕查询路径:vmselect tenant 过滤和权限验证
- Q16-Q20 围绕存储和配额:vmstorage 分区和 Enterprise 配额
Q1. accountID 和 projectID 的关系是什么?
accountID 是账户级别,projectID 是项目级别,类似于"公司 + 部门"的关系。同一 accountID 下的所有 projectID 共享账户级别的配额。accountID/projectID 的组合唯一确定一个租户。例如,accountID=123, projectID=456 表示"账户 123 的项目 456"。
Q2. 为什么使用两段式设计而不是单一段式?
两段式设计支持账户级和项目级的双重隔离和配额管理。单一段式(如只有一个 tenantID)无法实现账户级别的聚合。例如,云服务商可以按账户设置总配额,账户内的多个项目共享该配额;如果只有一个 tenantID,就无法区分"账户 A 的总配额"和"账户 B 的总配额"。
Q3. TenantID 的最大值是多少?能支持多少租户?
accountID 和 projectID 都是 uint32,最大值约 42 亿。理论上可以支持数十亿个租户。实际限制取决于总 active series 数量和存储能力。建议单个 vmstorage 节点支持 1000-10000 个活跃租户。
Q4. Cluster 模式下 tenant 数据是如何分布的?
数据在各 vmstorage 节点间均匀分布(evenly spread),不是按 tenant 哈希到特定节点。这意味着同一个 tenant 的数据可能分布在多个 vmstorage 节点上。查询时 vmselect 需要 scatter-gather 跨节点聚合结果。这种设计保证了负载均衡,避免单个节点成为热点。
Q5. 如果 vmstorage 节点故障,同一个 tenant 的数据会丢失吗?
如果启用了副本机制,数据不会丢失,但可能短暂不可用。Enterprise 版本支持 vmstorage 节点的副本复制。当主节点故障时,从节点可以接管服务,实现高可用。
Q6. vminsert 如何解析 URL 中的 tenant 信息?
通过解析 URL 路径中的 accountID。路径格式为 /insert/{accountID}/prometheus/api/v1/write。vminsert 从路径中提取 accountID 字符串,然后通过 lib/auth/auth.go 的 NewToken 方法解析为 Token 结构体。
Q7. 写入时如果不指定 tenant,数据会存储在哪里?
如果不指定 tenant,默认为 0:0(匿名租户)。数据会存储在 accountID=0, projectID=0 的命名空间下。在多租户环境中,建议始终明确指定 tenant,避免数据混淆。
Q8. 如何在 Prometheus 配置中指定 tenant?
通过 remote_write URL 中的路径段指定。配置示例:url: "http://vminsert:8480/insert/123/prometheus/api/v1/write"(只用 accountID,projectID 省略时为 0)。多个 Prometheus 实例可以配置不同的 accountID,实现数据隔离。
Q9. vmselect 如何验证请求者是否有权限访问某个 tenant?
通过 vmauth 或 vmgateway 在入口处做权限验证。Enterprise 版本支持基于 API Key 的权限管理。每个 API Key 绑定到特定的 tenant 和权限级别。权限验证在 Cluster 入口网关层完成,不是在 vmselect 内部。
Q10. 查询时如果指定了错误的 tenant,会返回什么错误?
如果 API Key 没有权限访问该 tenant,返回 403 Forbidden 或空结果集。错误消息取决于具体配置。客户端需要确保使用正确的 API Key 和 tenant ID。
Q11. 同一 vmstorage 节点上不同 tenant 的数据会相互影响吗?
正常情况下不会,因为查询路径通过 tenant 过滤。每个 tenant 的数据通过分片键隔离,查询时只扫描对应分片。如果某个 tenant 的写入量突然飙升,可能影响同一节点的 CPU 和磁盘 I/O,但这是资源竞争问题,不是数据隔离问题。
Q12. 如何监控每个 tenant 的资源使用情况?
通过 /metrics 端点查看 tenant 级别的指标。VictoriaMetrics 暴露各种 per-tenant 统计指标,文档见 docs/victoriametrics/PerTenantStatistic.md。使用 /admin/tenants 接口可以查看所有租户的概览。
Q13. Single-Node 模式和 Cluster 模式的多租户有什么区别?
Single-Node 模式在本地按 tenant 隔离存储,Cluster 模式将数据分布到多个 vmstorage 节点。Single-Node 适合中小规模场景,多个 tenant 共享本地资源。Cluster 模式适合大规模场景,可以按 tenant 进行水平扩展,实现更好的资源隔离。
Q14. Enterprise 版本的配额管理有哪些类型?
支持五种配额类型:ingestionRate(写入速率)、storageSize(存储大小)、seriesCount(时间序列数量)、queriesPerSecond(查询速率)、queryResolution(查询分辨率)。这些配额可以在账户级别或项目级别设置,实现灵活的配额管理策略。
Q15. 配额超限后会发生什么?
配额超限后,写入请求会被拒绝,返回配额超限错误。错误消息包含超限的配额类型。客户端需要根据错误类型调整写入策略(如降低写入速率、删除过期数据释放空间)。
Q16. 如何为新租户创建 API Key?
通过 vmauth 或 Enterprise 管理界面创建 API Key。Enterprise 版本提供了 CLI 工具和 Web UI 来管理 API Key。每个 API Key 绑定到特定的 tenant 和权限级别(如只读、读写、管理员)。
Q17. 可以在运行时修改租户的配额吗?
可以,Enterprise 版本支持运行时动态调整配额。通过管理 API 或 CLI 工具可以实时修改租户的配额,新配额立即生效,不需要重启服务。
Q18. 多租户环境下如何进行故障排查?
通过 tenant 参数过滤日志和指标,定位问题租户。所有日志和指标都包含 tenant 标签。使用 /admin/tenants 接口可以查看所有租户的概览,使用 /internal/tenant/{tenantID}/status 接口可以查看特定租户的详细状态。
Q19. 如何迁移现有数据到多租户架构?
可以使用 vmctl 工具将现有数据迁移到指定 tenant。迁移时指定目标 tenant 参数,数据会被写入目标 tenant 的存储路径。迁移完成后,原有数据和新数据在逻辑上完全隔离。
Q20. 多租户架构下如何保证数据安全?
通过多层安全机制保证数据安全:存储层分片隔离、API 层权限验证、网络层 TLS 加密。存储层通过分片键隔离不同 tenant 的数据。API 层通过 API Key 或 JWT 验证权限。网络层通过 TLS 加密传输数据。
全篇必记总纲
VictoriaMetrics 多租户架构的核心是两段式 Token(accountID/projectID)+ 数据均匀分片 + 分片键隔离:写入时 vminsert 接收各协议数据,数据在各 vmstorage 节点间均匀分布,查询时 vmselect 通过 scatter-gather 聚合多节点结果并按 tenant 过滤。Token 结构体定义在 lib/auth/auth.go,per-tenant 指标追踪在 lib/tenantmetrics/。Enterprise 版本额外提供配额管理和细粒度权限控制。
九、Roadmap:后续预告
本篇覆盖了 VictoriaMetrics 的多租户架构设计,但还有很多细节尚未展开:
- #07 Go 工程实践:Goroutine 池/atomic/零拷贝/sync.Pool——理解 VM 的高性能实现
- #08 模块依赖图:从 import 语句看组件关系——理解 VM 的整体架构
- #09 性能模型:写入吞吐/查询延迟/内存占用的数学模型——理解 VM 的性能上限
- #10 与其他 TSDB 对比:Prometheus/InfluxDB/Thanos/VM——理解 VM 在竞品中的定位
- #153 多租户隔离:accountID/projectID 资源配额强制——Enterprise 级别的配额管理
本文参考与源码链接:
• lib/auth/auth.go · Token 结构体(AccountID/ProjectID)
• lib/tenantmetrics/counter_map.go · TenantID + per-tenant 统计
• app/vminsert/ · 写入入口
• app/vmselect/ · 查询入口
• VictoriaMetrics Cluster 架构文档

浙公网安备 33010602011771号