背景

我们的云平台需要采集所有集群正在运行的容器的性能数据与业务指标,最开始采用的每个集群一个独立Promethues的配置,如下图:

enter image description here

结构比较简单,这里就不累述了。需要解释下的是 prom-nanny 这个组件是个啥,其实就是对社区版本 prometheus 的封装,它主要实现以下功能:

  • watch 用户配置的自定义告警规则,然后更新到配置文件中
  • 更新配置文件后,调用 prometheus 接口热更新配置

以上则是以前老的监控体系下的拓扑图。很明显这里很多问题:

  • 存储、采集、查询单点:这里 Prometheus 承担了三个责任,但是它仅有一个实例,一旦故障,我们将失去所有功能
  • 无法水平扩容:由于只有一个实例,所以我们只能垂直扩容,而母机的配置是有上限的,你不可能无线扩下去,对于有的集群动则几千个节点,单一实例是不可能满足的。有的同学可能会提到 hashmodfederationremote_read,其实这三个功能可以解决采集和查询的水平扩容,但是解决不了存储的水平扩容,Prometheus 提出了 remote_write 来解决存储单点的问题
  • 权限未分开:由于我们不但采集性能数据,还采集业务系统自行暴露的业务指标,因此存在一些敏感数据,但是 grafana 和 prometheus 都共用一个的情况下,权限都是共用的,这带来的一定的安全风险

方案对比

由于现在集群的指标都是通过 Prometheus 的 exporter 来暴露,所以方案已经限定以 Prometheus 为基础了。 基于这个前提,在作者进行方案选型时(2019年中),了解到的有以下几种解决方案:

这里其中第一个方案由于此前隔壁组的同事以前做过预研,发现性能存在较大缺陷,所以不在考虑范围内,最终他们选择了 Thanos。

这里列个对比图:

简述 优点 缺点
Thanos 开源社区很有名的集群方案,它的核心点在于将查询最近时间的查询请求路由到各个 Prom 分片进行查询,同时通过 SideCar 将数据同步到对象存储中,作为历史存储。 已经有了很多案例资料可参考,查询快,功能成熟 历史查询慢,由于是是从对象存储转换过来,很显然这里存在复杂的转换过程,同时对象存储的数据库也不会没有针对时序型数据的优化;同一 Prom 分片承担采集与查询的责任,会互相影响
cortex WeaveCloud 公司搞的开源项目,通过 Prometheus 的 remote_write 写入自研的组件后将其转换为块存储,然后提供兼容 PromQL 的查询组件来提供外部使用 虽然使用的块存储,但是它还利用了额外的存储来简历索引,同时提供了优化查询的缓存组件,可以是说是煞费苦心了;功能应该是我了解的方案中最为丰富的一个 太复杂,它的优点其实也是它的缺点,为了能优化查询的速度,让整个拓扑图的复杂度增高了,每多一个组件在分布式系统里面其实都是一种负担。
m3 这是 Uber 在2014 年开始自研的一个方案,他们在开始之前,也尝试了很多开源组件作为底层存储,发现都无法满足他们的要求,于是他们从底层存储开始自研了这套方案,有兴趣的同学可以看下 这篇文章,m3 的理念非常简单,直接通过 remote_write 写到转换组件,然后存入自研的时序数据库,并且提供兼容 PromQL 和 Graphite 语法的查询组件 拓扑简单,落地方便;由于自研的时序数据专门针对时序型数据进行了优化,所以无论短期还是历史数据查询效率和压缩率都非常高;已经在 Uber 内部经过大量数据的验证 案例太少,由于 2018 年才刚刚对外开源,网上关于 m3 实践的资料非常少,偶尔有几篇文章也只是在浅显地跑了起来而已,规模不可考;还没有稳定,版本还是0.x.x

