- 时序数据库
- 后端
- 数据存储
- 可观测性
- 云原生
【免费下载链接】mimir
Grafana Mimir provides horizontally scalable, highly available, multi-tenant, long-term storage for Prometheus.
导读:Compartments 是 Grafana Mimir 中的一项实验性架构,它将一个 Mimir cell 横向切分为多个相互独立的子 cell(sub-cell),从而让单个超大租户在保持故障与扩缩容域更小的前提下被一个 cell 承载。本文以仓库docs/internal/compartments/目录下的设计文档为主体,结合源码与本地开发环境,系统讲解 Compartments 的动机、两级分片机制、读写分区设计、全局 Ruler、命名与配置约定以及当前实现状态,帮助读者完整理解这一面向"十亿级活跃序列单租户"的存储架构。
为什么需要 Compartments
Grafana Mimir 以cell为基本部署单元对外提供多租户长期存储。在 Grafana Labs 的实践中,一个 cell 通常在其活跃序列规模逼近约10 亿(1 billion)时新建。这种做法对"由大量小租户组成的 cell"运转良好,但对"单租户 cell"或"由单个超大租户主导的 cell"并不适用:
- 部分客户倾向于把全部指标放在单一租户中,因为跨租户查询更简单,且与 Grafana Labs 其他产品集成更顺畅;
- 设计目标是一个 cell 内舒适地运行约100 亿(10 billion)活跃序列的单租户。
对写入路径的压测表明,若不将写入路径组件分片,其稳定运行规模难以超过上述目标的一半——这正是 Compartments 架构要解决的问题。
推动 Compartments 的两大核心动因:
- 爆炸半径(Blast radius)。当前一个 cell 只有一条共享的写入主干:一个 Kafka topic、一个分区 ring、一个由 ingester 组成的消费池。无论是写入还是查询路径出问题,都会波及 cell 内每一条序列,因为所有数据都流经同一个 topic 与同一批 ingester。整个 cell 之内不存在更小的隔离边界。
- 扩展性上限(Scaling limits)。单一 partition ring 与单一 topic 存在实际的规模天花板。越过一定规模后,"多加分区"本身就会成为新的扩展问题。
Compartments 通过把写入路径拆成相互独立的切片同时解决上述两个问题:每条序列被确定性路由到某个 compartment,每个 compartment 拥有自己的 topic 与自己的 partition ring。单个 compartment 内的问题被限制在其负责的那部分序列上,每个 compartment 也能独立扩缩容与独立运维。
核心术语与组件划分
compartment是承载 cell 中一部分负载的 Pod 组。所有 compartment Pod 运行在同一个 namespace 中,但由独立的 Deployment 与 StatefulSet 管理。
系统中存在两套相互独立的 compartment,它们的数量不需要一致:
- 写分区(Write compartments)——写入侧资源池:distributor 外加专属 Kafka 集群(Kafka 按写分区隔离;Grafana Labs 内部通常使用 Warpstream 集群,也可以是普通 Kafka broker)。
- 读分区(Read compartments)——指标存储侧组件:ingester、store-gateway、block-builder 与 compactor。
全局查询层(Global query layer)——query-frontend、query-scheduler、querier 与 ruler——横跨所有 compartment,不属于上述任何一套。
Compartments 及其按分区归属的资源遵循wc-<id>/rc-<id>命名约定(<id>为从 0 开始的分区索引),详见 Naming conventions,核心映射如下表:
| 资源 | 命名约定 | 示例 |
|---|---|---|
| 写分区 | wc-<id> | wc-0、wc-1 |
| 读分区 | rc-<id> | rc-0、rc-1 |
| 写分区的 distributors | distributor-wc-<id> | distributor-wc-0 |
| 写分区的 Kafka 集群 | kafka-wc-<id> | kafka-wc-0 |
| 读分区的 ingest topic | ingest-rc-<id> | ingest-rc-0 |
该命名约定同时贯穿源码:例如 ingester 的 partition ring 名称通过WithReadCompartmentSuffix追加-rc-<id>后缀(见 pkg/ingester/ingester.go),store-gateway 的 ring 也以同样方式命名(见 pkg/storegateway/gateway.go)。
总体架构与读写路径
架构图
各路径概要
- 写入路径(Write path):ingress 认证网关(或负载均衡器)把写入请求转发给随机写分区中的随机 distributor。distributor 先将每条序列按读分区分片,再在该分区内按分区(partition)分片。
- 分片(Sharding):写入请求在写分区之间随机分布;序列则按**指标名(metric name)**分片到读分区,再沿用现有的"序列标签哈希"分区分片。详见 Sharding。
- 读取路径(Read path):每个读分区针对专属 topic 运行自己的存储组件。
- Ruler:Ruler 留在全局查询层,通过 compartment 感知的查询路径评估规则,并通过横跨各写分区的 distributor 写入生成的序列。详见 Ruler。
Topic 布局
每个写分区拥有自己的 Kafka 集群(kafka-wc-<id>),而每个集群内包含每个读分区各一个 topic(ingest-rc-<id>);一个读分区的 ingester 从每个写分区的集群中消费自己的 topic。
关键推论:ingest-rc-<id>topic 在每一个写分区的 Kafka 集群中都存在一次——因为每个写分区都会向每个读分区写入(详见 Write compartments)。
多可用区(Multi-zone)
该架构保留了多可用区部署能力:每个 compartment 的组件(包括写分区与读分区)都跨可用区部署。Ruler 本身以 cell 级多可用区 fleet 的形式运行。
两级分片(Sharding):先读分区、后分区
两级分片公式
Compartments 采用两级分片:序列先被分配到一个读分区,再在该读分区内被分配到某个分区(partition)。
按指标名分片到读分区。每条序列(以及每个 metadata 条目)通过哈希"租户 + 指标名"被分配到读分区:
read_compartment = hash(user_id, metric_name) % num_read_compartments该分配是按租户进行的:因为 user ID 参与哈希,同一个指标名在不同租户下可以映射到不同的分区。
按序列标签分片到分区。在某个读分区内部,序列沿用 Mimir 已有的"序列标签哈希"分片逻辑分配到该分区的 partition。
源码层面,该公式由 pkg/compartments/router.go 的Router.CompartmentForMetric实现:hash := mimirpb.ShardByMetricName(userID, metricName); return int(hash % uint32(numCompartments))。而mimirpb.ShardByMetricName(见 pkg/mimirpb/series_sharding.go)先以 user ID 初始化哈希,再叠加 metric name——与文档公式一一对应。
为消费者分片,而非为生产者
分片由读侧驱动:distributor 按N个 topic 分片,其中N是读分区的数量。写分区的数量可以不同,它属于部署关注点,不会改变序列的分片方式。
在本阶段,读分区的数量被假定为静态的——调整分区数量(以及对既有序列重新分片)不在当前范围内。
为什么用指标名作为分段标签
分段标签(segmentation label)的意义在于:一条查询只需访问持有被选中取值的那部分分区。如果某个分区出问题,只有需要该分区数据的查询受影响——这正是查询爆炸半径得以缩小的机制。
一个好的分段标签需要满足两个性质:
- 绝大多数查询都使用等值匹配器(equality matcher)对它进行过滤;
- 序列在其取值空间上分布均匀。
在 Prometheus 生态中,指标名非常契合:通常存在大量不同的指标名,且几乎每条查询都会引用指标名。概念上这与 DynamoDB、Google Spanner 等系统中"以良定义的 key 分片数据"的分区键思想类似。
在 OTLP 生态中这一选择更困难:租户往往拥有极多序列却只有相对较少的独立指标名,因此 OTLP 租户可能需要不同的分段标签(例如cluster或namespace),前提是它的大多数查询都能用等值匹配器命中。
最大租户分析结论
对 Grafana Labs 最大租户的分析得出两个结论:
- 除指标名外,没有任何租户拥有更好的分段标签;
- 对大多数租户而言,指标名都是良好的分段候选。
下表为每个被分析租户的逐项结论(列含义见表后图例)。每个租户以匿名字母表示:
| Tenant | Ecosystem | Unique names | Series dist | Top metric series % | Queries shardable | Avg metrics / query |
|---|---|---|---|---|---|---|
| A | Prometheus | 29061 | 0.91 | 4% | 90% | 1.53 |
| B | Prometheus | 80942 | 0.96 | 4% | 100% | 1.52 |
| C | Prometheus | 51642 | 0.98 | 1% | 87% | 1.76 |
| D | Prometheus | 48454 | 0.68 | 20% | 97% | 1.34 |
| E | Prometheus | 82321 | 0.92 | 4% | 94% | 1.33 |
| F | Prometheus | 5527667 | 0.99 | 3% | 100% | 1.01 |
| G | Prometheus | 12520 | 0.93 | 7% | 99% | 1.51 |
| H | Prometheus | 47837 | 0.94 | 3% | 100% | 1.31 |
| I | Prometheus | 34744 | 0.93 | 4% | 100% | 1.13 |
| J | Prometheus | 13474 | 0.70 | 36% | 96% | 1.05 |
| K | OTLP | 110268 | 0.99 | 4% | 96% | 1.15 |
| L | OTLP | 7905 | 0.79 | 16% | 77% | 2.13 |
| M | OTLP | 7899 | 0.81 | 11% | 76% | 2.13 |
| N | OTLP | 8237 | 0.87 | 8% | 77% | 2.14 |
| O | OTLP | 90205 | 0.82 | 19% | 100% | 1.10 |
| P | OTLP | 15029 | 0.93 | 6% | 87% | 1.37 |
| Q | OTLP | 5883 | 1.00 | 1% | 85% | 1.60 |
| R | OTLP | 7601 | 0.75 | 20% | 96% | 1.26 |
| S | OTLP | 449 | 0.20 | 85% | 100% | 1.87 |
| T | OTLP | 5043 | 0.97 | 4% | 100% | 1.42 |
| U | OTLP | 2901 | 0.70 | 33% | 96% | 1.05 |
| V | OTLP | 1199 | 0.87 | 9% | 95% | 1.63 |
| W | OTLP | 7194 | 0.87 | 8% | 100% | 1.86 |
| X | OTLP | 855 | 1.00 | 2% | 99% | 1.06 |
| Y | OTLP | 6018 | 0.85 | 12% | 90% | 1.48 |
| Z | OTLP | 5970 | 0.93 | 7% | 93% | 1.28 |
| AA | OTLP | 120196 | 0.90 | 3% | 100% | 1.00 |
列含义图例:
- Tenant—— 匿名租户标识。
- Ecosystem—— 指标生态(Prometheus 或 OTLP)。
- Unique names—— 独立指标名数量。
- Series dist—— 序列在指标名上的均匀程度(0–1;越高越均匀,越低表示集中在少数名称上;越高越好)。
- Top metric series %—— 基数最高的单一指标名所占全部序列的比例(越低越好)。
- Queries shardable—— 可按指标名分片的查询占比,即用等值匹配器过滤指标名的查询占比(越高越好)。
- Avg metrics / query—— 每条查询引用的平均独立指标名数量(越低越好)。
结论:对几乎所有租户而言,绝大多数查询都能按指标名分片;对大多数租户,没有任何单一指标名主导序列。少数租户会把很大比例的序列集中在一个指标名上(表现为低 "Series dist"、高 "Top metric series %")。
某些场景下爆炸半径缩减有限
在以下情况下,爆炸半径缩减受限:
- 租户把很大比例的序列或查询集中在单一指标名上,导致其大部分负载仍落在一个分区;
- 查询横跨大量指标名(多个 vector selector,或无指标名的查询),因为每个 selector 都可能解析到不同分区,查询最终会触及许多(甚至全部)分区。
写分区(Write compartments)设计
本节描述的是目标架构。当前实际实现情况请见 Status and limitations。
一个写分区是 compartment 的写入侧:专属 distributor 资源池外加一个专属 Kafka 集群(Kafka 按写分区隔离)。
工作方式
- ingress 认证网关或负载均衡器把每条写入请求转发给随机写分区中的随机 distributor,写入请求在写分区之间随机分布。
- 各写分区以运行相当数量的 distributor Pod 为目标,使负载在各写分区间均衡、不出现热点分区。这是目标而非硬性要求。
- distributor 将每条序列先分片到读分区、再分片到分区(见 Sharding),并写入其写分区 Kafka 集群中对应的 topic。
由于序列按读分区分片、而写入请求在写分区间随机分布,因此每个写分区都会写入每个读分区的 topic:每个读分区 topic 都存在于每个写分区的 Kafka 集群中。
Distributor 共享单个全局 ring
Distributor没有按写分区划分的 ring;所有写分区的每个 distributor 都加入一个全局 distributor ring。该 ring 仅用于统计 distributor 数量,从而驱动全局按租户限流:每个 distributor 执行的本地限流等于该租户全局限流除以 ring 中的 distributor 数量。
全局 ring 是必须的,因为写入请求随机分布在所有写分区上,租户流量由所有写分区的全部 distributor 服务。限流除数必须统计到每一个distributor:若把 ring 按写分区隔离,全局限流只会被一小部分 distributor 均分,每个租户的准入额度会被放大一个"写分区数量"的倍数。
这也要求所有写分区的 distributor 共享同一个 ring 后端存储。若给每个写分区独立的后端,就会重新引入按分区超额准入的问题。
HA tracker 是全局的
HA tracker 用于对高可用 Prometheus 对的样本去重,它横跨所有写分区,是全局的。它不使用 distributor ring,而是通过共享的 key-value store 传播"被选举副本"状态。它必须是全局的,因为 HA 对的两个副本会随机分布在各写分区之间,可能落在不同分区的 distributor 上——只有所有 distributor 共享同一个"被选举副本"状态,去重才能生效。这同样要求所有写分区的 distributor 共享同一个 HA tracker key-value store。
为什么每个写分区一个专属 Kafka 集群
按写分区隔离 Kafka,限制了任何单一集群必须吸收的写入负载,并让每个写分区可以独立于其他分区扩缩容和运维。这是写入路径上"爆炸半径与扩展性"论点的具体体现(见 Mimir compartments)。
Warpstream 特化点
在 Grafana Labs,每个写分区专属的 Kafka 集群是一个 Warpstream 集群。以下为 Warpstream 特有内容;上述设计本身对 Kafka 集群是通用的。
- 每个写分区一个专属 Warpstream 虚拟集群(VC),以突破超大规模下 Warpstream 控制面的扩展限制。每个写分区运行一个 VC,把控制面工作分散到多个 VC 上。
- 每个 VC 一个专属对象存储 bucket,以突破对象存储限流。处理一条写入请求时,Warpstream 只需写入处理该请求的写分区的那个 bucket。
N² 数量级的 Produce 请求扩展问题
每个 distributor 都向几乎每个 write agent 发送 Produce 请求,因此 distributor 到 write agent 的连接数随二者乘积增长:distributors × write agents。将写入路径拆成分区可以缓解这一点,因为一个分区内的 distributor 只与该分区的 write agent 通信,而每个分区都只持有两者的一部分。
例如,有 1,000 个 distributor 与 100 个 write agent 时:
- 不分区:1,000 × 100 = 100,000 条连接;
- 分 10 个分区:10 个分区 × 每分区 100 个 distributor × 每分区 10 个 write agent = 10,000 条连接——减少了一个数量级。
这一结论在约 10 亿活跃序列、10 个写分区的压测中得到验证:Produce 请求速率从不分区时的约 170K/s 降至分区后的约 25K/s。
读分区(Read compartments)设计
本节描述的是目标架构。当前实际实现情况请见 Status and limitations。
一个读分区是 compartment 的指标存储侧:运行自己的 ingester、store-gateway、block-builder 与 compactor,负责被分片到它的序列。
工作方式
- 每个读分区使用专属 topic。读分区 0 的 partition 0 与读分区 1 的 partition 0 是不同的 topic-partition,因此 partition 所有权天然按分区隔离。
- 每个读分区拥有自己的 partition ring,ingester 拥有其读分区的某个 partition。每个读分区独立水平扩缩容,因此一个读分区的 partition 数量可以与另一个不同。
- 一个 ingester 从每个写分区的 Kafka 集群消费它的 partition——因为每个写分区都会(在不同 Kafka 集群中)向每个读分区 topic 写入。
在源码中,读分区的 partition ring 通过-ingester.read-compartment-id标志选定(见 pkg/ingester/ingester.go),ring 名称与 key 都带有-rc-<id>后缀;ingester 也据此把 Kafka topic 模板中的<read-compartment-id>占位符替换为实际分区 ID。
跨写分区摊分消费资源
部分读分区组件需要从每个写分区的 Kafka 集群消费:ingester 从所有集群消费自己的 partition,block-builder 则跨所有集群读取同一个 partition 以构建 block。这类组件为每个写分区维护一个独立的 consumer,并把专用于消费的资源均分到这些 consumer 上,从而使组件的总消费资源用量与写分区数量无关,而不是随之增长。
这一设计在源码中有直接体现:WriteCompartmentConfigs(见 pkg/storage/ingest/config.go)为每个写分区生成一个独立的 KafkaConfig(替换地址与 SASL 凭据中的<write-compartment-id>占位符),并把fetchConcurrencyMax、maxBufferedBytes、ingestionConcurrencyMax等按客户端预算通过divideBudget均分(正数预算下限为 1),确保 fan-out 后峰值资源用量与分区数量解耦。
跨写分区记录排序
一个从每个写分区的 Kafka 集群消费同一个 partition的组件,看到的每个集群流彼此独立,集群之间没有固有序。若先追加一个集群的记录再追加下一个集群的记录,当这些集群的记录在时间上重叠时可能产生乱序样本——后一个集群的时间戳会回退到窗口更早的位置。为缓解此问题,可以把各集群流合并为按 Kafka 记录时间戳排序的单一流后再消费。
- Ingesters可以选择在实时路径上做合并。实时流是无界的,因此合并是在有界窗口内的尽力而为。
- Block-builders总是以这种方式合并一个 job 的各集群区间。因为一个 job 的开始与结束 offset 预先已知,合并可以在该窗口内对各集群做精确排序。
Blocks 存储
每个读分区端到端拥有自己的 blocks 存储:
- 读分区拥有专属对象存储 bucket。其 ingester 与 block-builder 将 block 上传到该 bucket,compactor 在 bucket 内压缩 block,store-gateway 从 bucket 服务查询。一个分区的 block 永远不会与另一个分区的混在一起。
- store-gateway 与 compactor按读分区运行,与该分区的 ingester 并列。
- 每个读分区拥有自己的 store-gateway ring与自己的 compactor ring。跨 store-gateway 的 block 分片与副本、跨 compactor 的压缩 job 分片因此都限定在分区内:一个 store-gateway 或 compactor 只负责自己分区 block 的工作。
这是存储侧"爆炸半径与扩展性"目标的体现(见 Mimir compartments):bucket 级别的问题——例如对象存储限流或坏 block——被限制在单个分区内,且每个分区的存储组件与 bucket 相互独立扩缩容。
全局查询层以与查询 ingester 相同的方式查询每个读分区的 store-gateway:把查询收窄到可能持有被选指标名的分区,否则 fan-out(见下文"查询读分区")。
Block-builders
每个读分区运行自己的 block-builder scheduler 与 block-builder 池。scheduler 监视该分区 topic 在每个写分区 Kafka 集群中的情况并切分 job;block-builder 消费这些集群、构建该分区的 block,并上传到其 bucket。
- 一个 job 可以横跨每个写分区。一个 job 限定于一个 partition,但为每个集群携带独立的 offset 区间,最终按记录时间戳合并各区间、构建成单个 block。若把 job 限定到单一集群,则同一窗口会产生每集群一个 block,block 与 chunk 数量会被放大"写分区数量"的倍数,直到压缩把它们合并掉——这会抬高对象存储成本与查询负载(store-gateway 每条查询要加载更多 block)。
- job 跨集群是"全有或全无"的。block-builder 一并读取每个集群的区间,任何一次读取失败都会重试整个 job,因此已提交的 offset 永远不会越过未读数据。一个不健康的写分区会阻塞该分区的 block 构建,直到恢复——这与不分区时单个 Kafka 集群宕机相比并不更糟。Block 构建在实时查询之后运行、不在 ingest 关键路径上,因此能吸收一些延迟,上限取决于 ingester 保留未构建数据的服务时长。
- 重启时的积压重建。与不分区模式相同,scheduler 在重启时从 Kafka 中已存在的数据重建积压——探测历史 offset 并按时间戳重放,以复现它在实时切分时会切出的 job 边界。分区模式的区别在于:每个集群探测到的 offset 按时间分组,因此各集群在相近时间摄取的数据落入同一个 job,保持"每窗口一个 block"。
查询读分区(Querying read compartments)
全局查询层通过不分区时相同的路径查询读分区的 ingester,但扩展为 compartment 感知。由于序列按指标名分片到读分区(见 Sharding),查询层会把查询收窄到可能持有被选序列的分区:
- 当查询固定单一指标名(即用等值匹配器过滤指标名)时,其全部序列都位于恰好一个分区,因此只查询该分区的 ingester。
- 当查询把指标名限制为有限集合(例如等价于
a|b|c交替的 regex 匹配器)时,查询被发送到持有这些名字的分区并集,它仍可能是全部分区的子集。 - 其他情况——当指标名无法被归约为有限名称集合时(没有指标名等值匹配器,也没有可枚举的 regex,例如
.+这种开放式 regex)——查询 fan-out 到每个分区并合并结果。不带指标名过滤的 metadata 与整租户统计查询总是 fan-out。
正是这种定向机制带来查询爆炸半径的缩减:一个分区出问题只影响指标名路由到它的查询,而其他指标名的查询不受影响。
定向依赖于读分区数量静态:指标名到分区的映射必须与写入路径使用的映射一致,查询才能从序列被写入的同一分区读取。改变分区数量会改变该映射,因此不在范围内。
源码层面,查询定向由Router.CompartmentsForMatchers(见 pkg/compartments/router.go)实现:精确等值匹配器映射到单一分区;regex 通过SetMatches()得到可枚举名称集合时映射到这些名称的分区并集(去重后);无法枚举时返回全部分区。查询侧在 pkg/querier/blocks_store_queryable.go 的targetCompartments中调用它,并在forEachCompartment中对多个目标分区并发执行查询。
为什么每个读分区一个专属 topic
每个读分区一个专属 topic 简化了分区管理:各分区的 partitions 是独立的 topic,因此 partition 与其所属 ingester 之间存在清晰、按分区的映射。
例如两个读分区对应两个 topicingest-rc-0与ingest-rc-1。拥有 rc-0 partition 0 的 ingester 消费ingest-rc-0的 partition 0,拥有 rc-1 partition 0 的 ingester 消费ingest-rc-1的 partition 0——尽管都是 "partition 0",却是两个不同的 topic-partition、由两个不同 ingester 所有。
Warpstream 特化点
在 Grafana Labs,各写分区的 Kafka 集群是 Warpstream 集群。以下为 Warpstream 特有内容;上述设计对 Kafka 集群是通用的。
- Read agents(逻辑上)运行在写分区而非读分区。每个 Warpstream 虚拟集群(VC)一个 read-agent 池,而 VC 由写分区驱动。一个 ingester 只从每个 VC 消费单个 partition,消费单个 partition 只需连接一个 read agent,因此 ingester 每个写分区只连接恰好一个 read agent。这把 ingester 到 read agent 的直接连接数限制为写分区数量。
- Read-agent 分布式缓存。Warpstream read agents 构建分布式内存缓存,可以互相获取文件片段。因此,跨所有 VC 消费一个 partition 时,底层可能需要来自任一写分区中任一 read agent 的数据。这是一个潜在的扩展性与可用性限制,需要进一步调查。
Ruler:全局查询层中的规则评估与写入
Ruler 属于全局查询层。它作为 cell 的一个多可用区 fleet 运行,而不是读或写分区的一部分。Ruler 实例使用单一全局 ring 来分片 rule groups,并从共享的 ruler 存储读取 rule groups。
规则评估
Rulers 使用远程规则评估(remote rule evaluation)。每个 Ruler 把每次评估发送给其可用区内的 ruler query-frontend。查询穿过 ruler query-scheduler 到达 ruler querier,后者使用与其他查询相同的定向与 fan-out 行为查询读分区(见 Read compartments)。
一条规则表达式可以只读一个读分区,也可以 fan-out 到多个。持有输入序列的分区不决定结果存储在哪里:生成的序列使用它们自己的指标名被单独路由。
写入规则结果
评估完一条 recording 或 alerting 规则后,Ruler 通过 gRPC 把生成的序列发送给其可用区内的headless distributor service。该服务暴露该可用区内每个写分区的 distributor,gRPC 客户端负载均衡为每个请求选择一个端点。
被选中的 distributor 通过其写分区的 Kafka 集群发送请求。它与任何其他写入一样,先把每条生成序列按租户与指标名分片到读分区、再按序列标签分片到分区(见 Sharding)。因此,一条规则可以从一组读分区读取序列,并把结果写入另一个读分区。
图中展示两个读分区与两个写分区仅为示例。两类分区都横跨可用区,数量相互独立,彼此不存在一一配对。zone 内展示的 Ruler、ruler 查询路径与 distributor 分片在每个可用区都会重复,而每个写分区只有一个逻辑上的、cell 级的 Kafka 集群。
在 compartments 部署中,Rulers不为规则结果写入运行内部 distributor。规则表达式评估与结果摄取是两个独立步骤:PromQL 表达式评估可以成功,但如果结果写入失败,规则会被报告为不健康并记录一次评估失败。Alertmanager 通知继续使用既有通知路径,不经过 compartments。
为什么 Rulers 是全局的
把 Rulers 部署进单一写分区,会把所有规则结果请求导向该分区。把 Rulers 分散到读分区并把每个分配给某个写分区又无法干净映射,因为读分区与写分区数量相互独立。
Rulers 保留在全局层,通过 zonal distributor service 发送每次结果写入请求。客户端负载均衡从"包含所有写分区在该可用区分片"的池中选择一个 distributor 端点,被选中的 distributor 再应用常规的读分区与分区 sharding。
代价是 compartments 不隔离 ruler 的调度与评估:fleet 级问题可以影响那些查询本来会定向到不同读分区的规则。但当规则表达式可以被定向到部分读分区时,读分区仍然限制了数据路径上的影响面。
配置设计:顶层块与组件专属配置
Configuration 页面解释了compartments 配置放在哪里的动机;配置项本身以生成的配置参考文档为准,此处不重复列出。
为什么使用根级 compartments 配置块
是否启用 compartments、有多少读分区,是每个Mimir 组件都需要的信息。因此共享的 compartments 配置放在单一顶层块中,而不是嵌套在任一组件配置之下。
源码印证:compartments.Config(见 pkg/compartments/config.go)是一个独立的顶层结构体,通过标志-compartments.enabled、-compartments.read.num-compartments、-compartments.write.num-compartments注册,并带有 YAML 映射字段enabled、read.num_compartments、write.num_compartments。启用时若任一数量不大于 0,Validate会分别返回ErrInvalidNumReadCompartments/ErrInvalidNumWriteCompartments。
为什么部分设置是组件专属的
某些 compartment 相关设置只对单一组件有意义。例如,某个 ingester 服务的读分区是该 ingester 专属的。这类设置应放在该组件自己的配置中,而不是共享的顶层块。
对应地,源码为各组件提供了专属标志:
-ingester.read-compartment-id——该 ingester 服务的读分区,同时决定其 partition ring 的-rc-<id>后缀(见 pkg/ingester/ingester.go);-store-gateway.read-compartment-id——store-gateway 服务的读分区与其专属 ring(见 pkg/storegateway/gateway.go);-distributor.write-compartment-id——该 distributor 所属的写分区,决定其 Kafka 集群地址(见 pkg/distributor/distributor.go);-compactor.read-compartment-id——compactor 服务的读分区与其专属压缩 ring;-ingest-storage.kafka.address/-ingest-storage.kafka.topic中的<write-compartment-id>/<read-compartment-id>占位符,由ReplaceWriteCompartment/ReplaceReadCompartment在启动时展开(见 pkg/compartments/config.go)。
本地开发环境中的真实配置样例
本地开发环境 development/mimir-compartments 的 config/mimir.yaml 展示了真实可用的配置骨架(2 个写分区 + 2 个读分区):
multitenancy_enabled: false common: storage: backend: s3 s3: endpoint: minio:9000 access_key_id: mimir secret_access_key: supersecret insecure: true ingest_storage: enabled: true kafka: # 全局启用 ingest_storage,因此每个组件都会校验 Kafka 地址与 topic, # 即使它从不连接(compactor、store-gateway、querier、ruler、alertmanager)。 # 实际消费/生产的组件(distributors、ingesters、query-frontend、 # block-builders 与 schedulers)通过 CLI 标志覆盖这些默认值, # 指向各自的写/读分区。 address: kafka-wc-0:9092 topic: mimir-ingest-rc-0 last_produced_offset_poll_interval: 500ms fetch_concurrency_max: 15 compartments: enabled: true read: num_compartments: 2 write: num_compartments: 2配套的 docker-compose.jsonnet 展示了组件级标志如何落地,例如:
- distributor:
-distributor.write-compartment-id=%d与-ingest-storage.kafka.topic='mimir-ingest-rc-<read-compartment-id>'; - ingester:
-ingester.read-compartment-id=%d与-ingest-storage.kafka.address='kafka-wc-<write-compartment-id>:9092'; - block-builder / block-builder-scheduler:按 rc 拆分,并各自指定
-blocks-storage.s3.bucket-name=mimir-blocks-rc-<id>; - compactor / store-gateway:
-compactor.read-compartment-id=%d、-store-gateway.read-compartment-id=%d与专属 bucket。
docker-compose.yml由docker-compose.jsonnet经 jsonnet 生成(见 Makefile,运行jsonnet -S docker-compose.jsonnet -o docker-compose.yml),make check会校验两者是否同步。
多可用区与命名约定的落地
多可用区部署通过把每个分区(写与读)的组件跨可用区铺开实现,这一点同时体现在命名与部署两层:wc-<id>/rc-<id>是资源级别的全局唯一命名,而每个可用区内的 distributor 分片等资源再以 zone 为维度组织(如 Ruler 一节中"zonal distributor service"所示)。
状态与限制(Status and limitations)
Compartments 是实验性的且默认禁用。上述架构描述中只有一部分当前已实现,status-and-limitations.md 跟踪这一差距。
目前已实现
- compartments 配置与按指标名的按租户路由。
- 写入路径把序列分片到读分区,并写入各分区自己的 topic;每个写分区写入自己的 Kafka 集群。
- 每个读分区拥有自己的 partition ring,ingester 在正确分区的 ring 中拥有自己的 partition。
- 多集群消费:ingester 从每个写分区的 Kafka 集群消费其读分区的 topic,并把记录并集写入自己的 TSDB;每个集群的消费独立跟踪自己的 offsets。
- Compartment 感知的 ingester 与 store-gateway 查询:查询层把查询收窄到可能持有被选指标名的分区(等值匹配器对应单一分区、可枚举指标名集对应并集),否则 fan-out 到所有分区,再合并结果(见 Read compartments)。对于 blocks,它查询每个被定向分区的 store-gateway,并从其专属 bucket 发现该分区的 blocks。
- 按分区的 store-gateway 与 compactor rings:配置了读分区的 store-gateway 或 compactor 注册到该分区专属的 ring,与不分区 ring 及其他分区 ring 相互分离。
- Compartment 感知的 block-builders:每个读分区运行自己的 block-builder scheduler 与 block-builder 池。scheduler 为每个 partition 与时间窗口切出单个 job,捆绑每个写分区集群的 offset 区间;block-builder 一起消费这些区间、按记录时间戳合并,并把每窗口一个 block 构建进该分区的 bucket。
- Compartment 感知的强读一致性:query-frontend 监视每个写分区 Kafka 集群中每个读分区 topic 的最后产出 offset 并传播给 ingester,使执行强读一致性的 ingester 等待各 Kafka 集群的指定 offset(offset 未传播时回退到最后产出 offset)。
- 全局 Ruler 评估与写入:Rulers 通过远程的、compartment 感知的查询路径评估规则,并通过横跨所有写分区的 zonal distributor 端点池发送生成的序列;被选中的 distributor 随后应用常规的读分区 sharding(见 Ruler)。
- 一个本地开发环境(development/mimir-compartments)。
尚未解决
- 按分区的 block-builders 目前只运行在本地开发环境中,尚未纳入 jsonnet 部署库。
- 即使在写入路径上,有些工作也尚未完成——例如 shuffle sharding 与集成测试。在 compartments 端到端跑通之前,不会有 changelog 或实验性特性文档条目。
本地开发环境
development/mimir-compartments运行 2 个写分区与 2 个读分区。每个写分区拥有自己的单 broker Kafka 集群,每个读分区拥有自己的 topic 与 ingester 组。各读分区的 ingester 从两个 Kafka 集群消费该分区的 topic。
任一分区的指标都可查询:固定指标名的查询由所属分区服务,不带指标名等值匹配器的查询 fan-out 到两个分区。环境还通过limits.ingest_storage_read_consistency: strong本地演练强读一致性路径(见 config/mimir.yaml)。
文档索引与演进约定
Compartments 的完整设计文档位于docs/internal/compartments/目录,建议按以下顺序阅读:
- Sharding——序列如何被分片到分区与 partition,以及为什么用指标名作为分段标签;
- Write compartments——写分区设计;
- Read compartments——读分区设计;
- Ruler——全局 Ruler 如何在 compartments 下评估规则并写入生成序列;
- Naming conventions——分区及其资源使用的名称;
- Configuration——compartments 配置所在位置的动机;
- Status and limitations——当前已实现与未实现的内容;
- AGENTS.md——该文档目录的维护约定:每次 compartments 设计决策或实现迭代时,应同步更新相应文件,把这套文档当作追踪架构演进的"活文档";内容只描述行为与"为什么",不包含源码细节、内部租户数据或前后对比叙事。
对于希望深入代码的读者,可以沿着以下源码路径验证本文中的机制:分片哈希在 pkg/mimirpb/series_sharding.go,路由与查询定向在 pkg/compartments/router.go,写入侧按分区分组在 pkg/distributor/compartments.go 与 pkg/distributor/distributor.go 的sendWriteRequestToCompartments,多集群消费资源摊分在 pkg/storage/ingest/config.go 的WriteCompartmentConfigs,查询定向落地在 pkg/querier/blocks_store_queryable.go,强读一致性 offset 编码在 pkg/frontend/querymiddleware/read_consistency.go。
小结
Compartments 以"每分区独立 topic、独立 partition ring、独立存储 bucket 与组件池"的方式,把一个 Mimir cell 切成多个独立故障域与扩展域,从而为超大单租户(目标约 100 亿活跃序列)提供可运维性:写入请求随机散布到写分区,序列按"租户 + 指标名"确定性路由到读分区,查询按指标名定向到可能持有数据的分区、否则 fan-out 并合并。需要注意的是,该架构目前仍为实验性、默认禁用,且依赖分区数量静态不变;生产化部署(如按分区的 block-builders 进入 jsonnet 部署库、shuffle sharding 与集成测试)仍在演进中。
- 时序数据库
- 后端
- 数据存储
- 可观测性
- 云原生
【免费下载链接】mimir
Grafana Mimir provides horizontally scalable, highly available, multi-tenant, long-term storage for Prometheus.
相关推荐
Grafana Mimir架构深度解析:多租户设计如何支撑10亿级时间序列
Grafana Mimir架构深度解析:多租户设计如何支撑10亿级时间序列 Grafana Mimir作为业界领先的 水平可扩展时间序列数据库 ,其 多租户架构
时序数据库后端数据存储可观测性云原生Grafana Mimir 读 compartment(Read Compartments)架构深度解析:专属 Topic、多集群消费与块存储隔离
Grafana Mimir 读 compartment(Read Compartments)架构深度解析:专属 Topic、多集群消费与块存储隔离 本文围绕 G
时序数据库后端数据存储可观测性云原生Hasura live queries 架构解析:如何将 GraphQL 订阅扩展到百万级活跃连接
Hasura live queries 架构解析:如何将 GraphQL 订阅扩展到百万级活跃连接 本篇文章基于 architecture/live queri
后端API网关数据库GraphQL
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考