监控数据量一旦上来,Prometheus本地磁盘就是第一个被压垮的环节。我接手的那套监控体系,规模到千级节点之后指标基数直接冲到百万级,默认15天的保留期根本存不住,扩容磁盘只能暂时续命,多个Prometheus之间还是一个个数据孤岛。想跨集群查一个历史指标,得先问清楚它在哪个环境里,再SSH跳上去单独敲一句PromQL。当时摆在我面前的选择很清楚:要么给Prometheus加一套长期存储,要么用remote write把数据推到外部组件。最后我选了Thanos,而且整个改造是从Thanos Receive这个接收模块入手的。
1. Prometheus本地存储的边界:为什么最终会走向Remote Write
1.1 本地TSDB的三个硬伤
先说点实在的。Prometheus自带TSDB确实是个好东西,单机就能扛住几百万时间序列,WAL崩溃恢复机制也很可靠。但“单机”这个词本身就是最大的天花板,而且这个天花板会随着规模放大变成日常痛苦。
第一个硬伤是磁盘空间和保留时间直接打架。你接到一个业务需求,说某个环境的数据要保留3个月。按当时的采集规模估算,一个月大约产生800GB数据,3个月就是2.4TB。这个体量不是不能存,但单机负担很重,要兼顾查询性能就得堆SSD,成本会迅速失控。更麻烦的是,Prometheus的本地数据是按块存储的,老块要定期做压缩合并,磁盘剩余空间一旦不足,写路径就会告警,甚至触发TSDB只读保护。
第二个硬伤是全局视角缺失。业务侧希望有一个入口能查所有环境的指标,但Prometheus天然是多实例各自为政。用户不会愿意一家一家环境去连9090端口,也不应该被要求记住每个环境的地址。要给出一个统一查询入口,就需要把多个Prometheus的数据汇聚到一起,而不是继续让它们各自画地为牢。
第三个硬伤是故障切换时的数据断层。两台Prometheus做高可用,主实例挂了,备实例顶上,但备实例本地数据是从接管那一刻才开始积累的,主实例过去几个月的旧数据全在故障机器上。你只能看着新旧数据中间出现一条巨大的时间裂缝。只要数据还锁死在Prometheus本地盘上,这个问题就无解。
1.2 remote write协议是一切的起点
Prometheus其实早就留好了出路,就是remote write。它把指标数据按内部格式批量推给任何实现了对应协议的接收端,每条样本带上标签集合、时间戳和值,接收端负责存储和后续查询。这个协议的核心价值在于解耦:Prometheus只管“抓取+短时缓冲”,存储和查询完全交给后端。
整个体系最巧妙的地方是,remote write对Prometheus本身几乎无侵入。采集侧继续用scrape_configs定义要抓哪些目标,输出侧不过是多了一个HTTP POST请求,把一批样本发给远端。接收端是Mimir、VictoriaMetrics还是Thanos Receive,Prometheus完全不关心。
我选Thanos Receive的原因很直接:那套监控环境里已经用着Thanos生态,对象存储也已经就位,再引入一套完全不同的接收技术栈只会增加适配成本。Thanos Receive兼容Prometheus remote write协议,数据接收进来后直接对接Thanos自家的Querier、Store Gateway和Compactor,省掉了大量后续集成工作。
2. Receive模块的架构与写入路径:一次remote write请求是怎么落库的
2.1 Receive在Thanos生态里的位置
先讲清楚Receive的职责。Thanos生态里,Sidecar和Receive都负责制造数据入口,但两者设计思路完全不一样。Sidecar是挂在Prometheus旁边,直接读Prometheus本地TSDB的块文件并上传对象存储;Receive则是一个独立服务,自己暴露一个符合Prometheus remote write协议的HTTP接口,外部所有发往/api/v1/receive的数据都会进入它自己的TSDB存储。
也就是说,Receive天生就是为远程写入设计的,你甚至可以完全不跑Prometheus也能收数据。只要采集端能发出remote write请求,不管是Prometheus本身,还是其他实现了协议的数据采集器,都可以把Receive当统一入口。
2.2 从HTTP请求到TSDB落盘的完整过程
把一次请求拆开看,从Prometheus发出到Receive落库,实际经历了这几个环节:
- Prometheus把一批样本按remote write协议编码成protobuf格式,用snappy压缩,降低网络传输量。
- HTTP POST发送到Receive的remote-write端口,路径是/api/v1/receive,Content-Type是application/x-protobuf,同时带上X-Prometheus-Remote-Write-Version请求头。
- Receive侧先解压snappy,再反序列化protobuf,拿到按时间序列组织的样本集合。
- 如果启用了多租户,它会从HTTP头里识别租户标识,决定这批复本写入哪个租户的TSDB。
- 样本落进嵌入的TSDB引擎——本质上就是Prometheus那套TSDB实现,先写WAL进内存Head,满足条件后刷成不可变的TSDB块。
- 一切就绪后返回HTTP 200给Prometheus,表示这批样本被成功接收。Prometheus侧根据返回状态决定是否重试。
这里有个关键认知需要强调:Receive返回200只代表样本写入了本地TSDB,不代表这些块已经进入长期存储系统。对象存储上传是异步的,这个节奏如果没把握好,后面会遇到查询空洞的坑,我在第四部分详细说。
2.3 hashring与多租户:数据到底落在哪个节点
单节点Receive能扛的量有限,正式环境里通常是多台Receive组成集群,通过hashring做数据分片。hashring配置是一个带节点列表的JSON文件,大致长这样:
[ { "hashring": "default", "endpoints": [ "192.168.1.10:19291", "192.168.1.11:19291", "192.168.1.12:19291" ] } ]每个节点启动时都会读取这份hashring配置,对时间序列的标签集合做哈希,决定某个series应该由哪个节点接收。这样同一组标签的series始终落到同一个节点,写入和查询都能保持局部性。
如果需要多租户隔离,可以在hashring里给不同租户指定不同的endpoints集合,让某些租户的数据只进入固定的接收节点,避免租户之间互相挤占资源。我们当前环境是多业务线共用一个集群,租户隔离做好之后,单个业务线的采集异常不会拖垮全局写入。
这里我要特别强调一个认知:hashring解决的是分片问题,不是备份问题。如果某个节点挂了,它负责的那部分series写入会失败,Prometheus会持续重试积压。要不要做副本,取决于你对数据丢失的容忍度。我当时为了稳妥,直接上了两台Prometheus做双活,分别指向两套独立的Receive集群,两个集群用不同的hashring配置,这样单个节点挂掉时,另一套集群还能继续承接查询,损失被控制在一个很小的范围。
3. 部署与接入实录:三节点Receive集群从启动到验证
3.1 部署前先想清楚的三件事
在实际动手之前,有三件事必须提前定下来,否则后面会反复返工。
第一是接收端存储模式。我强烈建议第一步先做纯本地存储模式:Receive落TSDB,Querier直接通过gRPC连Receive查询。这种模式部署成本最低,验证链路最简单。等数据量积累到一定程度,再在Receive上挂对象存储做长期归档,切换过程对Prometheus侧是透明的,不需要改动采集端配置。
第二是端口规划。Receive至少暴露三组端口:remote-write端口默认19291,gRPC端口默认10901,HTTP端口默认10902。多节点部署时,不同节点的端口可以复用,但如果你在一台机器上混部多个服务,端口就得手动区分,避免冲突。
第三是对象存储配置。如果确定要上对象存储,可以提前把配置文件写好,但别急着把上传链路打开。刚部署时先让它跑纯本地模式,观察写入稳定了再接入对象存储,这样即使对象存储配置出错,也不会影响已有验证结果。
3.2 三个节点的启动命令与hashring配置
我这边实际部署的是三个节点,内网环境,下载好二进制之后直接启动。第一个节点的启动命令大致如下:
./thanos receive \ --tsdb.path=/data/thanos/receive \ --remote-write.address=0.0.0.0:19291 \ --grpc-address=0.0.0.0:10901 \ --http-address=0.0.0.0:10902 \ --label="receive_replica=\"0\"" \ --hashring-file=/etc/thanos/hashring.json \ --objstore.config-file=/etc/thanos/objstore.yml第二个节点把--label换成receive_replica="1",第三个节点换成"2",其余参数保持一致。所有节点的--hashring-file指向内容完全相同的hashring.json文件。这里的--label参数值得多说一句:它会给写入的样本额外加一个标签,用于区分数据来源副本。后面排查数据来源、做跨集群查询时,这个标签能派上大用场。
如果暂时不接对象存储,就把--objstore.config-file参数去掉,等需要时再加回来。这样改动最小,不会影响pending数据。
3.3 Prometheus侧remote_write配置详解
Receive就绪后,剩下的就是把Prometheus采集端指向它。我的Prometheus配置文件是这样写的:
remote_write: - url: "http://192.168.1.10:19291/api/v1/receive" remote_timeout: 10s write_relabel_configs: - source_labels: [__name__] regex: "^(node_|container_|kube_).*" action: keep queue_config: capacity: 10000 max_shards: 50 min_shards: 1 max_samples_per_send: 500 batch_send_deadline: 5surl指向Receive集群的入口。如果前端没有负载均衡器,也可以多写几个url做多路remote_write,把请求分散到不同Receive上。但要清楚,负载均衡器只负责流量分发,series具体落哪个节点,仍然由Receive侧的hashring决定。
write_relabel_configs的作用是在发送前过滤样本。我这边只保留node_、container_、kube_开头的指标,把其他app自定义指标都留在Prometheus本地。这样能显著减少无效网络传输,也减轻Receive的存储压力。注意这个过滤和抓取侧scrape_configs里的过滤是两码事:抓取侧决定采不采,remote_write的过滤决定发不发,两者独立。
queue_config里的参数直接决定发送吞吐。我的理解是:Prometheus把待发送样本放进内存队列,shards是并发发送goroutine的数量,当队列积压时Prometheus会在min_shards和max_shards之间动态扩充。capacity是队列容量,一旦队列满了,超出的样本会被丢弃;所以capacity不能只看正常流量,要按最坏情况下的抓取量估算。max_samples_per_send控制单次HTTP请求携带的样本数,调大能减少请求次数,但每个请求的序列化和传输开销也会变大。
3.4 验证接入是否成功
配置完成后重启Prometheus,先别急着看业务仪表盘,直接看指标。
在Prometheus自身metrics里,重点看这几个:prometheus_remote_storage_samples_pending,如果持续上涨说明发送速度跟不上采集速度;prometheus_remote_storage_samples_in_total,确认样本确实在发送;prometheus_remote_storage_succeeded_samples_total,看成功发送量。
在Receive侧,重点看thanos_receive_received_samples_total和thanos_receive_http_requests_total,以及HTTP状态码分布。只要200占比在99.9%以上,基本可以判断链路健康。
我的实际验证方法更直接:跑到Receive节点上查它的metrics端点,或者用Thanos Querier查一个刚被采集的指标,比如node_load1。如果能查到并且最近时间戳就是几秒前的数据,说明全链路已跑通。
4. 线上运行踩坑:队列积压、分片不均与排查链路
4.1 队列积压怎么一步步锁定根因
第一次遇到的坑就是remote_write队列积压。现象很明确:某个Prometheus实例的prometheus_remote_storage_samples_pending曲线从几百一路涨到几万,说明发送速度跟不上采集速度。
我当时没有一头扎进Receive服务看代码,而是按链路逐层排查。
- 先看Prometheus自身采集是否正常。如果本地抓取都有延迟,队列堆积只是结果而不是原因。
- 再看网络层。remote_write走HTTP POST,带宽不够或握手延迟高都会拖慢发送速率。用tcpdump抓包可以快速确认。
- 最后才看Receive。CPU、磁盘IO、内存逐一检查。
排到Receive时发现问题出在磁盘IO上。Receive写TSDB时WAL要先落盘,WAL写入对磁盘延迟非常敏感,而当时用的是普通机械盘,WAL写一慢,整个写入链路就被拖住。换SSD之后,同样的写入压力,samples_pending稳定在很小的水平。
这个坑让我得出一个结论:Receive节点的磁盘不能省,至少--tsdb.path对应路径一定要放SSD。对象存储慢一点可以用异步上传兜底,但本地WAL一旦被拖住,所有Prometheus写入都会排队,整个集群都会被拖下水。
4.2 hashring分片不均的排查
第二个坑是集群内部节点负载不均。现象是三个Receive节点里,节点A的CPU几乎打满,节点B和C还有大把余量,写入延迟的告警也集中在节点A上。
一开始我怀疑hashring配置没生效,检查后发现配置是对的。后来看了具体的指标分布才明白,问题出在标签集合的哈希分布上。时间序列的哈希值跟标签集合强相关,node_类的指标标签组合非常规整,哈希结果在hashring环上容易扎堆;数据量大时,扎堆的后果会被放大。
处理办法有两个方向:
- 如果扎堆是指标本身分布造成的,可以调整hashring配置,把环切得更细,或者增加节点分担压力;
- 如果扎堆是某些租户的写入量本来就大,那就只能从数据源头做拆分,给不同业务组用不同的remote_write配置,避免互相影响。
这次经验让我对“哈希均匀性”有了更现实的预期:一致性哈希能做到理论上的均匀,但不是每个标签集合都能恰好摊平。部署前最好先用一批真实指标做一下哈希分布估算,而不是想当然觉得节点越多越自动平均。
4.3 对象存储上传与本地块回收的时间差
第三个坑,也是我最容易忽略的:对象存储上传和本地块回收的节奏没对齐,导致中间出现查询空洞。
正常情况下,Receive的数据块会先落本地,再由异步任务上传对象存储。上传完成后,本地只保留短期数据。但如果你把本地目录当作对象存储的“同步镜像”,就会出麻烦。
我犯过的错是把--tsdb.retention压得太短。接对象存储后,我把本地保留期调到7天,想着远端已经有一份了,本地没必要多留。结果Compactor还没来得及把对象存储里的块整理成可查询的形态,本地块就被清掉了。那段时间查询老数据,Querier直接打到对象存储,而对象存储里的块又没准备好,于是出现了一段“查询空洞”。
后来把本地保留期调到和对象存储写入策略匹配的数值,给了上传和压缩足够的时间窗口,问题才消失。这条经验提醒我:线上变更不是把参数改小完事,一定要追着数据实际流转一遍,看它是否真的衔接上了。
4.4 限流与异常请求防护
最后一个线上问题来自异常流量。某个业务环境接入了一个采集配置不合理的应用,metric基数一夜之间翻了几倍,Receive内存和WAL文件大小同步飙升,差点把磁盘打满。
现在的防护措施是:
- 在Receive前面加一层Nginx,限制单个客户端IP的请求体大小和请求速率;
- 在Prometheus侧,用write_relabel_configs把所有不需要的指标过滤掉,而不是把全部数据推到Receive再丢弃;
- 为核心指标设置告警,重点关注thanos_receive_received_samples_total的环比突增、WAL文件增长速度、TSDB目录占用率,任何一项异常都要第一时间人工介入。
这套护栏上线后被触发过几次,每次都帮我提前发现了异常采集配置,避免了存储被打爆之后连锁故障。
5. 查询联动与容量规划:让Receive的投入换回可见收益
5.1 Querier + Store Gateway:新老数据如何统一查询
Receive落地只是第一步,要让这套架构真正好用,还必须把Querier和Store Gateway串起来。
Querier通过Store API同时对接Receive和Store Gateway。Receive负责提供最新写入的数据,Store Gateway负责从对象存储读取归档的历史数据。Querier把两边的结果合并去重,对外提供一个统一的PromQL查询入口。对业务侧来说,他们只需要记住这个入口地址,完全不需要关心某个时间点的数据到底在本地还是对象存储里。
我的Querier配置里,--store参数至少写两类:Receive节点的gRPC地址,例如192.168.1.10:10901;Store Gateway的gRPC地址,例如192.168.1.20:10901。有了这个组合,查“三天前和现在对比”这类需求再也不用临时抽数,前端就是一个查询入口,冷热数据分层由Querier自动处理。
5.2 写入量估算与节点配置参考
最后给一份容量规划参考。写入量可以这样粗算:单个Prometheus每个采集周期产生的样本数乘以采集周期数量,再乘以Prometheus数量,得到总写入TPS。
举个例子,一个环境里有5个Prometheus节点,每个节点管理8000个采集目标,每个目标大约暴露200个指标,采集间隔15秒,那么每秒产生的样本大约是 5 × 8000 × 200 ÷ 15,约53万。这个量级用三节点Receive扛是可行的,每个节点建议至少4核CPU、16GB内存、500GB以上SSD。
不过我的经验是,真正先被撑满的往往不是CPU和内存,而是WAL落盘的磁盘IO。所以节点选型时,SSD低延迟比大容量更重要。对象存储那边慢一点问题不大,异步上传可以慢慢补,但Receive本地WAL一旦拖后腿,整个写入链路都会被拖住。
5.3 关于副本和故障域的最后提醒
再提醒一次:hashring只管把数据拆到多个节点,不负责做数据冗余。如果业务对数据连续性要求很高,建议在架构上做双写:Prometheus配置两个remote_write url,指向两套独立Receive集群。成本翻倍,但换来的是单集群故障时查询侧依然有数据可用。
我们当前就是双集群加对象存储归档,Compactor定期压缩。整体架构比单机Prometheus复杂了一截,但运维收益是实打实的:故障恢复时间从“看运气”变成了“可预期”,查询体验从“到处找机器”变成了“一个入口查所有”。这套改动落地后,业务反馈查询慢和查不到数据的情况明显减少了。
如果真的要给一句话总结这套改造的核心价值,那就是:把Prometheus从“采集+存储+查询”的全部职责里解放出来,让它专注做好采集,存储和查询交给更合适的组件去扛。而Thanos Receive,就是这个架构里最靠前、也最不能出问题的那道入口。
最后分享一点体会。第一次接触Thanos Receive时,我只盯着部署文档和数据吞吐,完全忽略了WAL磁盘和对象存储节奏这些细枝末节,结果上线第一周就被队列积压和查询空洞教训了一顿。如果你是第一次做这套改造,别急着把全部组件一次铺开:先搭单节点Receive,用Prometheus写一小批指标验证链路,再逐步加hashring、加对象存储、加Store Gateway。每一步都确认可控后再往前走,比一次性搭建再排错要省心得多。