考虑良久,还是选择 M3。因为无论架构还是出身,它都比另外两个方案出色。考虑到我们集群的规模和以后的长期发展,它都是更好的选择。虽然资料和案例都少了些,没关系,最坏情况无非就是自己去啃代码嘛,反正是 go 写的,问题不大(过程中也确实是这样,由于m3的维护团队在西雅图,有时差,很多时候都回复不及时,所以只能自己去看代码找答案)。

另外这里提一下,最近有同事给我推了另一个方案:VictoriaMetrics,这是 fasthttp 作者从头写的一个时序数据库,我看了下,很酷!性能非常高,架构简洁且功能齐全,唯一的问题在于:它推荐单点存储,虽然也支持集群模式,但看过去似乎没有经过大规模的验证,关于集群模式的资料也非常少,我不太了解它的时序数据库集群模式是如何工作的。另外这个项目完全弃用了 Prometheus ,都是用自己的组件替换,如果有强依赖 Prometheus 的同学要注意。

方案落地

M3架构

在开始聊落地之前,先让我们来看下 M3 的架构图 enter image description here

绿色部分是 M3 的组件,这里介绍各个组件的职责:

  • Aggregator:它承担 Coordinator 中关于 下采样(后面我会解释下采样的具体内容) 的工作,是一个可选组件,它的功能 Coordinator 都是完全具备的。
  • Coordiantor:它是上游系统( 比如Prometheus )和 M3DB 一个桥梁,接受上游系统的数据,转换后存入 DB。
  • DB:自研的时序DB,支持集群部署
  • Query: 一个分布式的查询引擎,支持 PromQL 和 graphite 的查询语法,用于提供实时和历史的数据查询。

架构图非常简单,这里想要提出几点比较重要的是:

  • 从图上看 M3Coordinator 是独立部署的组件,可以配合 M3Aggregator 一起工作,但其实它也可以作为 Sidecar 跟采集的 Prometheus 部署到同一个 Pod 中
  • Aggregator 是可选组件,不要执着于部署它
  • 上图只是一个最简单的架构图,实际大规模 M3DB 集群都会使用 ETCD 来管理集群的信息。

M3DB

了解完了架构,让我们一起看看如何部署这些组件,先从最核心的 M3DB 开始,我们先来了解下 M3DB 的存储原理。

存储架构

如果有同学熟悉 Prometheus 的存储架构的话,那么你可能会很熟悉这个图,其实 M3DB 的存储架构与它类似:

  • 输入的数据会写一份到的内存数据块中,同时也直接写一份到WAL(预写日志)文件里面,如果M3DB 挂掉了的话,可以根据 WAL 恢复还没落盘的数据。内存数据块会在达到指定的时间长度后将数据压缩(M3TSZ),写入到文件中( 图中的 Data 部分 ),然后释放掉老旧的数据块,并同时在内存中构建一个新内存块以存储即将到来的时序数据。
  • 跟 Prometheus 不同的在于,M3DB 的内存块存储的时间跨度可配。Prometheus 默认为2个小时,不可更改。
  • 当 M3DB 尚未初始化完毕,却有数据请求进来时,M3DB 会写入到 WAL 中,待初始化完毕后再行填充到内存,这时存在一个风险:如果初始化时间过长,那么 WAL 日志文件会一直膨胀下去,所以需要关注 M3DB 是否在预期时间内初始化完毕。

在上面的架构图中,我屏蔽了NamespaceShard 概念,是为了让大家都能聚焦下视角。 其实在 内存 和 文件(无论 Data 还是)中,数据都是由上至下分配为 Namespace、shard。Namespace 类似关系数据库中 Table 的理念,每一个 Namespace 的数据都是独立的,可以指定 Namespace 的保留配置,包括 存储架构 中提到的内存块的时间跨度,以及最长保留多少时间等。我会在之后的配置中详细解释。

更详细的文件结构说明可以参考:StorageEngine

集群架构

