
时序数据库后端数据存储可观测性云原生【免费下载链接】mimirGrafana Mimir provides horizontally scalable, highly available, multi-tenant, long-term storage for Prometheus.项目地址https://gitcode.com/gh_mirrors/mimir/mimir点击查看免费下载导读Compartments 是 Grafana Mimir 中的一项实验性架构它将一个 Mimir cell 横向切分为多个相互独立的子 cellsub-cell从而让单个超大租户在保持故障与扩缩容域更小的前提下被一个 cell 承载。本文以仓库docs/internal/compartments/目录下的设计文档为主体结合源码与本地开发环境系统讲解 Compartments 的动机、两级分片机制、读写分区设计、全局 Ruler、命名与配置约定以及当前实现状态帮助读者完整理解这一面向十亿级活跃序列单租户的存储架构。为什么需要 CompartmentsGrafana 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-idwc-0、wc-1读分区rc-idrc-0、rc-1写分区的 distributorsdistributor-wc-iddistributor-wc-0写分区的 Kafka 集群kafka-wc-idkafka-wc-0读分区的 ingest topicingest-rc-idingest-rc-0该命名约定同时贯穿源码例如 ingester 的 partition ring 名称通过WithReadCompartmentSuffix追加-rc-id后缀见 pkg/ingester/ingester.gostore-gateway 的 ring 也以同样方式命名见 pkg/storegateway/gateway.go。总体架构与读写路径架构图各路径概要写入路径Write pathingress 认证网关或负载均衡器把写入请求转发给随机写分区中的随机 distributor。distributor 先将每条序列按读分区分片再在该分区内按分区partition分片。分片Sharding写入请求在写分区之间随机分布序列则按**指标名metric name**分片到读分区再沿用现有的序列标签哈希分区分片。详见 Sharding。读取路径Read path每个读分区针对专属 topic 运行自己的存储组件。RulerRuler 留在全局查询层通过 compartment 感知的查询路径评估规则并通过横跨各写分区的 distributor 写入生成的序列。详见 Ruler。Topic 布局每个写分区拥有自己的 Kafka 集群kafka-wc-id而每个集群内包含每个读分区各一个 topicingest-rc-id一个读分区的 ingester 从每个写分区的集群中消费自己的 topic。关键推论ingest-rc-idtopic 在每一个写分区的 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 最大租户的分析得出两个结论除指标名外没有任何租户拥有更好的分段标签对大多数租户而言指标名都是良好的分段候选。下表为每个被分析租户的逐项结论列含义见表后图例。每个租户以匿名字母表示TenantEcosystemUnique namesSeries distTop metric series %Queries shardableAvg metrics / queryAPrometheus290610.914%90%1.53BPrometheus809420.964%100%1.52CPrometheus516420.981%87%1.76DPrometheus484540.6820%97%1.34EPrometheus823210.924%94%1.33FPrometheus55276670.993%100%1.01GPrometheus125200.937%99%1.51HPrometheus478370.943%100%1.31IPrometheus347440.934%100%1.13JPrometheus134740.7036%96%1.05KOTLP1102680.994%96%1.15LOTLP79050.7916%77%2.13MOTLP78990.8111%76%2.13NOTLP82370.878%77%2.14OOTLP902050.8219%100%1.10POTLP150290.936%87%1.37QOTLP58831.001%85%1.60ROTLP76010.7520%96%1.26SOTLP4490.2085%100%1.87TOTLP50430.974%100%1.42UOTLP29010.7033%96%1.05VOTLP11990.879%95%1.63WOTLP71940.878%100%1.86XOTLP8551.002%99%1.06YOTLP60180.8512%90%1.48ZOTLP59700.937%93%1.28AAOTLP1201960.903%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 共享单个全局 ringDistributor没有按写分区划分的 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 ringingester 拥有其读分区的某个 partition。每个读分区独立水平扩缩容因此一个读分区的 partition 数量可以与另一个不同。一个 ingester 从每个写分区的 Kafka 集群消费它的 partition——因为每个写分区都会在不同 Kafka 集群中向每个读分区 topic 写入。在源码中读分区的 partition ring 通过-ingester.read-compartment-id标志选定见 pkg/ingester/ingester.goring 名称与 key 都带有-rc-id后缀ingester 也据此把 Kafka topic 模板中的read-compartment-id占位符替换为实际分区 ID。跨写分区摊分消费资源部分读分区组件需要从每个写分区的 Kafka 集群消费ingester 从所有集群消费自己的 partitionblock-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 上传到该 bucketcompactor 在 bucket 内压缩 blockstore-gateway 从 bucket 服务查询。一个分区的 block 永远不会与另一个分区的混在一起。store-gateway 与 compactor按读分区运行与该分区的 ingester 并列。每个读分区拥有自己的 store-gateway ring与自己的 compactor ring。跨 store-gateway 的 block 分片与副本、跨 compactor 的压缩 job 分片因此都限定在分区内一个 store-gateway 或 compactor 只负责自己分区 block 的工作。这是存储侧爆炸半径与扩展性目标的体现见 Mimir compartmentsbucket 级别的问题——例如对象存储限流或坏 block——被限制在单个分区内且每个分区的存储组件与 bucket 相互独立扩缩容。全局查询层以与查询 ingester 相同的方式查询每个读分区的 store-gateway把查询收窄到可能持有被选指标名的分区否则 fan-out见下文查询读分区。Block-builders每个读分区运行自己的 block-builder scheduler 与 block-builder 池。scheduler 监视该分区 topic 在每个写分区 Kafka 集群中的情况并切分 jobblock-builder 消费这些集群、构建该分区的 block并上传到其 bucket。一个 job 可以横跨每个写分区。一个 job 限定于一个 partition但为每个集群携带独立的 offset 区间最终按记录时间戳合并各区间、构建成单个 block。若把 job 限定到单一集群则同一窗口会产生每集群一个 blockblock 与 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。该服务暴露该可用区内每个写分区的 distributorgRPC 客户端负载均衡为每个请求选择一个端点。被选中的 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。启用时若任一数量不大于 0Validate会分别返回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.topicmimir-ingest-rc-read-compartment-idingester-ingester.read-compartment-id%d与-ingest-storage.kafka.addresskafka-wc-write-compartment-id:9092block-builder / block-builder-scheduler按 rc 拆分并各自指定-blocks-storage.s3.bucket-namemimir-blocks-rc-idcompactor / 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.ymlmake check会校验两者是否同步。多可用区与命名约定的落地多可用区部署通过把每个分区写与读的组件跨可用区铺开实现这一点同时体现在命名与部署两层wc-id/rc-id是资源级别的全局唯一命名而每个可用区内的 distributor 分片等资源再以 zone 为维度组织如 Ruler 一节中zonal distributor service所示。状态与限制Status and limitationsCompartments 是实验性的且默认禁用。上述架构描述中只有一部分当前已实现status-and-limitations.md 跟踪这一差距。目前已实现compartments 配置与按指标名的按租户路由。写入路径把序列分片到读分区并写入各分区自己的 topic每个写分区写入自己的 Kafka 集群。每个读分区拥有自己的 partition ringingester 在正确分区的 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 集群的指定 offsetoffset 未传播时回退到最后产出 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 与集成测试仍在演进中。赞分享时序数据库后端数据存储可观测性云原生【免费下载链接】mimirGrafana Mimir provides horizontally scalable, highly available, multi-tenant, long-term storage for Prometheus.项目地址https://gitcode.com/gh_mirrors/mimir/mimir点击查看免费下载相关推荐Grafana Mimir架构深度解析多租户设计如何支撑10亿级时间序列Grafana Mimir架构深度解析多租户设计如何支撑10亿级时间序列 Grafana Mimir作为业界领先的 水平可扩展时间序列数据库 其 多租户架构时序数据库后端数据存储可观测性云原生Grafana Mimir 读 compartmentRead Compartments架构深度解析专属 Topic、多集群消费与块存储隔离Grafana Mimir 读 compartmentRead Compartments架构深度解析专属 Topic、多集群消费与块存储隔离 本文围绕 G时序数据库后端数据存储可观测性云原生Hasura live queries 架构解析如何将 GraphQL 订阅扩展到百万级活跃连接Hasura live queries 架构解析如何将 GraphQL 订阅扩展到百万级活跃连接 本篇文章基于 architecture/live queri后端API网关数据库GraphQL创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考