ClickHouse 的 Keeper 故障和 part 过多
目录
背景#
复制表的写入要经过两样东西:Keeper 负责领块号、去重和复制日志,merge 负责把每次写入产生的小 part 合掉(写入路径那篇)。这篇看这两样出问题时,写入是什么表现、有什么前兆、平时盯什么。
现有集群上见过的前兆在 Keeper 上。写出重复行的那三个小时里,Keeper 的在途请求从常态的 5 到 15 涨到几千,副本只读只闪了两下,用瞬时采样根本看不到(超时重投那篇)。这套集群托管在 Aiven 上,每个节点同机跑一个 ZooKeeper,只在集群内可达,运维碰不到这一层。
还有第三种会让写入被拒的情形,和 Keeper、part 都无关:Aiven 在 profile 里锁了每个节点的并发查询数,A、B、C 三档部署分别是 100、375、200(档位见架构那篇),INSERT 也算在里面,超出时直接报 Code: 202 TOO_MANY_SIMULTANEOUS_QUERIES,查询里改设置也绕不过去。
下面的现象来自 clickhouse-lab 的实验 11、12(25.3.14.14,三副本加一个单节点 Keeper),Keeper 的故障是把容器停掉或冻住造出来的。现有集群每个节点跑的是 ZooKeeper,lab 是 ClickHouse Keeper;冻住之后约 10 秒才转只读,这 10 秒来自 ClickHouse 这一侧 ZooKeeper 客户端的 operation_timeout_ms(默认 10000,现有集群改没改看不到),两种服务端之间的差别 lab 没测。
Keeper 停掉和卡住,表现不一样#
实验 12 分别把 Keeper 容器停掉(docker stop,进程没了、连接当场断开)和冻住(docker pause,进程卡住、连接还在):
| 观测项 | 停掉 | 冻住 |
|---|---|---|
| 副本转只读 | 立刻,第一次轮询就已经是只读 | 约 10 秒后,要等发出去的 Keeper 请求超时 |
期间的 SELECT |
照常返回,读不碰 Keeper | 同左 |
期间的 INSERT |
重试到上限后报 Code: 242 TABLE_IS_READ_ONLY,默认参数下卡了约 143 秒 |
客户端 30 秒超时断开;Keeper 一回来,服务端那条 INSERT 照常提交 |
| Keeper 回来之后 | 2 到 12 秒内自己退出只读,被拒的那几条不会补写 | 同左;晚到提交的那条已经在表里 |
INSERT 卡约 143 秒是默认的重试参数决定的:insert_keeper_max_retries = 20,每次失败后的等待从 100 ms 翻倍、封顶 10 秒,加起来 142.7 秒,实测卡满这么久才报错。
读一直是好的,所以只看查询成功率发现不了 Keeper 出事。停掉的影响是写入全停,Keeper 回来之后自愈,数据不会坏。
卡住比停掉更麻烦#
冻住时客户端第 30 秒超时放弃,那条 INSERT 还卡在服务端;Keeper 回来之后,服务端把它提交了,query_log 里是正常的 QueryFinish。对客户端,这一批是「超时失败」;对服务端,是「成功提交」。Kafka Connect 框架接着会把同一批原样重投,块级去重的窗口还记得它就拦下,记不得就落下第二份。lab 上冻住 Keeper 的这一段,和现有集群写出重复行那次推断的经过对得上,完整的经过和那三个小时的证据写在超时重投那篇。
反过来,Keeper 一直不回来,这条 INSERT 最终报错、没写进去,也就谈不上重复:停掉的情形重试约 143 秒才报错,冻住不恢复的情形 lab 没跑。重复的前提是晚到提交。
part 过多:先拖慢、后拒绝#
另一处是 part 数。每次 INSERT 都产生新 part,merge 跟不上时,单个分区的活跃 part 数到 parts_to_delay_insert(默认 1000)开始故意拖慢 INSERT,到 parts_to_throw_insert(默认 3000)直接拒绝。实验 11 把两个阈值压到 20 和 25、停掉 merge 后一条一条插:
- 第 26 条 INSERT 被拒,报
Code: 252 … Too many parts,此时分区里正好 25 个活跃 part; - 在这之前
DelayedInserts计数从 10 涨到 15,被故意拖慢了 5 次; - INSERT 耗时的中位数,阈值之前是 7 ms,进入拖慢区之后是 311 ms,耗时随机器变,硬证据是
DelayedInserts计数; - 把 merge 放回去,part 合成 1 个,写入马上恢复。
先拖慢、后拒绝,中间那段拖慢就是预警窗口。等到报错再看就晚了,所以要盯的是 part 数,不是错误率。
还有一个例外:分区里 part 的平均大小超过 max_avg_part_size_for_too_many_parts(默认 1 GiB)时,上面两道闸都不生效,大 part 多不算 part 太多。
两亿行那套部署三个副本合计每秒新建一百多个十几行的小 part,全靠 merge 持续合掉;全表有两三千个活跃 part,大部分在历史分区上,单个分区最多有多少还没查过。容易把 part 数推上去的是人为操作:迁移或实验时停了 merge,或者一条大 INSERT … SELECT 一次切出很多个 part(三亿行的一天估计四五百个,写入路径那篇)。
写入被拒时,sink 那一侧不会丢数据:242、252、202 这几个错误码都在 connector 当作可重试的列表里(Utils.java:76-91),框架把这一批留在内存里,等下一个 offset 提交点再投,表现是消费组的 lag 往上涨。
平时盯什么#
巡检的价值在于有基线,每一项都先记常态,超线才有意义。和这两处有关的几项:
| 项 | 查法 | 线 |
|---|---|---|
| 单分区活跃 part 数 | system.parts 按库、表、分区数 active 的 part |
到 1000 的一半就处理 |
| Keeper 在途请求 | system.metrics 的 ZooKeeperRequest(现有集群的 Prometheus 上叫 clickhouse_metrics_zoo_keeper_request) |
现有集群的基线是 5 到 15,涨到几百倍就是停摆的前兆 |
| 复制队列和延迟 | system.replicas 的 queue_size、absolute_delay |
持续高于基线 |
| INSERT 耗时的尾部 | connector 侧的指标,以及 query_log 里 INSERT 的分位数 |
到 24 秒(30 秒超时的八成) |
| 副本只读 | system.replicas.is_readonly,告警用窗口内的最大值 |
窗口内最大值为 1;瞬时采样会漏掉只闪一下的只读 |
多副本的取值一律走 clusterAllReplicas,连接随机落到哪个节点,看到的就是哪个副本的状态。
INSERT 尾部那一项要配合下游一起看。把超时调长只能让超时少一些:Keeper 卡得比新的超时还久时,服务端照样晚到提交、框架照样重投,下游没有按键幂等的那一层,重复还是会出现。
Keeper 这一层:托管和自建各能做什么#
托管服务下 Keeper 碰不到。ClickHouse 这一侧能看到的是在途请求数,ZooKeeper 自己的服务端延迟、fsync 耗时看不到,要找服务商要这些监控指标,否则下次抖动还是只能看到结果、看不到原因。官方的运维建议是把 ClickHouse 和 ZooKeeper/Keeper 放在不同的服务器上,或者至少放在不同的盘上,理由是 Keeper 对磁盘延迟很敏感,而 ClickHouse 会占满能用的资源(OSS usage recommendations)。现有集群是每个节点同机跑 ZooKeeper,数据是不是放在单独的盘上看不到,可以和监控指标一起问服务商。
自建时,Keeper 至少 3 个节点:官方文档的说法是 3 节点的集群「will continue working correctly if only 1 node crashes」。Keeper 默认 force_sync = true,每写一条日志都要 fsync,日志和快照要放在 local 类型的 disk 上,对象存储类型的 disk 不支持持久化同步(ClickHouse Keeper)。
两种情况都适用的一条:把 part_log 和 query_log 的保留期拉长。现有集群上它们只留约 4 天,重复行那次事故能查的证据很快就没了,后来只能在 lab 上把推断一条条复现出来。
小结#
- Keeper 停掉:副本立刻只读,读照常,INSERT 默认卡约 143 秒后报
TABLE_IS_READ_ONLY,Keeper 回来后自愈。 - Keeper 卡住:副本约 10 秒后才转只读,客户端 30 秒超时,服务端在 Keeper 回来后照样提交,接着的重投就是重复行的来源。
- part 数先让 INSERT 变慢、再让它被拒,盯 part 数比盯错误率早;平均 part 超过 1 GiB 的分区不受这道闸管。
- 前兆在 Keeper 在途请求、复制队列和 INSERT 耗时的尾部上,副本只读这类硬指标用瞬时采样会漏掉。
- 官方建议把 Keeper 和 ClickHouse 分开放,至少分盘;托管服务下要找服务商要 Keeper 的指标。
参考资料#
- ClickHouse Keeper:3 节点容忍 1 个故障、
force_sync和local类型的 disk - OSS usage recommendations:Keeper 和 ClickHouse 分机器或分盘
- Replication:Keeper 不可用时复制表转只读
- MergeTree settings:
parts_to_delay_insert、parts_to_throw_insert、max_avg_part_size_for_too_many_parts Utils.java@ v1.3.9:connector 当作可重试的错误码- clickhouse-lab 实验 12、实验 11:输出在
results/下的同名文件