跳到主要内容
跳到主要内容

选择插入策略

高效的数据摄取是高性能 ClickHouse 部署的基础。选择合适的插入策略可以显著影响吞吐量、成本和可靠性。本节概述了最佳实践、权衡取舍和配置选项,帮助您为工作负载做出正确的决策。

注意

以下内容假设您通过客户端将数据推送到 ClickHouse。如果您是将数据拉取到 ClickHouse,例如使用内置表函数如 s3gcs,我们推荐参考指南 "优化 S3 插入和读取性能"

默认情况下为同步写入

默认情况下,对 ClickHouse 的写入是同步的。每个 insert 查询都会立即在磁盘上创建一个存储分片(part),包括元数据和索引。

如果可以在客户端进行批处理,请使用同步写入

如果不行,请参见下文的 异步写入

我们在下面简要回顾 ClickHouse 的 MergeTree 写入机制:

写入流程

客户端侧步骤

为了获得最佳性能,数据必须进行 ①批处理,这使得批大小成为首要决策

ClickHouse 将写入的数据存储在磁盘上,并按照表的主键列排序后存储第二个决策是是否在发送到服务器之前,对数据进行 ② 预排序。如果一个批次在到达时已经按主键列预排序,ClickHouse 就可以跳过第 ⑩ 步排序,从而加速数据摄取。

如果要摄取的数据没有预定义格式,那么关键决策是选择一种格式。ClickHouse 支持以超过 70 种格式写入数据。不过,当使用 ClickHouse 命令行客户端或编程语言客户端时,这个选择通常会自动完成。如果需要,也可以显式覆盖这种自动选择。

下一个重要决策是 ④ 是否在将数据传输到 ClickHouse 服务器之前对其进行压缩。压缩可以减少传输大小并提升网络效率,从而加快数据传输并降低带宽占用,尤其对大规模数据集尤为有效。

数据会被 ⑤ 传输到 ClickHouse 的某个网络接口——原生接口或 HTTP 接口(我们会在本文后面对其进行比较)。

服务器侧步骤

在 ⑥ 接收数据之后,如果使用了压缩,ClickHouse 会先对其进行 ⑦ 解压,然后从原始发送格式中进行 ⑧ 解析。

使用该格式化数据中的值以及目标表的 DDL 语句,ClickHouse 会以 MergeTree 格式构建一个内存中的 ⑨块(block),在数据未预先排序的情况下,按主键列对行进行 ⑩排序,创建 ⑪稀疏主索引,对每一列应用 ⑫按列压缩,并将数据作为一个新的 ⑭数据分片(data part)写入磁盘。

同步写入时请进行批量插入

上述机制表明,无论插入规模如何,开销基本是固定的,这使得批量大小成为提升摄取吞吐量最关键的优化手段。批量插入可以将这一固定开销在总插入时间中的占比降到更低,并提升处理效率。

我们建议以至少 1,000 行为一批进行插入,理想情况下在 10,000–100,000 行之间。更少次数但批量更大的插入可以减少写入的数据部分(parts)数量、降低合并负载,并减少整体系统资源使用。

要让同步插入策略有效发挥作用,必须在客户端进行批量处理。

如果你无法在客户端对数据进行批量处理,ClickHouse 支持将批处理转移到服务器端的异步插入(参见)。

提示

无论插入的批量大小如何,我们建议将插入查询的数量控制在每秒大约 1 条插入查询。这样建议的原因在于,创建出来的数据部分会在后台被合并成更大的部分(以优化读查询性能),而每秒发送过多的插入查询可能会导致后台合并无法跟上新部分产生的速度。不过,当你使用异步插入时,可以采用更高的每秒插入查询速率(参见异步插入)。

确保幂等重试

同步写入同样是幂等的。在使用 MergeTree 引擎时,ClickHouse 默认会对写入进行去重。这可以防止以下不明确的失败场景:

  • 写入已经成功,但由于网络中断,客户端从未收到确认。
  • 写入在服务器端失败并发生超时。

