DoubleCloud 即将停止运营。在限时免费迁移服务期间迁移到 ClickHouse。立即联系我们 ->->

博客 / 工程

ClickHouse 与 Elasticsearch:计数聚合的机制

author avatar
Tom Schreiber
2024 年 5 月 7 日

简介

Elasticsearch_blog2_header.png

在另一篇博文中,我们研究了ClickHouse 与 Elasticsearch 在大型数据分析和可观察性用例中常见的负载(对数十亿行表数据进行 count(*) 聚合)上的性能。我们展示了 ClickHouse 在处理大型数据量上的聚合查询方面大大优于 Elasticsearch。具体来说

ClickHouse 中的 Count(*) 聚合查询能够高度有效地利用硬件,从而导致聚合大型数据集的**延迟至少降低 5 倍**,与 Elasticsearch 相比。这对于实现与 Elasticsearch 相同的延迟,只需要更小且**成本降低 4 倍的硬件**。

由于上述原因,我们越来越多地看到用户从 Elasticsearch 迁移到 ClickHouse,客户案例强调了以下几点:

  • PB 级可观察性用例中成本大幅降低

“从 Elasticsearch 迁移到 ClickHouse 后,我们可观察性硬件的成本降低了 30% 以上。” 滴滴出行

  • 提升数据分析应用的技术限制

“这释放了新功能、增长和更轻松扩展的潜力。” Contentsquare

  • 监控平台的可扩展性和查询延迟大幅提升

“ClickHouse 帮助我们将每月处理的行数从数百万扩展到数十亿。”
“切换后,我们看到平均读取延迟提高了 100 倍” The Guild

您可能会好奇:“为什么 ClickHouse 比 Elasticsearch 快得多且效率更高?” 本文将为您深入解答这个问题。

ClickHouse 和 Elasticsearch 中的计数聚合

数据分析场景中聚合的一个常见用例是计算和排名数据集中值的频率。例如,ClickPy 应用程序(分析近 9000 亿行Python 包下载事件)的屏幕截图中的所有数据可视化都使用了 SQL GROUP BY 子句结合 count(*) 聚合在后台Elasticsearch_blog2_01.png

类似地,在日志记录用例(或更一般地可观察性用例)中,聚合最常见的应用之一是统计特定日志消息或事件发生的频率(并在频率异常时发出警报)。

ClickHouse 中 SELECT count(*) FROM ... GROUP BY ... SQL 查询在 Elasticsearch 中的等价物是terms 聚合,它是一种 Elasticsearch bucket 聚合

ClickHouse 的 GROUP BY 结合 count(*) 和 Elasticsearch 的 terms 聚合在功能上大体等价,但在实现、性能和结果质量方面存在很大差异,如下所述。

我们在一篇配套的博文中比较了计数聚合的性能。

除了 bucket 聚合之外,Elasticsearch 还提供了指标聚合。我们将把 ClickHouse 和 Elasticsearch 在指标用例方面的比较留待另一篇博文讨论。

计数聚合方法

并行化

ClickHouse

ClickHouse 从一开始就被设计用于尽可能快速和高效地筛选和**聚合**互联网规模的数据。为此,ClickHouse 会将 SELECT 查询(包括 count(*) 等聚合函数以及所有其他90 多个聚合函数)并行化到①列值、②表块和③表分片级别:Elasticsearch_blog2_02.png

① SIMD 并行化

ClickHouse 利用 CPU 的SIMD 单元(例如AVX512)对列中连续的值应用相同的操作。这篇博文详细介绍了其工作原理。

② 多核并行化

在一台具有 n 个 CPU 内核的机器上,ClickHouse 使用 n并行执行通道(或根据用户通过max_threads设置请求的更多或更少通道)运行聚合查询:Elasticsearch_blog2_03.png

