当前位置: 首页 > Web前端 > HTML

Prometheus在CeresDB上的进化之路

时间:2023-03-28 15:39:08 HTML

文章|刘佳才(花名:陈香)蚂蚁集团资深开发工程师专注时序存储领域校对|冯家纯本文10分钟阅读7035字其中一篇CeresDB在早期设计的目标是对接开源协议,系统目前支持OpenTSDB和Prometheus两种协议。与OpenTSDB相比,Prometheus协议非常灵活,类似于时序领域的SQL。随着内部使用场景的增多,查询性能和服务稳定性等方面也逐渐暴露出一些问题。本文将回顾CeresDB在改进PromQL查询引擎方面所做的一些工作,希望能起到抛砖引玉的作用。不足之处请指出。部分。1内存控制对于一个查询引擎来说,大多数情况下性能瓶颈在于IO。为了解决IO问题,一般会将数据缓存在内存中。对于CeresDB,主要包括以下几个部分:MTSDB:根据数据时间维度缓存数据,并根据时间范围对应剔除ColumnCache:根据时间轴维度缓存数据,当内存使用达到指定阈值时,按照时间线访问到的LRU将被淘汰。IndexCache:以上部分会根据访问频率通过LRU进行淘汰。内存占用比较固定,内存波动影响最大的是在查询中间。结果。如果控制不好,服务很容易触发OOM。中间结果的大小可以从两个维度来衡量:水平时间线和垂直时间线。控制中间结果最简单的方法就是限制这两个维度的大小,在构建查询计划的时候直接拒绝,但是会影响用户体验。比如在SLO场景下,指标会要求月度统计数据。相应的PromQL通常类似于sum_over_time(success_reqs[30d])。如果查询不支持按月范围,则需要对业务层进行适配。要解决这个问题,需要了解CeresDB中的数据组织和查询方式。对于时间线中的数据,它每30分钟存储在一个压缩块中。查询引擎采用矢量化火山模型。不同任务之间调用next时,数据以30分钟为一批进行传输。执行上面的sum_over_time函数时,会依次检出30天的数据,然后解压,然后进行求和运算。该方法会导致内存使用量随查询间隔线性增加。如果能去掉这种线性关系,即使查询次数翻倍,对内存的占用也不会有太大的影响。为了达到这个目的,可以对累加函数运算实现流计算,比如sum/max/min/count等函数,即每个压缩块解压后,立即进行函数求值,中间的result是用一个临时变量保存起来的,所有数据块都计算完后返回结果。采用这种方法后,之前GB级别的中间结果可能最终只有几KB。部分。2函数下推与单机版Prometheus不同。CeresDB采用无共享分布式架构。集群中主要有3个角色:datanode:存储具体的metric数据,一般分配若干shards(sharding),Statefulproxy:写入/查询路由,statelessmeta:存储shards,租户等信息,stateful。PromQL查询的大致执行过程:1.proxy首先将PromQL查询语句解析成语法树,同时根据meta中的分片信息找出涉及的datanode。2.将语法树中可以下推执行的节点通过RPC接受所有datanode的返回值给datanode3.proxy,执行语法树中不能下推的计算节点,返回最终的结果给客户端sum(rate(write_duration_sum[5m]))/sum(rate(write_duration_count[5m]))执行示意图如下:为了尽量减少proxy和datanode之间的IO传输,CeresDB会尽量推送语法树中的节点到数据节点层。比如查询sum(rate(http_requests[3m])),理想的效果是将sum和rate这两个函数Push到datanode中执行,这样返回给proxy的数据会大大减少,即与传统关系型数据库中“下推选择”的思想一致,即减少操作涉及的数据量。根据PromQL涉及的分片数量,下推优化可以分为两类:单分片下推和多分片下推。###单分片下推对于单分片来说,数据存在于一台机器上,所以只需要在datanode层实现Prometheus中的功能就可以下推了。这里重点介绍子查询[1]的下推支持,因为它的下推不同于一般的功能。其他不了解其用法的读者可以参考SubquerySupport[2]。子查询类似于query_range【3】接口(也称为范围查询),主要有三个参数:start/end/step,分别代表查询的范围和数据的步长。对于即时查询,时间参数在子查询中是end,这个没有争议,但是对于区间查询,它还有start/end/step三个参数,它们和子查询中的参数是怎么对应的呢?假设有一个步长为10s,查询间隔为1h的区间查询,查询语句为avg_over_time((a_gauge==bool2)[1h:10s]),那么对于每一步,需要计算3600/10=360数据点,按照一小时的间隔计算,总共会涉及360*360=129600个点,但是因为子查询和区间查询的步长是同,有些点是可以复用的,实际只涉及到720个点,也就是2h对应子查询的数据量。可以看出,对于步长不一致的情况,涉及的数据会非常大。Prometheus在2.3.0版本之后做了改进。当子查询的步长不能被区间查询的步长整除时,忽略区间查询的步长。Long,直接复用子查询的结果。下面举例分析:假设区间查询开始时间为t=100,步长为3s,子查询区间为20s,步长为5s。对于区间查询,通常:1.第一步需要t=80,85,90,95,100这五个时刻的点2.第二步需要t=83,88,83,98,103这五个时刻的点五时刻可以看出每一步都需要错开,但是如果忽略区间查询步长,先计算子查询,然后将子查询的结果作为范围向量传递给上层。范围查询每一步看到的点是t=80,85,90,95,100,105...,这样逻辑和步长一致性是一样的。另外,经过这样的处理,子查询和其他返回范围向量的函数没有区别。下推的时候只需要封装成一个调用(也就是一个函数)节点进行处理即可,但是这个调用节点并没有具体的计算,只是按照步长重新组织数据即可。call:avg_over_timestep:3└─call:subquerystep:5└─binary:==├─selector:a_gauge└─literal:2在这个优化启动之前,带子查询的query是不能下推的,不仅需要a时间长了,而且会产生大量的中间结果,内存波动很大。该功能上线后,不仅有利于内存控制,而且查询耗时基本上增加了2-5倍。多分片下推对于分布式系统来说,真正的挑战是如何解决涉及多个分片的查询性能。在CeresDB中,基本的分片方法是基于指标名称。对于那些规模较大的指标,采用metric+tags的方式进行路由,tag由用户指定。因此,对于CeresDB来说,多分片查询可以分为两种:1.涉及到一个metric,但是该metric有多个shards2.涉及多个metrics,但属于不同的shards。Singlemetricmulti-shard是针对单metricmulti-shard的Shard查询,如果查询的过滤条件携带了shard标签,那么自然可以对应一个shard,例如(cluster就是shard标签):up{cluster="em14"}这里还有一个特例,就是sumby(cluster)(up)在这个查询中,虽然过滤条件里没有分片标签,但是聚合条件的by里有。这样虽然查询涉及到多个shard,但是每个shard上的数据是不交叉计算的,所以也可以下推。在这里我们可以更进一步。对于具有累积属性的聚合算子,即使过滤条件和by语句中没有分片标签,也可以通过插入节点的方式下推。比如下面两个查询是等价的:sum(up)#等价于sum(sumby(cluster)(up))内层的sum可以下推,因为它包含分片标签,这一步会大大减少数据传输量,即使总和不往外推也问题不大。通过这种优化方式,可以将之前耗时22s的聚合查询缩短为2s。另外,对于一些二元算子,可能只涉及一个metric,比如:time()-kube_pod_created>600,其中time()600可以作为常量,和kube_pod_created一起下推到datanode计算。Multi-metricmulti-sharding对于多metric场景,由于数据分布不相关,所以不需要考虑如何优化分片规则。并发查询多个指标是一种直接的优化方法。另一方面,SQLrewrite可以借鉴。思路是根据查询的结构做适当的调整,达到下推的效果。例如:sum(http_errors+grpc_errors)#等同于sum(http_errors)+sum(grpc_errors)对于一些聚合函数和二元运算符的组合,可以通过重写语法树将聚合函数移到最内层。达到下推的目的。需要注意的是,并不是所有的二元运算符都支持这样的改写,例如下面的改写是不等价的。sum(http_errorsorgrpc_errors)#不等同于sum(http_errors)或sum(grpc_errors)另外,这里也可以使用常用的表达式淘汰技术,比如totalin(total-success)/total只需要查询一次,然后重复使用这个结果。部分。3索引匹配优化对于时序数据的查找,主要依靠tagk->tagv->postings等索引来提速,如下图:对于up{job="app1"},可以直接找到对应的贴子(即timelineID列表),但是对于up{status!="501"}这样的负匹配,是无法直接找到对应贴子的。常规的方法是将两次遍历全部结合起来,包括第一次遍历找到所有符合条件的tagvs,第二次遍历找到所有贴子。但是在这里我们可以利用集合[4]的操作性质将负匹配变成正匹配。比如查询条件是up{job="app1",status!="501"},合并的时候先查job对应的postings,然后直接查status=501对应的postings,然后用jobPostings对应的postings减去对应的cluster,所以不需要遍历status的tagv。#常规计算方法{1,4}∩{1,3}={1}#求逆相减的方法{1,4}-{2,4}={1}与上述思路类似,对于Regular类似up{job=~"app1|app2"}的匹配可以拆分成两个job的精确匹配,这样也省去了tagv的遍历。另外,对于云原生的监控场景,时间线变化频繁,一旦一个pod被销毁或者创建,就会产生大量新的时间线,所以需要对索引进行拆分。一个常见的思路是按时间划分,比如每两天生成一个新的索引,查询时根据时间范围合并多个索引。为了避免索引切换带来的写/查询抖动,实现时增加了预写逻辑。思路大致是这样的:在写入的时候,索引切换并不严格按照时间窗口,而是提前指定一个预写点。预写点之后的索引会被双写,即写入当前索引和下一个索引。其基础是时间局部性。这些时间表很可能在下一个窗口中仍然有效。通过提前预写,一方面可以预热下一个索引,另一方面可以减少查询扩容和分片查询的压力,因为下一个分片已经包含了上一个分片的数据由于预写点,这对于跨越整个点的查询尤为重要。部分。4全链路追踪在实现性能优化的过程中,除了参考一些metric信息外,非常重要的是追踪整个查询链路,从proxy收到请求开始,到proxy返回结果为止。可与客户端传入的traceID关联,解决用户查询问题。有意思的是,trace跟踪性能提升最高的优化是删除一行代码。由于原生的Prometheus可能会连接多个远程终端,所以远程终端的结果会按照时间线排序,然后用merge的思想合并,将n个远程终端的结果合并,时间复杂度为O(n*m)复杂性。数据(假设每个遥控器有m个时间线)。但是对于CeresDB来说,remoteend只有一个,所以这个排序是没有必要的。去掉这个排序后,那些不能下推的query基本提升了2-5倍。部分。5持续集成虽然有一套成熟的基于关系代数和SQL重写规则的优化规则,但是仍然需要集成测试来保证每次开发迭代的正确性。CeresDB目前是通过linke的ACI做持续集成。测试用例包括两部分:Prometheus自带的PromQL测试集[5]CeresDB针对上述优化编写的测试用例,每次提交MR时都会运行这两部分测试。允许合并到主分支。部分。6PromQLPrettier在对接Sigma云原生监控的过程中,他发现SRE会写一些特别复杂的PromQL,肉眼很难区分层次,于是他基于开源的PromQL做了一个格式化工具解析器,效果如下:原文:topk(5,(sumwithout(env)(instance_cpu_time_ns{app="lion",proc="web",rev="34d0f99",env="prod",job=“集群管理器”})))漂亮的打印:topk(5,没有(env)的总和(instance_cpu_time_ns{app=“lion”,proc=“web”,rev=“34d0f99”,env=“prod”,job="cluster-manager"}))下载和使用见项目README[6]。【小结】本文介绍Prometheus随着使用场景的增加,对CeresDB所做的一些改进。目前CeresDB的查询性能,相比Thanos+Prometheus的架构,在大部分场景下都有2-5倍的提升。具有多个优化条件的查询可以提高10倍以上。CeresDB已经覆盖了AntMonitor(Ant内部监控系统)上的大部分监控场景,比如SLO、基础设施、定制化、Sigma云原生等,本文列举的优化点不难说,难点在于如何正确获取这些细节。在具体的开发中,我遇到了一个严重的问题。由于pipeline不同下阶段的executor返回的timeline可能不一致,加上Prometheus特有的回溯逻辑(默认5分钟),在某些场景下会出现数据丢失的情况,用了一个星期的时间来解决这个问题。记得之前看WhyClickHouseIsSoFast?【7】的时候,非常认同里面的观点。在这里作为本文的结语分享给大家:“真正让ClickHouse脱颖而出的是对底层细节的关注。”我们是招聘中的蚂蚁智能监控技术中心的时序存储团队,我们正在使用Rust构建具有高性能、低成本和实时分析能力的新一代时序数据库。蚂蚁监控风险情报团队持续招募中。团队主要负责蚂蚁集团技术风险领域的智能化能力和平台建设,在技术风险的几大战场(应急、容量、变更、性能等)为各类智能场景提供算法支持,包括时序数据异常检测、因果推理与根因定位、图学习与事件关联分析、日志分析与挖掘等领域,旨在打造全球领先的AIOps智能能力。欢迎发问:jiachun.fjc@antgroup.com《参考》PromQLSubqueriesandAlignment[1]子查询:https://prometheus.io/docs/prometheus/latest/querying/examples/#subquery[2]子查询支持:https://prometheus.io/blog/2019/01/28/subquery-support/【3】query_rangehttps://prometheus.io/docs/prometheus/latest/querying/api/#range-queries【4】操作性质https://zh.wikipedia.org/wiki/%E8%A1%A5%E9%9B%86【5】PromQL测试集https://github.com/prometheus/prometheus/tree/main/promql/testdata【6]READMEhttps://github.com/jiacai2050/promql-prettier【7】为什么ClickHouse这么快?https://clickhouse.com/docs/en/faq/general/why-clickhouse-is-so-fast/本周推荐阅读如何排查生产环境中Rust内存占用过高新生代日志的应用SOFAJRaft中的系统终于来了!SOFATracer完成链路可视化之旅蚂蚁科技风险编码平台实践(MaaS)