在这两种情况下,只要批次的内容和顺序保持完全一致,重试写入就是安全的。基于此原因,客户端在重试时必须保持一致,不得修改或重新排序数据。

选择合适的写入目标

对于分片集群,有两种选择:

  • 直接写入 MergeTreeReplicatedMergeTree 表。当客户端可以在分片之间执行负载均衡时,这是最高效的选项。设置 internal_replication = true 时,ClickHouse 会透明地处理复制。
  • 写入一个 Distributed 表。这样客户端可以将数据发送到任意节点,由 ClickHouse 将其转发到正确的分片。这更简单,但由于多了一次转发步骤,性能会略低。仍然建议将 internal_replication 设为 true

在 ClickHouse Cloud 中,所有节点都会对同一个分片进行读写。插入负载会在各节点间自动均衡分布。用户只需将插入请求发送到对外暴露的端点即可。

选择合适的格式

为高效地在 ClickHouse 中进行数据摄取选择正确的输入格式至关重要。ClickHouse 支持 70 多种格式,选择性能最佳的选项会显著影响插入速度、CPU 与内存使用以及整体系统效率。

在数据工程和基于文件的导入场景中,灵活性固然重要,但应用程序应优先选择面向性能的格式

  • Native 格式(推荐):最高效。列式存储,服务端只需进行最少的解析。Go 和 Python 客户端默认使用该格式。
  • RowBinary:高效的行式格式,如果在客户端进行列式转换比较困难,这是理想选择。Java 客户端使用该格式。
  • JSONEachRow:易于使用但解析开销大。适用于低流量场景或快速集成。

使用压缩

压缩在降低网络开销、加速插入以及减少 ClickHouse 存储成本方面起着关键作用。若使用得当,无需更改数据格式或 schema 即可提升摄取性能。

对插入数据进行压缩可减少通过网络发送的有效载荷大小,从而最大限度降低带宽使用并加快传输速度。

对于插入操作,压缩与 Native 格式配合使用时尤为高效,因为该格式已经与 ClickHouse 的内部列式存储模型相匹配。在此设置下,服务端可以高效地解压并以最小的转换直接存储数据。

使用 LZ4 获取速度,使用 ZSTD 获取压缩率

ClickHouse 在数据传输过程中支持多种压缩编解码器。两种常见选项为:

  • LZ4:快速且轻量。能够在仅施加极小 CPU 开销的情况下显著减小数据大小,非常适合高吞吐插入,并且是大多数 ClickHouse 客户端的默认选项。
  • ZSTD:压缩率更高,但对 CPU 要求更高。当网络传输成本较高时(例如跨区域或跨云厂商场景),它非常有用,不过会略微增加客户端计算以及服务端解压时间。

最佳实践:除非带宽受限或会产生数据出口成本,否则使用 LZ4;在这些情况下再考虑使用 ZSTD。

注意

FastFormats 基准测试 中,使用 LZ4 压缩的 Native 插入将数据大小减少了 50% 以上,在 5.6 GiB 数据集上将摄取时间从 150 秒缩短至 131 秒。切换到 ZSTD 后,同一数据集被压缩到 1.69 GiB,但服务端处理时间略有增加。

压缩可降低资源使用

压缩不仅减少网络流量——还提升了服务端的 CPU 与内存效率。对于压缩数据,ClickHouse 接收的字节更少,并且在解析大体量输入时花费的时间更短。当从多个并发客户端(例如在可观测性场景中)进行摄取时,这一优势尤为重要。

对于 LZ4,压缩对 CPU 和内存的影响较小;对于 ZSTD,则影响适中。即使在高负载下,由于数据量减少,服务端效率仍然会得到提升。

将压缩与批量插入以及高效输入格式(如 Native)结合使用,可获得最佳摄取性能。

在使用原生接口(例如 clickhouse-client)时,会默认启用 LZ4 压缩。你也可以通过设置切换为 ZSTD。

