关于我们

质量为本、客户为根、勇于拼搏、务实创新

< 返回新闻公共列表

2026 服务器租用长期指标库 Mimir 落地全解:对象存储、租户隔离与查询路径六维对比 + 避坑避雷手册

发布时间:2026-10-08

2026 服务器租用长期指标库 Mimir 落地全解:对象存储、租户隔离与查询路径六维对比 + 避坑避雷手册

大多数团队把 Prometheus 的保留期配到 15 天就不敢再往上加了,理由很朴素:本机那块盘撑不住,一次非正常重启 TSDB 回放半小时起步,回放期间什么都查不了。真想看到"去年同期这个接口延迟是多少",单机 Prometheus 这条路基本走到了头。Grafana Mimir 是 Grafana Labs 把 Cortex 重做之后推出来的那条路:写侧拆成无状态的 distributor,"热数据"交给有状态的 ingester,冷数据全部甩进对象存储,读的时候由 query-frontend 把长区间查询切成一堆小查询并行跑掉。这套东西在云上开箱即用,搬到自建服务器上也一样跑得起来,差别只是组件一多,机器配比、参数边界、故障半径全压在你自己身上。这篇就把读写路径、对象存储账单、租户隔离这三件事讲透,顺带给出能直接拿去下单的资源配比。

  • 写路径的关键动作是分片加复制:distributor 按租户和标签做哈希,落到 ingester 环上的多个实例,复制因子默认 3,两个副本确认才算写成功。
  • ingester 是唯一真正吃内存的角色:活跃 series 全在内存里攒着,两小时凑一个 block 再刷进对象存储,本地盘只放 WAL。
  • 长区间查询能不能跑完,看的是 query-frontend 有没有按天拆分:90 天区间不拆分,单台 querier 要自己扛几百次聚合。
  • 对象存储账单里,请求次数往往比容量更扎眼:block 小而多会把 GET 和 LIST 打爆,compactor 定时合并是唯一的解药。
  • 租户隔离靠 X-Scope-OrgID 这一个请求头区分:每租户的写入速率、series 上限、最长查询区间单独卡住,一个租户写飞不该让全集群陪葬。

Mimir 不是一台大 Prometheus,而是九个角色各管一段

刚接触的人容易按"一个二进制搞定"的思路去理解 Mimir,结果部署完发现进程列表里有七八个东西。它的核心思路是把 Prometheus 原生 TSDB 的能力拆开:distributor 负责接写入请求和做哈希分片;ingester 负责在内存里维护最近两小时的 TSDB head 并落 WAL;compactor 负责把两小时的小 block 逐级合并、去重、按保留期删除;store-gateway 负责给老数据提供查询能力,它手上只有对象存储的指针加本地缓存;query-frontend 负责把 PromQL 拆成子查询、做结果缓存、排队;query-scheduler 维护队列,让无状态的 querier 主动来领活;querier 真正执行 PromQL;ruler 负责跑 recording rule 和 alerting rule;alertmanager 负责告警去重、静默、路由。前七个是读写链路上的必选项,ruler 和 alertmanager 可以按需启用。

这种拆法的直接收益是每类角色能单独伸缩。周末跑大促的时候你只需要横向加 querier,不用动 ingester;反过来说,指标基数突然翻倍的那一周,你只加 ingester 内存就行,querier 那边一点不用动。代价是运维复杂度上去了:九个角色意味着九份 resource limit、九份滚动升级顺序、九份告警规则。所以在自建服务器上落地之前,先想清楚第一版要开几个组件——我个人倾向第一版就上完整读写链路,ruler 和 alertmanager 先不上,Prometheus 那边的告警体系继续用着,等 Mimir 稳定了再迁。

2026 年自建 Mimir 这件事变得划算的三个原因

第一个原因是对象存储便宜到可以忽略容量焦虑。两年前还在纠结要不要买 8 块 16T 盘做本地保留,现在自建一台 MinIO 或者用现有 Ceph RGW 的 S3 兼容接口,几个 TB 的指标数据放进去几乎不占预算。第二个原因是内存单价下来了,单台机器塞 256G 甚至 512G 内存不再是奢侈品,而 ingester 恰恰是全靠内存吃饭的角色。第三个原因是社区把默认参数调得更像样了:Mimir 的默认配置基本能撑起"几十万到几百万活跃 series、保留几个月"这个区间的中小场景,不需要一上来就把所有参数翻一遍。

Mimir 写路径拆解:distributor 的哈希分片与 ingester 的两小时 block 怎么咬合

一次 remote_write 打进来,第一站是 distributor。它做四件事:校验请求合法性(标签数量、样本时间戳是否超出允许的过去/未来窗口、标签值长度),按租户做限流判断,然后把每个系列按照"租户 ID 加指标名加全部标签值"算出一个哈希,到 ingester 的一致性哈希环上找三个节点(复制因子由 -distributor.replication-factor 控制,默认 3),再并发写过去。写成功的判定是 quorum,也就是三个副本里至少两个 ack,写请求才返回成功。

这里有个容易被忽略的点:它是完全无状态的,连刚转发过什么都不往本地落。这意味着你可以随便扩缩它,只要 remote_write 的客户端配置了负载均衡(或者直接走一层四层均衡)。也意味着它的资源画像非常清晰——纯 CPU。protobuf 解析、snappy 或 gzip 解压、标签重排,全是 CPU 活。给 distributor 配机器的时候,别照着"高可用"的惯性给它堆内存,2 核到 8 核、内存 4G 到 8G 就够了,真正需要堆内存的是后面的 ingester。

落到 ingester 之后,数据先在 WAL 里写一份(这是本地 NVMe SSD 的事情),然后进入内存中的 TSDB head。每两小时(-blocks-storage.tsdb.block-ranges-period,默认 2h),ingester 把这期间攒出来的 head 切成一个 block,把它的 index 和 chunks 传进对象存储,标记为可查询。这个两小时窗口决定了几件事:block 越小越多,对象存储里的对象数量越密;block 越大,ingester 内存峰值越高,崩溃时要回放的 WAL 也越多。社区默认给 2h,是因为它在"内存峰值"和"对象数量"之间取了个还不错的折中。除非你非常清楚自己在干什么,我不建议动这个参数。

ingester 刷完盘之后并没有马上丢掉数据,它还会把刚上传的 block 保留一段时间用来应答最近的查询——具体窗口由 -querier.query-store-after(默认 12h)之类的配置共同决定。所以读一条"五分钟前"的数据,是不需要去对象存储的,直接从 ingester 内存里拿;这也是为什么 ingester 挂掉会直接影响最近数据的查询结果可用性,而不会影响老数据。

Mimir 的 ingester 到底把什么放在内存里,内存该怎么估

把这件事说清楚,机器选型就完成一半了。ingester 的内存里放着:当前两小时窗口内的全部 series 的索引结构(postings、符号表),每个 series 最近的样本,还有正在攒的那个 head block。磁盘上放的是 WAL(用于崩溃恢复)。对象存储上放的是已经封口的 block。三者的关系是:内存里的数据每两小时结一次账变成对象,WAL 在 restart 时用来补最后一小段没结账的写。

内存估算这件事我一般用实测法,不给拍脑袋的经验值。做法:先拿一台目标机型,挂上一个真实租户的完整抓取目标,让 ingester 稳定跑满 24 小时以上(必须跨过一个 scrape 高峰期和一个 compactor 周期的 2h 切块),然后取 process_resident_memory_bytes 的峰值,除以当时的 cortex_ingester_memory_series,得到"每个活跃 series 占多少字节"。这个数字在不同业务形态下差异很大——标签多、标签值长的多吃一两倍。拿到每 series 字节数之后,乘以规划总 series 数,再乘 1.5 的安全系数,就是你要的单实例内存。业界经验区间是每百万活跃 series 大约 3 到 8 GB RSS(预估,取决于标签宽度),但这个值只能用来做第一次采购的大致量级判断,不能拿来做容量承诺。

复制因子默认 3,意味着同一个 series 在三个 ingester 上各有一份。所以"总内存需求"是上面算出来的数乘以 3,而不是按机器台数摊薄的。很多团队第一次算成本时忘了乘这个 3,采购回来的机器只有实际需要的三分之一。另一方面,RF=3 也给你换来了"一次滚动重启一个 ingester 不影响写入"的能力,前提是你打开了区域感知复制(zone-aware replication),让三个副本分布在不同的可用区或者不同的机架。自建环境里没有云厂商的 AZ,我们通常按"物理机所在机柜/接入交换机"划 zone,至少保证副本不落在同一台宿主机上。

WAL 那一侧的坑在磁盘。ingester 重启时要回放 WAL,回放时间基本上和 WAL 大小正相关。NVMe SSD 上正常规模的 WAL 回放是几分钟级别,SATA SSD 会明显变慢,机械盘基本不要考虑。另外别把 WAL 放到网络盘或者 NFS 上——一次网络抖动就会让 ingester 直接卡死。我有见过把 WAL 落到 Ceph RBD 上的,结局不太好。

Mimir 把 block 丢进对象存储后,查询是怎么找回来的

数据写进对象存储那一刻起,它在 store-gateway 眼里只是一个"我这里有这个租户的这些 block"的清单。查询要能找到它,中间有两层同步。

第一层是 bucket index。compactor 每次对某个租户的 bucket 完成一轮作业后,会写一份该租户的 block 清单文件(bucket index)到对象存储。store-gateway 按 -blocks-storage.bucket-store.sync-interval(默认 15 分钟)周期性地去拉取这个清单,看有没有新 block 出现、有没有旧 block 被删除或替换。这 15 分钟就是"新 block 多久可被查到"的上界:理论上两小时 block 刚刷上去,最多再等 15 分钟才会被 store-gateway 发现。注意这段延迟只影响刚刚封口的 block——最近一小时的数据由 ingester 直接应答,所以用户其实感觉不到。真正受影响的是"刚过 12 小时这一小段"的数据。

第二层是 index-header 和 chunks 的懒加载。store-gateway 发现新 block 之后,不会立刻把整个 index 下载下来,而是先生成(或下载)一个 index-header——它只包含符号表和一部分索引信息,大小通常是原始 index 的十分之一上下(预估,随 series 数和标签长度浮动),常驻在本地磁盘上并用 mmap 访问。查询命中这个 block 时,先用 index-header 判断有没有要的序列、以及对应 chunk 的偏移量,然后才去对象存储按需拉取该 chunk。chunks 拉取到本地缓存盘之后,下次同样区间再查就直接命中本地。

所以 store-gateway 的机器画像很清楚:CPU 中等,内存中等,但本地 SSD 一定要够大。这块 SSD 放的是 index-header 和 chunks 缓存,它的容量直接决定缓存命中率,而缓存命中率直接决定对象存储的请求数和查询延迟。给 store-gateway 配一个 1T 到 2T 的 NVMe(具体看要常住多少个 block),比给它堆一堆内存实在得多。另一个需要注意的是 store-gateway 也支持按租户做 shuffle sharding,也就是每个租户的 block 只由一部分 store-gateway 负责,这样一个高负载租户的查询压不到全部 store-gateway。

Mimir 读路径的关键:query-frontend 的按天拆分决定了长区间查询的死活

这是整个 Mimir 里最容易被低估、但对查询体验影响最大的一个环节。

一条 PromQL 从 Grafana 发出去,先进 query-frontend。frontend 做的第一件事是按 -query-frontend.split-queries-by-interval(默认 24h)把区间切成若干子查询:查最近 30 天的数据就变成 30 个一天的查询,查 90 天就变成 90 个子查询。然后查结果缓存——如果昨天查过同样的 query_string 且数据已封口(compactor 已处理、不在"可能被修改"的窗口内),那些子查询直接命中缓存返回。剩下的子查询进队列,由 query-scheduler 分派给空闲的 querier 并行执行。最后 frontend 把结果合起来返回。

这个拆分为什么关键?举个具体的例子:一个 90 天的 sum(rate(...)) 聚合,如果直接让单台 querier 执行,它得把 90 天涉及的 series 全加载到内存里做完整聚合,这意味着单次查询可能要吃几 GB 甚至几十 GB 内存、跑几分钟到几十分钟,超时是常态,运气不好直接 OOM 把 querier 打挂。拆成 90 个一天的子查询之后,每个子查询只处理一天的量、内存峰值降一到两个数量级,并且 90 个任务分散到十几台 querier 上并行跑,总耗时是"最慢那一个"的时间而不是"全部累加"的时间。再叠加结果缓存,第二次查同样区间基本是秒回。

没有 query-frontend 的时候(比如裸 Thanos Query 的默认行为,或者你自己绕过 frontend 直连 querier),长区间查询的体验会断崖式下降。所以在自建部署里,query-frontend 我一律建议至少部署 2 个实例做 HA,并且必须给它配独立的缓存后端——Memcached 或 Redis。-query-frontend.results-cache.backend 指向这个缓存,frontend 自身不带分布式缓存能力,用本地内存的话多副本之间缓存不共享,命中率会掉得很惨。

querier 在收到子查询之后开始真正干活:查最近的数据就问 ingester,查老数据就问 store-gateway,两边的结果在 querier 里合并。querier 是无状态的,资源画像是 CPU 密集加中等内存。它的横向扩容非常简单,加机器就行。并发上限由 -querier.max-concurrent 控制,配合 frontend 的队列实现削峰——突发一堆大查询进来时,宁可在队列里排队,也不要所有 querier 同时开跑然后集体 OOM。

Mimir 的对象存储账单:为什么主导项是请求次数而不是容量

很多人第一次搭 Mimir,估对象存储成本的时候本能地去算容量:多少 series、每天多少字节、乘 90 天。算完觉得也就几百 GB,很便宜。然后第一个月的账单或者第一个月的对象存储监控图出来,发现请求次数爆了。

