ClickHouse 数据少了的排查顺序
目录
背景#
数据出问题大致分三类:多了、少了、不对。多了有明确的特征,重复的几行长得一样,写入间隔集中在几档(超时重投那篇);不对至少能看见数字不合理。少了最难查,它不报错,往往在事故之后很久才被发现。
这条链路是业务服务写 Kafka,Kafka Connect 的 ClickHouse sink(clickhouse-kafka-connect v1.3.9)消费后写进 ClickHouse。少了的那部分可能卡在任何一段,所以排查按链路顺序走,每一步都能排除链路上的一段:上游 → Kafka → connector → offset → INSERT 有没有真写进去 → 写进了哪个分区。
下面的 connector 行为来自 clickhouse-lab 的实验 09、21、23:Kafka 3.7.0(Confluent Platform 7.7.0 的社区版镜像)加 connector v1.3.9,topic 也和这套系统一样建成 8 个分区。插件版本和这套系统一样,Connect 运行时不同:这套系统的 Connect 是 Confluent Cloud 托管的,lab 是 Apache Kafka 3.7.0,下面 errors.* 和 DLQ 的行为是在后者上测的,托管侧没核过。
先对上游和 Kafka 的量#
第一步确认上游到底产出了多少,这一步不花 ClickHouse 的资源。上游和 topic 里的量对不上,问题在 producer 或 broker,和 ClickHouse 无关。topic 里有、ClickHouse 里没有,才往下查。
connector 的错误处理决定坏批次去哪#
connector 写库失败之后数据去哪,取决于两项配置:errors.tolerance 和死信队列(DLQ)。三种配置下的结局完全不同。实验 09 往 8 个分区的 topic 里混进一条类型不合的坏记录:
| 配置 | 坏记录进来之后 | 少了的数据在哪 |
|---|---|---|
errors.tolerance=none(默认) |
task 约 2 秒后变成 FAILED,之后这个 task 管的分区都不再前进,重启 task 还是 FAILED |
都还在 Kafka 里;信号是 task 状态和消费组 lag |
errors.tolerance=all 加 DLQ |
task 不停;坏记录所在分区的那一整批,连同好记录一起进 DLQ,别的分区照常写,offset 照常往前提交 | DLQ topic |
errors.tolerance=all,没配 DLQ |
task 不停;那一整批静默消失,offset 照常往前提交 | 只剩源 topic 里那一份,过了保留期就真没了 |
默认配置下,坏记录是一颗毒丸:task 停了之后再来的 8 条正常记录,15 秒后一条都没进 ClickHouse,消费组的 lag 至少是 9;不跳过这条坏记录,重启多少次都会再停在它上面。lab 只有 1 个 task,停的是整个 topic;现有 sink 的 tasks.max 是多少还没核,多个 task 时只停坏记录所在那个 task 分到的分区。数据没丢,只是没进来,所以这种「少了」要看 task 状态和 lag,不看 ClickHouse。
配了 DLQ,task 不停,但送进 DLQ 的不只是坏记录。实验 09 B 里一批 11 条,坏记录在分区 2,和它同分区的 3 条全进了 DLQ,其中 2 条是好记录,ClickHouse 里只有其余分区的 8 条。原因是 connector 按 topic-partition 分批写,一批写失败就把整批交给 DLQ,不会把坏记录挑出来、重试其余的。DLQ 记录的 header 里 stage 是 TASK_PUT,报错信息记的是这一批的 offset 区间(MinOffset、MaxOffset),不指明是哪一条坏了。这套系统的 topic 有 8 个分区,按每个块的行数算,一批在每个分区上是十几到九十行,一条坏记录带走的就是它那个分区的这么多行。补数的时候按整批重放。
只开 errors.tolerance=all、不配 DLQ 是最危险的一种:失败的那一批 3 条都没进 ClickHouse,后面来的照常写进,消费组的 offset 已经越过了这 3 条,task 是 RUNNING,lag 也正常(实验 09 D)。从 task 状态、lag、ClickHouse 三处都看不出少了,只能靠和上游对账发现。
为什么框架的 errors.tolerance 管不住这一层:Kafka Connect 框架自己的容错只包住转换和 SMT 阶段(RetryWithToleranceOperator.java:66-72);put() 里写库失败之后进不进 DLQ,是 connector 自己调 ErrantRecordReporter 决定的(ProxySinkTask.java:82-108、ClickHouseSinkTask.java:238-254)。没配 DLQ 时它拿到的是一个什么都不做的 reporter,失败的批次就被吞掉了。所以 errors.tolerance=all 一定要和 DLQ 一起配。现有 sink 配的是哪一种,也还没核。
这条链路的 sink 没开 exactlyOnce。开了的话,还会多出 State MISMATCH、State CONTAINS 两种状态冲突,其中 State CONTAINS 那一段 ClickHouse 里可能根本没有,没配 DLQ 时和上面一样静默丢掉(实验 23,怎么处理见四个入口那篇)。
committed offset 和积压#
消费组的 committed offset 和 topic 的 log-end offset 之差是积压,差值一直在涨说明写不进去;committed 长时间不动,说明卡住了,回到上一步看 task 状态。committed 往前走了、ClickHouse 里却没有,上面三种配置里后两种都会这样:配了 DLQ 的,这一段在 DLQ topic 里;没配 DLQ 的,只剩源 topic 里那一份。
确认 INSERT 真的写进去了#
connector 那一侧正常,再看 ClickHouse 有没有收到。system.query_log 里查这段时间写这张表的 INSERT,对比每条的 written_rows 和批次大小:
SELECT event_time, hostName() AS h, query_id, type, written_rows, exception_code
FROM clusterAllReplicas('default', system.query_log)
WHERE query_kind = 'Insert' AND has(tables, '{db}.{table}')
AND type IN ('QueryStart', 'QueryFinish', 'ExceptionWhileProcessing')
AND event_time >= {t_from} AND event_time < {t_to}
ORDER BY event_time;
query_log 有两个盲区。一是客户端超时之后晚到提交的 INSERT,可能只有 QueryStart、没有结束记录(实验 21 里等副本确认的那次没有,实验 12 冻住 Keeper 的那次有),只看它会以为这一批没写成。二是被块级去重拦下的重投照样记成 QueryFinish,written_rows 等于批次大小(实验 21)。一批到底写没写进去,以 system.part_log 为准,看那一刻有没有 event_type = 'NewPart' AND error = 0 的记录;error = 389 的 NewPart 是被块级去重拦下的那次插入,没写数据(实验 03)。
Aiven 上的现有集群里,query_log 和 part_log 都只留约 4 天,发现得晚就查不到这一层了。这两张表的保留期拉长,是这一步能不能查下去的前提。
写进了别的分区#
按事件时间查不到的行,未必没写进来,可能落进了相邻那一天的分区。在前后几天的分区里用 _partition_id 反查它到底在哪,不带分区条件会把每个日分区的跳数索引都过一遍,其中大部分在对象存储上:
SELECT _partition_id, count()
FROM {db}.{table}
WHERE _partition_id IN ('{D-1}', '{D}', '{D+1}')
AND id IN ({几个已知丢失的 id})
GROUP BY _partition_id;
一种可能是时区:分区键按服务端时区或列声明的时区截断,UTC 16 点到 24 点之间的时刻,按 UTC 和按上海声明会落进相邻两天的分区。这一类不算丢数据,算「不对」,写在数字对不上的排查顺序。
小结#
- 少了按链路顺序查:上游 → Kafka → connector 的错误处理 → offset → INSERT 是否写进去 → 落在哪个分区。
- 默认的
errors.tolerance=none下,一条坏记录让 task 停住,数据都还在 Kafka 里,信号是 task 状态和 lag。 - 配了 DLQ,进 DLQ 的是坏记录所在分区的整批,好记录也在里面,补数按整批重放。
- 只开
errors.tolerance=all、不配 DLQ,失败的整批静默消失,offset 照常提交,task 和 lag 都看不出来。 - 判断一批写没写进 ClickHouse 以
part_log为准,晚到提交的 INSERT 在query_log里可能没有结束记录。
参考资料#
- Kafka 3.7.0
ConnectorConfig.java:errors.tolerance默认none - clickhouse-kafka-connect v1.3.9
ProxySinkTask.java、ClickHouseSinkTask.java:写库失败时按 topic-partition 整批交给 reporter - clickhouse-lab 实验 09:三种错误处理配置,输出在
results/09-bad-records-dlq.log;实验 21、23 是exactlyOnce的两种报错