文|刘家财(花名:尘香 )
蚂蚁团体高级开发工程师 专一时序存储畛域
校对|冯家纯
本文 7035 字 浏览 10 分钟
CeresDB 在晚期设计时的指标之一就是对接开源协定,目前零碎曾经反对 OpenTSDB 与 Prometheus 两种协定。Prometheus 协定相比 OpenTSDB 来说,十分灵活性,相似于时序畛域的 SQL。
随着外部应用场景的减少,查问性能、服务稳定性逐步暴露出一些问题,这篇文章就来回顾一下 CeresDB 在改善 PromQL 查问引擎方面做的一些工作,心愿能起到抛砖引玉的作用,不足之处请指出。
PART. 1 内存管制
对于一个查问引擎来说,大部分状况下性能的瓶颈在 IO 上。为了解决 IO 问题,个别会把数据缓存在内存中,对于 CeresDB 来说,次要包含以下几局部:
- MTSDB:按数据工夫维度缓存数据,相应的也是按工夫范畴进行淘汰
- Column Cache:按工夫线维度缓存数据,当内存应用达到指定阈值时,按工夫线拜访的 LRU 进行淘汰
- Index Cache:依照拜访频率做 LRU 淘汰
下面这几个局部,内存应用相对来说比拟固定,影响内存稳定最大的是查问的两头后果。如果管制不好,服务很容易触发 OOM。
两头后果的大小能够用两个维度来掂量:横向的工夫线和纵向的工夫线。
管制两头后果最简略的形式是限度这两个维度的大小,在构建查问打算时间接回绝掉,但会影响用户体验。比方在 SLO 场景中,会对指标求月的统计数据,对应的 PromQL 个别相似 sum_over_time(success_reqs[30d]),如果不能反对月范畴查问,就须要业务层去适配。
要解决这个问题须要先理解 CeresDB 中数据组织与查问形式,对于一条工夫线中的数据,依照三十分钟一个压缩块寄存。查问引擎采纳了向量化的火山模型,在不同工作间 next 调用时,数据按三十分钟一个批次进行传递。
在进行上述的 sum_over_time 函数执行时,会先把三十天的数据顺次查出来,之后进行解压,再做一个求和操作,这种形式会导致内存使用量随查问区间线性增长。如果能去掉这个线性关系,那么查问数量即便翻倍,内存应用也不会受到太大影响。
为了达到这个目标,能够针对具备累加性的函数操作,比方 sum/max/min/count 等函数实现流式计算,即每一个压缩块解压后,立刻进行函数求值,两头后果用一个长期变量保存起来,在所有数据块计算实现后返回后果。采纳这种形式后,之前 GB 级别的两头后果,最终可能只有几 KB。
PART. 2 函数下推
不同于单机版本的 Prometheus,CeresDB 是采纳 share-nothing 的分布式架构,集群中有次要有三个角色:
- datanode:存储具体 metric 数据,个别会被调配若干分片 (sharding),有状态
- proxy:写入 / 查问路由,无状态
- meta:存储分片、租户等信息,有状态。
一个 PromQL 查问的大略执行流程:
1.proxy 首先把一个 PromQL 查问语句解析成语法树,同时依据 meta 中的分片信息查出波及到的 datanode
2. 通过 RPC 把语法树中能够下推执行的节点发送给 datanode
3.proxy 承受所有 datanode 的返回值,执行语法树中不可下推的计算节点,最终后果返回给客户端
sum(rate(write_duration_sum[5m])) / sum(rate(write_duration_count[5m])) 的执行示意图如下:
为了尽可能减少 proxy 与 datanode 之间的 IO 传输,CeresDB 会尽量把语法树中的节点推到 datanode 层中,比方对于查问 sum(rate(http_requests[3m])),现实的成果是把 sum、rate 这两个函数都推到 datanode 中执行,这样返回给 proxy 的数据会极大缩小,这与传统关系型数据库中的“下推抉择”思路是统一的,即缩小运算波及的数据量。
依照 PromQL 中波及到的分片数,能够将下推优化分为两类:单分片下推与多分片下推。
### 单分片下推
对于单分片来说,数据存在于一台机器中,所以只需把 Prometheus 中的函数在 datanode 层实现后,即可进行下推。这里重点介绍一下 subquery【1】的下推反对,因为它的下推有别于个别函数,其余不理解其用法的读者能够参考 Subquery Support【2】。
subquery 和 query_range【3】接口(也称为区间查问)相似,次要有 start/end/step 三个参数,示意查问的区间以及数据的步长。对于 instant 查问来说,其 time 参数就是 subquery 中的 end,没有什么争议,然而对于区间查问来说,它自身也有 start/end/step 这三个参数,怎么和 subquery 中的参数对应呢?
假如有一个步长为 10s、查问区间为 1h 的区间查问,查问语句是 avg_over_time((a_gauge == bool 2)[1h:10s]),那么对于每一步,都须要计算 3600/10=360 个数据点,依照一个小时的区间来算,总共会波及到 360*360=129600 的点,然而因为 subquery 和区间查问的步长统一,所以有一部分点是能够复用的,真正波及到的点仅为 720 个,即 2h 对应 subquery 的数据量。
能够看到,对于步长不统一的状况,波及到的数据会十分大,Prometheus 在 2.3.0 版本后做了个改良,当 subquery 的步长不能整除区间查问的步长时,疏忽区间查问的步长,间接复用 subquery 的后果。这里举例剖析一下:
假如区间查问 start 为 t=100,step 为 3s,subquery 的区间是 20s,步长是 5s,对于区间查问,失常来说:
1. 第一步
须要 t=80, 85, 90, 95, 100 这五个时刻的点
2. 第二步
须要 t=83, 88, 83, 98, 103 这五个时刻的点
能够看到每一步都须要错开的点,然而如果疏忽区间查问的步长,先计算 subquery,之后再把 subquery 的后果作为 range vector 传给上一层,区间查问的每一步看到的点都是 t=80, 85, 90, 95, 100, 105…,这样就又和步长统一的逻辑雷同了。此外,这么解决后,subquery 和其余的返回 range vector 的函数没有什么区别,在下推时,只须要把它封装为一个 call(即函数)节点来解决,只不过这个 call 节点没有具体的计算,只是从新依据步长来组织数据而已。
call: avg_over_time step:3
└─ call: subquery step:5
└─ binary: ==
├─ selector: a_gauge
└─ literal: 2
在上线该优化前,带有 subquery 的查问无奈下推,这样不仅耗时长,而且还会生产大量两头后果,内存稳定较大;上线该性能后,不仅有利于内存管制,查问耗时根本也都进步了 2-5 倍。
多分片下推
对于一个分布式系统来说,真正的挑战在于如何解决波及多个分片的查问性能。在 CeresDB 中,根本的分片形式是依照 metric 名称,对于那些量大的指标,采纳 metric + tags 的形式来做路由,其中的 tags 由用户指定。
因而对于 CeresDB 来说,多分片查问能够分为两类状况:
1. 波及一个 metric,然而该 metric 具备多个分片
2. 波及多个 metric,且所属分片不同
单 metric 多分片
对于单 metric 多分片的查问,如果查问的过滤条件中携带了分片 tags,那么天然就能够对应到一个分片上,比方(cluster 为分片 tags):
up{cluster="em14"}
这里还有一类非凡的状况,即
sum by (cluster) (up)
该查问中,过滤条件中尽管没有分片 tags,然而聚合条件的 by 中有。这样查问尽管会波及到多个分片,然而每个分片上的数据没有穿插计算,所以也是能够下推的。
这里能够更进一步,对于具备累加性质的聚合算子,即便过滤条件与 by 语句中都没有分片 tags 时,也能够通过插入一节点进行下推计算,比方,上面两个查问是等价的:
sum (up)
# 等价于
sum (sum by (cluster) (up) )
内层的 sum 因为包含分片 tags,所以是能够下推的,而这一步就会极大缩小数据量的传输,即使里面 sum 不下推问题也不大。通过这种优化形式,之前耗时 22s 的聚合查问能够降到 2s。
此外,对于一些二元操作符来说,可能只波及一个 metric,比方:
time() - kube_pod_created > 600
这外面的 time() 600 都能够作为常量,和 kube_pod_created 一起下推到 datanode 中去计算。
多 metric 多分片
对于多 metric 的场景,因为数据分布没有什么关联,所以不必去思考如何在分片规定上做优化,一种间接的优化形式并发查问多个 metric,另一方面能够借鉴 SQL rewrite 的思路,依据查问的构造做适当调整来达到下推成果。比方:
sum (http_errors + grpc_errors)
# 等价于
sum (http_errors) + sum (grpc_errors)
对于一些聚合函数与二元操作符组合的状况,能够通过语法树重写来将聚合函数挪动到最内层,来达到下推的目标。须要留神的是,并不是所有二元操作符都反对这样改写,比方上面的改写就不是等价的。
sum (http_errors or grpc_errors)
# 不等价
sum (http_errors) or sum (grpc_errors)
此外,公共表达式打消技巧也能够用在这里,比方 (total-success)/total 中的 total 只须要查问一次,之后复用这个后果即可。
PART. 3 索引匹配优化
对于时序数据的搜寻来说,次要依赖 tagk->tagv->postings 这样的索引来减速,如下图所示:
对于 up{job=”app1″},能够间接找到对应的 postings(即工夫线 ID 列表),然而对于 up{status!=”501″} 这样的否定匹配,就无奈间接找到对应的 postings,惯例的做法是把所有的两次遍历做个并集,包含第一次遍历找出所有符合条件的 tagv,以及第二次遍历找出所有的 postings。
但这里能够利用汇合的运算性质【4】,把否定的匹配转为正向的匹配。例如,如果查问条件是 up{job=”app1″,status!=”501″},在做合并时,先查完 job 对应的 postings 后,间接查 status=501 对应的 postings,而后用 job 对应的 postings 减去 cluster 对应的即可,这样就不须要再去遍历 status 的 tagv 了。
# 惯例计算形式
{1, 4} ∩ {1, 3} = {1}
# 取反,再相减的形式
{1, 4} - {2, 4} = {1}
与下面的思路相似,对于 up{job=~”app1|app2″} 这样的正则匹配,能够拆分成两个 job 的准确匹配,这样也能省去 tagv 的遍历。
此外,针对云原生监控的场景,工夫线变更是频繁产生的事件,pod 的一次销毁、创立,就会产生大量的新工夫线,因而有必要对索引进行拆分。常见的思路是按工夫来划分,比方每两天新生成一份索引,查问时依据工夫范畴,做多份索引的合并。为了防止因切换索引带来的写入 / 查问抖动,实现时减少了预写的逻辑,思路大抵如下:
写入时,索引切换并不是严格依照工夫窗口,而是提前指定一个预写点,该预写点后的索引会进行双写,即写入以后索引与下一个索引中。这样做的根据是工夫局部性,这些工夫线很有可能在下一个窗口仍然无效,通过提前的预写,一方面能够预热下一个索引,另一方面能够减缓查问扩分片查问的压力,因为下一分片曾经蕴含上一分片自预写点后的数据,这对于跨过整点的查问尤为重要。
PART. 4 全链路 trace
在施行性能优化的过程中,除了参考一些 metric 信息,很重要的一点是对整个查问链路做 trace 跟踪,从 proxy 承受到申请开始,到 proxy 返回后果终止,此外还能够与客户端传入的 trace ID 做关联,用于排查用户的查问问题。
说来乏味的是,trace 跟踪性能晋升最高的一次优化是删掉了一行代码。因为原生 Prometheus 可能会对接多个 remote 端,因而会对 remote 端的后果按工夫线做一次排序,之后合并时就能够用归并的思路,以 O(n*m) 的复杂度合并来自 n 个 remote 端的数据(每个 remote 端假如有 m 条工夫线)。但对于 CeresDB 来说,只有一个 remote 端,因而这个排序是不须要的,去掉这个排序后,那些不能下推的查问根本进步了 2-5 倍。
PART. 5 继续集成
只管基于关系代数和 SQL rewrite rule 等有一套成熟的优化规定,但还是须要用集成测试来保障每次开发迭代的正确性。CeresDB 目前通过 linke 的 ACI 做继续集成,测试用例包含两局部:
- Prometheus 本身的 PromQL 测试集【5】
- CeresDB 针对上述优化编写的测试用例
在每次提交 MR 时,都会运行这两局部测试,通过后才容许合入主干分支。
PART. 6 PromQL Prettier
在对接 Sigma 云原生监控的过程中,发现 SRE 会写一些特地简单的 PromQL,肉眼比拟难分清档次,因而基于开源 PromQL parser 做了一个格式化工具,成果如下:
Original:
topk(5, (sum without(env) (instance_cpu_time_ns{app="lion", proc="web", rev="34d0f99", env="prod", job="cluster-manager"})))
Pretty print:
topk (
5,
sum without (env) (instance_cpu_time_ns{app="lion", proc="web", rev="34d0f99", env="prod", job="cluster-manager"}
)
)
下载、应用形式见该我的项目 README【6】。
「总 结」
本文介绍了随着应用场景的减少 Prometheus on CeresDB 做的一些改良工作,目前 CeresDB 的查问性能,相比 Thanos + Prometheus 的架构,在大部分场景中有了 2-5 倍晋升,对于命中多个优化条件的查问,能够晋升 10+ 倍。CeresDB 曾经笼罩 AntMonitor(蚂蚁的外部监控零碎)上的大部分监控场景,像 SLO、基础设施、自定义、Sigma 云原生等。
本文列举的优化点说起来不算难,但难在如何把这些细节都做对做好。在具体开发中曾遇到一个比较严重的问题,因为执行器在流水线的不同 next 阶段返回的工夫线可能不统一,加上 Prometheus 特有的回溯逻辑(默认 5 分钟),导致在一些场景下会丢数据,排查这个问题就花了一周的工夫。
记得之前在看 Why ClickHouse Is So Fast?【7】时,非常同意外面的观点,这里作为本文的结束语分享给大家:
“What really makes ClickHouse stand out is attention to low-level details.”
招 聘
咱们是蚂蚁智能监控技术中台的时序存储团队,咱们正在应用 Rust 构建高性能、低成本并具备实时剖析能力的新一代时序数据库。
蚂蚁监控危险智能团队继续招聘中,团队次要负责蚂蚁团体技术危险畛域的智能化能力及平台建设,为技术危险几大战场(应急,容量,变更,性能等)的各种智能化场景提供算法反对,蕴含时序数据异样检测,因果关系推理和根因定位,图学习和事件关联剖析,日志剖析和开掘等畛域,指标打造世界领先的 AIOps 智能化能力。
欢送投递征询:jiachun.fjc@antgroup.com
「参 考」
· PromQL Subqueries and Alignment
【1】subquery:
https://prometheus.io/docs/prometheus/latest/querying/examples/#subquery
【2】Subquery Support:
https://prometheus.io/blog/2019/01/28/subquery-support/
【3】query_range
https://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】README
https://github.com/jiacai2050/promql-prettier
【7】Why ClickHouse Is So Fast?
https://clickhouse.com/docs/en/faq/general/why-clickhouse-is-so-fast/
本周举荐浏览
如何在生产环境排查 Rust 内存占用过高问题
新一代日志型零碎在 SOFAJRaft 中的利用
终于!SOFATracer 实现了它的链路可视化之旅
蚂蚁团体技术危险代码化平台实际(MaaS)