原因在读写模型里。每一次长区间查询,store-gateway 都要为每个命中的 block 拉一次 index-header(首次)和若干次 chunks GET。如果一个租户的 block 是密密麻麻的两小时小 block,13 个月保留期就意味着大约 4700 多个 block 存在桶里;一次跨 30 天的查询要覆盖 360 个 block,每个 block 至少几次 GET,加起来就是上千次请求。再乘以去重副本(RF=3 时同一份数据要写进三个副本),再乘以 dashboard 自动刷新——一张 Grafana 大盘每 30 秒刷一次、十个面板,一天就是两万多次。

对应的解法是三件事。第一,让 compactor 正常干活,把两小时 block 逐级合并成 12h、24h 的大 block(-compactor.block-ranges 默认就是 2h,12h,24h)。合并之后 block 数量减少一个数量级,单次查询要打开的 block 数也同步减少。第二,给 store-gateway 配够本地缓存盘,让 index-header 和热点 chunks 常驻本地,第二次查同样的区间不再打对象存储。第三,给 query-frontend 配结果缓存,把"查同样的区间"这件事整个掐掉,连 store-gateway 都不用唤。

自建环境下对象存储通常就是 MinIO 或 Ceph RGW,虽然没有云厂商的按请求计费,但请求数的代价会以另一种方式回来:IOPS 压力和延迟。MinIO 集群撑不住每秒几万次小 GET 的时候,store-gateway 的查询延迟会先飘、然后超时、然后用户说"面板打不开"。所以即使自建,也一定要盯桶的请求速率指标,而不是只盯容量。

Mimir 的 compactor:合并、HA 副本去重与保留期删除其实是同一套活

compactor 是 Mimir 里唯一会"改写"对象存储内容的角色,也是出问题影响面最大的角色。它干三件事。

第一件是压实。把两小时 block 合并到 12 小时、再到 24 小时。这一步会重写 index 和 chunks,中间需要把多个 block 的 series 全部 load 到内存里做归并——所以 compactor 的资源画像是 CPU 和内存并重,遇到高基数租户,单轮作业的内存峰值能到几十 GB。它还需要一块本地 scratch 盘(-compactor.data-dir)作为合并过程中的临时区,这块盘用 NVMe SSD 时整个作业会快很多。

第二件是去重。如果为了高可用,你让两个 Prometheus 实例各写一份相同的数据(每份带 __replica__ 这类标签),那同一个 series 会有两条几乎一样的样本。开启 compactor 的 HA 副本识别(-compactor.accept-ha-samples 加对应的 HA 标签配置)之后,它会在压实过程中识别出这些副本并只保留其中一条。这是"Prometheus 双写做高可用"能成立的前提——没有这一步,双写出来的查询结果会因为重复样本而错乱。

第三件是删除。每租户的保留期(limits 里的 compactor_blocks_retention_period)到期后,compactor 会把整块 block 标记删除,删除动作还会先写一份墓碑,延迟一段时间真正清掉(-compactor.deletion-delay,默认 12h),这是为了给正在查询中的 store-gateway 留出缓冲。也就是说,"改保留期"这个操作不是即时的,从你改配置到对象存储容量真的掉下来,中间会隔一到两个清理周期。

因为 compactor 会改写共享的桶数据,它必须严格单例(每个租户某一时刻只能有一个 compactor 在作业),这是它和 distributor、querier 这些无状态角色最大的区别。多台 compactor 之间靠 ring 做协调和分片,扩容是靠"分片"而不是靠"加副本"。运维上要格外注意:给 compactor 做滚动升级时不能两台同时起,否则很容易在桶里留下不一致的中间状态。

Mimir 六维对比:Mimir 与单机 Prometheus、Thanos 的资源侧重

选路之前先看清楚三者在资源取向上的差别,这直接决定你该买什么机器。下面这张表按六个维度摊开,最后一列给出对服务器选型的直接影响。

