TT Lab
开始
学习 学习路径 课程

ClickHouse — 从内部理解列式分析数据库

堆积部分、合并部分,再撞上上限

在 TT Lab 中继续学习

目标

通过 system.parts、system.part_log、system.query_log 确认 INSERT 会创建多少个数据片段、合并如何减少它们,以及数据片段过多时什么会被挡住。用异步 INSERT 把多次 INSERT 汇集成一个数据片段。

为什么重要

ClickHouse 生产故障的常客是“Too many parts”,而原因在于写入的方式。要修改写入一侧的代码,就必须会数“这次 INSERT 创建了多少个数据片段”。合并会在后台随意发生,所以本实验从停止合并开始。评分器不会相信你写的数字——它会按现在这张表的 uuid 过滤并统计 part_log 中的事件,TOO_MANY_PARTS 则通过 query_log 中的错误码来确认。

步骤

  1. 创建数据库 parts 和表 parts.events——列 id UInt64, grp UInt8, v UInt32,引擎 MergeTree,ORDER BY id。然后用 SYSTEM STOP MERGES parts.events 停止这张表的合并。
  2. 运行一次 /opt/lab/fixtures/parts/batches.sql(10 条 INSERT),然后把活动数据片段数和名称以 active_parts、names 写入 /root/ch/parts/parts.json。
  3. 新建列相同的表 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。
  4. 在 SYSTEM START MERGES parts.events 之后,用 OPTIMIZE TABLE parts.events FINAL 把数据片段合并成一个,然后把该数据片段的名称和级别,以及 part_log 中这张表的 MergeParts 事件数,以 part、level、merge_events 写入 /root/ch/parts/merge.json。
  5. 用相同的列创建带 SETTINGS parts_to_throw_insert = 5 的表 parts.guarded 并停止合并,然后以六次 INSERT 逐行写入 id 1–6。把出现的错误(stderr)汇集到 /root/ch/parts/toomany.txt。
  6. 重新开启 parts.guarded 的合并,用 OPTIMIZE ... FINAL 合并之后,把曾被拒绝的 id 6 再写入一次,使表成为 id 1–6 共六行。
  7. 新建列相同的表 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。
  8. 对 parts 数据库中现有的每张表,统计 part_log 中的 NewPart 事件数,以 {"표이름": 수, ...}(占位符依次为表名与数量)的形式写入 /root/ch/parts/summary.json(包括 events、big、guarded、async_ev)。

参考

创建表并停止合并

创建数据库 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 条语句。决定数据片段数的不是行数,而是写入的方式。