分布式时序数据库在流利说的应用

admin 2022年8月31日20:36:52评论23 views字数 2392阅读7分58秒阅读模式

背景

随着越来越多的监控指标(机器,容器,服务网格,网关,业务等)产生,使用单个巨大的 Prometheus 来存储和提供查询无论从可用性还是性能都不能满足我们的要求,所以我们对此进行了类别维度的垂直切分,通过多个较小的 Prometheus 节点存储相应的数据。这种方式虽然解决了上面提到的部分问题,但是对于查询来说不够友好,查询具体指标时去找实例和类别指标之间的映射关系,降低了研发效率和幸福感。为了解决这个问题,我们把所有实例的指标通过 RemoteWrite 方式同步到云厂商提供的服务里作为统一操作界面。运行一段时间我们发现无论存储成本还是查询响应时间都不够理想,于是我们开始了分布式时序数据库自建的历程。

需求

我们想要什么样的时序数据库呢?因为数据时刻被研发和告警等服务使用,所以需要服务高可用&数据不丢失。因为数据量巨大,所以需要可扩展到多个节点。因为不想使用者感到明显的查询延迟,所以需要查询高性能。因为不想部分不合理的请求导致全面的不可用,所以需要提供类似多租户的服务隔离。因为想把数据存储时长变长以及成本降下来,所以需要使用性价比高的存储方式。基于以上需求以及我们自己的品味,最后我们选择了基于 Mimir 来构建我们的分布式时序数据存储系统。下面是该系统的基本结构和设计。

设计

从数据流的角度来看,该系统分成两部分,一部分支持数据写入,一部分支持数据读取。下面基于相应的一些考虑点进行分析。

写入

  • 可扩展: 既然指标数据的本质是 <K,V>,那么可以创建多个实例组成存储集群,每个实例负责整个 K 空间的一部分,根据 K 空间真实数据的多少来动态调整集群实例数目。考虑到兼容性,基于 PromQL 引擎添加集群相关功能形成 Ingester。由于客户端是通过 RemoteWrite 写入数据的,无法获知 <K-Range, Node> 的映射关系,所以提供一个路由服务(Distributor)来作为流量接入层,把数据写入到具体的存储实例。
  • 高可用: 由于集群存储实例所在的底层硬件故障或者部署新版本等因素,单一副本不足以提供高可用性,所以 Distributor 在写入数据的时候,会同时写入相同数据的多份到不同的实例中,这样查询时如果某个存储实例不能提供数据,可以从其他存储实例中获取。
  • 低成本: 为了能够提供长期并可靠的数据存储,对象存储是较理想的选择,这样每个存储实例就可以在本地生成新的 Block 后把他上传到对象存储系统。

读取

  • 存储网关: 对于一个查询来说,请求的数据分为短期(比如 12h 以内)和长期两部分。短期的数据在存储实例(Ingester)中,长期数据在对象存储中。为了简化对象存储处理,提供一个服务(Store Gateway)来支持长期数据的查询。
  • Compactor: 他的作用主要是两方面,一方面是存储成本,一方面是查询效率。从成本方面来看,由于我们会把同一份数据存储多份到不同的存储实例,进而上传到对象存储,那么把重复的数据去重就变得很重要,存储空间可以变为原来的 1/N(N 表示副本数)。从查询效率方面来看,一个目的是减小上传的 Block&Index 数,由于上传的每个 Block 都包含对应的倒排索引文件,每个 Block 上传的间隔如果是 2h 的话,那么一天会产生多个索引文件,在时序指标变化不大的情况下,这个冗余度是非常高的,查询某一天的数据就要通过多个索引文件来定位具体 K 在不同文件中的位置。如果把多个 Block 以及索引文件合并到一个较大的 Block 和索引文件的话,这个效率就要高很多。另一个目的是让存储网关变得可扩展,假设在一定时间内 Ingester上传的 Block 从时间维度和 K 空间维度构成的是”田“的话,Compactor 可以把数据合并成”口“交给一个存储网关实例,也可以合并成一个”日“或”目“交给两个或三个存储网关实例(类似 Map-Reduce 中的 Shuffle),这样我们就可以根据 K 空间里面实际的值数量来动态的调整存储网关实例的数目来提供高效的查询服务。
  • 高性能: 对于一个查询来说,可以抽象为两个维度,一个是时间维度 [T0,Tn],一个是 K 的空间维度 [Ks, Ke]。那么想要降低查询时延,可以把一个查询从两个维度进行切割(口→田),然后采用 Scatter-Gather 的方式让尽量多的节点并行计算再聚合。另外一个优化是时序数据是只读的,并且查询模式较固定,区别在于时间范围不同,所以基于整点时刻的查询切割来缓存的话,命中率会非常高。
  • 多租户: 当有一组实例提供服务的时候,如果不对实例提供隔离,很有可能某些消耗资源大的请求把所有实例的资源占用掉,影响其他的请求。如果把实例进行分组,每个分组支持不同请求来源的话,那么影响面会变成 1/N(N 是分组数)。这些都不够理想,所以 AWS 很早就在内部服务采用了一种叫做 Shuffle Sharding 的算法来解决这个问题。这个算法简单描述是这样的,假设有一组租户 T{T1,…,Tn} 来请求服务实例 S{S1,…,Sn},我们可以从这 N 个 S 实例中取 M 个实例赋给每个租户 Tx 并且能做到每个租户拿到的 M 个实例不完全相同,这样如果某个 Tx 的请求是有问题的,并不会导致其他的租户对应的 M 个 S 实例完全不可用。其中 N 个实例取 M 个实例并且完全不同的个数是 Combination(N,M),完全相同的概率是组合数的倒数,组合数随着 M 变大指数级增长。

效果

该方案上线后 90 分位查询时延降为原来的 1/10,基本控制在 300ms 以内。成本变为原来的 1/10。

改进

由于进行 RemoteWrite 的 Prometheus 只需要依赖 write-ahead log(WAL)即可,所以后面可以切换到其 Agent 模式,这样可以把相应节点的规格大大降低,进一步节省成本。当然这需要先把相关的 Record&Alert Rules 迁移到新系统上。


原文始发于微信公众号(流利说技术团队):分布式时序数据库在流利说的应用

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年8月31日20:36:52
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   分布式时序数据库在流利说的应用http://cn-sec.com/archives/1267126.html

发表评论

匿名网友 填写信息