使用 HTTP 接口 时,请通过 Content-Encoding 头部应用压缩(例如 Content-Encoding: lz4)。整个有效载荷必须在发送前完成压缩。

如果代价低则预排序

在插入前按主键对数据进行预排序,可以提升 ClickHouse 中的摄取效率,尤其是对于大批量插入。

当数据以预排序形式到达时,ClickHouse 在创建数据 part 的过程中可以跳过或简化内部排序步骤,从而降低 CPU 使用并加快插入过程。预排序还会提升压缩效率,因为相似值被聚集在一起——使 LZ4 或 ZSTD 等编解码器可以获得更好的压缩率。在与大批量插入和压缩结合使用时,这种方式尤为有益,因为它同时减少了处理开销和传输数据量。

不过,预排序是一种可选优化——不是必需条件。 ClickHouse 通过并行处理对数据进行高效排序,在许多情况下,服务端排序比在客户端进行预排序更快或更方便。

我们仅在数据本身已经接近有序,或客户端侧资源(CPU、内存)充足且有富余时,才建议进行预排序。 在对延迟敏感或高吞吐量的场景(例如可观测性)中,数据往往是乱序到达或来自大量 Agent,此时通常更好的做法是跳过预排序,直接依赖 ClickHouse 的内置性能。

异步插入

在客户端无法进行批量处理时,ClickHouse 中的异步插入提供了一种强大的替代方案。这在可观测性工作负载中尤为重要,因为数百甚至数千个 agent 会持续发送数据——日志、指标、追踪数据(traces)——通常以小而实时的负载形式发送。在这类环境中在客户端缓冲数据会增加复杂度,需要一个集中式队列来确保可以发送足够大的批次。

注意

不推荐在同步模式下发送大量小批次,这会导致创建大量数据片(parts)。这将导致查询性能下降,并产生 "too many part" 错误。

异步插入通过将批处理的责任从客户端转移到服务器来实现:将传入数据写入内存缓冲区,然后根据可配置阈值刷新到存储中。此方法显著降低了数据片创建的开销,减少 CPU 使用率,并确保在高并发下摄取仍然高效。

核心行为通过 async_insert 设置进行控制。

异步插入

启用(1)后,插入会被缓冲,只有在满足以下任一刷新条件时才会写入磁盘:

(1) 缓冲区达到指定大小(async_insert_max_data_size) (2) 达到时间阈值(async_insert_busy_timeout_ms),或 (3) 累积的插入查询数量达到上限(async_insert_max_query_number)。

这一批处理过程对客户端是透明的,有助于 ClickHouse 高效合并来自多个源的插入流量。但在刷新发生之前,这些数据无法被查询。需要注意的是,对于每种插入数据结构(insert shape)与设置组合会有多个缓冲区,在集群中,缓冲区按节点维护——从而在多租户环境中实现细粒度控制。其余插入机制与同步插入中描述的相同。

选择返回模式

异步插入的行为可通过 wait_for_async_insert 设置进行进一步细化。

当设置为 1(默认值)时,ClickHouse 仅在数据成功刷新到磁盘后才确认插入。这确保了强持久性保证,并使错误处理变得简单:如果在刷新期间出现问题,错误会返回给客户端。此模式推荐用于大多数生产场景,尤其是在必须可靠跟踪插入失败时。

基准测试表明,它在并发规模下表现良好——无论你运行的是 200 还是 500 个客户端——这得益于自适应插入和稳定的数据片创建行为。

wait_for_async_insert = 0 设置为 0 会开启 “fire-and-forget”(“发出即忘”)模式。在该模式下,服务器在数据被缓冲后立即确认插入,而不会等待其写入存储。

这提供了超低延迟的插入和最大吞吐量,非常适合高速度、低重要性的数据。然而,这也带来了权衡:无法保证数据一定被持久化,错误可能只会在刷新时才暴露,而且难以追踪失败的插入。仅当你的工作负载可以容忍数据丢失时才使用此模式。

