ClickHouse中的异步数据插入
ClickHouse 不仅被设计成快速查询,还适用于快速插入。ClickHouse 的表旨在每秒接收数百万行的插入,并存储大量数据(数百 PB)。高吞吐量的数据插入通常需要适当的客户端进行数据批处理。 在本文中,我们将描述高吞吐量下的另一种数据写入方式的机制:ClickHouse 异步数据插入将数据批处理的方式从客户端转移到服务器端,并支持客户端不适合批处理的场景。我们将深入了解异步插入,并使用模拟现实场景的示例应用来演示、基准测试和传统同步、异步插入方式的参数调优。 对于MergeTree引擎系列中的传统插入,对于接收到插入的查询后,数据会以new data part的形式立即(同步地)写入到数据库存储中。下图说明了这一过程: 当 ClickHouse ① 接收到插入查询时,查询的数据 ② 会立即(同步地)以(至少)一个new data part(每个分区键)的形式写入到数据库存储中,之后, ③ ClickHouse 会确认插入查询的成功执行。 同时(以及以任何顺序),ClickHouse 还可以接收和执行其他插入查询(请参见图表中的 ④ 和 ⑤)。 在后台,为了逐步优化用于读取的数据,ClickHouse 不断地将data parts合并成较大的parts。合并过的parts会被标记为非活动状态,并最终在设定时长后(由old_parts_lifetime参数控制)被删除。创建和合并part需要消耗集群资源。为每个part创建和处理的文件,在宽格式中,每个表列都存储在单独的文件中。在复制模式中,会为每个part创建 ClickHouse Keeper 条目。此外,当写入新part时,数据会被排序和压缩。当parts合并时,需要对数据进行解压缩和合并排序。此外,在将合并的数据再次压缩并写入存储之前,还会应用特定于表引擎的优化。 用户应避免创建过多的小插入和过多小的parts。因为这会产生(1)在创建文件时的开销,(2)增加的写放大(导致更高的 CPU 和 I/O 使用率),以及(3)ClickHouse Keeper 请求的开销。这是因为在频繁小插入的情况下,由于高 CPU 和 I/O 使用率的开销,导致写入性能下降。留下较少的资源可用于查询和其他操作。 实际上,ClickHouse 实际上有内置保护机制(由参数parts_to_throw_insert控制),可以防止它花费太多资源来创建和合并parts:对于表 T 的插入查询,在 T 的单个分区上存在超过300个活动parts时,它将返回“Too many parts”的错误。为了防止发生这种情况,我们建议通过在客户端缓冲数据并将数据批量插入来发送少量但较大数据的插入,而不是许多小数据量的插入。理想情况下,每次插入至少有 1000 行或更多行。默认情况下,单个新part最多可以包含约 100 万行。如果单个插入查询包含超过 100 万行,ClickHouse 将为查询的数据创建多个new parts。 总体上,ClickHouse 能够通过传统的同步插入提供非常高的数据写入吞吐量。这也是用户特别选择 ClickHouse的原因之一。Uber 使用 ClickHouse 来每秒写入数百万条日志,Cloudflare 在 ClickHouse 中每秒存储 1100 万行,Zomato 每天可以写入多达 50 TB 的日志数据。 使用 ClickHouse 就像驾驶一辆高性能的一级方程式赛车 🏎。有大量的原始马力可用,您可以达到最高速度。但是,为了实现最佳性能,您需要在适当的时候换到足够高的档位,并相应地进行数据批处理。 有一些情况下,客户端批处理是不可行的。想象一下,有 100 多个或 1000 多个专用代理发送日志、指标、跟踪等的可观性用例,其中实时传输这些数据对于尽快检测到问题和异常至关重要。此外,在观察系统中可能会出现事件峰值,这可能导致在尝试在客户端缓冲可观性数据时出现大内存峰值和相关问题。 为了演示和基准测试一个不适合客户端批处理的场景,我们实现了一个名为 UpClick 的简单示例应用程序,用于监视 clickhouse.com(或任何其他网站)的全球延迟。以下图示描述了 UpClick 的架构:
我们在基准测试中使用了一个具有6核24GiB的 ClickHouse Cloud 服务。该服务由 3 个2核8GiB的计算节点组成。 此外,我们 ③ 实现了一个实时的 Grafana 仪表板,每隔 n 秒更新一次,并在地理地图上显示了在欧洲、北美和亚洲的所有位置的过去 m 秒内的平均延迟,每个地理位置都部署了云函数。以下屏幕截图显示了欧洲的延迟情况: 在上面的仪表板截图中,通过颜色编码,我们可以看到访问 clickhouse.com 的延迟在荷兰和比利时低于(可配置的)指标,而在伦敦和赫尔辛基则高于此指标。 我们将使用 UpClick 应用程序来比较具有不同设置的同步和异步插入。 为了匹配实际的可观测性场景,我们使用一个负载生成器,可以创建和驱动任意数量的模拟云函数实例。通过这种方式,我们为基准测试运行创建和调度了 n 个云函数实例。每个实例每 m 秒执行一次。 在每次基准测试运行后,我们使用三个查询系统表的SQL进行监控(和可视化),以了解以下内容随时间变化情况:
请注意,其中一些查询必须通过利用 clusterAllReplicas 表函数在具有特定名称的群集上执行。
实际上,每隔 10 秒就会向 ClickHouse 发送 200 次插入操作,导致 ClickHouse 每隔 10 秒创建 200 个新的数据分区。如果有 500 个云函数实例,ClickHouse 将每隔 10 秒创建 500 个新的数据分区,依此类推。这里可能会出现什么问题呢? 以下三个图表代表了基准测试运行期间目标表中活动parts的数量、所有parts(活动和非活动)的数量,以及 ClickHouse 集群的 CPU 利用率: 在基准测试开始后的 5 分钟内,活动parts的数量达到了上面提到的 Too many parts 错误的阈值,我们终止了基准测试。以每秒创建 200 个新的parts速度,ClickHouse 无法足够快地合并目标表的parts,以保持在 300 个活跃parts的阈值以下,防止自身陷入无法管理的局面。在 Too many parts 错误被触发时,云函数的表总共存在着近 30,000 个parts(活动和非活动)。请记住,合并过的parts会被标记为非活动,并在几分钟后删除。如上所述,创建和合并(过多的)parts需要消耗资源。我们试图让我们的方程式一号车 🏎 以最高速度行驶,但却选择了一个过低的档位,即通过频繁地发送非常小的插入操作。 请注意,对于我们的云函数来说,客户端批处理是不可行的设计模式。我们本可以使用聚合器(aggregator)或网关架构来批处理数据。然而,那会复杂化我们的架构并需要额外的第三方组件。幸运的是,ClickHouse 已经提供了我们问题的完美内置解决方案。 异步插入。 描述 在传统的插入查询中,数据是同步插入到表中的:当ClickHouse 收到插入的query时,数据会立即写入到数据库存储中。 而在异步插入中,数据首先插入到缓冲区,然后稍后或异步地写入到数据库存储中。以下图表说明了这一点: 启用异步插入后,当 ClickHouse ① 接收到插入查询时,查询的数据首先被写入内存缓冲区中(②)。异步于 ①,只有在下一次刷缓冲区时(③),缓冲区的数据才会被排序并写入数据库存储中。请注意,在数据被刷到数据库存储之前,是搜索不到的;刷缓冲区是可配置的,我们稍后会展示一些示例。 在刷缓冲区之前,缓冲区会收集来自同一客户端或其他客户端的其他异步插入查询的数据。从缓冲区创建的parts可能包含来自多个异步插入查询的数据。总体上,这些机制将数据的批处理从客户端端转移到服务器端(ClickHouse 实例)。对于我们的 UpClick 用例来说非常适合。 可能会有多个parts 异步插入缓冲区中的行可能包含几个不同的分区键值,因此在刷缓冲区期间,ClickHouse 会为缓冲区中包含的不同分区键值,创建(至少)一个新的data part。此外,对于没有分区键的表,根据在缓冲区中收集的行数,缓冲区刷新可能会导致多个parts。 可能会有多个缓冲区 每个插入查询的形状(插入查询的语法,不包括值子句/数据)和setting都将有一个缓冲区。在多节点集群(例如 ClickHouse Cloud)上,每个节点将有自己的缓冲区。以下图表说明了这一点: 查询 ①、② 和 ③(以及 ④)具有相同的目标表 T,但不同的语法形状。查询 ③ 和 ④ 具有相同的形状,但不同的设置。查询 ⑤ 具有不同的形状,因为它的目标是表 T2。因此,所有 5 个查询都将有一个单独的异步插入缓冲区。当查询通过分布式表(自管理集群)或load balancer(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 提供了一种类似于异步插入的数据插入机制。缓冲区表将接收的数据缓冲在主内存中,并定期将其刷新到目标表中。尽管如此,缓冲区表与异步插入之间存在重大差异:
总体而言,与buffer表相比,从客户端的角度来看,异步插入的缓冲机制是完全透明且完全由 ClickHouse 管理的。异步插入可以被视为缓冲区表的继任者。 支持的接口和客户端 异步插入支持 HTTP 和TCP协议,和一些流行的客户端,如 Go 客户端在开启查询设置、用户设置或连接设置级别上支持异步插入,或直接支持异步插入。 您可以选择异步插入查询何时返回给查询的发送者以及何时插入的确认操作发生。通过 wait_for_async_insert 设置进行配置:
这两种模式都有相当显著的优缺点。因此,我们将在下面的两节中详细讨论这两种模式。 描述 以下图表描绘了异步插入的默认返回行为(wait_for_async_insert = 1 ): 当 ClickHouse ① 接收到插入查询时,查询的数据首先被写入内存缓冲区中(②)。当 ③ 发生下一次缓冲区刷新时,缓冲区的数据被排序并作为一个或多个data parts写入数据库存储中。在缓冲区刷新之前,其他插入查询的数据可以在缓冲区中被收集。仅在下一次常规的缓冲区刷新发生后,来自 ① 的插入查询将返回给发送者,并附带插入的确认。换句话说,发送插入查询的客户端端调用会在下一次缓冲区刷新发生时被阻塞。因此,上述图表中的 3 个插入不可能来自同一个单线程的插入循环,而是来自不同的多线程并行插入循环或不同的并行客户端/程序。 优势
劣势 这种模式的缺点在于,在使用单线程插入循环进行数据写入的情况下,可能会在某些场景中产生反压(back pressure):
在这种情况下,通过适当地对数据进行客户端批处理并使用多线程并行插入循环,可以增加写入吞吐量。 基准测试 我们进行了两个基准测试。
① 启用异步插入。通过 ②,我们设置了上述异步插入的默认返回行为。我们配置了缓冲区应该在每秒刷新一次,或者如果 ④ 数据达到 1 MB,或者如果 ⑤ 收集了来自 450 个插入查询的数据。无论发生什么情况,都会触发下一次缓冲区刷新。在 ClickHouse 中,②、③、④、⑤ 都是默认值(OSS 中 ③ 的默认值为 200 ,在 ClickHouse Cloud 中为 1000 )。 以下三个图表显示了云函数基准测试的第一个小时内活动parts的数量、云函数目标表中所有parts(活动和非活动parts)的数量,以及 ClickHouse 集群的 CPU 利用率: 您可以看到,不管我们运行了多少个云函数实例(200 个或 500 个,甚至更多的情况,如 1000 个等等),活动parts的数量都保持稳定在 8 个以下。而云函数目标表中所有parts(活动和非活动parts)的数量在运行了多少个云函数实例(200 个或 500 个)时都保持稳定在 1300 个以下。 这就是使用异步插入进行 UpClick 云函数的优势所在。无论我们运行多少个云函数实例 - 200 个、500 个,甚至 1000 个以上 - ClickHouse 都会对从云函数接收的数据进行批处理,并每秒创建一个新的数据分区。因为我们将 async_insert_busy_timeout_ms 设置为 1000 。我们在足够高的挡位上以足够高的速度驾驶着我们的 F1 赛车🏎,从而最大程度地减少了用于数据写入的 I/O 和 CPU 周期。正如您所看到的,与此前在本文中所做的传统同步插入基准测试相比,两个基准测试运行的 CPU 利用率要低得多。使用 500 个并行客户端运行的基准测试的 CPU 利用率高于使用 200 个并行客户端运行的基准测试的 CPU 利用率。对于具有 500 个客户端的情况,当每秒刷新一次时,缓冲区包含更多数据。并且在缓冲区刷新期间创建新的parts和较大的parts合并时,ClickHouse 需要花费更多的 CPU 周期来对数据进行排序和压缩。 请注意, async_insert_max_data_size 或 async_insert_max_query_number 可能会在不到一秒的时间内触发缓冲区刷新,尤其是在大量的云函数或客户端的情况下。您可以将这两个参数设置更高的值,以确保仅时间参数触发缓冲区刷新,从而牺牲以暂时性更高的主内存使用量为代价。 版权声明:本文为 clickhouse 社区用户原创文章,遵循 CC BY-NC-SA 4.0 版权协议,转载请附上原文出处链接和本声明。 评论
|