目录
介绍
ClickHouse 的设计目标是快速,不仅查询快,插入也快。ClickHouse 表旨在接收每秒数百万行的插入,并存储大量(数百 PB)的数据。传统上,非常高的摄取吞吐量需要适当的客户端数据批量处理。
在本文中,我们将描述高吞吐量数据摄取的另一种方式背后的动机和机制:ClickHouse 异步数据插入将数据批量处理从客户端转移到服务器端,并支持客户端批量处理不可行的用例。我们将深入了解异步插入的内部机制,并使用模拟实际场景的示例应用程序来演示、基准测试和调整具有不同设置的传统同步和异步插入。
同步数据插入入门
使用传统的 插入到 MergeTree 引擎族表的语句,数据快速地以新的数据part的形式同步写入数据库存储,与接收插入查询同步。下图说明了这一点: 当 ClickHouse ① 接收到插入查询时,查询的数据 ② 会立即(同步地)以(至少)一个新数据部分的形式写入数据库存储(每个分区键),之后,③ ClickHouse 确认插入查询成功执行
并行地(且以任何顺序),ClickHouse 可以接收和执行其他插入查询(参见上图中的 ④ 和 ⑤)。
数据需要批量处理以获得最佳性能
在后台,为了逐步优化读取数据,ClickHouse 持续地合并数据部分到更大的部分。合并后的部分被标记为非活动,并在可配置的分钟数后最终删除。创建和合并数据部分需要集群资源。文件为每个部分创建和处理,并且在宽格式中,每个表列都存储在单独的文件中。在复制设置中,ClickHouse Keeper 条目为每个数据部分创建。此外,数据在写入新部分时会被排序和压缩。当合并部分时,数据需要解压缩和合并排序。此外,表引擎特定的优化在合并后的数据被压缩并再次写入存储之前应用。
用户应避免创建过多的小插入和过多的小初始部分。因为这会产生 (1) 文件创建的开销,(2) 增加写放大(导致更高的 CPU 和 I/O 使用率),以及 (3) ClickHouse Keeper 请求的开销。由于高 CPU 和 I/O 使用率开销,这会导致频繁的小插入情况下摄取性能下降。从而为其他操作(如查询)留下更少的资源。
ClickHouse 实际上有一个内置的安全措施来保护自身免受花费过多资源用于创建和合并部分的影响:当表 T
的单个分区中存在超过 300 个活动部分时,它将向表 T
的插入查询返回 Too many parts
错误。为了防止这种情况发生,我们建议发送更少但更大的插入,而不是通过在客户端缓冲数据并将数据作为批次插入来发送许多小插入。理想情况下,至少1000 行或更多。默认情况下默认,单个新部分最多可以包含约 100 万行。并且,如果单个插入查询包含超过 100 万行,ClickHouse 将为查询的数据创建多个新部分。
通常,ClickHouse 能够通过传统的同步插入提供非常高的摄取吞吐量。用户特别是因为这种能力而选择 ClickHouse。Uber 正在使用 ClickHouse 来摄取每秒数百万个日志,Cloudflare 在 ClickHouse 中存储每秒 1100 万行,而 Zomato 每天最多摄取50 TB 的日志数据。
使用 ClickHouse 就像驾驶高性能的一级方程式赛车 🏎。有大量的原始马力可用,您可以达到最高速度。但是,为了获得最佳性能,您需要在正确的时间升到足够高的档位并分别适当批量处理数据。
有时客户端批量处理不可行
在某些情况下,客户端批量处理不可行。想象一下,有 100 个或 1000 个单用途代理发送日志、指标、追踪等的可观测性用例,其中实时传输该数据是快速检测问题和异常的关键。此外,观察到的系统中存在事件激增的风险,这可能会在尝试在客户端缓冲可观测性数据时导致大量的内存激增和相关问题。
示例应用程序和基准测试设置
为了演示和基准测试客户端批量处理不可行的场景,我们实现了一个名为 UpClick
的简单示例应用程序,用于监控 clickhouse.com(或任何其他网站)的全局延迟。下图概述了 UpClick 的架构: 我们正在使用一个简单的无服务器 Google 云函数,它每
n
秒(n
是可配置的)被调度和执行一次,然后执行以下操作: ① Ping clickhouse.com(URL 是可配置的,并且该函数可以轻松地适应以支持 URL 数组)
② 通过 HTTP 接口 将来自 ① 的结果与云函数的地理位置一起摄取到 ClickHouse Cloud 服务中的目标表中
我们正在使用 ClickHouse Cloud 服务,服务规模为 24 GiB 主内存和 6 个 CPU 核心进行基准测试。此服务由 3 个计算节点组成,每个节点具有 8 GiB 主内存和 2 个 CPU 核心。
此外,我们 ③ 实施了一个实时 Grafana 仪表板,该仪表板每 n
秒更新一次,并在地理地图上显示欧洲、北美和亚洲所有位置最近 m
秒的平均延迟,云函数部署在这些位置。以下屏幕截图显示了欧洲的延迟: 在上面的仪表板屏幕截图中,我们可以通过颜色编码看到,访问 clickhouse.com 的延迟低于荷兰和比利时的(可配置)KPI,而高于伦敦和赫尔辛基。
我们将使用 UpClick 应用程序来比较具有不同设置的同步和异步插入。
为了匹配实际的可观测性场景,我们使用负载生成器,它可以创建和驱动任意数量的模拟云函数实例。通过它,我们为基准测试运行创建和调度了 n
个云函数实例。每个实例每 m
秒执行一次。
在每次基准测试运行之后,我们使用了三个 SQL 查询来检查 ClickHouse 系统表,以查看(和可视化)云函数目标表中以下指标随时间的变化
请注意,其中一些查询必须通过使用 clusterAllReplicas 表函数在具有特定名称的集群上执行。
同步插入基准测试
我们使用以下参数运行基准测试: • 200 个 UpClick 云函数实例
• 每个云函数每 10 秒调度/执行一次
实际上,每 10 秒向 ClickHouse 发送 200 个插入,导致 ClickHouse 每 10 秒创建 200 个新部分。对于 500 个云函数实例,ClickHouse 将每 10 秒创建 500 个新部分。依此类推。这里可能会出现什么问题?
以下三个图表可视化了基准测试运行期间云函数目标表中的活动部分数量、所有部分数量(活动和非活动)以及 ClickHouse 集群的 CPU 利用率:
在基准测试开始 5 分钟后,活动部分的数量达到了上面提到的 阈值,即 Too many parts
错误,我们中止了基准测试。由于每秒创建约 200 个新部分,ClickHouse 无法足够快地合并目标表的部分,以使其保持在 300 个活动部分的阈值以下,从而保护自身免于陷入无法管理的境地。当引发 Too many parts
错误时,云函数表总共存在近 3 万个部分(活动和非活动)。请记住,合并的部分被标记为非活动,并在几分钟后删除。如上所述,创建和合并(过多)部分是资源密集型的。我们试图以最高速度驾驶我们的一级方程式赛车 🏎,但档位太低,因为发送非常小的插入过于频繁。
请注意,客户端批量处理对于我们的云函数来说不是可行的设计模式。我们可以使用聚合器或网关架构来批量处理数据。但是,这将使我们的架构复杂化,并需要额外的第三方组件。幸运的是,ClickHouse 已经为我们的问题提供了完美的内置解决方案。
异步插入。
异步插入
描述
使用传统的插入查询,数据是同步插入到表中的:当 ClickHouse 接收到查询时,数据会立即写入数据库存储。
使用异步插入,数据首先插入到缓冲区,然后稍后或异步写入数据库存储。下图说明了这一点: 启用异步插入后,当 ClickHouse ① 接收到插入查询时,查询的数据 ② 会立即首先写入内存缓冲区。异步于 ①,只有当 ③ 下一次缓冲区刷新发生时,缓冲区的数据才会被排序并作为一部分写入数据库存储。请注意,在刷新到数据库存储之前,数据不可用于查询搜索;缓冲区刷新是可配置的,我们稍后会展示一些示例。
在缓冲区被刷新之前,来自同一客户端或其他客户端的其他异步插入查询的数据可以收集在缓冲区中。从缓冲区刷新创建的部分可能包含来自多个异步插入查询的数据。通常,这些机制将数据批量处理从客户端转移到服务器端(ClickHouse 实例)。非常适合我们的 UpClick 用例。
可能存在多个部分
异步插入缓冲区中缓冲的表行可能包含几个不同的分区键值,因此,在缓冲区刷新期间,ClickHouse 将创建(至少)每个缓冲区中包含的不同分区键值一个新部分。此外,对于没有分区键的表,取决于缓冲区中收集的行数,缓冲区刷新可能会导致多个部分。
可能存在多个缓冲区
每个插入查询形状
(插入查询的语法,不包括 values 子句/数据)和设置将有一个缓冲区。并且在多节点集群(如 ClickHouse Cloud)上,缓冲区将按节点存在。下图说明了这一点: 查询 ①、② 和 ③(以及 ④)具有相同的目标表 T,但语法
形状
不同。查询 ③ 和 ④ 具有相同的形状,但设置不同。查询 ⑤ 具有不同的形状,因为它以表 T2 为目标。因此,所有 5 个查询都将有一个单独的异步插入缓冲区。当查询通过分布式表(自托管集群)或负载均衡器(ClickHouse Cloud)以多节点集群为目标时,缓冲区将按节点存在。
每个设置缓冲区允许同一表的数据具有不同的刷新时间。在我们的 UpClick 示例应用程序中,可能存在应近乎实时监控的重要站点(具有较低的 async_insert_busy_timeout_ms 设置)以及不太重要的站点,这些站点的数据可以以更高的时间粒度刷新(具有较高的 async_insert_busy_timeout_ms 设置),从而减少此数据使用的资源。
插入是幂等的
对于 MergeTree 引擎族表,ClickHouse 将默认自动去重异步插入。这使得异步插入成为幂等的,因此在以下情况下具有容错性
-
如果包含缓冲区的节点由于某种原因在缓冲区刷新之前崩溃,则插入查询将超时(或获得更具体的错误)并且不会获得确认。
-
如果数据已刷新,但由于网络中断而无法将确认返回给查询的发送者,则发送者将收到超时或网络错误。
从客户端的角度来看,1. 和 2. 可能很难区分。但是,在这两种情况下,未确认的插入都可以立即重试。只要重试的插入查询包含相同顺序的相同数据,如果(未确认的)原始插入成功,ClickHouse 将自动忽略重试的异步插入。
缓冲区刷新期间可能发生插入错误
当缓冲区刷新时,可能会发生插入错误:异步插入也可能发生 Too many parts
错误。例如,使用选择不当的分区键。或者,缓冲区刷新发生的集群节点在缓冲区刷新时存在一些操作问题。此外,来自异步插入查询的数据仅在缓冲区刷新时才针对目标表的模式进行解析和验证。如果由于解析或类型错误而无法插入来自插入查询的某些行值,则不会刷新该查询的任何数据(来自其他查询的数据刷新不受此影响)。ClickHouse 会将缓冲区刷新期间插入错误的详细错误消息写入日志文件和系统表。我们稍后将讨论客户端如何处理此类错误。
异步插入 vs. 缓冲表
通过 Buffer 表引擎,ClickHouse 提供了一种类似于异步插入的数据插入机制。缓冲表在主内存中缓冲接收到的数据,并定期将其刷新到目标表。但是,缓冲表和异步插入之间存在主要差异
-
缓冲表需要显式创建(在多节点集群中的每个节点上)并连接到目标表。异步插入可以通过简单的 设置 更改来打开和关闭。
-
插入查询需要显式地以缓冲表为目标,而不是
真正的
目标表。异步插入并非如此。 -
每当目标表的 DDL 更改时,缓冲表都需要 DDL 更改。异步插入不需要这样做。
-
如上文所述,即使所有插入查询都以同一表为目标,异步插入也为每个插入查询
形状
和设置提供缓冲区。为同一表中的不同数据启用细粒度的数据刷新策略。缓冲表没有这样的机制。
通常,与缓冲表相比,从客户端的角度来看,异步插入的缓冲机制是完全透明的,并且完全由 ClickHouse 管理。异步插入可以被视为缓冲表的后继者。
支持的接口和客户端
异步插入同时受 HTTP 和 原生接口的支持,流行的客户端(如 Go 客户端)要么对异步插入有直接支持,要么在查询设置或用户设置或连接设置级别启用时间接支持它们。
配置返回行为
您可以选择异步插入查询何时返回给查询的发送者,以及何时发生插入确认。可通过 wait_for_async_insert 设置进行配置
-
默认返回行为是,插入查询仅在下一次缓冲区刷新发生且插入的数据驻留在磁盘上之后才返回给发送者。
-
或者,通过将设置设置为
0
,插入查询会在数据刚插入到缓冲区后立即返回。我们在下面将其称为即发即弃
模式。
两种模式都具有非常显着的优点和缺点。因此,我们将在以下两个部分中更详细地讨论这两种模式。
默认返回行为
描述
此图概述了异步插入的默认返回行为(wait_for_async_insert = 1
): 当 ClickHouse ① 接收到插入查询时,查询的数据 ② 会立即首先写入内存缓冲区。当 ③ 下一次缓冲区刷新发生时,缓冲区的数据会被排序并作为一个或多个数据部分写入数据库存储。在缓冲区刷新之前,来自其他插入查询的数据可以收集在缓冲区中。④ 只有在下一次定期缓冲区刷新发生后,来自 ① 的插入查询才会返回给发送者,并确认插入。或者换句话说,发送插入查询的客户端调用被阻塞,直到下一次缓冲区刷新发生。因此,上图中草绘的 3 个插入不能来自同一个单线程插入循环,而是来自不同的多线程并行插入循环或不同的并行客户端/程序。
优点
此模式的优点是持久性保证 + 易于识别失败的批次
-
持久性保证:当客户端获得插入的确认时,数据保证写入数据库存储(并且可用于查询搜索)。
-
返回插入错误:当发生缓冲区刷新期间的插入错误时,查询的发送者会收到详细的错误消息,而不是确认。因为 ClickHouse 会等待返回插入的确认,直到缓冲区被刷新。
-
易于识别失败的数据集:因为如上所述,插入错误会及时返回,所以很容易识别无法插入的数据集。
缺点
自 ClickHouse 24.2 版本以来,此缺点已通过 自适应异步插入 解决。
一个缺点是,在单个客户端用于通过单线程插入循环摄取数据的场景中,此模式可能会产生反压
1. 获取下一批数据
2. 将包含数据的插入查询发送到 ClickHouse
现在调用被阻塞,直到下一次缓冲区刷新发生
3. 接收插入成功的确认
4. 转到 1
在这种情况下,可以通过在客户端适当批量处理数据并使用多线程并行插入循环来提高摄取吞吐量。
基准测试
我们运行两个基准测试。
基准测试 1
• 200 个 UpClick 云函数实例
• 每个云函数每 10 秒调度/执行一次
• 1 秒缓冲区刷新时间
基准测试 2
• 500 个 UpClick 云函数实例
• 每个云函数每 10 秒调度/执行一次
• 1 秒缓冲区刷新时间
我们对两个基准测试运行都使用以下异步插入设置
① async_insert = 1
② wait_for_async_insert = 1
③ async_insert_busy_timeout_ms = 1000
④ async_insert_max_data_size = 1_000_000
⑤ async_insert_max_query_number = 450
① 启用异步插入。通过 ②,我们为异步插入设置了上述默认返回行为。我们配置了缓冲区应在以下情况下刷新:③ 每秒一次,或 ④ 数据达到 1 MB,或 ⑤ 收集了 450 个插入查询的数据。任何先发生的情况都会触发下一次缓冲区刷新。②、③、④、⑤ 是 ClickHouse 中的默认值(③ 在 OSS 中的默认值为 200
,在 ClickHouse Cloud 中为 1000
)。
以下三张图表可视化了在云函数的目标表中,活跃 part 的数量、所有 part(活跃和非活跃)的数量,以及在两次基准测试运行的第一个小时内 ClickHouse 集群的 CPU 利用率:
您可以看到,活跃 part 的数量稳定在 8 以下,与我们运行的云函数实例数量无关。并且所有 part(活跃和非活跃)的数量稳定在 1300 以下,与我们运行的云函数实例数量无关。
这就是为 UpClick 云函数使用异步插入的优势。无论我们运行多少云函数实例 - 200 或 500,甚至 1000 或更多 - ClickHouse 都会批量处理从云函数接收的数据,并每秒创建一个新的 part。因为我们将 async_insert_busy_timeout_ms
设置为 1000
。我们分别以足够高的档位全速驾驶我们的 F1 赛车 🏎。这最大限度地减少了数据摄取所使用的 I/O 和 CPU 周期。正如您所看到的,两次基准测试运行的 CPU 利用率都远低于我们在本文 前面进行的传统同步插入的基准测试运行。拥有 500 个并行客户端的基准测试运行的 CPU 利用率高于拥有 200 个并行客户端的基准测试运行。对于 500 个客户端,当每秒刷新一次时,缓冲区包含更多数据。当在缓冲区刷新期间创建新的 part 以及当较大的 part 合并时,ClickHouse 需要花费更多的 CPU 周期来排序和压缩这些数据。
请注意,async_insert_max_data_size
或 async_insert_max_query_number
可能会在不到一秒的时间内触发缓冲区刷新,尤其是在有大量云函数或客户端的情况下。您可以将这两个设置设置为人为的高值,以确保只有时间设置触发缓冲区刷新,但这可能会牺牲更高的主内存使用量,因为需要临时缓冲更多数据。
即发即弃的返回行为
描述
此图表说明了异步插入的可选 fire-and-forget
返回行为 (wait_for_async_insert = 0
): 当 ClickHouse ① 接收到插入查询时,查询的数据 ② 首先立即写入内存缓冲区。之后,③ 插入查询向发送方返回插入确认。④ 当下一次定期缓冲区刷新发生时,缓冲区的数据将被 排序 并作为 一个或多个 数据 part 写入数据库存储。在缓冲区刷新之前,可以从其他插入查询中收集数据到缓冲区中。
优势
此模式的一个优势是,具有单线程插入循环的客户端可以实现非常高的摄取吞吐量(以及最小的集群资源利用率)
1. 获取下一批数据
2. 将包含数据的插入查询发送到 ClickHouse
3. 立即收到插入已缓冲的确认
4. 转到 1
缺点
然而,此模式也存在缺点
-
无持久性保证:即使客户端收到了插入查询的确认,这也不一定意味着查询的数据已经或将要写入数据库存储。插入错误可能在稍后的缓冲区刷新期间 发生。并且当 ClickHouse 节点崩溃或在下次定期刷新内存缓冲区之前关闭时,可能会发生数据丢失。更糟糕的是,这可能是静默数据丢失,因为客户端很难发现此类事件,因为原始插入到缓冲区中的操作已成功确认。为了优雅地关闭 ClickHouse 节点,有一个 SYSTEM 命令 可以刷新所有异步插入缓冲区。此外,服务器端 设置 确定是否在优雅关闭时自动刷新异步插入缓冲区。
-
不返回插入错误:当在缓冲区刷新期间 发生 插入错误时,原始插入到缓冲区中的操作仍然成功地向客户端确认。客户端只能通过事后检查日志文件和 系统表 来发现这些插入错误。在第二篇文章中,我们将为此提供指导。
-
识别失败的数据集很复杂:在上述静默插入错误的情况下,识别此类失败的数据集非常棘手和复杂。ClickHouse 目前不会在任何地方记录这些失败的数据集。在
fire and forget
模式下,针对失败的异步插入的死信队列 可能 有助于事后识别无法插入的数据集。
通常,顾名思义,fire-and-forget
模式下的异步插入只应在可以接受数据丢失的场景中使用。
基准测试
我们运行两个基准测试。
基准测试 1
• 500 个 UpClick 云函数实例
• 每个云函数每 10 秒调度/执行一次
• 5 秒缓冲区刷新时间
基准测试 2
• 500 个 UpClick 云函数实例
• 每个云函数每 10 秒调度/执行一次
• 30 秒缓冲区刷新时间
我们对两个基准测试运行都使用以下异步插入设置
① async_insert = 1
② wait_for_async_insert = 0
③ async_insert_busy_timeout_ms = 5000 (基准测试 1)
async_insert_busy_timeout_ms = 30_000 (基准测试 2)
④ async_insert_max_data_size = 100_000_000
⑤ async_insert_max_query_number = 450_000
① 启用异步插入。通过 ②,我们为异步插入启用了上述 fire and forget
返回行为。对于基准测试 1,缓冲区应每 5 秒刷新一次,对于基准测试 2,缓冲区应每 30 秒刷新一次。请记住,只有在缓冲区刷新到数据库存储上的 part 之后,数据才能用于查询搜索。通过 ④ 和 ⑤,我们将另外两个缓冲区刷新阈值设置为人为的高值,以确保只有时间设置 ③ 触发缓冲区刷新。
以下三张图表可视化了在云函数的目标表中,活跃 part 的数量以及所有 part(活跃和非活跃)的数量,以及在两次基准测试运行的第一个小时内 ClickHouse 集群的 CPU 利用率:
对于两次基准测试运行,活跃 part 的数量都稳定在 7 以下。当缓冲区刷新频率降低 6 倍时,所有 part(活跃和非活跃)的数量都降低了约 6 倍。与我们在本文 前面进行的默认异步插入返回行为的基准测试运行中每秒刷新一次缓冲区相比,每 5 秒或 30 秒刷新一次缓冲区时,CPU 利用率更低。但是,这里两次基准测试运行的 CPU 利用率非常相似。当缓冲区仅每 30 秒刷新一次时,ClickHouse 创建的 part 数量更少但更大,从而导致对数据进行排序和压缩的 CPU 需求增加。
总结
在这篇博文中,我们探讨了 ClickHouse 异步数据插入的机制。我们讨论了传统的同步插入在高摄取吞吐量场景中需要适当的客户端数据批处理。相比之下,使用异步插入,part 创建的频率会自动受到控制,方法是将数据批处理从客户端转移到服务器端,这支持客户端批处理不可行的场景。我们使用了一个示例应用程序来演示、基准测试和调整具有不同设置的传统同步和异步插入。我们希望您学会了加速 🏎 ClickHouse 用例的新方法。
在即将发布的文章中,我们将指导如何监控和调试异步插入的执行步骤。
敬请关注!