上图显示了 ClickHouse 如何并行处理 n 个不重叠的数据范围。这些数据范围可以是任意的,例如,它们不需要基于分组键。当聚合查询包含 WHERE 子句形式的过滤器并且可以利用主索引来评估此过滤器时,ClickHouse 会找到匹配的表数据范围并将这些数据范围动态地分配到 n 个执行通道中。

这种并行化方法是使用部分聚合状态实现的:每个 n 个执行通道都会生成部分聚合状态。最终,这些部分聚合状态会合并成最终的聚合结果。

对于 count(*) 聚合,部分聚合状态只是一个递增更新的计数变量。事实上,count(*) 聚合是最简单的聚合类型,即使没有部分聚合状态的概念,也可以在内部进行并行化。为了给出部分聚合状态如何实现并行化的具体示例,我们使用此聚合查询来计算基于英国房产价格数据集的每个城镇的平均房价。

SELECT
    town,
    avg(price) AS avg_price
FROM uk_price_paid
GROUP BY town;

假设数据库想要使用两个并行执行通道计算伦敦的平均房价:Elasticsearch_blog2_04.png

执行通道 1 对其数据范围内的所有行的房价进行平均,其中包含两条关于伦敦的记录。对于avg,生成的部分聚合状态通常包括

  • 一个sum(这里对于执行通道 1 为:500,000,作为伦敦的房价总和)和
  • 一个count(这里对于执行通道 1 为:2,作为处理的伦敦记录数)。

执行通道 2 计算类似的部分聚合状态。这两个部分聚合状态会被合并,最终结果由此产生:London 记录的房屋平均价格为 (500,000 + 400,000) / (2 + 1) = 900,000 / 3 = 300,000

部分聚合状态对于计算正确的结果是必要的。简单地对子范围的平均值取平均会产生错误的结果。例如,如果我们将第一个子范围的平均值(250,000)与第二个子范围的平均值(400,000)取平均,我们将得到 (650,000 / 2) = 325,000,这是错误的。

ClickHouse 将其支持的所有 90 多个聚合函数 以及它们与 聚合函数组合器 的组合并行化到所有可用的 CPU 内核上。

③ 多节点并行化

如果聚合查询的数据源表是 分片 的并在多个节点上分布,那么 ClickHouse 将聚合函数并行化到所有可用节点的所有可用 CPU 内核上。

每个节点(并行)使用上述多核并行化技术在本地执行聚合。生成的局部聚合状态会流式传输到发起节点(最初接收聚合查询的节点)并由其合并。

Elasticsearch_blog2_05.png

作为一种 优化,如果聚合查询的 GROUP BY 密钥是 分片密钥 的前缀,那么发起节点不需要合并局部聚合状态,合并将在每个节点上作为最后一步发生,最终结果将流式传输回发起节点。

增量聚合

