百度可观测系列 | 如何构建亿级指标的高可用 TSDB 存储集群?
导读 在前一篇《采集亿级别指标,Prometheus 集群方案这样设计中》,我们为大家介绍了针对针对亿级指标场景,百度云原生团队基于 Prometheus 技术方案的研究,包括资源的优化,引入流式计算,降低 CPU 消耗,以及高可用的建设。本文将深入存储专题,为大家介绍如何构建亿级指标的高可用 TSDB 存储集群。
随着云原生的不断发展,Prometheus 的应用场景不再只局限于针对资源指标的监控,也可以应用于金融场景的业务交易指标的趋势观察,物联网的 IOT 全场景观测等。为此百度云原生团队设计了具有较高写入性能,可以支持复杂计算聚合查询,可满足高可靠高可用场景,并且能兼容 PromQL 语法的远端分布式 TSDB 存储。
TSDB 架构
接入层:负责数据的接入和接口的透出。写通路:支持两种写入方式,一种是兼容 Prometheus remote write 的 HTTP API 写入,另一种是支持从 Kafka 中订阅指标时序数据,通过这种方式可以更好的控制峰值写入,避免存储过载。读通路:提供 HTTP API 接口,兼容 Prometheus 查询接口。
计算层:TSDB 的核心计算,包括写入和查询时的索引计算,数据的降采样窗口计算,数据的实时转换以及查询时 PromQL 的计算。
缓存层:TSDB 的缓存数据,包括索引数据的缓存,压缩后的数据缓存等。
存储层:TSDB 的存储引擎,可以支持多种通用储存引擎。
TSDB 核心技术
数据的降采样
在检索数周、数月甚至数年的数据时,因为数据跨度的增加和数据点的增加,增长的复杂性会使查询变得越来越慢。
这个问题我们使用降采样来解决,这是一个聚合窗口数据并降低数据采样率的过程。使用降采样数据,可以在更大的时间范围内保持相同的数据点,从而保持查询响应。对已有数据进行降采样是任何长期存储解决方案的必然要求,但这也是原生 Prometheus 不支持的。
针对不同层级聚合窗口的需要,我们设置灵活可配置的层级和时间窗口大小,默认使用 5 分钟,1 小时的两层时间窗口聚合,降采样的每条指标数据写入的第一个点的时间作为本指标数据的时间窗口起始点,之后每个层级的窗口聚合保留该窗口的 avg/sum/count/max/min/counter 值,低层级聚合后进入高层级降采样窗口进行二次聚合。
查询情况根据查询条件分类取值,默认使用 avg 值
时序数据压缩
对于 Prometheus 而言,Prometheus 的压缩统一使用了 Facebook Gorilla 论文的方式 timestamp: delta-of-delta 方式压缩时序点的时间值;value: xor 方式压缩时序点的 value 值;使用这种方式可以把原有时序数据压缩 12 倍,如下图所示:
对于长期存储而言,磁盘的占用空间大小也是一个重要指标,我们希望在不影响写入/查询性能的情况下压缩更多的数据来减小对磁盘的压力,也可以减小对内存的压力,为此进行了针对性压缩,新数据的写入将使用和 Prometheus 一致的算法进行压缩,积累满时间窗口后将使用 ZSTD 的算法提取 XOR 块中的压缩 bytes 进行二次压缩,二次压缩后的数据将不会再有新数据写入,但可以被查询,这种方式可以实现使用较少的内存实现写查一体的高效性。
时序索引高效检索的构建
由于 PromQL 构成的查询语句极为灵活,可以实现不同指标名,任意维度的各种组合,而且还需要支持模糊查询。
所谓模糊查询就是给出的并不是一个完整的时间序列定义,而可能是标签键值对的全量匹配,或者基于标签 key 或者标签 value 的模糊查询,需要能够找到相应的时间序列。
所以如何设计索引缓存的结构,能高效查出指标就变得至关重要。
我们在进入缓存层前将每一个完整包含所有标签的指标都映射成一个 uint64 的 series id,使用倒排索引存储 series id 集合,若是没有倒排索引,那么我们必须遍历内存中全部的 series id 可能是几十万甚至几百万,一一按照标签去比对,这在性能上显然是不可接受的。
对于内层的 series id 的存储,我们使用了位图结构,位图可以帮助我们使用很小的内存空间存储大量的 series id 而且可以利用位图的特性更快更顺利的算出交集/差集/并集,而有了倒排索引与位图,就能够得到标签和对应所有 value 集合,一个请求中的正则表达也可以很容易地分解为若干个非正则请求,并通过求交/并/差集,即可得到最终所需要的时序数据索引。
而有了索引后,我们发现位图的交/差/并集的计算实际上是一个 CPU 消耗较大的计算,当 QPS 比较高的时候很容易对系统的稳定性造成一定的影响,对此我们额外增加了存储查询语句和对应 series id 的缓存层,当检测到对应新查询语句和缓存中查询语句的时间轴中位图数据没有变化的时候,将直接复用之前的索引数据来得到 series id,并且缓存层将稳定维护固定的内存空间,当使用超过设定空间值或者缓存中查询语句失效的时候,会将对应的查询语句移除。
用超过设定空间值或者缓存中查询语句失效的时候,会将对应的查询语句移除。
高可用建设
TSDB 的高可用,重点体现在过载防御、故障容灾两个方面。
过载防御
a) 写入速率的防御
在使用 Remote write 写入数据库时,很容易会有陡增的波峰波谷写入流量对 TSDB 的稳定性造成影响。
对此使用了 Kafka 来支持,Kafka 是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,而且还有限流削峰的作用,避免因为流量过大而导致系统稳定性受到影响。
b) 异常指标的防御
对于 Prometheus 来说,持续变化的指标一直是一个挑战场景,在剧烈变化的指标影响下,Prometheus 很可能被打爆,而由于重启需要恢复之前的缓存数据,以至于在这种场景下,Prometheus 只能进行磁盘清库重启。
对于 TSDB 来说单指标名对应指标变化超过给定配置的变化将使用单指标名的位图储存模式,我们也有离线计算运营指标的能力,来检测每一个区间内的指标变化情况,并将连续变化的指标自动检测出来进行过期时间的调整必要时进行清理,当然也可设置黑名单,将不符合要求的指标写入配置项过滤,防止陡增的指标变化影响稳定性。
c) 查询限流
次数限制:在数据库的查询场景下,经常会遇到突发查询流量将数据库打挂,或者引发稳定性影响,对此我们对 API 级别使用漏斗令牌策略,限制每秒 QPS,对储存引擎的大查询,进行拆分请求拆分成一个个小查询,来限制陡增查询压力。
单次查询量限制:对于单次使用 PromQL 的聚合查询,如果涉及的时间序列过多会引发陡增的 IO 压力和计算压力过载,所以我们限制每次查询所能涉及计算/查询的最大时序线数量。
故障容灾
a) 单实例容灾
数据部分:TSDB 每个实例独立运行,单节点宕机不会影响其他节点,有单机宕机时对应消费的 Kafka 会重置并 rebalance 到其他节点,数据进行重新消费。
索引部分:由于查询大量依赖内存索引数据,所以内存索引的持久化就变得较为重要,我们的内存索引会异步刷入存储引擎,而且每隔一段时间内存索引会做全量快照,防止在刷入引擎的时候宕机造成索引丢失,而对应刷盘索引在重启的时候可重新灌入。
经过这样的设计可以保证,单机房故障、也不会影响线上场景,保证可用性,单实例故障,也不会影响线上功能。
b) 多集群热备
在现实场景中,经常出现某一个机房断电或者出现故障的问题,为此架构上使用双集群热备,数据流在后端入口处双写,TSDB 提供跨机房容灾能力且一时刻只有一个集群提供查询,这样在一个机房出现故障的时候,可以无缝使用另一个机房的数据提供服务。
结语
本文中,我们聚焦于存储专题,介绍了亿级指标的 Prometheus 远程存储 TSDB 的集群设计方案,包括架构,核心技术点,高可用高可靠三方面。在后续文章中我们将继续从计算,报警等各环节出发,继续探讨如何基于 Prometheus 构建高性能,低延迟,高可用的监控系统,敬请期待。
评论