如何优化 PromQL 和 MetricsQL 查询

Aliaksandr Valialkin 2023年4月10日

20230410190940

PromQL和MetricsQL是强大的查询语言。它们允许编写简单的查询,用于构建漂亮的时间序列数据图形。它们还允许编写复杂的查询,用于SLI / SLO计算和警报。但优化PromQL查询可能很困难。本文介绍了如何确定缓慢的PromQL查询,如何理解查询成本以及如何优化这些查询,使其执行更快并消耗更少的CPU和RAM。

如何确定 PromQL 查询是否缓慢?

很遗憾,仅凭外观无法确定PromQL查询是快还是慢 :( PromQL查询性能除了查询本身之外还取决于以下因素:

  • 查询选择的series数量。如果查询选择了数百万个series,则在执行期间可能需要占用几千兆字节的RAM、高数量的磁盘IOPS和大量CPU时间。
  • 查询需要扫描的原始样本数。这个数字等于所选时间范围内所有匹配series的原始样本之和。样本数量还取决于数据库中存储的采样间隔(即Prometheus世界中的scrape_interval)-较短的间隔意味着更多的采样。
  • Grafana中用于构建图形所使用到的时段。较长时段意味着必须从数据库中读取更多数据。请注意,较长时段可能会增加要扫描和匹配时间series数目,因为老series会随着新series而被替换掉(也称为series流失率)。
  • 所选时间范围内数据库中总共有多少个series。通常情况下,更多series表示查找匹配series速度变慢。请注意,如果 series 有很高的流失率,活跃的 series 数量可能会远小于给定时段内的总 series 数量。

例如,简单的查询 up 只匹配了选定时间范围内几千个样本的少数时间序列,可能会在一眨眼之间就执行完了。但是,如果该查询匹配了选定时间范围内数十亿个样本和成百上千个时间序列,则相同的查询 up 可能需要花费数十秒钟,并且可能会消耗几GB的RAM。

查询性能也取决于查询本身。例如,不同的函数具有不同的性能。MetricsQL 和 PromQL 有以下函数类型:

  • 标签操作函数,例如label_replace、label_join和label_set。尽管在查询中使用时看起来很复杂,但它们是最轻量级的函数。
  • 转换函数,例如abs、round和time。这些通常是快速的函数。有一些例外,如histogram_quantile、prometheus_buckets和其他直方图相关的函数,在执行过程中需要更多时间和资源。
  • 聚合函数,例如sum、count、avg、min和max。这些函数大多数也是轻量级的,除了一些非常规的聚合函数,如count_values、quantile或histogram。
  • 滚动函数,如rate、increase、min_over_time和quantile_over_time。这些是最昂贵的函数。滚动函数使用存储在数据库中的原始样本,而其他函数则使用从滚动函数返回的计算结果。每个滚动函数都接受方括号(在Prometheus中称为范围向量)中的lookbehind窗口(一个时间范围,比如1m表示1分钟)。例如,rate(http_requests_total[1h]) 示例中的lookbehind窗口-1小时-更改了查询需要处理每个匹配时间序列的原始样本数量。较大的窗口意味着需要处理更多原始样本。

PromQL和MetricsQL有一个额外的功能,如果使用不当可能会显著减慢查询速度 - 子查询。了解子查询的工作原理,以了解它们如何影响查询性能和资源使用情况。

正如您所看到的,仅凭查询本身很难确定查询是快还是慢。确定缓慢的查询最好的方法是在生产环境中运行它们,并查看在实际数据上执行时哪些查询较慢。VictoriaMetrics提供以下其他选项,可能有助于确定缓慢的查询:

  • 慢查询日志。如果执行时间超过传递给-searchlogSlowQueryDuration命令行标志的值,则VictoriaMetrics会记录查询。
  • 查询统计数据。它可以在 /api/v1/status/top_queries 页面中找到。它显示了最常执行的查询以及最慢的查询。有关更多详细信息,请参阅这些文档

好的,检测到慢查询。如何确定为什么这些查询很慢?

为什么 PromQL 查询很慢?

让我们重复一下PromQL和MetricsQL查询变慢的最常见情况:

  • 当它选择大量时间序列时。
  • 当它选择大量原始样本时。
  • 当查询中使用的个别标签过滤器匹配大量时间序列时。

因此,您需要检查每个案例以便检测导致查询缓慢的情况。

给定的 PromQL 查询选择了多少时间序列?

使用count和last_over_time函数,结合查询中的系列选择器。例如,如果查询看起来像avg(rate(http_requests_total[5m])) by (job),那么以下查询将返回初始查询需要选择的时间序列数量:

count(last_over_time(http_requests_total[5m]))

这适用于即时查询,这些查询通常用于警报和记录规则(recoding rules)。这些查询仅返回给定时间的值。如果把查询语句放到Grafana中来构建给定时间范围内的图形,则会使用范围查询。他们为图表上的每个点计算独立的结果。在高流失率下,实际查询和处理的series数量远高于count(last_over_time(...))查到的值。在这种情况下,您应该将所选的时间范围放在方括号中,以获取查询需要选择的时间序列的实际数量。例如,为了选择原始查询在过去24小时内构建图表所涉及的系列数量,必须使用以下查询:

count(last_over_time(http_requests_total[24h]))

如果count(last_over_time(…))查询返回的值小于几千个,则原始查询在选择时间序列数量方面没有瓶颈。如果返回的数字超过数万个,则可能会有问题。尝试以下方法来减少所选时间序列的数量:

  • 在系列选择器中添加更多的标签过滤器,以便选择更少的时间序列。
  • 检测和修复高流失率的来源。这些通常是随时间定期更改值的标签。此类标签可以通过 /api/v1/status/tsdb 页面检测到。
  • 将方括号中的回溯窗口减小或在Grafana中缩小图形的时间范围。这可能有助于在高流失率下减少所选时间序列的数量。

给定的 PromQL 查询选择了多少个样本?

使用sum和count_over_time函数结合查询中的系列选择器。例如,如果查询看起来像avg(rate(http_requests_total[5m])) by (job),那么以下查询将返回初始查询需要选择的样本数:

sum(count_over_time(http_requests_total[5m]))

这适用于警报和记录规则。如果您需要确定在给定时间范围内选择多少原始样本来构建Grafana中的图形,则必须将方括号中的lookbehind窗口更改为给定的时间范围。例如,以下查询返回了构建最近24小时图形所需的原始样本数量:

sum(count_over_time(http_requests_total[24h]))

如果 sum(count_over_time(...)) 查询返回的值小于几百万,则初始查询在扫描原始样本数方面没有瓶颈。如果返回的数字超过了数亿,则您可能需要通过以下技术来优化查询:

  • 通过前面解释的办法来减少所选时间序列的数量
  • 减少方括号中的回溯窗口大小,从而在查询期间需要处理的原始样本数量更少
  • 在Grafana中缩短图表的时间范围
  • 在Grafana中调大分辨率选项,以便请求更少的数据点来构建图表。请参阅这些文档

这个 PromQL 查询处理了多少个样本?

通常情况下,处理的原始样本数量与选择的原始样本数量相匹配。但有时候,处理的原始样本数量可能显著超过选择的原始样本数量。如果满足以下条件,PromQL 和 MetricsQL 会多次处理同一份原始样本:

  • 该查询用于在Grafana中构建图表。
  • 方括号中的回溯窗口超出了图表上点之间的时间间隔(Grafana通过step查询参数传递此间隔到/api/v1/query_range)。

例如,如果使用以下查询来构建过去一小时的图表:max_over_time(process_resident_memory_bytes[30m]),Grafana会把step参数传给/api/v1/query_range,step比30分钟显然会小很多。这意味着每个原始样本都会被多次用于计算,以计算图表上每个点的max_over_time。以下的MetricsQL查询可用于确定查询需要处理的样本数量:

range_last(
  running_sum(
    sum(
      count_over_time(process_resident_memory_bytes[30m])
    )
  )
)

如果返回的数字超过亿级别,那么就需要通过减少方括号中的lookbehind窗口来优化查询。另一个优化选项是通过调高分辨率选项来指示Grafana调大step参数。有关详细信息,请参阅这些文档

高流失率和PromQL查询性能

时间序列的高流失率可能会导致查询性能下降。高流失率会增加数据库中时间序列的总数。这也增加了查询中每个标签过滤器匹配时间序列的数量。这反过来会减慢查找匹配时间序列的过程。

这是Kubernetes监控中常见的问题,特别是在频繁部署时。每次新部署都会增加时间序列的变化率,因为被部署对象(pod、endpoint、service)暴露出来的指标可能具有与部署相关的标签(如deployment id、pod id、image id等)的新值。解决这个问题的方法是使用Prometheus relabeling规则删除或重写这些标签,以便它们不会随着每次部署而经常更改。最经常更改值的标签可以通过/api/v1/status/tsdb页面检测到。详细信息请参阅此文档

优化复杂的PromQL查询

PromQL允许编写具有多个系列选择器的查询。例如,foo + bar包含两个系列选择器 - foobar。这些系列选择器独立地进行评估。这意味着优化过程应该针对查询中每个系列选择器分别执行。

当二元运算符左右两侧的时间序列选择器返回不同数量和标签集合的时间序列时,复杂查询存在常见性能问题。PromQL 和 MetricsQL 剥离指标名称,然后将二元运算分别应用于左侧和右侧系列上具有相同标签集的系列,而其他系列则被丢弃。查阅此文档了解更多细节。这意味着在二元运算符的另一侧没有相应匹配对的系列只会浪费计算资源(CPU 时间、RAM、磁盘 IO)。解决方法是在二元运算符两边的指标选择器中添加公共标签过滤器。这将减少所选时间序列的数量,从而减少资源使用。例如,查询 foo{instance="x"} + bar 可以被优化为 foo{instance="x"} + bar{instance="x"},这样 bar 就只会将搜索范围缩小到带有 {instance="x"} 标签的时间序列。

结语

MetricsQL 和 PromQL 查询优化并不是一项容易的任务。它取决于查询本身和被查询的数据。Prometheus 和 VictoriaMetrics 提供了一些工具来确定缓慢的查询,并检测哪个部分的查询速度较慢。本文介绍了确定查询缓慢部分的常见技术,并提供可能的优化解决方案。我希望这篇文章能够帮助在生产环境中优化 PromQL 查询。

本文翻译自:https://valyala.medium.com/how-to-optimize-promql-and-metricsql-queries-85a1b75bf986。机翻,凑合读吧,也基本能理解作者的意思。

开源版
Flashcat
Flashduty