聊完了存储架构,再让我们来看下它的集群模式是怎样的工作的。 M3DB的设计是一个高可用的分布式系统,也就是 CAP 理论中的 AP。这意味着它不保证数据的一致性。当然,不保证数据一致性指的是 强一致性 但是它保证了 最终一致性。 在它的集群架构中有两个很重要的概念:分片(Sharding)副本(Replication)

  • 分片:就像大多数分布式数据库所做的一样,M3对数据进行了分片,然后分配到各个节点上。分片的优点在于,更易于内存管理和水平扩容。M3DB 默认分片数为 4096 个,可以在配置中修改
  • 副本:保证高可用的核心策略就是:冗余,因此很容易想到 M3DB 中肯定会有数据冗余,M3DB 中有专门的参数,被叫做 ReplcationFactor(副本因子),通常为奇数,推荐设置为3。关于副本因子和能容忍的故障程度我会在后面说到。

更详细的内容参考 ShardingAndReplcation

知道了这两个概念后,我们来看下 M3DB 的分片分配规则。 假设条件为:

  • 分片数为 3
  • 副本因子为 3
  • 共计5 台节点

你可能会奇怪,为什么节点是5台,这样每台分配到分片不就不均匀了吗?是的,我就是想据这样的例子来说明下 M3DB 的分片分配规则。 这 5 台节点可以通过接口将其信息注册到ETCD中,默认情况下,M3DB 会保证相同的分片不会落到相同实例的上,那么结果可能会如下:

  • 机器 A 持有分片:1 2
  • 机器 B 持有分片:1 3
  • 机器 C 持有分片:1 3
  • 机器 D 持有分片:2 3
  • 机器 E 持有分片:2

(我没有去查看 M3DB 关于这部分的源码,结果和我上面例举的不一样也有可能。)

可以看出,这种默认的分配方式很灵活,分片数、副本因子和节点数不需要成一定的比例关系,M3DB会自动为你搞定它。 可是缺点也很明显:一个副本集的分片有可能分散到各个节点中。 这会导致我们根本无法控制容灾性,在大规模集群的情况下,有可能随机挂掉三个节点我们就永远丢失了某个甚至某几个分片上的数据。极端情况下,如果某个地区机房集体宕机,我们失去了三分之一的节点,这对集群来说将会是灾难性的,这将导致:

  • 集群不可用
  • 丢失数据

所以 M3DB 提出了一个参数名叫:IsolationGroup(隔离组),是我们可以对节点进行分组,M3DB 将会保证同一个副本集的分片不会落在相同的隔离组中。这样一来我们就可以精确控制分片分配在我们想要的地区节点上,实现多地容灾。 最后附上一张目前我们 M3DB 部署的大致拓扑图,集群总计 134 台机器,不过机器的配置都还挺高,64c256g 的配置。

enter image description here

(ETCD我们这里也将其分在了三个地区,进行多地容灾。)

最后说下,M3DB 还设置每个节点的权重值,用于在副本集机器配置不相等的情况下分配到更多的分片

实战

聊完了原理了,让我们来实际部署一个集群看看。

M3Coordinator

在聊 Coordinator 之前,我们先仔细说说它主要做了啥:

  • 适配 Prometheus 的 remote_write 数据协议,转换为M3DB 适配的数据,存入 DB
  • 下采样:所谓 下采样 即指将原本连续的数据点每隔一定时间进行取样,所形成的新序列点就被称为 下采样。面对不同的 Namespace,它们的保留时间都不同,有时查看较老的历史数据我们不需要保持监控数据的无损(这里无损指,如果15秒采集一个点,那么数据库存储的点也是15秒一个),而只需要它的一个趋势,那么对输入序列进行下采样就是一个很好的解决办法,比如我们可以设置一个以 2分钟 为间隔时间的下采样规则,这样我们就可以将原序列由两分钟 8 个点降低到 1 个点,大大减少了存储消耗。不过对应的,我们也丧失了更多数据变化细节,这称为 Resolution(解析度)

架构分析

实践落地