本文旨在为已经深度使用 Prometheus 并遭遇其单点存储瓶颈的工程师与架构师,提供一套完整的、基于 Thanos 的长期存储与全局查询视图解决方案。我们将从 Prometheus 本身的局限性出发,深入探讨 Thanos 背后的分布式系统原理,剖析其核心组件的设计与实现细节,并最终给出一套从零到一、分阶段落地的架构演进路线图。这不只是一篇工具介绍,而是一次深入监控系统底层,理解其设计哲学与工程权衡的深度探索。
现象与问题背景
Prometheus 以其强大的 PromQL、高效的 Pull 模型和简洁的单体架构,成为了云原生监控领域的事实标准。然而,当监控规模从单个集群扩展到跨地域、跨业务线的数百个集群,数据保留周期从几天延长到数年时,Prometheus 原生的架构便会暴露出其固有的局限性,主要体现在以下几个方面:
- 有限的数据保留周期 (Limited Retention):Prometheus 的 TSDB (Time Series Database) 是为本地磁盘设计的。数据量的增长与磁盘容量和成本直接挂钩。为了控制成本,我们不得不设置较短的数据保留策略(如 15-30 天),这使得长周期趋势分析、容量规划和满足合规性审计(通常要求数据保留一年以上)变得几乎不可能。
- 缺乏全局查询视图 (Lack of Global View):在微服务和多集群环境中,服务实例和服务依赖关系分散在不同的 Kubernetes 集群或数据中心。每个集群通常部署一套独立的 Prometheus。当需要进行全局性的聚合查询,例如“计算过去24小时全站所有API网关的请求总量 P99 延迟”时,原生 Prometheus 无法直接实现。传统的解决方案是使用 Federation,但这会带来额外的配置复杂性、数据冗余和查询瓶颈。
- 高可用性 (High Availability) 的两难:实现 Prometheus 的高可用,标准做法是部署两套完全相同的 Prometheus 实例,采集相同的 Targets。但这立刻引入了新问题:当查询数据时,如何处理来自两个副本的重复数据?Grafana 等上游系统需要特殊的去重逻辑,且这种模式本质上是数据孤岛,并未解决存储融合的问题。
- 单点性能瓶颈 (Scaling Bottlenecks):单个 Prometheus 实例的性能受限于其所在节点的垂直扩展能力,包括 CPU(用于 ingestion 和 query)、内存(用于索引和 a chunk cache)和磁盘 I/O。当监控的 Targets 数量或 Series 基数(Cardinality)过大时,单点 Prometheus 会成为整个监控体系的瓶颈。
Thanos 的出现,正是为了解决上述所有问题。它并非要替代 Prometheus,而是像一个外置的“增强插件集”,无缝地为现有的 Prometheus 系统赋予无限存储、全局视图和高可用的能力。
关键原理拆解
在深入 Thanos 的架构之前,我们必须回归到几个核心的计算机科学原理。理解这些原理,是理解 Thanos 设计哲学“为何如此”的关键。此刻,让我们切换到大学教授的视角。
- 分布式系统:Shared-Nothing 架构与去中心化
Thanos 的设计哲学深受 Shared-Nothing 架构思想的影响。系统中的每个组件(尤其是 Prometheus 实例)都是独立、自治的,不共享内存或存储。Thanos 在这些独立的 Prometheus 节点之上,构建了一个逻辑上的统一层。它没有引入一个中心化的、需要强一致性协议(如 Raft/Paxos)的存储集群,而是通过一系列功能独立的、可水平扩展的无状态组件来协作。这种设计极大地降低了系统的复杂性和运维成本,并天然地避免了中心化架构的单点故障问题。它更倾向于满足 CAP 定理中的 AP (Availability, Partition Tolerance),而将一致性问题通过特定机制(如去重)在查询时解决。 - 数据结构:LSM-Tree 与不可变数据块 (Immutable Blocks)
Prometheus 的 TSDB 设计借鉴了 Log-Structured Merge-Tree (LSM-Tree) 的思想。数据首先被写入内存中的 a head block 和预写日志 (WAL)。当内存中的数据达到一定规模或时间阈值(默认为 2 小时),它会被持久化到磁盘,形成一个完全不可变的块 (Immutable Block)。这个“不可变”的特性是 Thanos 设计的基石。因为块一旦生成就不会再被修改,Thanos Sidecar 组件可以安全地、异步地将这些块上传到外部存储,而无需担心数据竞争或复杂的分布式锁。这使得将廉价、高耐用的对象存储作为长期存储介质成为可能。 - 存储抽象:对象存储作为无限容量的持久层
对象存储(如 AWS S3, Google Cloud Storage, MinIO)提供了一个近乎无限容量、高持久性(通常是 11 个 9)且成本低廉的存储后端。它的 API 模型(PUT/GET/DELETE)非常简单,与上传和下载不可变的数据块完美契合。Thanos 巧妙地利用对象存储作为其“单一事实来源 (Single Source of Truth)”。所有历史数据都存储在对象存储中,而 Prometheus 本地只保留最近的、仍在写入的数据。这彻底解决了本地磁盘的容量限制问题。
系统架构总览
Thanos 并非一个单一的程序,而是一组协同工作的组件。我们可以将它的架构想象成一个在现有 Prometheus 实例之上构建的、分层的查询与存储系统。
一个典型的 Thanos 部署包含以下核心组件:
- Sidecar: 与每个 Prometheus 实例一同部署(通常在同一个 Pod 中)。它有两个核心职责:
- 数据上传:监控 Prometheus 的数据目录。一旦 Prometheus 生成一个新的、不可变的 2 小时数据块,Sidecar 就会将其上传到预先配置的对象存储桶中。
- 实时查询:暴露一个 Store API 接口,允许 Thanos Query 组件查询其宿主 Prometheus 实例内存中最新(通常是 2-6 小时内)的数据。这保证了查询的实时性。
- Query (or Querier): 它是用户查询的统一入口(例如,Grafana 的数据源应该指向它)。Query 本身是无状态的,可以水平扩展。它会:
- 服务发现:通过 gRPC 发现所有可用的 Store API 端点(包括所有的 Sidecar 和 Store Gateway)。
- 查询扇出 (Fan-out):将收到的 PromQL 查询分解并发送给所有相关的 Store API。
- 结果聚合与去重:从所有 Store API 收集查询结果,进行合并,并根据预定义的外部标签(如 `replica` 标签)对来自 HA Prometheus 对的数据进行去重。
- Store Gateway: 位于对象存储前面的“看门人”。它也实现了 Store API。它的职责是响应关于历史数据的查询。Store Gateway 会下载对象存储中所有数据块的索引和元数据到本地内存,当收到查询时,它能高效地定位到需要的数据块,并仅下载相关的 chunk 数据进行计算。它也是无状态的,可以水平扩展。
- Compact: 这是一个全局单例的后台任务组件。它负责对对象存储中的数据进行维护,主要执行两个操作:
- 压实 (Compaction):将对象存储中小的、2 小时的数据块合并成更大的块(例如,24 小时或 7 天的块),以减少索引大小和查询时的文件句柄开销。
- 降采样 (Downsampling):为老数据生成低分辨率的版本(例如,将原始数据降采样为 5 分钟或 1 小时一个点)。这使得对长周期(如一年)的数据进行查询时,速度能提升几个数量级,并极大降低了查询成本。
- Ruler: 提供了全局告警规则和记录规则的评估能力。它连接到 Thanos Query,因此可以基于全局数据视图进行告警判断,解决了单个 Prometheus 无法看到全局数据而无法做出准确告警的问题。
数据流描述:一个用户的查询请求(例如,查询过去 30 天的数据)会首先到达 Thanos Query。Query 会将查询同时发送给所有的 Sidecar(获取最近 2 小时的数据)和所有的 Store Gateway(获取 2 小时前到 30 天前的数据)。Sidecar 直接查询本地 Prometheus 的内存和磁盘。Store Gateway 则根据其内存中的索引,从对象存储中拉取所需的数据块。所有结果返回给 Query,Query 进行合并和去重,最终将完整的结果返回给用户。
核心模块设计与实现
现在,让我们戴上极客工程师的帽子,深入代码和配置,看看这些组件在真实世界中是如何工作的。
Sidecar: Prometheus 的“贴身护卫”
Sidecar 的实现精髓在于其与 Prometheus 的松耦合。它通过共享的存储卷来“观察”Prometheus 的数据目录。在 Kubernetes 中,这通常通过在一个 StatefulSet 的 Pod 中定义两个容器(Prometheus 和 Thanos Sidecar),并挂载同一个 PersistentVolumeClaim 来实现。
# A snippet from a Kubernetes StatefulSet definition
apiVersion: apps/v1
kind: StatefulSet
spec:
template:
spec:
containers:
- name: prometheus
image: prom/prometheus:v2.37.0
args:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus/"
# External labels are CRITICAL for deduplication
- "--web.external-url=http://..."
- "--storage.tsdb.retention.time=6h" # Keep short retention locally
volumeMounts:
- name: prometheus-data
mountPath: /prometheus/
- name: thanos-sidecar
image: thanos/thanos:v0.28.0
args:
- "sidecar"
- "--tsdb.path=/prometheus/"
- "--prometheus.url=http://localhost:9090"
- "--objstore.config-file=/etc/thanos/object-storage.yaml"
- "--grpc-address=0.0.0.0:10901"
- "--http-address=0.0.0.0:10902"
volumeMounts:
- name: prometheus-data
mountPath: /prometheus/
- name: object-storage-config
mountPath: /etc/thanos/
volumeClaimTemplates:
- metadata:
name: prometheus-data
spec:
# ... PVC definition
工程坑点:`storage.tsdb.retention.time` 必须设置得足够短(例如 6h-12h),以避免本地磁盘无限增长。数据安全的责任已经转移给了对象存储。同时,Prometheus 配置中必须包含 `external_labels`,这是 Thanos Query 进行去重的唯一依据。例如,一个 HA 对的两个 Prometheus 实例,应该有相同的标签,除了一个专门用于标识副本的标签,如 `replica: “a”` 和 `replica: “b”`。
Query: 分布式查询的“大脑”
Thanos Query 的核心是其 `StoreSet`,它维护了所有后端 Store API 的连接。当查询到来时,它会并发地向所有 stores 发起请求。去重逻辑是其最巧妙的部分。
# Example command to run Thanos Query
thanos query \
--http-address 0.0.0.0:9090 \
--grpc-address 0.0.0.0:9091 \
# Discover all Store APIs via a Kubernetes Headless Service
--store=dnssrv+_grpc._tcp.thanos-store-gateway.monitoring.svc.cluster.local \
--store=dnssrv+_grpc._tcp.thanos-sidecar.monitoring.svc.cluster.local \
# The label key to use for deduplication
--query.replica-label replica
实现细节:去重发生在数据合并阶段。当 Query 从多个 store(例如,两个 HA Sidecar)收到带有完全相同标签集和时间戳的 series 时,它会检查 `replica` 标签。它会选择一个 `replica` 值(通常是字典序最小的那个,例如 ‘a’)作为主副本,并丢弃来自其他副本(’b’)的相同数据点。这保证了即使后端有两个数据源,用户看到的也是一条平滑、无重复的曲线。
Store Gateway: 历史数据的“图书管理员”
Store Gateway 的性能关键在于其内存中的索引缓存。启动时,它会扫描对象存储桶的顶层目录,下载每个 block 的 `meta.json` 和 `index` 文件。`meta.json` 很小,包含了时间范围、基数等信息。`index` 文件则包含了从 label 到 chunk 位置的倒排索引。
内存管理:一个常见的问题是 Store Gateway 的内存占用。其内存消耗约等于对象存储中所有 `index` 文件大小的总和。随着数据量的增长,这可能成为一个问题。Thanos 提供了 `index-cache` 相关的配置来管理这部分内存,例如使用 LRU 策略来缓存索引,或者通过分片(sharding)部署多个 Store Gateway,每个 Gateway 只负责对象存储中一部分 block,以此来水平分散内存压力。
Compact: “数据管家”与分布式锁
Compactor 必须是单例,因为它会修改对象存储中的数据(合并、删除块)。为了在 Kubernetes 等动态环境中保证单例,Thanos 实现了一种基于对象存储的分布式锁机制。
工作流程:
- 启动时,Compactor 会尝试在对象存储桶中创建一个名为 `compactor.lock` 的文件,并写入自己的 ID。
- 它会定期刷新这个锁文件,证明自己“还活着”。
- 如果一个 Compactor 实例启动时发现锁文件已存在且未过期,它就会退出。
- 如果持有锁的实例崩溃,锁文件会因未能刷新而过期,此时其他备用实例就可以获取锁并接管工作。
降采样是 Compactor 的另一个核心价值。它会创建新的、分辨率更低的数据块。例如,它会读取 12 个 2 小时的原始数据块,计算出每 5 分钟的 `avg/min/max/sum/count`,然后写入一个新的、24 小时范围、5 分钟分辨率的数据块。当 Thanos Query 处理一个跨度很长的查询时,它会智能地选择使用这些降采样后的数据,而非扫描海量的原始数据点。
性能优化与高可用设计
构建一个生产级的 Thanos 系统,仅仅部署组件是不够的,还需要深入考虑性能和可用性。
- 查询性能优化:
- 垂直分片 (Vertical Sharding): 可以部署多个 Thanos Query 实例,一些专门服务于实时性要求高的告警查询(只连接 Sidecar),另一些则服务于需要查询历史数据的仪表盘(连接 Sidecar 和 Store Gateway)。
- 查询前端缓存: 在 Thanos Query 前面部署如 `Trickster` 或 `thanos-query-frontend` 这类支持 PromQL 结果缓存的组件,可以极大地降低对后端组件的压力,特别是对于重复性高的仪表盘查询。
- 合理配置降采样: 确保 Compactor 正确配置了降采样(`–downsampling.resolutions`)。默认是 5m 和 1h。对于需要保留多年的数据,查询一年跨度的数据时,使用 1 小时分辨率的数据将比原始数据快上百倍。
- 高可用性策略:
- 无状态组件的冗余: Thanos Query, Store Gateway, Ruler 都是无状态的。在 Kubernetes 中,只需将它们的 `replicas` 设置为 2 或更多,并创建一个 Service 来负载均衡流量,即可轻松实现高可用。
- Compactor 的“热备”: 虽然 Compactor 是逻辑单例,但你可以部署两个或多个实例。由于分布式锁的存在,只有一个会处于 active 状态,其他的则处于 standby。当 active 实例失败,standby 实例会自动接管。
- 依赖的健壮性: 整个系统的可用性下限取决于其最弱的依赖——对象存储。因此,选择一个高可用的、跨区域复制的对象存储服务(如 S3 Cross-Region Replication)是保证历史数据可用性的关键。
架构演进与落地路径
对于一个已经拥有大量 Prometheus 实例的团队,一次性切换到完整的 Thanos 架构是不现实的。一个务实的、分阶段的演进路径至关重要。
第一阶段:实现长期存储 (MVP)
- 目标: 解决数据保留周期问题,将历史数据备份到对象存储。
- 行动:
- 为每个现有的 Prometheus 实例部署 Thanos Sidecar。
- 配置好对象存储(例如,创建一个 S3 桶)。
– 部署一个单实例的 Thanos Store Gateway 和一个 Thanos Compact 组件。
- 成果: 所有 Prometheus 的数据开始被安全地、持续地备份到对象存储。本地 Prometheus 的保留周期可以被缩短以节省磁盘空间。可以通过直接访问 Store Gateway 来查询历史数据,验证数据链路的通畅。
第二阶段:构建全局视图与查询 HA
- 目标: 统一查询入口,解决数据孤岛和 HA 去重问题。
- 行动:
- 部署至少两个 Thanos Query 实例,并配置一个 LoadBalancer/Service 指向它们。
- 配置 Query 连接到所有的 Sidecar 和 Store Gateway。
- 在 HA Prometheus 对中配置正确的 `external_labels` (特别是 `replica` 标签)。
- 将 Grafana 等上游系统的数据源从各个 Prometheus 地址切换到 Thanos Query 的地址。
- 成果: 团队获得了统一的全局查询视图。无论是查询单个集群的实时数据,还是跨所有集群的历史数据,都可以在同一个 Grafana 仪表盘上完成。HA Prometheus 的数据被无缝去重。
第三阶段:中央化全局告警
- 目标: 实现基于全局数据的告警和记录规则。
- 行动:
- 部署 Thanos Ruler 组件,并配置其连接到 Thanos Query。
- 将那些需要全局视野的告警规则(例如,全站用户总量、跨区域服务延迟)从各个 Prometheus 迁移到 Thanos Ruler。
- 成果: 告警逻辑与数据采集解耦。告警规则可以基于整个系统的宏观状态进行判断,避免了因单个 Prometheus 视野局限而导致的告警风暴或漏报。至此,一个功能完备、高可用、可扩展的云原生监控平台便构建完成。
延伸阅读与相关资源
-
想系统性规划股票、期货、外汇或数字币等多资产的交易系统建设,可以参考我们的
交易系统整体解决方案。 -
如果你正在评估撮合引擎、风控系统、清结算、账户体系等模块的落地方式,可以浏览
产品与服务
中关于交易系统搭建与定制开发的介绍。 -
需要针对现有架构做评估、重构或从零规划,可以通过
联系我们
和架构顾问沟通细节,获取定制化的技术方案建议。