到目前为止讨论的所有技术都应用于“查询时”,即当用户运行 SELECT count(*) FROM ... GROUP BY ... 查询时。如果相同的昂贵聚合查询被重复运行,例如每小时一次,或者如果需要亚秒级的延迟,但查询的数据集太大,那么 ClickHouse 提供了一种额外的优化,将负载从查询时间转移到插入时间和(大部分)后台合并时间。具体来说,上面提到的 部分聚合技术(基于 部分聚合状态)也可以在 数据块 级别期间(并行后台数据块合并 中使用。

这产生了一种强大且高度可扩展的 持续数据汇总技术,聚合查询将发现大部分数据已经被聚合。下图对此进行了概括: Elasticsearch_blog2_06.png

-State 聚合函数组合器可用于指示 ClickHouse 查询引擎仅合并部分聚合状态( 并行执行通道 生成 的),而不是 合并并计算最终结果。

 SELECT
    town,
    avgState(price) AS avg_price_state
FROM uk_price_paid
GROUP BY town;

当你 运行 此查询时,avg_price_state 中的部分聚合状态实际上并不打算打印在屏幕上。

然后,这些组合的部分聚合状态可以作为 数据块 写入具有 AggregatingMergeTree 表引擎的表中。

INSERT INTO <table with AggregatingMergeTree engine>
SELECT
    town,
    avgState(price) AS avg_price_state
FROM uk_price_paid
GROUP BY town;

② 此表引擎 继续 增量(并且 并行)地执行部分聚合,在后台数据块合并期间。合并的结果等效于对所有原始数据运行聚合查询。

一个 物化视图 会自动执行上述插入步骤。这里 是一个基于我们上面使用的“每个城镇的房屋平均价格”示例的具体示例。

③ 在查询时,-Merge 聚合函数组合器可用于将部分聚合状态组合成最终结果聚合值。

SELECT
    town,
    avgMerge(avg_price_state) AS avg_price
FROM <table with AggregatingMergeTree engine>
GROUP BY town;

Elasticsearch

与 ClickHouse 相比,Elasticsearch 采用了一种完全不同的并行化方法来处理其 术语聚合(用于在 Elasticsearch 中计算计数)。这导致硬件利用率低于 ClickHouse: Elasticsearch_blog2_07.png 术语聚合 始终需要一个大小参数(在下面称为 n),Elasticsearch 使用 一个 CPU 线程处理每个 分片,而与 CPU 内核数量无关。每个线程根据每个分组的最大 count 值(默认)计算其处理的分片的本地 top n 结果。分片本地结果最终合并成一个全局 top n 最终结果。

Elasticsearch 的 术语聚合 多节点并行化工作方式类似。当分片分布在多个节点上时,每个节点(使用上面解释的技术)生成一个节点本地 top n 结果,这些节点本地结果由协调节点(最初接收聚合查询的节点)合并成最终的全局结果。

根据数据在分片上的分布方式,这种并行化方法可能存在精度问题。我们用一个例子来说明这一点: Elasticsearch_blog2_08.png 我们假设,根据 英国房产价格数据集,我们想要计算销售房产数量最多的前两个城镇。数据分布在两个分片上。上图抽象地显示了①每个城镇的分片本地房产销售记录计数。每个处理线程②返回一个分片本地 top 2 结果,最终③合并成最终的全局 top 2 结果。然而,此结果是不正确的。根据分片全局数据,以下是每个城镇的正确记录计数。

  • Town 1: 11
  • Town 2: 8
  • Town 3: 9
  • Town 4: 9

Elasticsearch 计算的③结果的 Town 2 的计数值和排名都是错误的。可以通过分析返回的 计数错误 并调整 分片大小 参数来提高精度——这命令每个处理线程从每个分片返回比查询请求更大的 top n 结果,从而增加内存需求和运行时间。

Elasticsearch 还利用基于 JVM 自动矢量化和 Java Panama Vector API 的 SIMD 硬件单元 基于。此外, Elasticsearch 8.12 可以由查询处理线程并行搜索,例外术语聚合。因为将上述技术应用于段而不是分片会遇到同样的精度问题。由于一个分片包含多个段,这也会在增加 分片大小参数 的情况下使每个处理线程的工作量倍增。

精度

Elasticsearch

上所述,当查询的数据拆分为多个 分片 时,Elasticsearch 的 术语聚合 中的计数值 默认情况下是近似的。可以通过分析返回的 计数错误 并调整 分片大小 参数来提高结果的准确性,但这会增加运行时间和内存需求。

ClickHouse

ClickHouse 中的 count(*) 聚合函数计算完全准确的结果。

ClickHouse count(*) 聚合易于使用,不需要像 Elasticsearch 中那样的额外配置。

(无)限制子句

Elasticsearch

由于其 执行模型,Elasticsearch 中无法进行没有限制子句的计数聚合——用户必须始终指定 大小 设置。即使使用较大的大小值,对高基数数据集的桶聚合也受 max_buckets 设置的限制,或者需要使用 昂贵复合聚合 来分页浏览结果。

ClickHouse

ClickHouse count(*) 聚合不受大小限制。如果查询内存消耗超过用户指定的(可选)最大内存 阈值,它们还可以 支持 将临时结果溢出到磁盘。此外,无论数据集大小如何,如果聚合中的分组列构成主键的前缀,ClickHouse 可以 以最小的内存需求运行聚合。

同样,与 Elasticsearch 相比,ClickHouse count(*) 聚合的复杂度更低。

持续数据汇总方法

无论数据库中的聚合和查询处理效率如何,聚合 数十亿数万亿 行(在 现代数据分析 使用案例中很常见)从本质上来说总是成本高昂的,因为必须处理海量数据。

因此,专门用于分析工作负载的数据库通常会提供数据汇总作为构建块,供用户自动将传入数据转换为汇总数据集,以 预聚合 且通常 显著较小 的格式表示原始数据。查询将利用预计算的数据在交互式用例中提供亚秒级的延迟,例如上面提到的 ClickPy 应用程序

Elasticsearch 和 ClickHouse 都提供了用于自动持续数据汇总的内置技术。它们的技术具有相同的功能,但在实现、效率以及由此产生的计算成本方面存在巨大差异。

Elasticsearch

Elasticsearch 提供了一种名为 Transforms 的机制,用于将现有索引批量转换为汇总索引或持续转换摄取的数据。

基于 Elasticsearch 的 磁盘格式,我们在此 详细描述 Transforms 的工作原理。

我们注意到 Elasticsearch 方法的三个缺点,即

  • 需要保留旧的原始数据:否则,Transforms 无法正确重新计算聚合结果。

  • 可扩展性差且计算成本高:每当在 检查点之后检测到桶的新原始数据文档时,都会从不断增长的原始数据源索引中查询所有桶数据并重新聚合。这无法扩展到十亿甚至万亿级文档集,并导致高计算成本。

  • 不是实时:Transforms 预聚合目标索引仅在下一个 检查间隔后才会与原始数据源索引保持同步。

专用于时间序列度量数据,Elasticsearch 还提供了一种 降采样 技术,通过以降低的粒度存储数据来减少数据占用空间。降采样是 Rollups 的继任者,相当于一个 Transform,它根据转换为固定时间间隔(例如小时、天、月或年)的时间戳对度量数据文档进行分组,然后应用一组固定的聚合(minmaxsumvalue_countaverage)。与 ClickHouse 链式物化视图 的比较可能是未来博客的主题。

ClickHouse

ClickHouse 使用 物化视图 结合 AggregatingMergeTree 表引擎和 部分聚合状态 来实现自动且(与 Elasticsearch 相比)增量数据转换。

基于 ClickHouse 的 磁盘格式,我们在此 详细解释 增量物化视图的机制。

与 Elasticsearch Transforms 相比,ClickHouse 物化视图具有三个主要优势

  • 无需依赖原始数据:永远不会查询源表中的原始数据,即使用户想要执行精确的聚合计算也是如此。这允许对源表和预聚合目标表应用不同的 TTL 设置。此外,在仅应执行相同聚合查询的场景中,用户可以选择在预聚合后完全放弃原始数据(使用 Null 表引擎)。

  • 高可扩展性和低计算成本:增量聚合专为原始数据源表包含数十亿或万亿行数据的场景而设计。当存在该组的新原始数据行时,ClickHouse 不会重复查询不断增长的源表并重新计算属于同一组的所有现有行的聚合值,而只是从(仅)新插入的原始数据行的值计算 部分聚合状态。此状态会与之前计算的状态在后台 合并。换句话说,每个原始数据值都与其他原始数据值精确聚合一次。与通过蛮力对原始数据进行聚合相比,这大大降低了计算成本。

  • 实时:当成功确认对原始数据源表的插入操作时,保证预聚合目标表是最新的。

回填预聚合

在我们 配套的基准测试博文 中,我们使用 Elasticsearch 中的持续 Transforms 和 ClickHouse 中等效的物化视图来动态地将摄取的数据预聚合到单独的数据集中。有时,这不可行。例如,当很大一部分数据已被摄取时,重新摄取是不可能的或成本过高,并且引入了可以从在预聚合格式下运行这些数据中受益的查询。

Elasticsearch

我们通过在 Elasticsearch 中运行一个 batch Transform 来模拟这种情况,该 Transform 处理已经摄取的 100 亿行数据集,将计数预计算到一个单独的数据集中,以加快我们用作基准测试一部分的聚合查询的速度。continuousbatch Transforms 都使用上面描述的相同的基于检查点的机制。由于这种机制的缺点,其中相同的数值在检查点被重复查询和聚合

通过批量 Transform 进行回填花费了 5 天时间(使用了大量的计算成本)。在查询能够从预聚合数据中受益之前,需要等待很长时间。

ClickHouse

在 ClickHouse 中,回填预聚合的工作原理是使用 INSERT INTO SELECT 语句直接插入物化视图的目标表 使用 视图的 SELECT 查询(转换)

对于 100 亿行数据集,这需要 20 秒 而不是 5 天。.

这 20 秒包括聚合整个 100 亿行数据集并将结果(作为部分聚合状态)写入目标表,目标表随后将用于物化视图处理其他传入数据。根据原始数据集的基数,这可能是一种内存密集型方法,因为整个原始数据集是临时聚合的。或者,用户可以使用 变体,该变体需要最少的内存。

请注意,手动聚合原始数据集并将结果直接插入目标表的“20 秒 ClickHouse 方法”在 Elasticsearch 中不可行。理论上,这可以通过 Reindex(索引到索引的复制)操作来实现。但是,这需要在原始数据集中保留 _source 数据(需要大幅增加存储空间)。它还需要一种机制来手动创建必要的检查点,以便在流数据开始时正确继续预聚合过程。在 ClickHouse 中,在我们按上述方式运行回填后,物化视图将继续对新的传入数据进行增量聚合过程。

ClickHouse 中的高性能聚合

大多数数据库,包括 ClickHouse,都使用哈希聚合算法的某种变体来实现 GROUP BY,其中输入行的聚合值存储在 哈希表 中,并使用分组列作为键进行更新。选择合适的哈希表类型对于性能至关重要。在底层,ClickHouse 利用一个复杂的 哈希表框架 来实现聚合。根据分组列的数据类型、估计的基数和其他因素,为每个聚合查询从 30 多个(截至 2024 年 4 月)不同的实现中单独选择最快的哈希表。ClickHouse 的设计初衷是为了在海量数据上实现高性能聚合。

ClickHouse 目前是市场上 速度最快 的数据库之一,具有用于数据分析的独特功能

总结

在这篇博文中,我们深入探讨了 ClickHouse 为什么在处理计数聚合方面比 Elasticsearch 更快、更高效 的技术答案,这些聚合在数据分析和日志记录/可观察性用例中很常见。

我们解释了并行化方法以及 ClickHouse 和 Elasticsearch 中计数聚合的结果质量和可用性复杂性的差异。我们探讨了 Elasticsearch 和 ClickHouse 的内置机制来预计算计数。我们重点介绍了为什么 ClickHouse 物化视图比 Elasticsearch Transforms 更高效,更适合处理数十亿/万亿级行数据集。

我们建议阅读我们的配套博文 ClickHouse 与 Elasticsearch:10 亿行数据对比,以了解 ClickHouse 高性能聚合的实际应用。作为预告,我们在此提供了一些基准测试结果

Elasticsearch_blog2_09.png

Elasticsearch_blog2_10.png

Elasticsearch_blog2_11.png

分享此文章

订阅我们的时事通讯

随时了解功能发布、产品路线图、支持和云服务!
加载表单...
关注我们
Twitter imageSlack imageGitHub image
Telegram imageMeetup imageRss image