堆积部分、合并部分,再撞上上限
目标
通过 system.parts、system.part_log、system.query_log 确认 INSERT 会创建多少个数据片段、合并如何减少它们,以及数据片段过多时什么会被挡住。用异步 INSERT 把多次 INSERT 汇集成一个数据片段。
为什么重要
ClickHouse 生产故障的常客是“Too many parts”,而原因在于写入的方式。要修改写入一侧的代码,就必须会数“这次 INSERT 创建了多少个数据片段”。合并会在后台随意发生,所以本实验从停止合并开始。评分器不会相信你写的数字——它会按现在这张表的 uuid 过滤并统计 part_log 中的事件,TOO_MANY_PARTS 则通过 query_log 中的错误码来确认。
步骤
- 创建数据库
parts和表parts.events——列id UInt64, grp UInt8, v UInt32,引擎MergeTree,ORDER BY id。然后用SYSTEM STOP MERGES parts.events停止这张表的合并。 - 运行一次
/opt/lab/fixtures/parts/batches.sql(10 条 INSERT),然后把活动数据片段数和名称以active_parts、names写入 /root/ch/parts/parts.json。 - 新建列相同的表
parts.big,用numbers(1000000)以一次 INSERT 写入 100 万行,并加上SETTINGS min_insert_block_size_rows = 250000。从 part_log 中取出该 INSERT 创建的新数据片段的query_id、个数和行数列表,以query_id、parts、rows写入 /root/ch/parts/blocks.json。 - 在
SYSTEM START MERGES parts.events之后,用OPTIMIZE TABLE parts.events FINAL把数据片段合并成一个,然后把该数据片段的名称和级别,以及 part_log 中这张表的MergeParts事件数,以part、level、merge_events写入 /root/ch/parts/merge.json。 - 用相同的列创建带
SETTINGS parts_to_throw_insert = 5的表parts.guarded并停止合并,然后以六次 INSERT 逐行写入id1–6。把出现的错误(stderr)汇集到 /root/ch/parts/toomany.txt。 - 重新开启
parts.guarded的合并,用OPTIMIZE ... FINAL合并之后,把曾被拒绝的id6 再写入一次,使表成为id1–6 共六行。 - 新建列相同的表
parts.async_ev,分 5 次单独发送两行的INSERT ... VALUES,并加上SETTINGS async_insert = 1, wait_for_async_insert = 0, async_insert_use_adaptive_busy_timeout = 0, async_insert_busy_timeout_max_ms = 600000。用SYSTEM FLUSH ASYNC INSERT QUEUE清空缓冲区之后,把 query_log 中创建表之后的 INSERT 数、part_log 中的新数据片段数、创建该数据片段的 query_id,以insert_queries、new_parts、flush_query_id写入 /root/ch/parts/async.json。 - 对
parts数据库中现有的每张表,统计 part_log 中的NewPart事件数,以{"표이름": 수, ...}(占位符依次为表名与数量)的形式写入 /root/ch/parts/summary.json(包括events、big、guarded、async_ev)。
参考
- 查看数据片段:
SELECT name, rows, level FROM system.parts WHERE database = 'parts' AND table = '...' AND active。 - part_log 每 1 秒清空一次。如果看不到刚发生的事情,请执行
SYSTEM FLUSH LOGS。只想看现在这张表的记录,用table_uuid = (SELECT uuid FROM system.tables WHERE database = 'parts' AND name = '...')。 SYSTEM STOP MERGES在重新启动服务器后会解除。对停止了合并的表执行 OPTIMIZE,会因Cancelled merging parts被拒绝——要先SYSTEM START MERGES。- 常见错误:第 2 步运行了两次。即使 TRUNCATE,part_log 的记录也仍然存在,所以要重做的话,先
DROP TABLE parts.events,再从第 1 步开始。在第 7 步中,如果一条语句写入 10 行,或者用等待模式(默认)一条一条地发送,数据片段就不会汇集成一个。 - 第 7 步的不等待模式,错误不会返回给客户端,所以不建议在生产环境使用(文档建议:
wait_for_async_insert = 1)。这里用它,是为了不依赖时间就能汇集。 - 官方文档:Table parts · Part merges · system.part_log · parts_to_* settings · Asynchronous inserts · Selecting an insert strategy · OPTIMIZE
创建表并停止合并
创建数据库 parts 和表 parts.events。列依次为 id UInt64, grp UInt8, v UInt32,引擎 MergeTree,ORDER BY id。接着用 SYSTEM STOP MERGES parts.events 停止这张表的后台合并。
如果不停止合并,下一步创建的小数据片段会在几秒内被服务器合并掉,就看不到数据片段产生的样子。SYSTEM STOP MERGES 给出表名,就只停止那张表。
INSERT 10 次 → 10 个数据片段
运行一次 /opt/lab/fixtures/parts/batches.sql(10 条 INSERT,每条 1000 行),并把活动数据片段数和名称列表以 active_parts、names 写入 /root/ch/parts/parts.json。
用 --queries-file 运行,每条语句都会单独成为一次 INSERT。名称中间的两个数字是块编号的起止,最后一个数字是合并级别。用 groupArray 把名称收集成数组,再以 JSONEachRow 取出,就可以直接保存。
一次 INSERT 变成多个数据片段时
新建列相同的表 parts.big,用 numbers(1000000) 以一次 INSERT ... SELECT 写入 100 万行,并加上 SETTINGS min_insert_block_size_rows = 250000。从 part_log 中读取这张表的 NewPart 事件,把创建它们的 query_id、新数据片段的个数,以及每个数据片段的行数列表,以 query_id、parts、rows 写入 /root/ch/parts/blocks.json。
INSERT ... SELECT 会把读到的块拼在一起,攒够 min_insert_block_size_rows 之后再写成数据片段。默认值约为 100 万行,保持不变的话,一个数据片段就结束了。part_log 中的 query_id 就是你发送的那次 INSERT 的 id。如果重新创建过表,必须按 table_uuid 过滤,才不会混入旧记录。
开启合并并合成一个
在 SYSTEM START MERGES parts.events 之后,用 OPTIMIZE TABLE parts.events FINAL 把数据片段合并成一个。把合并后的数据片段的名称、级别(system.parts 的 name、level),以及 part_log 中这张表的 MergeParts 事件数,以 part、level、merge_events 写入 /root/ch/parts/merge.json。
在合并停止的情况下执行 OPTIMIZE,会因 Cancelled merging parts 被拒绝。开启合并的那一刻,后台可能先行合并,而 FINAL 即使已经只有一个数据片段也会再写一次——所以级别可能是 1,也可能是 2。走的是哪条路径,看 part_log 的 merged_from 就知道。
降低阈值,触发 TOO_MANY_PARTS
用相同的列和 ORDER BY id 创建带 SETTINGS parts_to_throw_insert = 5 的表 parts.guarded,并用 SYSTEM STOP MERGES parts.guarded 停止合并。以六次 INSERT 逐行写入 id 1–6,把出现的错误输出(stderr)汇集到 /root/ch/parts/toomany.txt。
阈值针对的是一个分区的活动数据片段数。一行的 INSERT 也会创建一个数据片段。clickhouse-client 的错误是从 stderr 输出的,所以用 2>> 来收集。评分器不会只相信文件,还会查看 query_log 中是否确实存在以错误码 252 被拒绝的 INSERT。
减少数据片段来解开
用 SYSTEM START MERGES 重新开启 parts.guarded 的合并,用 OPTIMIZE TABLE parts.guarded FINAL 合并之后,把曾被拒绝的 id 6 再写入一次。表中必须有 id 1–6 各一次,共六行。
调高阈值只是推迟症状。数据片段数降到阈值以下,同样的 INSERT 就能写进去。注意不要把 id 6 写入两次——MergeTree 不会阻止重复。
异步 INSERT 5 次 → 一个数据片段
新建列相同的表 parts.async_ev,分 5 次单独发送两行的 INSERT ... VALUES。每个 INSERT 都加上 SETTINGS async_insert = 1, wait_for_async_insert = 0, async_insert_use_adaptive_busy_timeout = 0, async_insert_busy_timeout_max_ms = 600000。用 SYSTEM FLUSH ASYNC INSERT QUEUE 清空缓冲区之后,把 query_log 中这张表 CREATE 之后结束的 INSERT 数(query_kind = 'Insert')、part_log 中的新数据片段数,以及创建该数据片段的 query_id,以 insert_queries、new_parts、flush_query_id 写入 /root/ch/parts/async.json。
在不等待模式下,INSERT 一放进缓冲区就返回,所以用 SELECT 查询,目前仍是 0 行。关闭自适应定时器并把上限设得很大,就不会随着时间推移自动清空。创建数据片段的查询不是你的 INSERT,而是清空缓冲区的查询(query_log 中 query_kind 为 AsyncInsertFlush)。
把各种写入方式的数据片段数放进一张表
对 parts 数据库中现有的每张表,统计 part_log 中的 NewPart 事件数,以 {"표이름": 수, ...}(占位符依次为表名与数量)的形式写入 /root/ch/parts/summary.json。必须包含 events、big、guarded、async_ev 四张表。
用 uuid 把 system.tables 和 system.part_log 连起来,就只会统计现有表的记录。请把四个数字并排看一看——10 条语句、被按块切开的一条语句、一行一行地写、用缓冲区汇集的 5 条语句。决定数据片段数的不是行数,而是写入的方式。