对于任何大规模微服务体系,Prometheus 几乎是时序监控领域的事实标准。然而,当监控规模跨越数十个 Kubernetes 集群、数据保留周期要求长达数年时,其原生的单体架构与本地存储便会成为巨大的瓶颈。本文旨在为中高级工程师与架构师,系统性地剖析基于 Thanos 构建 Prometheus 长期存储与全局查询视图的完整方案。我们将从其解决的核心问题出发,下探到底层 TSDB 与分布式系统原理,穿梭于核心组件的实现细节,权衡架构设计中的得失,并最终给出一套可落地的演进路线图。
现象与问题背景
在不引入长期存储方案之前,依赖原生 Prometheus 的监控体系通常会面临以下几个棘手的工程问题:
- 数据孤岛与查询联邦的无奈: 在拥有多个数据中心或 Kubernetes 集群的环境中,每个集群通常会部署独立的 Prometheus 实例。当需要进行全局性的业务指标分析或故障排查时,工程师不得不在多个 Prometheus UI 之间切换,或者依赖 Federation 机制。Federation 本质上是数据的二次抓取和存储,它不仅增加了中心节点的存储压力,还损失了指标的原始精度和标签,使其成为一种治标不治本的妥协方案。
- 存储成本与性能的矛盾: Prometheus 的本地 TSDB(Time Series Database)性能极高,但它是为本地高性能 SSD 设计的。若要实现长达一至两年的数据保留,意味着需要为每个 Prometheus 实例挂载巨大的、昂贵的磁盘卷。这在公有云环境下,TCO(总拥有成本)会急剧攀升。同时,单个实例承载过长时间跨度的数据,其内存占用、启动恢复时间、查询性能都会显著恶化。
- 高可用(HA)的复杂性: 为保证监控系统的可靠性,通常会为每个业务集群部署两套完全相同的 Prometheus 实例,抓取同样的目标。这解决了单点故障问题,但又带来了新的麻烦:查询时会看到两条完全一样的时序数据。这不仅对 Grafana 等可视化工具造成困扰,也让告警规则的编写变得复杂。我们需要一个聪明的聚合层来处理这种数据冗余。
- 全局告警规则的缺失: 业务级别的告警,例如“全站总 QPS 下降 30%”,是无法在任何一个独立的 Prometheus 实例上配置的。因为它需要聚合所有集群的数据。这使得跨集群的宏观业务监控难以实现。
这些问题共同指向一个核心诉求:我们需要一个统一的、水平扩展的、成本可控的监控数据平台,它既能保留 Prometheus 生态的强大能力,又能突破其单体架构的限制。Thanos 正是为此而生的解决方案之一。
关键原理拆解:从单体 TSDB 到分布式时序系统
要理解 Thanos 的设计哲学,我们必须回归到计算机科学的基础原理,审视 Prometheus TSDB 的内在特性以及分布式系统设计的核心思想。
(教授视角)
Prometheus 的核心是一个高度优化的时序数据库(TSDB)。它的数据在物理上以“块”(Block)的形式组织。每个块通常覆盖一个固定的时间范围(默认为 2 小时),并包含该时段内的所有时序数据(Chunks)、一个用于快速检索的索引(Index)和元数据(Meta.json)。当前正在写入的数据存在于内存中的“头块”(Head Block),当达到一定时间或大小后,会被持久化到磁盘,成为一个新的不可变块。这种设计极大地优化了写操作的吞吐量,因为写入主要发生在内存中,磁盘操作是批量的、顺序的。同时,针对时间范围的查询也极为高效,因为可以快速定位到相关的块文件。
然而,这种设计的边界也十分清晰:它是一个典型的单体(Monolithic)存储架构。其所有的性能和容量都受限于单个节点的计算、内存和磁盘 I/O 能力。这是一个典型的 Scale-Up(垂直扩展)模型,当数据量超过单机极限时,便无能为力。分布式系统的核心思想,正是用 Scale-Out(水平扩展)模型来解决单点瓶颈。
Thanos 的架构思想,本质上是将 Prometheus 的存储和查询能力进行解耦(Decoupling)与分布式化。它引入了几个关键的抽象:
- 将存储层抽象为对象存储(Object Storage): Thanos 巧妙地利用了现代云原生环境中最成熟、可无限扩展且成本低廉的组件——对象存储(如 AWS S3, Google GCS, MinIO)。它不尝试自己去构建一个复杂的分布式存储系统,而是将 Prometheus 生成的、不可变的 TSDB 块视为对象,直接上传到一个共享的、高可用的存储桶中。这使得存储容量不再是问题,并且数据持久性得到了保障。
- 查询的无共享(Shared-Nothing)联邦模型: Thanos 的查询引擎(Querier)本身是无状态的。它像一个智能路由器,将一个 PromQL 查询分解,并分发给所有能够提供数据的组件。这些数据源可以是服务于近期数据的 Prometheus Sidecar,也可以是服务于历史数据的 Store Gateway。每个数据源独立地处理其负责时间范围内的子查询,并将结果返回给 Querier 进行聚合。这种无共享架构使得查询层可以根据负载轻松地水平扩展。
- 通过外部标签(External Labels)实现全局数据视图: 为了区分来自不同 Prometheus 实例的数据,并实现高可用副本的去重,Thanos 依赖于一个简单而强大的机制——外部标签。每个 Prometheus 实例被赋予一组唯一的标签(如 `cluster=”prod-us-east-1″`, `replica=”A”`)。这些标签会附加到该实例产生的所有时序数据上。查询时,Querier 能够利用这些标签识别出哪些序列是来自同一个源的副本,从而进行合并去重,呈现给用户一个干净、统一的全局视图。
–
从根本上说,Thanos 并没有重新发明时序数据库,而是构建了一个“胶水层”或“适配层”,将成千上万个独立的 Prometheus TSDB“孤岛”粘合成一个逻辑上统一的、无限扩展的分布式时序数据库。它尊重并复用了 Prometheus TSDB 的高效设计,同时用标准的分布式系统原则解决了其扩展性难题。
系统架构总览:Thanos 的组件协同
一个典型的 Thanos 部署由以下几个核心组件构成,它们协同工作,共同提供一个无缝的全局监控体验:
- Sidecar: 作为“代理”部署在每个 Prometheus Pod 中。它有两个主要职责:1) 实时监控 Prometheus 的 `data/` 目录,一旦有新的 TSDB 块生成(通常是每 2 小时),就将其上传到中心的对象存储桶。2) 它实现了一套 gRPC Store API,允许 Thanos Querier 直接查询其宿主 Prometheus 实例内存中最近的、尚未上传的数据(通常是 2-3 小时内)。
- Querier: 用户和 Grafana 的查询入口。它本身不存储任何数据。当收到一个 PromQL 查询时,它会首先通过服务发现(如 DNS 或 Kubernetes Service)找到集群中所有的 Store API 提供者(即所有的 Sidecar 和 Store Gateway),然后将查询请求广播给它们。最后,它将从各处收到的结果进行合并、排序和去重,返回给客户端。
- Store Gateway: 历史数据的守护者。它持续监控对象存储桶,并维护桶中所有 TSDB 块的索引元数据。当 Querier 查询历史数据时,Store Gateway 会根据时间范围,从对象存储中按需下载相关的索引和数据块(Chunks),然后返回给 Querier。为了提升性能,它会大量使用本地缓存。
- Compactor: 唯一的、有状态的后台任务组件(通常以 Singleton 模式运行)。它负责对象存储桶中数据的生命周期管理。其核心工作包括:1) 合并(Compaction): 将小的、2 小时的块合并成更大的、覆盖更长时间范围的块(如 24 小时),以减少块的数量,提高查询效率。2) 降采样(Downsampling): 创建低分辨率的数据副本。例如,将原始 15 秒抓取间隔的数据,聚合成 5 分钟或 1 小时粒度的聚合数据。当用户查询长达数月或一年的趋势图时,Querier 会智能地选择查询降采样后的数据,极大地降低了查询的 I/O 和计算量,实现了“秒级”响应。
- Ruler: 全局告警规则评估器。它和 Querier 类似,可以查询全局数据,但其目的是周期性地执行预定义的告警和记录规则,并将结果(新的时序数据或告警事件)写回 Thanos 系统或推送到 Alertmanager。
一个典型的查询流程如下:用户在 Grafana 中发起一个跨度为 30 天的查询。请求首先到达 Thanos Querier。Querier 将查询的时间范围分解,对于最近 2 小时的数据,它会去问询所有 Prometheus 的 Sidecar;对于 2 小时前到 30 天前的数据,它会去问询 Store Gateway。Store Gateway 在分析查询后,发现请求的是一个粗粒度、大跨度的查询,于是它会智能地选择读取由 Compactor 生成的 1 小时降采样数据块,而不是原始数据。最终,Sidecar 和 Store Gateway 返回各自的结果,Querier 将它们合并去重后,呈现给用户。
核心模块设计与实现
(极客工程师视角)
理论听起来很酷,但魔鬼全在细节里。搞不定配置和实现,再好的架构也是白搭。
Sidecar 与 Prometheus 的“无缝”集成
Thanos 的集成是非侵入式的,关键就在于 Sidecar。你不需要修改 Prometheus 的任何代码。但是,有一个配置是强制性且至关重要的:`external_labels`。你必须在每个 Prometheus 的配置文件 `prometheus.yml` 中加上它。
global:
scrape_interval: 15s
evaluation_interval: 15s
external_labels:
cluster: 'k8s-prod-cluster-1'
replica: '0' # For HA pair, the other one is '1'
没有 `external_labels`,Querier 就成了瞎子,无法区分来自不同集群的数据,更别提对 HA 副本进行去重了。`cluster` 标签用于逻辑隔离,`replica` 标签是去重的关键。Querier 看到具有相同时间戳、相同标签集但 `replica` 标签不同的两个序列点,会保留其中一个,从而实现 deduplication。
Sidecar 的启动参数也很直白,主要就是告诉它 Prometheus 在哪,以及对象存储的配置在哪。
/bin/thanos sidecar \
--tsdb.path="/prometheus/data" \
--objstore.config-file="/etc/config/thanos-objstore.yaml" \
--grpc-address="0.0.0.0:10901" \
--http-address="0.0.0.0:10902"
其中 `thanos-objstore.yaml` 就是对象存储的凭证,比如对于 AWS S3:
type: S3
config:
bucket: "my-thanos-metrics-bucket"
endpoint: "s3.us-east-1.amazonaws.com"
# access_key and secret_key are usually injected via secrets
Querier 的查询聚合与去重
Querier 是无状态的,你可以水平扩展无数个实例。启动时,你只需要告诉它去哪里找数据源(Sidecar 和 Store Gateway)。在 Kubernetes 环境下,通常通过一个 Headless Service 来暴露所有的 Store API端点。
/bin/thanos query \
--grpc-address="0.0.0.0:10901" \
--http-address="0.0.0.0:10902" \
--store="dnssrv+_grpc._tcp.thanos-store-gateway.monitoring.svc.cluster.local" \
--store="dnssrv+_grpc._tcp.prometheus-operated.monitoring.svc.cluster.local" \
--query.replica-label="replica"
这里的 `–store` 参数使用了 DNS SRV 记录,这是 Kubernetes 中服务发现的最佳实践。`–query.replica-label=”replica”` 这个参数就是明确告诉 Querier,使用名为 `replica` 的标签来做去重。这个值必须和你在 Prometheus `external_labels` 中设置的副本标签名完全一致。
Compactor 的降采样与生命周期管理
Compactor 是整个系统中唯一需要小心处理的“有状态”组件。它必须以单实例(Singleton)模式运行,以防止多个实例同时对对象存储中的块进行操作,导致数据损坏或状态不一致。在 Kubernetes 中,通常使用 `StatefulSet` 并将副本数设置为 1 来保证这一点。
它的核心配置在于降采样规则。默认情况下,Thanos 会创建两个降采样层级:
- 5分钟降采样(5m resolution): 对原始数据进行5分钟粒度的聚合,保留 14 天。
- 1小时降采样(1h resolution): 对5分钟降采样数据再次聚合,形成1小时粒度的数据,永久保留。
这些都是通过启动参数控制的,例如 `–downsampling.resolution=0s,5m,1h`。当 Compactor 运行时,它会扫描对象存储,找到可以合并的块,执行合并操作,然后对合并后的大块执行降采样,生成带有 `downsample` 分辨率标记的新块。同时,它也会根据 `–retention.resolution-*` 参数来清理过期的块。这个过程是自动的、持续的,但它的执行状态和日志是排查长期存储问题的关键入口。
性能优化与高可用设计
部署 Thanos 只是第一步,让它在生产环境中稳定、高效地运行,需要关注以下几点:
- Store Gateway 的索引缓存(Index Cache): Store Gateway 的性能瓶颈往往在于与对象存储的交互。为了定位时间序列,它需要读取每个块的索引文件。如果每次查询都去对象存储下载,延迟将无法忍受。因此,必须为 Store Gateway 配置一个足够大的内存缓存(或 Memcached),用于缓存块的索引信息。这基本上是一个用内存换查询时间的典型 trade-off。一个常见的配置是为每个 Store Gateway Pod 分配几十 GB 的内存,并把大部分用于索引缓存。
- 高可用(HA)架构:
- Querier / Ruler: 完全无状态,可以直接部署多个副本,并通过 LoadBalancer 对外提供服务。
- Store Gateway: 虽然它有本地缓存,但其核心状态(块列表)来自对象存储,因此也可以部署多个副本以实现高可用和负载均衡。
- Sidecar: 与其宿主 Prometheus 实例生命周期绑定。Prometheus 的高可用(部署两个抓取相同目标的实例)天然地带来了 Sidecar 的高可用。
- Compactor: 如前所述,它是单点。它的宕机不会影响数据的写入和查询,只会暂停后台的优化工作。因此,需要有完善的监控和自动恢复机制(如 Kubernetes 的 `StatefulSet` 提供的能力)。
- 网络与成本考量: 数据在组件间的流动是有成本的。Sidecar 上传数据到对象存储会产生出向流量。Querier 查询 Store Gateway 时,后者会从对象存储下载数据,这既有 API 调用成本,也有流量成本。降采样是控制查询成本的最有效手段。对于长周期报表,确保 Grafana 或 API 调用方在 PromQL 查询中使用了适当的 `step` 参数,以触发 Querier 使用降采样数据,这能将成本和延迟降低几个数量级。
架构演进与落地路径
对于一个已经存在大量 Prometheus 实例的组织,引入 Thanos 不应该是一蹴而就的,而应分阶段进行,以控制风险、平滑过渡。
- 阶段一:单集群试点与全局视图验证
- 目标: 在一个非核心但有代表性的集群中,验证 Thanos 的核心能力。
- 行动: 为该集群的 Prometheus 部署 Sidecar。在中心化的管理集群中部署 Querier、Store Gateway 和一个对象存储服务(可以使用 MinIO 快速搭建)。将该集群的数据上传到对象存储,并验证可以通过中心的 Thanos Querier 查询到近期和历史数据。
- 收益: 以最小的代价,打通了整个技术栈,获得了全局查询视图的初步体验,并开始实现数据的长期备份。
- 阶段二:全量推广与后台优化
- 目标: 将 Sidecar 推广到所有 Prometheus 实例,并上线 Compactor 进行数据优化。
- 行动: 在所有业务集群的 Prometheus 中注入 Sidecar 配置,并将它们的 `external_labels` 标准化。在中心集群部署 Compactor,开始对对象存储中的数据进行合并与降采样。
- 收益: 实现了所有监控数据的集中存储和统一查询。通过降采样,长期查询的性能得到质的提升,存储成本也因数据生命周期管理而得到控制。
- 阶段三:生产级加固与能力扩展
- 目标: 提升整个 Thanos 平台的稳定性和可用性,并提供全局告警能力。
- 行动: 为 Querier 和 Store Gateway 部署多个副本,并配置高可用。部署 Thanos Ruler,将原先分散在各个 Prometheus 中的、需要全局数据的告警规则迁移上来。建立对 Thanos 组件自身的监控仪表盘和告警。
- 收益: 建成一个生产可用的、高可用的、统一的监控平台。架构能力从单纯的数据聚合,扩展到了全局的智能告警和数据治理。
通过这样的演进路径,团队可以在每个阶段都获得明确的收益,同时逐步积累运维经验,最终平稳地从一个分散的监控孤岛群,演进为一个强大、统一、可扩展的云原生可观测性平台。
延伸阅读与相关资源
-
想系统性规划股票、期货、外汇或数字币等多资产的交易系统建设,可以参考我们的
交易系统整体解决方案。 -
如果你正在评估撮合引擎、风控系统、清结算、账户体系等模块的落地方式,可以浏览
产品与服务
中关于交易系统搭建与定制开发的介绍。 -
需要针对现有架构做评估、重构或从零规划,可以通过
联系我们
和架构顾问沟通细节,获取定制化的技术方案建议。