☰
Grafana Mimir 多租户成本优化设计解析:从每租户一个 TSDB 到 Tenancy Multiplexing
2026/10/14 15:00:57 网站建设 项目流程
  • 时序数据库
  • 后端
  • 数据存储
  • 可观测性
  • 云原生

【免费下载链接】mimir

Grafana Mimir provides horizontally scalable, highly available, multi-tenant, long-term storage for Prometheus.

项目地址:https://gitcode.com/gh_mirrors/mimir/mimir
点击查看免费下载

本文是一份关于 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(单用户序列数上限)

网关需要在内存中维护"该租户已拥有的序列集合"的表示,并且这个表示必须满足四个硬性要求:

  1. 访问快(每条写入的每个序列都要查一次);
  2. 占用小(这些信息要在网关集群成员之间交换);
  3. 可合并(用于聚合各成员的信息);
  4. 可序列化(便于交换传输)。

提案对比了两种实现路径:

  • 朴素方案:把所有租户的所有序列元数据都存在内存里。这会浪费数百 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-old
  • invalid-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无需变更
ingestermax_series_per_user与max_series_per_metric需要以不同方式计算
querier / query-frontend / query-scheduler无需变更
compactorretention 是按 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)适配:逐组件分析

组件变更程度
ingesterTSDB 指标不再按用户细分
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 及其代码,就可以从两个层面研究如何提升资源效率——

  1. Mimir 如何使用 TSDB 代码;
  2. 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.

项目地址:https://gitcode.com/gh_mirrors/mimir/mimir
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询