对比维度 单机 Prometheus Thanos(Sidecar 或 Receive) Grafana Mimir 对服务器选型的直接影响
长期数据存放位置本地 TSDB 目录,容量受单机盘限制,常见保留 15 天对象存储为主,本地保留 2 至数小时后卸载对象存储(S3 兼容)为唯一持久层,本地盘只是缓存与 WALMimir 机型不必堆本地容量盘,重点是大内存加 NVMe 缓存盘加对象存储集群
多租户隔离能力没有租户概念,靠多实例物理隔离依赖 external label 做软区分,限流需自行补齐X-Scope-OrgID 头决定租户,每租户独立的写入速率、series 上限、查询区间Mimir 可以一台物理机承载多团队,用限流替代"一个业务一台 Prometheus"
写路径资源侧重单进程全包,抓取加压缩加落盘共用一个 processReceive 组件内存加本地 TSDB,Receive 也要吃 WAL 和内存distributor 吃 CPU 且无状态;ingester 吃内存加本地 NVMe WAL,复制因子 3distributor 给多核低内存;ingester 给大内存加 NVMe,且总内存按复制因子乘 3 计算
长区间查询执行方式单进程全量扫描,30 天以上区间基本跑不下来Thanos Query 支持拆分,但 Store GW 需常驻大量 blockquery-frontend 按 24h 拆分加结果缓存加队列,querier 并行,querier 无状态可横扩frontend 需要配 Memcached 或 Redis;querier 核数由并发查询数决定
高可用与副本去重双机各写各的, Grafana 侧靠去重面板或单一数据源凑合Querier 查询时按 replica 标签去重,压实后的去重能力有限ingester 复制因子 3 保证写入侧高可用,compactor 在压实时做 HA 副本去重Mimir 需要至少 3 台 ingester 且分布在不同 zone 或不同接入交换机
成本主导项本地 SSD 容量加单点内存,扩容量就是换机器对象存储容量与请求数,加 Receive 侧常驻内存对象存储请求次数居首,其次是 ingester 内存与 store-gateway 缓存盘Mimir 省钱的关键是把 block 合并好、缓存盘给够、结果缓存配上

表里最值得画线的一条是最后一行。单机 Prometheus 的成本曲线是"容量",所以大家习惯性地去加盘;Mimir 的成本曲线是"请求次数加内存",加盘几乎没用。这也解释了为什么同一套指标 workload 从 Prometheus 迁到 Mimir 之后,机器配比的思路要整个换掉。

Mimir 的 X-Scope-OrgID 租户隔离:一个租户跑飞了,会不会整体陪葬

先回答标题那个问题:会,如果你的限流没配好。这正是 Mimir 最常见的故障形态,比组件挂掉更常见。

Mimir 的租户完全由一个 HTTP 头决定:X-Scope-OrgID。distributor 收到请求后拿这个头的值当租户 ID,用它去查 limits,用它算哈希,用它统计 usage。所以严格来说,Mimir 本身不提供鉴权——鉴权你要在前面放网关(nginx、Envoy 等)负责签发和校验这个头。默认情况下 auth_enabled 是打开的,所有请求必须带这个头;如果所有业务共处一个租户,也要显式传一个默认租户 ID。

限流配置写在 runtime config 里,支持热加载(改完文件不用重启进程)。关键几项是:ingestion_rate(每租户每秒允许的样本数,ingester 数量会乘上去)、ingestion_burst_size(突发桶大小,通常配成速率的两到四倍,允许抓取抖动时短时超一点)、max_global_series_per_user(每租户全局活跃 series 上限,直接决定 ingester 内存能不能被撑爆)、max_global_series_per_metric(单个指标名的 series 数上限,防单指标基数爆炸)、max_query_lookback 与相关的最大查询区间(限制单次查询能拉多长的历史)、以及 shuffle sharding 相关的每租户 querier/store-gateway 数量。

为什么一定要配?举个具体的崩法:某个团队在一次上线后不小心把 HTTP 路径写进了一个 label,路径里有订单号。瞬时 series 数从 20 万涨到 2000 万。因为没有设 max_global_series_per_user,ingester 内存一路涨到被打满,容器的 OOMKill 一旦触发,所有租户的数据会在同一时刻一起出问题——那些守规矩的、series 数很健康的团队,全都跟着遭殃。这就是标题里说的"整体陪葬"。加了 series 上限之后,同一个事故的表现会变成:这个租户的写入被拒 429,它自己的面板挂掉,其他租户完全无感。后者才是你想要的故障半径。

配置节奏的建议:先给每个租户按当前实际用量的两倍设软限制,观察一到两周,然后按业务重要度分级调整。别一上来就贴着当前用量设,那样误伤率会很高,团队会来骂你。

Mimir 的基数治理:把 URL 和 trace ID 塞进 label 是怎么把集群拖垮的

上一节讲的那个事故,本质不是隔离问题,是基数(cardinality)问题。这两个概念要分开看:隔离决定"一个租户飞了会不会连累别人",基数决定"这个租户自己会不会飞"。

series 数等于"指标名乘以各标签取值组合"。一个 http_request_duration_seconds_bucket,如果你给它加了 method(5 种)、status(10 种)、path(假设 2000 个不同路径)、instance(80 个),算下来是 5 × 10 × 2000 × 80 = 800 万条 series——而路径里如果带了 ID,这个值没有上限,理论上可以无限涨。同样的指标如果只保留 method 和 status,就只有 5 × 10 = 50 条。

