【问题标题】:Multiple small inserts in clickhouseClickhouse 中的多个小插件
【发布时间】:2026-01-22 00:35:01
【问题描述】:

我在 clickhouse 中有一个事件表 (MergeTree),并希望同时运行很多小插入。但是,服务器变得超载且无响应。此外,一些插入物丢失了。 clickhouse错误日志中有很多记录:

01:43:01.668 [ 16 ] <Error> events (Merger): Part 201 61109_20161109_240760_266738_51 intersects previous part

有没有办法优化这样的查询?我知道我可以对某些类型的事件使用批量插入。基本上,运行一个包含许多记录的插入,clickhouse 处理得很好。但是,某些事件(例如点击或打开)无法以这种方式处理。

另一个问题:为什么 clickhouse 确定存在类似记录,而实际上不存在?插入时有类似的记录,与索引中的字段相同,但其他字段不同。

我有时也会收到以下错误:

Caused by: ru.yandex.clickhouse.except.ClickHouseUnknownException: ClickHouse exception, message: Connect to localhost:8123 [ip6-localhost/0:0:0:0:0:0:0:1] timed out, host: localhost, port: 8123; Connect to ip6-localhost:8123 [ip6-localhost/0:0:0:0:0:0:0:1] timed out
    ... 36 more

主要是在项目构建期间运行针对 clickhouse 数据库的测试。

【问题讨论】:

    标签: clickhouse


    【解决方案1】:

    Clickhouse 为此提供了一种特殊类型的表格 - 缓冲区。它存储在内存中,允许许多小插入没有问题。我们每秒有近 200 种不同的插入 - 效果很好。

    缓冲表:

    CREATE TABLE logs.log_buffer (rid String, created DateTime, some String, d Date MATERIALIZED toDate(created))
    ENGINE = Buffer('logs', 'log_main', 16, 5, 30, 1000, 10000, 1000000, 10000000);
    

    主表:

    CREATE TABLE logs.log_main (rid String, created DateTime, some String, d Date) 
    ENGINE = MergeTree(d, sipHash128(rid), (created, sipHash128(rid)), 8192);
    

    手册详情:https://clickhouse.yandex/docs/en/operations/table_engines/buffer/

    【讨论】:

    • 缓冲表的其他时刻 - 如果您想在主表中添加新字段,您无法即时添加(。您应该停止所有并在主表和缓冲表中插入新字段,并且然后再次运行
    【解决方案2】:

    在处理大量小插入到(非复制的)MergeTree 时,这是一个已知问题。

    这是一个错误,我们需要调查和修复。

    对于解决方法,您应该按照建议批量发送插入:大约每秒一批:https://clickhouse.tech/docs/en/introduction/performance/#performance-when-inserting-data

    【讨论】:

    • 我们有在开关的基础上生成的事件。将它们批量插入CH,意味着我们需要为它们建立一个暂存区,可能是一个队列或其他一些临时位置。这顶帽子是我们必须做的,还是有另一种推荐的插入单行的方法?第二个后续问题:文档指出:“为了提高性能,您可以并行进行多个 INSERT 查询......”。这是否意味着我们可以运行并行进程/线程,每次插入,但每个进程需要每秒并行执行一个批处理?
    • 是的,你需要在服务的某个队列或进程内缓冲区中累积事件,然后批量插入。
    • 您可以并行执行许多 INSERT;建议在所有线程中总共每秒处理一个批次。
    【解决方案3】:

    我也遇到过类似的问题,虽然没有那么严重——每秒插入约 20 次会导致服务器达到高负载平均、内存消耗和 CPU 使用率。我创建了一个缓冲表来缓冲内存中的插入,然后它们会定期刷新到“真正的”磁盘表中。就像魔术一样,一切都很顺利:loadavg、内存和 CPU 使用率下降到正常水平。好消息是您可以对缓冲表运行查询,并从内存和磁盘中取回匹配的行——因此客户端不受缓冲的影响。见https://clickhouse.tech/docs/en/engines/table-engines/special/buffer/

    【讨论】:

    • 您是否使用 Buffer 进行单行插入?文档建议不要将 Buffer 用于多个单行插入:Note that it doesn't make sense to insert data one row at a time, even for Buffer tables. 我正在尝试找到一种插入多个单行的方法,我不想自己构建队列系统。
    • @GokhanSari - 声称“没有意义”是主观的。也许对于您的用例,您会发现它确实有意义。在尝试构建更复杂的机制之前尝试一下!如果您不想每秒插入数百个单行,缓冲表可能就可以正常工作。
    【解决方案4】:

    或者,您可以使用 https://github.com/nikepan/clickhouse-bulk 之类的东西:它会缓冲多个插入并根据用户策略将它们全部刷新。

    【讨论】:

      【解决方案5】:

      clickhouse MergeEngines 的设计并不意味着同时进行少量写入。据我所知,MergeTree 根据分区将写入表的数据的parts 合并到其中,然后重新组织parts 以获得更好的聚合读取。如果我们经常进行小型写入,您会遇到另一个异常 Merge

      Error: 500: Code: 252, e.displayText() = DB::Exception: Too many parts (300). Merges are processing significantly slow
      

      当您尝试了解为什么会引发上述异常时,您的想法会更加清晰。 CH需要合并数据,并且可以存在多少个部分有上限!并且批量中的每一次写入都会作为一个新部分添加,然后最终与分区表合并。

      SELECT
          table, count() as cnt
      FROM system.parts 
      WHERE database = 'dbname' GROUP BY `table` order by cnt desc
      

      上面的查询可以帮助你监控部分,观察部分如何增加并最终合并。

      我最好的办法是缓冲数据集并定期将其刷新到数据库,但这意味着没有实时分析。

      使用缓冲区是好的,但是请考虑以下几点:

      • 如果服务器异常重启,缓冲区中的数据会丢失。
      • FINAL 和 SAMPLE 无法正常用于缓冲区表。这些条件被传递给目标表,但不用于处理缓冲区中的数据
      • 向缓冲区添加数据时,其中一个缓冲区被锁定。 (所以没有读取)
      • 如果复制了目标表,则在写入缓冲区表时,复制表的一些预期特征会丢失。 (无重复数据删除)

      请仔细阅读,这是一个特例引擎:https://clickhouse.tech/docs/en/engines/table-engines/special/buffer/

      【讨论】: