[博客翻译]警报评估:ClickHouse中的增量合并


原文地址:https://www.highlight.io/blog/alert-evaluations-incremental-merges-in-clickhouse


标题:ClickHouse中的增量合并:优化警报评估性能

在Highlight,我们依赖ClickHouse,一个专为处理大数据集和实时分析而设计的开源列式数据库。它对存储和查询我们的时序数据(如日志、跟踪和错误)至关重要,帮助我们快速聚合和过滤信息。然而,切换到新数据库也带来了一些挑战。本文将探讨ClickHouse的一些特性如何帮助我们解决性能问题。

问题背景

最近,我们在优化警报系统时遇到了难题。当根据长时间窗口内的计算来决定是否触发警报时,速度变得非常慢。实时处理大量数据不可行,因为全量扫描数据集耗时且内存消耗大。于是,我们利用ClickHouse的聚合函数进行增量计算,即计算并存储部分结果,然后后续合并这些结果。

3.png

问题描述

假设用户希望在过去的1小时内,网络请求的99百分位(p99)持续时间超过1秒时收到警报。为了提供接近实时的反馈,我们需要每分钟评估一次。如果简单地每分钟重新计算整个一小时窗口,那么对于每个警报,每小时需要扫描数据60次,这会导致巨大的计算开销。

简单的增量合并

对于像计数和求和这样简单的聚合函数,我们发现可以通过保存先前的计算结果来优化。不是每次一分钟都从头开始计算,而是逐步累积中间结果。

例如,如果设置一个日志警报,当一小时内日志数量超过100条时触发,我们可以每分钟计数并保存日志数量,然后加载并累加过去60分钟的数据,检查是否达到阈值。

下面是日志行计数求和的简化示例计算过程:

但是,事情变得更复杂...

对于更复杂的聚合函数,如精确的中位数(p50),就不能简单地计算多个中间值并合并。相反,我们需要存储所有数据点来进行计算。

然而,ClickHouse的强大“-State”和“-Merge”组合器允许我们以类似“计数”或“求和”的方式计算中间值。-State函数返回中间状态,而-Merge函数则合并这些状态。

以下是关于状态函数(用QS表示量化状态)和合并函数(用QM表示量化合并)的工作原理的图解:

简要说明状态和合并函数

ClickHouse使用内存高效的近似算法实现许多复杂的聚合函数,比如Highlight使用的uniq(唯一值计数)和quantile(百分位)。uniq返回不同值的近似计数,而quantile用于计算近似的p50、p90等。这些算法通过不同的抽样方法从底层分布中获取代表性值。

uniqState和quantileState返回这些计算的底层状态表示。由于它们是有限最大大小的抽样算法,所以内存使用是受控的。计算并保存这些状态后,我们可以稍后使用uniqMerge和quantileMerge函数加载并使用它们。这些函数是逆过程,接收状态作为输入并返回结果值,例如uniqMerge(uniqState(x)) = uniq(x)。这样做的好处是,uniqState可以针对多个小片段输入数据运行并保存,然后在后期多次合并,中间状态的大小远小于原始输入数据,因此效率更高。

解决方案实现

我们的方法是每分钟加载所有新数据,对新数据计算中间状态,然后将这些状态与现有状态合并,得到所需时间段的汇总值。

我们的表结构如下:

  • MetricId:UUID
  • Timestamp:DateTime
  • GroupByKey:String
  • MaxBlockNumberState:max(UInt64)聚合函数
  • CountState:count(UInt64)聚合函数
  • UniqState:uniq(String)聚合函数
  • MinState:min(Float64)聚合函数
  • AvgState:avg(Float64)聚合函数
  • MaxState:max(Float64)聚合函数
  • SumState:sum(Float64)聚合函数
  • P50State:quantile(0.5, Float64)聚合函数
  • P90State:quantile(0.9, Float64)聚合函数
  • P95State:quantile(0.95, Float64)聚合函数
  • P99State:quantile(0.99, Float64)聚合函数

引擎设置为ReplicatedAggregatingMergeTree,按照MetricId、Timestamp和GroupByKey排序,并设置了索引粒度。

因为我们支持的每个聚合函数的状态列类型不同,所以我们为每个函数都有单独的列。数据以分钟粒度计算。

我们通过检查底层表中max_block_number大于上一次运行的最大值的所有部分来识别新数据。这会返回包含新数据的部分,但可能包含已读过的数据。为了避免重复计数,我们在底层表上启用了allow_experimental_block_number_column,并在查询中按每行的_block_number过滤。

总结

总的来说,利用ClickHouse的-State和-Merge函数组合器,我们的警报评估过程变得更加高效。在实际的Highlight环境中测试日志警报评估,我们实现了10倍的速度提升(从1.24秒降至0.11秒),同时将内存使用量从7.6GB降低到82MB。

如果你对此感兴趣,可以查看ClickHouse关于State和Merge操作符的文档。继续关注我们的博客,了解我们在Highlight如何解决技术挑战的幕后故事。

评论:

  • Edgared.isajanyan@gmail.com:嘿,Zane,我在构建增量求和时也遇到了相同的问题,无法让ClickHouse在更新时正确应用差分。我很想知道_block_number是否解决了这个问题,关于这个列的信息并不多。👍