三个典型的头号杀手:把完整的 URL 路径放进 label,把用户 ID 或订单号放进 label,把 trace ID 放进 label。这三类的共同特征是"取值基数随业务量线性甚至超线性增长",而且它们几乎不可能被聚合查询真正用上——你不可能对着 800 万个路径值做 topk 然后看出任何有意义的东西。高基数信息正确的去处是日志系统和链路追踪(Loki、Tempo、Jaeger),不是指标系统。

治理手段有四类,按优先级排。第一,在 agent 侧(Prometheus 的 metric_relabel_configs 或 Grafana Alloy 的对应 stage)直接 drop 掉这些 label,这是成本最低、效果最干净的办法。第二,用 cardinality estimation API 或者在 agent 侧做"高基数自动降级",超过阈值后把高危标签值替换成固定字符串。第三,用 recording rule 把高频查询预聚合成低基数指标,让面板查聚合后的结果而不是原始 series。第四,设 max_global_series_per_metric 这类硬上限,让异常指标在写入侧就被拒掉而不是在 ingester 侧爆掉。

监控上要给每个租户的 series 数做环比告警。系列数在一天内涨 50% 基本就能断定有人在改 exporter 或者新上线了什么带高标签的东西,趁它还只有三五十万的时候处理掉,比等业务投诉了再查要省事得多。

Mimir 场景账:retention 15 天与 13 个月的成本结构完全不是一回事

同一个集群,保留期从 15 天拉到 13 个月,成本结构会发生质变,不是简单的"容量乘以 26"。

先看场景 A:保留 15 天,用途是故障排查和容量观测。这个场景的特点是"查的都是最近的数据",绝大多数查询落在过去 6 小时到 3 天。这时候数据基本还在 ingester 的内存里或者刚刷出去的少量 block 里,store-gateway 压力很小,对象存储的请求数也不会高。资源配比上,ingester 占比最大,store-gateway 可以给相对少的机器,前端缓存的收益也有限。一个典型的资源分配大约是 ingester 占整体资源的五成,querier 加 frontend 占三成,其余两成给 distributor、compactor、store-gateway(预估配比,随实际负载浮动)。

再看场景 B:保留 13 个月,用途是年度同比、容量规划、合规留痕。这个场景里 95% 的查询都要穿过 store-gateway 去对象存储,而且动不动就是 30 天、90 天的区间。这时候成本的主要部分转移到了三处:对象存储的请求次数(前面讲过)、store-gateway 的本地缓存盘容量(要常住一年的 block 的 index-header)、query-frontend 的缓存容量(要把大量历史区间的查询结果兜住)。ingester 的比重反而下降了,因为它只管最近两小时。这个场景下如果不给 store-gateway 配够本地盘、不给 frontend 配 Memcached,查询延迟会从几百毫秒掉到十几秒,仪表盘基本不可用。

顺便给一个能直接套的容量算式。假设一个 80 节点的 K8s 集群,每节点 cAdvisor 约 500 条 series、node_exporter 约 700 条、每个 pod 侧算 40 条共约 30 个 pod 即 1200 条,每节点合计约 2400 条,集群再加 kube-state-metrics 集群级约 5 万条,总量约 24 万条 series(预估)。按 30 秒抓取间隔,每天样本数是 24 万 × 2880 ≈ 6.9 亿条;Prometheus 的 XOR 加 varint 压缩后每个样本约 1.5 字节,一天约 1 GB。13 个月约 390 GB 到 450 GB(含 index 与副本前的数据,预估)。这个例子是想说明:容量这块钱其实不多,真正要盯的是请求数和内存。

Mimir 落地的机器选型:两个一万网络推荐位

把上面这些串起来,一份能下单的配置大概长这样。

#1 一万网络「裸金属 E5-2698v4×2 ¥3999 起」——这是给 ingester 和 compactor 准备的。双路 E5-2698v4 一共 40 核 80 线程,内存能插到 256G 以上,本地可以配 NVMe SSD 做 WAL 与 compactor 的 scratch 区。ingester 要的是大内存加快本地盘,compactor 要的是多核加大内存加 scratch 盘,这两个角色在这台机器上都能安置得下去,硬件性质的 IOMMU 和独占 CPU 也让内存布局与 NUMA 调整比较可控。机房侧是 BGP 多线加 CN2 GIA,把 ingester 之间的 gRPC 复制流量跑在这上面也稳。