基准测试还表明,当缓冲刷新不频繁(例如每 30 秒一次)时,可以显著减少数据片数量并降低 CPU 使用率,但静默失败的风险依然存在。

我们强烈建议,如果使用异步插入,应设置 async_insert=1,wait_for_async_insert=1。使用 wait_for_async_insert=0 风险极高,因为你的 INSERT 客户端可能不知道是否发生了错误,并且在 ClickHouse 服务器需要减慢写入速度并施加一定背压以保证服务可靠性的情况下,如果客户端持续快速写入,还可能导致潜在的过载。

去重与可靠性

默认情况下,ClickHouse 会对同步插入执行自动去重,这使得在失败场景下重试是安全的。然而,对于异步插入,这一功能是禁用的,除非显式启用(如果你有依赖的物化视图,则不应启用——见相关 issue)。

在实践中,如果开启了去重并对同一插入执行重试——例如由于超时或网络中断——ClickHouse 可以安全地忽略重复记录。这有助于保持幂等性并避免数据被写入两次。不过,仍需注意,插入验证和 schema 解析仅在缓冲刷新时执行——因此错误(例如类型不匹配)只会在那个时间点才会暴露出来。

启用异步插入

可以为特定用户或特定查询启用异步插入:

  • 在用户级别启用异步插入。此示例使用用户 default,如果你创建了其他用户,请将其替换为相应的用户名:

    ALTER USER default SETTINGS async_insert = 1
    
  • 你可以在插入语句中通过 SETTINGS 子句指定异步插入相关设置:

    INSERT INTO YourTable SETTINGS async_insert=1, wait_for_async_insert=1 VALUES (...)
    
  • 在使用 ClickHouse 编程语言客户端时,你也可以通过连接参数指定异步插入相关设置。

    例如,以下是在使用 ClickHouse Java JDBC 驱动程序连接到 ClickHouse Cloud 时,在 JDBC 连接字符串中进行设置的方式:

    "jdbc:ch://HOST.clickhouse.cloud:8443/?user=default&password=PASSWORD&ssl=true&custom_http_params=async_insert=1,wait_for_async_insert=1"
    

选择接口——HTTP 或原生

原生

ClickHouse 提供两种主要的数据摄取接口:原生接口HTTP 接口——二者在性能与灵活性之间各有取舍。原生接口由 clickhouse-client 以及部分语言客户端(如 Go 和 C++)使用,专为高性能而设计。它始终以 ClickHouse 高效的 Native 格式来传输数据,支持基于数据块的 LZ4 或 ZSTD 压缩,并通过将解析和格式转换等工作下放到客户端,最大限度地减少服务端处理。

它甚至支持在客户端计算 MATERIALIZED 和 DEFAULT 列的值,从而使服务端可以完全跳过这些步骤。这使得原生接口非常适合对效率要求极高的高吞吐量摄取场景。

HTTP

与许多传统数据库不同,ClickHouse 也支持 HTTP 接口。相比之下,该接口优先考虑兼容性和灵活性。 它允许以任意受支持的格式发送数据——包括 JSON、CSV、Parquet 等——并在大多数 ClickHouse 客户端中得到广泛支持,包括 Python、Java、JavaScript 和 Rust。

这通常优于 ClickHouse 原生协议,因为它允许通过负载均衡器轻松切换流量。与原生协议相比,我们预计插入性能会有细微差异,原生协议的开销会略小一些。

然而,它缺少原生协议的深度集成能力,无法执行诸如物化值计算或自动转换为 Native 格式等客户端优化。虽然通过 HTTP 进行插入时仍可使用标准 HTTP 头进行压缩(例如 Content-Encoding: lz4),但压缩是针对整个负载而非单个数据块进行的。在那些更看重协议简单性、负载均衡或广泛格式兼容性而非极致性能的环境中,通常会优先选择该接口。

有关这些接口的更详细说明,请参见此处