- 时序数据库
- 后端
- 数据存储
- 可观测性
- 云原生
【免费下载链接】mimir
Grafana Mimir provides horizontally scalable, highly available, multi-tenant, long-term storage for Prometheus.
本文是一份关于 Grafana Mimir 社区设计提案(docs/proposals/reduce-multitenancy-cost.md,编号 0003 "Tenancy multiplexing")的深度解读。该提案直指 Mimir 在多租户场景下的核心成本痛点:每新增一个租户,ingester 就要为其创建/打开一个独立的 TSDB,导致 CPU、内存、磁盘文件与对象存储对象数量随租户数线性膨胀。文章完整还原了提案的三种候选方案(外部网关、基于标签的租户复用、底层 TSDB 开销优化),并结合当前仓库源码验证其技术依据与实现现状。读完本文,你将理解 Mimir 单租户 TSDB 架构的资源开销模型,掌握"租户复用(tenancy multiplexing)"的核心设计思路及其对各组件(ingester、store-gateway、compactor、ruler)的影响边界。
背景:Mimir 的三大组件与"每租户一个 TSDB"模型
提案开篇先界定了三个与多租户资源开销直接相关的核心组件:
- compactor:通过把某个租户的多个小 block 压缩合并为更大的优化 block,来提升查询性能;
- ingester:负责把每个租户的数据持久化到各自的 TSDB 中;
- store-gateway:负责提供对对象存储中 block 的访问能力。
从当前仓库源码可以印证这一"每租户一个 TSDB"的实现模型。在 pkg/ingester/ingester.go 中,ingester 内部维护了一张以userID为键的 TSDB 表:
tsdbs map[string]*userTSDB // tsdb sharded by userID对应的创建逻辑位于 pkg/ingester/ingester_tsdb.go 的getOrCreateTSDB(userID):当某个租户首次写入时,该函数会为它分配独立的目录(i.cfg.BlocksStorageConfig.TSDB.BlocksDir(userID))、独立的 Prometheus 注册表(prometheus.NewRegistry())和独立的userTSDB实例,并维护ingester_inmemory_tenants这一计数器(pkg/ingester/ingester.go)。当内存租户数超过实例限制MaxInMemoryTenants时,新的写入会被以reasonIngesterMaxTenants拒绝。
问题陈述:租户数量带来的资源开销放大效应
提案明确指出问题的本质:对于每个托管在 ingester 上的新租户,都会创建一个新的 TSDB;而 TSDB 本身是重量级对象,需要 CPU 调度、内存缓冲、磁盘文件句柄和对象存储 bucket 对象来支撑。再加上 Mimir 本身的负载均衡、复制因子(replication factor)与 shuffle sharding 机制,开销还会被副本数进一步放大。
提案给出了一个直观的量化示例:
- 每新增一个租户,ingester 大约消耗6MB 内存;
- 假设集群运行 3 个 ingester、复制因子为 3;
- 则单租户带来的内存开销为18MB(3 个 ingester × 1 个租户 × 6MB/租户)。
随着租户数量增长和 Mimir 水平扩展,这一开销会不断累积。值得注意的是,问题并不局限于 ingester——所有对租户数量及其关联 TSDB 敏感的组件都会受到波及,例如 store-gateway 需要按租户加载和维护 block 索引,compactor 需要按租户扫描和调度压缩任务。
源码层面可以佐证"每个租户一个 TSDB"带来的管理成本:ingester 通过tsdbsMtx互斥锁保护 TSDB 表,且为了控制并发建库压力,源码中专门定义了maxTSDBOpenWithoutConcurrency = 10(pkg/ingester/ingester.go):低于该值时租户 TSDB 顺序打开,高于该值才并发打开。这本身就是"租户数量成为资源瓶颈"的直接证据。
目标与非目标
目标:
- 从最终用户视角看,Mimir 提供的功能集与今天完全一致,没有破坏性变更;
- 降低高租户数量对性能的影响,从而让 Mimir能够支撑大规模租户数。
非目标:
- 不提供从常规多租户(regular multitenancy)到租户复用(tenancy multiplexing)的迁移路径。
这一点很重要:提案即使落地,也明确放弃了自动迁移能力,意味着切换租户复用模式需要从部署层面重新规划。
方案 0:什么都不做——由外部网关实现租户复用
第一个候选方案是把问题"挡在 Mimir 之外":由运维方自行实现一个 Mimir 网关,对外暴露租户复用能力,对内只使用唯一的 Mimir 租户作为后端。网关负责修改并代理所有发往 Mimir 集群的请求。
提案强调:该网关不能与 GEM(Grafana Enterprise Metrics)网关混淆,两者是完全不同的东西。
写路径(Write path)
- distributor 侧的限额:由于 distributor 是无状态进程,其强制执行的各项限额在另一个前端中"照搬"实现是直截了当的。如果网关用 Go 编写,代码甚至可以部分复用;
- ingester 侧的限额(pkg/ingester/ingester.go)则要复杂得多,需要逐项重新实现。
per_user_series_limit(单用户序列数上限)
网关需要在内存中维护"该租户已拥有的序列集合"的表示,并且这个表示必须满足四个硬性要求:
- 访问快(每条写入的每个序列都要查一次);
- 占用小(这些信息要在网关集群成员之间交换);
- 可合并(用于聚合各成员的信息);
- 可序列化(便于交换传输)。
提案对比了两种实现路径:
- 朴素方案:把所有租户的所有序列元数据都存在内存里。这会浪费数百 GB内存,且难以在集群成员间交换,直接否决;
- 推荐方案:采用HyperLogLog++ 基数估计器。它用有限的空间(每个估计器仅几 KB)维护数据集基数的表示,可序列化、可合并,误差受估计器精度控制(通常为几个百分点内)。
在水平扩展的网关集群中,成员之间通过memberlist + gRPC或Kafka 之类的 pub/sub 服务共享这些估计器,保证所有成员对每个租户的基数有一致的、足够准确的视图。由于"每个 meta-tenant 一个估计器"导致数据量偏大,提案建议只传播最近更新过的估计器,例如:每 15 秒交换一次,仅同步最近 1 分钟内更新过的估计器。
per_metric_series_limit(单指标序列数上限)
可以用与per_user_series_limit相同的方式实现,但提案指出这会带来显著的内存消耗——因为"每指标序列基数"与"租户基数"成正比,估计器的数量会成倍增加。
与租户无关的错误:直接透传
以下错误并非租户相关,网关无需自行计算,可以直接从后端转发:
sample-out-of-order(乱序样本)sample-too-old(样本过旧)sample-too-far-in-future(样本时间戳过未来)new-value-for-timestamp(同一时间戳写入新值)sample-timestamp-too-oldinvalid-native-histogram(非法原生直方图)
在源码中,这些错误原因都以常量形式定义在 pkg/ingester/ingester.go(如reasonSampleOutOfOrder、reasonPerUserSeriesLimit、reasonPerMetricSeriesLimit、reasonInvalidNativeHistogram等),这也印证了提案把它们视为"逐样本、逐序列"的通用校验点、与租户归属解耦的判断。
读路径(Read path)
网关需要在查询时动态改写查询,为查询的每个节点附加正确的标签/值(meta-tenant 标签),从而把查询限定在正确的租户数据范围内。
优点:
- Mimir 侧无需任何改动,所有逻辑都隔离在网关之外;
- 运维方可以自由构建任何定制化解决方案。
缺点:
- 网关引入额外一跳,增加延迟;
- 限额校验会被重复计算多次;
- 所有面临同样问题的不同运维团队,都得各自独立解决一遍,无法复用。
方案 1(核心提案):通过标签实现租户复用(Tenancy multiplexing through label)
这是提案的核心方案:让 Mimir 能够在单个 TSDB 中托管多个租户,而不是每个租户一个 TSDB。租户之间的区分完全靠一个标签完成。
租户复用通过一份配置文件开启,并与常规多租户配置文件并存。提案给出了配置示例:
# When set to true, incoming HTTP requests must specify tenant ID in HTTP # X-Scope-OrgId header. When set to false, tenant ID from -auth.no-auth-tenant # is used instead. # CLI flag: -auth.multitenancy-enabled [multitenancy_enabled: <boolean> | default = true] # The mode of multitenancy to use. Either "standard" or "multiplexing". # When using the "multiplexing" multitenancy several tenants share the same TSDB. # CLI flag: -auth.multitenancy-mode [multitenancy_mode: <string> | default = "standard"]对照当前仓库源码可以发现,提案设想的配置项并非凭空捏造:multitenancy_enabled已经在 pkg/mimir/mimir.go 中定义,其 CLI 形式正是-auth.multitenancy-enabled,默认值为true,说明"传入的 HTTP 请求必须通过X-Scope-OrgId头携带租户 ID;为false时使用-auth.no-auth-tenant指定的租户(默认anonymous)"(pkg/mimir/mimir.go)。而multitenancy_mode属于提案中的新增量:当前仓库中尚未出现该参数的实现,这也说明该提案尚处于设计/接受阶段(提案文档draft: true,且关联 PR 为 grafana/mimir#9978),属于"已接受但未落地"的设计蓝图。
提案强调:所有负责操作 block 和/或 TSDB 的组件都必须适配租户复用。
写路径(Write path)
写入路径上需要新增一个中间件,负责取出租户 ID,并将其作为标签值附加到每条序列上。该中间件仅在multiplexing模式被选中时启用。
Mimir 的 distributor 已经具备完善的中间件机制可以支撑这一设想:PushWithMiddlewares与wrapPushWithMiddlewares提供了对写入请求的中间件链包装能力(pkg/distributor/distributor.go 与 pkg/distributor/distributor.go),提案所述"写路径中间件"正是复用此类扩展点。
可选优化:为了避免产生单个超大 TSDB,可以将租户分布到固定数量的 TSDB 上,从而减少每个 TSDB 承载的租户数。
无需修改的组件:distributor 与 compactor 当前的行为不受租户复用影响,无需改动。
读路径(Read path)
ingester 与 store-gateway 在查询存储时,不再打开租户自己的 TSDB,而是把租户 ID 作为查询匹配器(matcher)直接在共享 TSDB 内过滤。
这从源码角度是可以印证可行性的:store-gateway 本来就以userID为维度组织 block 与索引加载逻辑(pkg/storegateway/bucket.go),只需把"按 userID 找 block"变成"在共享 block 上按 label matcher 过滤"。此外,Mimir 已有"用标签区分租户"的成熟先例——租户联邦(tenant federation)正是通过给所有指标追加__tenant_id__标签来实现跨租户查询(pkg/mimir/modules.go),提案中的 meta-tenant 标签思路与其同源。
Ruler 与 Alertmanager:基本不变
- Ruler 照常工作:内部模式下 Ruler 运行自己的 distributor,因此必须确保写路径应用与主链路相同的重打标(relabeling)逻辑;远程模式下只涉及读路径变化——过滤由 ingester 和 store-gateway 用租户 ID 完成;
- Alertmanager 多租户照常工作:Ruler 用租户 ID 调用 Alertmanager;
- 租户联邦照常工作:联邦机制不受影响。
限额(Limits)适配:逐组件分析
根据负责执行限额的组件不同,适配程度也不同:
| 组件 | 变更程度 |
|---|---|
| distributor | 无需变更 |
| ingester | max_series_per_user与max_series_per_metric需要以不同方式计算 |
| querier / query-frontend / query-scheduler | 无需变更 |
| compactor | retention 是按 TSDB 维度的;可以在读路径上通过钳制查询起始时间来执行;对比"计算成本 vs 存储成本"后,也可能更划算地让数据保留超过 retention 期,而不是额外处理丢弃过期数据 |
| ruler | 无需变更 |
| alertmanager | 无需变更 |
这里提案给出了一个非常重要的权衡思路:retention 与"租户"解耦后,执行策略从"物理删除"变成"查询时钳制时间范围"。源码佐证了 retention 目前确实是 per-tenant(按 TSDB)维度执行的——例如 block 上传校验中使用c.cfgProvider.CompactorBlocksRetentionPeriod(tenantID)判断 block 是否早于保留期(pkg/compactor/block_upload.go),而租户删除接口也强依赖X-Scope-OrgID认证注入的租户身份(pkg/compactor/tenant_deletion_api.go)。这正是提案在"Cons"中警告的适配难点。
指标(Metrics)适配:逐组件分析
| 组件 | 变更程度 |
|---|---|
| ingester | TSDB 指标不再按用户细分 |
| compactor | 指标按 TSDB 维度统计 |
| store-gateway | 无需变更 |
| distributor | 无需变更 |
| querier / query-frontend / query-scheduler | 无需变更 |
| ruler | 无需变更 |
| alertmanager | 无需变更 |
这一点意味着,租户复用后依赖 per-user 细粒度指标的可观测性会退化到 per-TSDB 粒度,运维需要提前评估监控告警的依赖。
优点
- 每租户开销被极大压缩,可以视为可忽略——这正是解决本提案问题陈述的核心手段。
缺点
- 除非额外修改,否则同一 TSDB 内的所有租户会共享部分限额,尤其是retention,可能成为阻塞点;
- 常规租户模式与租户复用之间没有便捷的双向迁移路径;
- 属于全有或全无:提案没有定义"租户 ID → TSDB"的映射关系,无法针对部分租户精细化配置;
- Compactor 的 block 上传(block upload)功能将无法正常工作:block 上传时"原样"写入 LTS(长期存储),无法在不重写 block 的前提下附加 meta-tenant 标签;
- Compactor 的租户删除功能将失效:它依赖"租户映射到专属 TSDB"这一前提;
- 把 meta-tenant 从一个 TSDB/集群迁移到另一个并不直接,也不容易。
为什么"块上传"与"租户删除"会失效(源码级补充)
这两条 Con 在源码中有明确的对应实现:
- 租户删除:Mimir 的租户删除 API 通过标记租户为"待删除"并清理其专属 TSDB 与 block 来实现,认证层强制要求
X-Scope-OrgID(pkg/compactor/tenant_deletion_api.go)。一旦多个租户共享一个 TSDB,"删除某租户"与"删除该 TSDB"不再等价; - 块上传:上传的 block 校验依赖
CompactorBlocksRetentionPeriod(tenantID)等 per-tenant 判断(pkg/compactor/block_upload.go),且 block 以原始状态写入对象存储,缺少区分租户的标签维度,后续查询无法正确路由。
方案 2:把开销降低至少一个数量级
提案明确声明:该方案仅为完整性而列出,作者团队对其可行性的认知有限,不确定其现实程度。
思路很直接:既然 Mimir 复用了 Prometheus TSDB 及其代码,就可以从两个层面研究如何提升资源效率——
- Mimir 如何使用 TSDB 代码;
- TSDB 代码本身。
优点:
- 无需修改 Mimir 的多租户机制;
- 整个 Prometheus 生态都能受益于这些改进。
缺点:
- 可能难以实现;
- 可能超出 Mimir 的职责范围。
其他参考
提案还指出:业界已有后端成功解决了这一问题,例如VictoriaMetrics 的多租户(cluster-victoriametrics 的 multitenancy)不受此类开销影响。这一对比说明"按租户建独立存储"并非唯一的多租户实现范式,为方案 1 的取舍提供了行业参照。
提案对比与落地现状小结
三个方案的定位差异非常清晰:
- 方案 0(外部网关):把复杂度推到 Mimir 之外,最灵活但延迟、重复校验、重复造轮子的问题突出,且要重写全部租户限额与隔离逻辑;
- 方案 1(标签复用):在 Mimir 内部实现租户复用,每租户开销趋近于零,是解决成本问题最直接的手段,但要承担共享限额(尤其 retention)、无迁移路径、block 上传与租户删除失效等副作用;
- 方案 2(底层优化):从 Prometheus TSDB 本体入手,惠及整个生态,但可行性未知、超出 Mimir 范围。
从当前仓库现状看:multitenancy_enabled(-auth.multitenancy-enabled)与no_auth_tenant(-auth.no-auth-tenant,默认anonymous)已经实现(pkg/mimir/mimir.go),而提案新增的multitenancy_mode(-auth.multitenancy-mode)尚不存在于源码中。这份文档是一份已接受(Accepted)但标为 draft 的设计提案,其价值在于完整呈现了 Mimir 团队对"大规模多租户成本"问题的分析框架与取舍权衡——无论其最终以何种形态落地,提案中对"按租户建 TSDB"的开销模型、写入中间件打标、读路径 matcher 过滤、retention 钳制执行等设计思想的阐述,都对理解 Mimir 的多租户架构演进具有直接的参考意义。
延伸阅读
- 提案原文:docs/proposals/reduce-multitenancy-cost.md
- ingester 每租户 TSDB 表与建库逻辑:pkg/ingester/ingester.go、pkg/ingester/ingester_tsdb.go
- ingester 丢弃样本原因与限额校验:pkg/ingester/ingester.go、pkg/ingester/user_tsdb.go、pkg/ingester/limiter.go
- distributor 写路径中间件机制:pkg/distributor/distributor.go
- store-gateway 按 userID 组织 block:pkg/storegateway/bucket.go
- 租户联邦的
__tenant_id__标签先例:pkg/mimir/modules.go - 多租户认证配置实现:pkg/mimir/mimir.go
- 时序数据库
- 后端
- 数据存储
- 可观测性
- 云原生
【免费下载链接】mimir
Grafana Mimir provides horizontally scalable, highly available, multi-tenant, long-term storage for Prometheus.
相关推荐
WeKan 多租户设计全解析:从"每租户一个进程"到"以组织为租户"的落地实践
WeKan 多租户设计全解析:从"每租户一个进程"到"以组织为租户"的落地实践 导读 本文以 docs/Design/Multitenancy/Multiten
后端前端协同办公NetBox 多租户模型(Tenancy)深入解析:租户、租户组与跨对象资源归属
NetBox 多租户模型(Tenancy)深入解析:租户、租户组与跨对象资源归属 NetBox 的 Tenancy(多租户) 是其数据模型的一项核心横切能力:绝
后端网络数据建模AX 多租户设计解析:Server 多租户与 Session 级 Harness 租户隔离
AX 多租户设计解析:Server 多租户与 Session 级 Harness 租户隔离 AX(Agent Executor)是 Google 开源的分布式
人工智能AI AgentAgent 框架自主智能体
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考