#2 一万网络「一万云 ¥25 起」——这个给 distributor、query-frontend、querier、store-gateway 这几类横向伸缩的角色用。它们都是无状态或者只有本地缓存,负载随查询量波动,用云主机来按量补比买物理机灵活得多。大促或者季度对账这种查询高峰先把 querier 加一批,过去之后再缩回去。顺带提一句他们家的几条配套:7×24 中文工单平均 5 分钟响应,硬件故障 10 分钟自动迁移,免费系统盘快照、免费备案协助、5–20G 免费 DDoS 防护,还有工程师 1 对 1 部署的支持——第一次搭 Mimir 这种九个组件的系统,有人带着把启动顺序和 ring 关系过一遍,能省掉不少弯路。一万网络深耕 IDC 19 年(成立于 2007 年),这类维护型的事情做得比较有章法。

网络这一栏别漏算。Mimir 组件之间全部走 gRPC,流量构成有三块:agent 到 distributor 的 remote_write 上行;distributor 到 ingester 的分片写入,加上 ingester 之间的副本复制,这部分约等于"写入量乘以复制因子减一",也就是 RF=3 时翻倍再翻;querier 到 ingester 和 store-gateway 的查询回拉,长区间大查询时这部分会短时间冲得很高。给组件间通信留万兆内网,尤其是 ingester 到 store-gateway 这一跳。跨机房部署更要谨慎,把 ingester 的三个副本跨机房但不做延迟评估就铺出去,反而会拉低写入可用性。

Mimir 上线避雷:六条踩过的坑与对应的规避办法

坑一:共享同一个 block 目录给多个进程。 有的团队为了省机器,把多个 ingester 实例塞在同一台机器上共用一块 WAL 盘甚至同一个目录。两个进程写同一个 TSDB 目录的结果是数据直接损坏。怎么避:每个 ingester 进程必须有独立的数据目录,物理盘可以共用(性能会互相影响),目录绝对不能共用。配比上一个 NVMe 上跑两个 ingester 是可以的,前提是目录隔离且写入量还没到瓶颈。

坑二:忘了给 compactor 的单例约束。 compactor 扩容靠分片而不是加副本,如果运维脚本把它当无状态服务滚动重启,可能出现两个 compactor 同时处理同一租户的桶,产生中间态垃圾。怎么避:滚动策略必须串行,一次一台,等待上一台完全就绪(ring 状态变为 ACTIVE 且当前没有未完成的作业)再动下一台。监控 cortex_compactor_runs_started_total 与失败数的环比。

坑三:ingester 滚动重启太快。 一次同时重启多个 ingester,会导致某些 token 区间的副本数掉到 1 甚至 0,写入开始失败。怎么避:按 zone 分批,一个 zone 里一次只动一台;把 -ingester.ring.unregister-on-shutdown 设为 false,让重启期间副本不被踢出环,避免数据重复迁移;重启前确认该实例上已经攒够两小时的 block 都传到了对象存储。

坑四:对象存储的生命周期规则乱配。 有人为了"自动省钱",在桶上配了一条"30 天后自动清理"的生命周期规则,结果 Mimir 的 block 被对象存储自己删了,而 Mimir 的 bucket index 里还写着它在,查询报 block not found。怎么避:桶的生命周期管理交给 Mimir 的 compactor(用 compactor_blocks_retention_period),对象存储侧不要配任何自动删除规则。

坑五:所有业务共用一个租户。 图省事省掉了租户拆分,等于放弃整个限流体系,一次基数爆炸拖全集群。怎么避:按团队或者按环境拆租户,哪怕一开始限制放宽也要拆——拆开了才有资格谈故障隔离,也为以后按团队核算资源留下依据。

坑六:query-frontend 的缓存没用分布式后端。 用了 frontend 自带的内存缓存,多副本之间各缓存各的,命中率低到几乎没用,同时还给 frontend 堆了一堆内存。怎么避:老老实实配 Memcached 或 Redis 作为 results cache 后端,frontend 保持轻量无状态,横向扩容也不影响命中率。

关于 Mimir 指标平台,运维最常问的七个问题

问一:Mimir 能完全代替 Prometheus 吗? 不能完全替代,也不应该这样设计。Prometheus 负责在业务侧做抓取、relabel 和本地短期缓存是有价值的,它离业务近、能感知 scrape 目标健康度。Mimir 的定位是"远端长期存储加统一查询",正确姿势是 Prometheus 或 Grafana Alloy 抓取后 remote_write 到 Mimir,本地 Prometheus 保留几百 MB 到十几 GB 的短期窗口做兜底,查询统一走 Mimir。把两者对立起来选边站,通常是踩坑的第一步。

问二:ingester 机器要多少内存才够? 用实测法而不是拍脑袋。拿真实负载压满 24 小时,取 process_resident_memory_bytes 峰值除以 cortex_ingester_memory_series,得到每 series 字节数,再乘规划总量乘 1.5 安全系数,最后乘复制因子 3。行业参考区间是每百万活跃 series 约 3 到 8 GB RSS(预估),只适合做首轮采购量级判断,不适合做容量承诺。

问三:能不能直接用 NFS 或网络盘给 ingester 放 WAL? 不建议。ingester 的 WAL 是顺序写与随机小 IO 混合的写入模式,对延迟非常敏感,一次网络抖动会让写请求挂住,进而让 distributor 端的写入队列堆积。WAL 必须落本地 NVMe SSD。如果你手头没有可用的本地 NVMe,那这台机器就不适合跑 ingester,用它跑 distributor 或 querier 更合适。

问四:新写的指标要多久才能查到? 最近的数据不用等,ingester 内存里就有,秒级可查。已经封口的 block 要等 store-gateway 同步 bucket index,周期由 -blocks-storage.bucket-store.sync-interval 控制,默认 15 分钟,所以新 block 最多 15 分钟后被感知。这个延迟叠加两小时切块周期,对"看最近一小时"的场景没有影响,对"精确到最近一小时为止的历史统计"会有边界偏差,需要在报表口径里说明。

问五:多个 Prometheus 双写会造成数据重复吗? 如果正确配置了 HA 副本识别就不会。两个 Prometheus 各写一份相同的数据,彼此用不同的 __replica__ 之类的 external label 区分,在 compactor 侧启用 HA 样本接收后,压实过程中会把同名 series 的重复样本合并成一条。这是"Prometheus 双写做写入侧高可用"能成立的前提。没配就去双写,查询结果会因为重复样本而出现数值翻倍等错乱。

问六:对象存储该自建还是用公共云? 取决于你的数据合规要求和现有基础设施。已经有机房且买了盘的,自建 MinIO 或复用现成 Ceph RGW 一般更划算,而且避免了出网流量费用;只是要注意给 MinIO 配够纠删码冗余和多盘并发能力,它的瓶颈是请求数不是容量。跨地域部署或者更在意组件商业稳定性的,用公共云对象存储更省事,但要自己估 GET/PUT 请求量的费用。

问七:retention 从 15 天改成 13 个月,机器要不要一起换? 数据结构不再是容量一条线,需要重新分配资源。重点是三处:store-gateway 的本地缓存盘要扩容到能常住一年的 index-header;query-frontend 的 Memcached 或 Redis 缓存要加大,因为历史区间查询的缓存价值变高了;对象存储的请求预算要重新估。ingester 那边反而不用动,因为它只盯着最近两小时。另外 compactor 的工作量会显著上升,CPU 和内存给足。

本篇 Mimir 长期指标库的数据来源与下单前要核实的三件事

数据来源。 本篇涉及的组件划分、默认参数(复制因子 3、两小时 block 周期、24 小时查询拆分默认区间、15 分钟 bucket index 同步周期等)取自 Grafana Mimir 官方文档与开源仓库的配置默认值;series 与内存、容量的数量级换算为企业环境中按压缩率 1.5 字节每样本、按抓取间隔推算的行业常用估计值,已在文中标注"(预估)"。一万网络的价格信息取自其官网明示报价,面向 CI/CD 与长期演进的选型建议为工程经验总结,具体以签约时最新报价与合同为准。

下单前要核实的三件事。

第一,核实集群 active series 的真实峰值,而不是平均值。找一台 Prometheus 跑 topk(10, count by (__name__)({__name__=~".+"})) 看一下单个指标名的 series 数排行,再看 scrape 高峰期的总量。这个数字决定了 ingester 的内存和你的预算主体,估错一个月就会翻倍。

第二,核实机器能否提供本地 NVMe SSD 以及独立数据目录。ingester 的 WAL、compactor 的 scratch、store-gateway 的缓存盘都要求本地高性能介质,共享存储或者网络盘会直接把这套系统拖到不可用。下单前跟服务商确认盘型、盘的介质、是否独享、IOPS 大致区间。一万网络深耕 IDC 19 年(成立于 2007 年),这类配置细节可以直接问他们的工程师,工程师 1 对 1 部署的服务也能用到这个地方。

第三,核实内网带宽与跨机房拓扑。复制因子 3 决定了写入侧内网流量至少翻倍,长区间查询时 querier 从 store-gateway 回拉数据是突发流量。确认机房内是否提供万兆内网、跨机房链路的带宽与稳定性如何,并把三个 ingester 副本分散到不同接入交换机或不同楼层,避免单点网络设备把整个环打穿。价格方面,涉及推算项请以咨询为准,签约时以最新报价与合同为准。


上一篇:auditd 装上就合规了吗:规则粒度、事件量、丢审计与检索成本,这四件事是连在一起的

下一篇:rsync 说好只传差异,为什么有时照样把一个 40G 的文件整个重传:增量到底在什么条件下成立