团队分享 · 交易明细数据链路
从一条 INSERT 到一张报表:part 和 merge、Kafka sink 的至少一次写入、物化视图聚合,以及运维会碰到的几类问题
单日一千多万到三亿多行
ClickHouse 25.3,1 shard × 3 replicas
Kafka Connect sink 至少一次写入
素材来自博客的 ClickHouse 系列和 clickhouse-lab(25.3.14.14 三副本实验台)。标「方案」的内容在 lab 上验过,还没在现有集群上执行。
单 topic,8 个分区;峰值每秒约 1 万条。producer 重试时可能把同一条存成两份。
clickhouse-kafka-connect v1.3.9,Confluent Cloud 托管。每次 poll 按分区各发一条带 token 的 INSERT;至少一次投递,没开 exactlyOnce。
ReplicatedMergeTree,按结算日分区,ORDER BY (settle_ms, id, rev);34 列、8 个跳数索引。单日一千多万到三亿多行。
方案
物化视图在明细插入时触发,聚合进 UTC 半小时桶,按天分区;日报、周报、月报按时区在查询时上卷。
存复制日志、块号分配和块级去重标记。Aiven 在每个节点同机跑一个 ZooKeeper,运维碰不到这一层。
tiered storage:create_time 满 45 天的 part 整个搬过去。B 档约三分之二、C 档近九成的数据已经在上面。
backoffice 按时间范围查明细、做对账,读明细表;按不同时区出日报、周报、月报,读报表表。
按 region 分成几套独立部署,ClickHouse 是 Aiven 托管的 25.3,1 shard × 3 replicas,任一节点都能接写入。三档规格(每个节点 / 单日分区):A business-16,4 vCPU / 16 GB,一千多万行;B business-32,8 vCPU / 32 GB,约两亿行;C business-64,16 vCPU / 64 GB,三亿多行。
系统概览 · 出处:ClickHouse 交易明细系统的整体架构
02
1
服务端按分区键把这批行拆开,每个分区单独成一块
2
每块按排序键排序、拆成列、逐列压缩(默认 LZ4,约 1 MB 一个压缩块)
3
写进一个新目录,这个目录就是 part;稀疏主键索引也写在里面
4
复制表提交前去 Keeper 领块号、按哈希或 token 查重、记一条复制日志
part 写完就不再改。后面的事都从这一条来:重复为什么会直接落库,去重为什么要等合并,改一行为什么要给整个 part 出新版本,修数据为什么按分区整体替换。
lab 上新写的一个 Wide part 目录
20260310_0_0_0/
分区 ID、起止块号、合并层级
k.bin k.cmrk2
每列一个压缩数据文件,加一个 mark 文件
primary.cidx
稀疏主键索引,每个 granule(8192 行)一条
minmax_d.idx
分区列的最小值和最大值
count.txt columns.txt
行数、列清单、校验和等元数据
sink 每批每个 Kafka 分区一个 part,一条 INSERT 只有十几到几十行。B 档三副本合计每秒约 124 个块、每块约 19 行;C 档每秒约 40 个块、每块约 90 行。
小于 10 MiB 的 part 用 Compact 存法(所有列放一个文件),sink 写进来的小 part 都是;大的用 Wide(每列一个文件)。
基本原理 · 出处:ClickHouse 的写入路径(clickhouse-lab 实验 01、03、11)
03
分区 20260310 里刚写进来的 6 个 part(层级 0,块号 0 到 5)
按排序键归并成一个大 part,层级加 1(例如 20260310_0_5_1)
被合掉的 part 标成非活动,默认 8 分钟后删文件;一次最多合 100 个,合到压缩后约 150 GiB 就不再往上合。合并要花 CPU 和磁盘 IO,和查询、写入抢资源。
改数据同样以 part 为单位。mutation 给涉及的 part 出一个新版本:Wide part 只重写改到的列,Compact part 整个重写;键列不能改,没有 UPSERT。
分区键按结算日,一天一个。ATTACH、REPLACE、DROP PARTITION 都以分区为单位,本地盘上只挂硬链接;文档建议分区别超过约一千个。
单分区活跃 part 到 1000(parts_to_delay_insert)开始拖慢,到 3000(parts_to_throw_insert)报 Too many parts。lab 把阈值压到 20 和 25:先被拖慢 5 次,第 26 条才被拒。中间那段就是预警窗口。
ReplacingMergeTree 把排序键相同的行只留一行,AggregatingMergeTree 把中间状态合成一行,都是合并时才做。没合并到的重复,普通查询照样读得到。
基本原理 · 出处:ClickHouse 的写入路径(clickhouse-lab 实验 01、08、11)
04
log/ 复制日志 · blocks/ 去重标记 · block_numbers/ 块号
写好 part,在 log/ 里记一条;part_log 记 NewPart。默认在这里提交就返回。
把日志拉进自己的 queue,执行 GET_PART 把 part 拉过来;part_log 记 DownloadPart。
同 ch2。MERGE_PARTS 各自在本地合,REPLACE_RANGE 是换分区。
三个副本各自照着 Keeper 里的复制日志执行(虚线:只传元数据)
ch2、ch3 各自从 ch1 经 9009 端口拉 part(实线:传数据)
INSERT 在收到它的那个副本上提交就返回(insert_quorum 默认 0),ALTER 默认只等当前副本做完(alter_sync = 1)。三个副本之间会短暂不一致。
Aiven 的连接随机落到任一节点。所以核对行数、看复制队列、确认 mutation 做完,一律用 clusterAllReplicas 问遍三个副本。
基本原理 · 出处:ClickHouse 的写入路径(clickhouse-lab 实验 03、12)
05
分区:条件里带分区键或 _partition_id,别的日分区整个跳过
part 的最值:settle_ms 范围对不上的 part 整个跳过
稀疏主键:每 8192 行一条,只读命中的 granule
跳数索引:minmax 和 bloom_filter 再筛一遍
只读用到的列,整块解压后按列计算
命中的 granule 怎么在文件里找到:每列一个 mark 文件,记着这个 granule 在哪个压缩块、块里的什么位置。位置不放进主键索引,索引才小到能常驻内存,用不到的列也不用读它的 mark。
这张表 PRIMARY KEY (settle_ms, id) 只取了排序键的前缀,rev 只影响同一个 (settle_ms, id) 内部的顺序。
344,064 行
lab 实验 14:一千万行、14 个 part 的表,按排序键查 50 秒的数据,命中 6 个 part × 7 个 granule,一千万多行里只读了这些。
主键对排序键的第一列最有效。这张表以毫秒时间戳 settle_ms 开头,按时间范围查最省;只按 id 单查,前一列基数太高排除不掉 granule,要靠 bloom filter,或者带上时间范围。
跳数索引只在值不散在每一段里时有效。id 每行不同,bloom filter 跳得动;tenant_id、currency_id 这类低基数、分布偏斜的维度列,大租户几乎每段都出现,很难跳过什么。
查得好不好怎么看:EXPLAIN indexes = 1 逐层列出裁掉多少;query_log 里 read_rows 和 result_rows 的比值,读了几百万行只返回几行就是前面几层没起作用。
基本原理 · 出处:ClickHouse 的查询路径和 FINAL 的代价(clickhouse-lab 实验 14)
06
Kafka topic,8 个分区
P0
P1
P2
P3
P4
P5
P6
P7
一次 poll(最多 500 条)把记录按 topic-partition 分组
最多 8 条 INSERT,每条带一个 token:insert_deduplication_token = topic-partition-minOffset-maxOffset。同步插入,一条十几到几十行。
ClickHouse:连接随机落到一个副本;服务端按 token(没有 token 时按块哈希)做块级去重,认出来的重复块直接跳过。
B 档一天约 2.05 亿行,峰值每秒约 1 万条;offset 默认每 60 秒提交一次。
超时和重投怎么发生(按 v1.3.9 源码和 Kafka 3.7 的 WorkerSinkTask)
INSERT 外面没有重试循环。插件的 retryCount 只管 ping 和 query;写超时被包成 RetriableException 交给框架,由框架重投。
30 秒是 clickhouse-java V1 client 的默认 socket_timeout,不是插件的 timeoutSeconds(那只管 ping)。想改它,配置里唯一能碰到的键是 jdbcConnectionProperties。
重投要等到下一个 offset 提交点,最长 offset.flush.interval.ms = 60 秒。所以一轮失败写出的两份,间隔落在 (30, 90] 秒;现有集群见过的 124 秒是两轮。
重投的是内存里保留的同一批:记录、顺序、边界、token 都和第一次一样。能不能被认出来,全看服务端的去重窗口还记不记得它。
Kafka sink · 出处:ClickHouse 里的重复行来自 Kafka Connect 超时重投
07
| 入口 | 怎么产生 | 见过的情形 | sink 的 exactlyOnce 挡得住吗 |
|---|---|---|---|
| producer 重试 | 请求其实写进了 broker,应答在抖动里丢了,重试又写一份;topic 里是两条 offset 不同的记录 | 现有集群:一个 45 分钟窗口多出一百多行,每个键正好两份 | 挡不住:对 connector 来说都是新数据 |
| sink 超时重投 | INSERT 过了 30 秒 client 超时,但服务端已经提交;框架把同一批原样再交一次 | 现有集群:234 行,写入间隔 32 到 124 秒 | 挡不住:状态停在 BEFORE_PROCESSING,原样重插,仍靠去重窗口 |
| task 重启重放 | 重启前没来得及提交 offset,接替的 task 从上一个提交点重读,批次边界会变 | lab 实验 21 造过 | 挡得住;但默认配置下崩溃重启常停在 State MISMATCH |
| 人工补发 | 发送超时的记录先存进重试表,事后补发时其中一部分其实早已写进去 | 补发前先按键查了 ClickHouse,没补出新的重复 | 挡不住:补发前按键查 |
1000 个块
25.3 默认的 replicated_deduplication_window:Keeper 只记最近 1000 个插入块的 token 或哈希,按块数算,不按时间
124 块/s
B 档三副本合计的建块速率,从 part_log 里 error = 0 的 NewPart 数出来;C 档约 40 块/s
8 秒 < 32–124 秒
1000 ÷ 124 ≈ 8 秒就把那批挤出窗口,而重投最快也要 32 秒才来。token 和内容都没变,输在记不住
Kafka sink · 出处:重复行的四个入口和 sink exactlyOnce 的覆盖范围;ClickHouse 的块级去重窗口
08
方案 · lab 验过,未上线
| 层 | 做法 | sink 超时重投 | task 重启重放 | producer 重试 |
|---|---|---|---|---|
| 入口 | 块级去重窗口调大(B 档 2 万、C 档 1 万,按建块速率 × 最长重投间隔算);核实 producer 幂等 | 窗口够大就拦得住 | 拦不住,token 变了 | 幂等只防会话内重试,超时后应用再发的防不住 |
| 存储加查询 | 明细表换成 ReplicatedReplacingMergeTree(create_time),键不变 (settle_ms, id, rev),读带 FINAL 或 profile 设 final = 1 | 能 | 能 | 能 |
| 派生数据 | sink 的写入带 deduplicate_blocks_in_dependent_materialized_views = 1;报表按天对账、从明细 FINAL 重算 | 物化视图去重能拦 | 只能靠重算 | 只能靠重算 |
| 修复 | 已经落库的重复,用快照加 REPLACE PARTITION 的 runbook 清 | 事后清 | 事后清 | 事后清 |
lab 用真的 Connect 测过:它挡得住批次边界变了的重放,挡不住同一批原样重投;打开后崩溃重启默认停在 State MISMATCH,只开 errors.tolerance=all 不配 DLQ 还会丢一段。根子是 ClickHouse 不能把 offset 和数据放进同一个事务提交。
报表金额只能做到最终一致:窗口外的重复会先让报表多算,等对账、重算之后才改回来。要求任何时刻都不能多算,就得走写入前去重(OpenMeter 那种自写消费端加 Redis 去重),这个方案没有设计这一条。
Kafka sink · 出处:ClickHouse 明细表的去重方案(clickhouse-lab 实验 14、21、23、24、25、27)
09
SELECT id, settle_ms, amount, status
FROM events FINAL
WHERE _partition_id = '20260310'
AND settle_ms >= {from_ms}
AND settle_ms < {to_ms}
AND tenant_id = {tenant}
ORDER BY settle_ms LIMIT 200
示意写法。FINAL 只在明细表换成 ReplacingMergeTree 之后才需要;现有集群的表还是 ReplicatedMergeTree。
1. _partition_id 把日分区写死,settle_ms 范围是排序键首列,主键索引靠它在这一天里跳 granule;去掉它就要把整个日分区扫一遍。
2. 只按 id 查单笔,靠 id 上的 bloom filter;最好让调用方同时带上结算时间范围。tenant_id 这类低基数条件基本跳不掉数据。
3. 大范围反复扫的报表类查询,改读增量聚合好的报表表,别每次对明细带 FINAL 现算。
FINAL 的代价(lab 实验 14:一千万行窄表混进 50 万行重投,停掉 merge,M2 Pro)
| 查询 | 不带 FINAL | 带 FINAL |
|---|---|---|
| count() | 1 ms,读 1 行 | 12 ms,读 1080 万行 |
| 按排序键查一个范围 | 4 ms,读 34.4 万行 | 9 ms,读 49.2 万行 |
和查询里手写去重比(重叠集中在前 10 万个键的那张表)
| 去重方式 | 耗时 | 内存 |
|---|---|---|
| FINAL | 27 ms | 17.13 MiB |
| 手写 GROUP BY | 169 ms | 1.01 GiB |
| 手写 LIMIT 1 BY | 1287 ms | 1.15 GiB |
FINAL 只归并互相重叠的键区间,代价跟着重叠范围走;按时间排序、只重投最近数据的表,重叠天然集中,落在便宜的一头。绝对耗时带不到现有集群,倍数关系也要在两三亿行的宽表上再测。
Kafka sink · 出处:ClickHouse 的查询路径和 FINAL 的代价(clickhouse-lab 实验 14)
10
方案 · lab 验过,未上线
ReplicatedReplacingMergeTree(create_time),按结算日分区,键不变。
插入时的触发器:只看这一次新插入的块,看不到之后的合并、mutation 和删除。代价和写入量成正比。
ReplicatedAggregatingMergeTree;UTC 30 分钟桶 × 维度,按天分区,和明细的日分区一一对应,修一天只换一个分区。
toDate(bucket, 'Asia/Shanghai'):日报、周报、月报都从 30 分钟桶现算,读时一律带 GROUP BY 再聚合一遍。
同一张上海时区的日报,三天全读(lab)
3,275 行 · 2 ms · 4.04 MiB
读报表表:行数 = 桶数 × 当天的维度组合数,不跟着明细涨
1,175,832 行 · 18 ms · 24.62 MiB
从明细 FINAL 现算:读整天的明细
指标怎么存:可加的用 SimpleAggregateFunction(sum, …),去重用户数写 uniqExactState、读 uniqExactMerge;不用近似的 uniq,对账口径修不回来。
桶按业务时间 settle_ms 切、按 UTC 切,不按 create_time:重复的几份落在同一个桶里,明细的一天正好对应报表的一个分区。明细和报表必须按同一个时区切天,每套部署先查 SELECT timezone()。
时区实测(实验 25):+8:00、+5:30、−5:00 连夏令时那天都逐天一致;+5:45 的加德满都日界落在桶中间,对不上,要 15 分钟桶,报表行数翻倍。
报表 · 出处:用物化视图做 ClickHouse 交易报表(clickhouse-lab 实验 24、25)
11
方案 · lab 验过,未上线
实验 25:三天各 30 万行,逐种注入后看报表偏多少
| 注入的情形 | 明细 FINAL | 报表 | lab 上偏了多少 |
|---|---|---|---|
| sink 超时重投:同一批同一 token,窗口还记得 | 对 | 对 | 不偏 |
| task 重启重放:边界和 token 都变了 | 对 | 多算 | 700 行,金额 360491.71 |
| producer 重试:Kafka 里存了两份 | 对 | 多算 | 和下一行合计 130 行,金额 64191.68 |
| 改数据:按 Replacing 惯例重发一条新版本 | 新值 | 新旧两份都算 | 同上 |
| 人工补发的正常迟到数据 | 对 | 对 | 不偏 |
明细这边按键折叠之后是对的,报表上的偏差只能靠重算改回来;对明细 OPTIMIZE … FINAL 也救不了。
1. 每天对账。报表按桶聚合,和明细 FINAL 按同样的桶现算,FULL JOIN 逐桶比,只输出偏了的天。lab 上准确找出了被注入的那两天,补发过正常迟到数据的那天不在其中。覆盖最近两周,人工补发会写进十几天前的分区。
2. 重算一天。建临时表;同一会话里 SYNC REPLICA 后从明细 FINAL 重算这一天(max_threads = 2);查 part_log 过闸,t0 之后这一天在任一副本上不能有新 part;REPLACE PARTITION 带 alter_sync = 2;换完再过一次闸。
3. 闸不能省。lab 上临时表写好之后迟到 50 行,不过闸直接换,报表反而少了 50 行、金额少 23939.59;过闸拦下、重来一次,前后两批 100 行都在。
4. 窗口内的重投不多算。所有写入一律带 deduplicate_blocks_in_dependent_materialized_views = 1(25.3 默认 0,26.2 起默认 1);报表表的窗口不能比明细表小;25.3 上不能和 async_insert 一起开。
报表 · 出处:ClickHouse 报表的对账和重算(clickhouse-lab 实验 24、25、26)
12
| 情形 | 表现(lab 实验 11、12) | 怎么认、怎么处理 |
|---|---|---|
| Keeper 停掉 | 副本立刻转只读,读照常;INSERT 按默认参数重试约 143 秒后报 Code 242 TABLE_IS_READ_ONLY;Keeper 回来 2 到 12 秒内自愈,被拒的那几条不会补写。 | 写入全停但数据不坏。242 在 connector 可重试的错误码里,这批留在内存等下一个提交点重投,表现是消费组 lag 上涨。 |
| Keeper 卡住 | 约 10 秒后才转只读;客户端 30 秒超时断开;Keeper 回来后服务端把那条 INSERT 照样提交,query_log 里是正常的 QueryFinish。 | 更麻烦的一种:客户端以为失败、服务端其实成功,框架接着重投就是重复行的起点。判断一批写没写进去以 part_log 的 NewPart 为准。 |
| part 过多 | 单分区活跃 part 到 1000 开始拖慢(DelayedInserts 计数上涨,lab 里 INSERT 中位耗时 7 ms 变 311 ms),到 3000 报 Code 252 Too many parts;平均 part 超过 1 GiB 的分区不受这道闸管。 | 盯 part 数,不盯错误率。常见起因是停了 merge 的迁移实验、一条大 INSERT … SELECT 一次切出几百个 part;merge 放回去、part 合掉,写入马上恢复。 |
| 并发查询到上限 | Aiven 在 profile 里锁了每个节点的并发查询数(A、B、C 档分别 100、375、200),INSERT 也算在内,超出报 Code 202 TOO_MANY_SIMULTANEOUS_QUERIES。 | 查询里改设置绕不过去。重算、大范围聚合这类查询限 max_threads,别和 sink 抢;202 同样在可重试列表里,不丢数据。 |
读一直是好的,只看查询成功率发现不了 Keeper 出事。把超时调长只能让超时少一些:Keeper 卡得比新的超时还久,服务端照样晚到提交、框架照样重投,下游没有按键幂等那一层,重复还是会出现。
运维 · 出处:ClickHouse 的 Keeper 故障和 part 过多(clickhouse-lab 实验 11、12)
13
| 项 | 查法(多副本一律走 clusterAllReplicas) | 线 |
|---|---|---|
| 单分区活跃 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 秒超时的八成);另一个 region 已经到 24.4 秒 |
| 副本只读 | system.replicas.is_readonly,告警用窗口内的最大值(max_over_time) | 窗口内最大值为 1;瞬时采样会漏掉只闪一下的只读 |
写出重复行的那 3 小时:吞吐看着正常,readonly_replica 瞬时值是 0,只在 30 分钟窗的 max_over_time 里闪了两下;Keeper 在途请求从常态的 5 到 15 涨到 3,415、6,256、2,157、1,748。前一天持续变慢 11 小时、吞吐掉到常态的 7%,反而一行重复都没有。
先做一件事:part_log 和 query_log 现在只留约 4 天,事故之后能查的证据很快就没了。把这两张表的保留期拉长,是所有排查能继续下去的前提。
运维 · 出处:ClickHouse 的 Keeper 故障和 part 过多;重复行来自 Kafka Connect 超时重投
14
少了:不报错,往往很久之后才发现
1. 先对上游和 Kafka 的量,对不上就是 producer 或 broker 的事。
2. 看 connector 的错误处理配置,坏批次去了哪(下表)。
3. 看 committed offset 和 log-end offset 的差:一直涨是写不进去,不动是卡住,越过去了就去 DLQ 或源 topic 找。
4. INSERT 真写进去没有,以 part_log 里 error = 0 的 NewPart 为准;query_log 有两个盲区:晚到提交的可能没有结束记录,被去重拦下的照样记 QueryFinish。
5. 可能写进了相邻分区:按 _partition_id 在前后几天反查。
不对:行数对得上,金额或人数差一截,全程不报错
1. 两边比的是不是同一条时间轴:事件时间还是写入时间,按哪个时区切天。UTC 16 点到 24 点的时刻,在 UTC 和上海两种列声明下会落进相邻两天的分区。
2. JOIN 有没有放大:右表键不唯一,INNER 和 LEFT 都翻倍;ANY 留哪一行由 join_any_take_last_row 定,要确定性就自己 LIMIT 1 BY。
3. 去重计数是不是 uniq:25.3 上误差两个方向都有,对账只用 uniqExact。
4. 上游改了字段名:schemaless JSON 不校验,那一列静默变成默认值,按天数 countIf(列 = 0)。
5. 读 ReplacingMergeTree 带没带 FINAL;6. 报表和明细 FINAL 逐桶对一遍。
| connector 的配置(lab 实验 09) | 一条坏记录进来之后 | 少了的数据在哪 |
|---|---|---|
| errors.tolerance=none(默认) | task 约 2 秒后 FAILED,它管的分区不再前进,重启还是停在这条上 | 都还在 Kafka 里,信号是 task 状态和消费组 lag |
| errors.tolerance=all 加 DLQ | task 不停;坏记录所在分区的那一整批连同好记录一起进 DLQ | DLQ topic,补数按整批重放 |
| errors.tolerance=all 不配 DLQ | task 不停;那一整批静默消失,offset 照常往前提交 | 只剩源 topic 里那一份,task 和 lag 都看不出来,最危险 |
运维 · 出处:ClickHouse 数据少了的排查顺序;数字对不上的排查顺序(clickhouse-lab 实验 09、10)
15
不要这样做:轻量删除不带 IN PARTITION(全表每个 part 都出新版本,两千多个 part 要把窗口读一千遍左右);OPTIMIZE … FINAL DEDUPLICATE(留哪行由合并定,没有回滚);多表 RENAME 当原子切换(不原子,用 EXCHANGE);不排空就切换(正在跑的 INSERT 绑定的是旧表)。
运维 · 出处:REPLACE PARTITION runbook;用只读权限评审补数方案;在线换引擎(clickhouse-lab 实验 08、18、19、20、27)
16
| 删法 | 上游 25.3 默认 | Aiven 上的现有集群 |
|---|---|---|
| DETACH PARTITION | ATTACH 回来,可逆 | 同左,不算删除 |
| DROP PARTITION | 只能靠事先 FREEZE 的备份 | 只有备份 |
| DROP TABLE 不加 SYNC | 480 秒内能 UNDROP | 宽限期是 0,没有 UNDROP |
| DROP TABLE … SYNC | 救不回来 | 救不回来 |
| 删超过 50 GB 的表或分区 | 报 Code 359 被拦下 | 大小保险是 0,不拦 |
Replicated 库里一条 DROP 删掉三个副本。备份由 Aiven 做,还原是从备份 fork 出一个新服务再切连接,上一次备份之后的数据靠上游重发。所以删之前的确认比删之后的补救重要:runbook 里的工作表、换引擎后的旧表,留到对账和下游确认之后再删,删之前再核一次表名和大小。
复制表每次 INSERT(每个分区一个 part)和每次 merge,都在 log/ 下建一个顺序节点;序号来自父节点上的 int32 计数器,删日志不减。按每秒 100 个序号约 248 天用完,每秒 1000 个不到 25 天,小批量高频写入的表最先到顶。
到 2147483647 之后:INSERT 全报 Code 244 UNEXPECTED_ZOOKEEPER_ERROR,Node exists,path 指向 log/log-;merge 分配不出来;读照常;换副本、重启副本都没用。
怎么确认:system.replicas 的 log_max_index 停在 2147483647。zkCli 里的 cversion 是「计数器 × 2 − 子节点数」,过了 2 的 30 次方会显示成负数,别直接读。
恢复只用 SQL:照原表建新表(路径带 {uuid},新路径的 log/ 从 0 起)→ ATTACH PARTITION … FROM 逐分区挂过去 → 各副本核行数 → EXCHANGE TABLES → DROP 坏表 SYNC。到顶期间失败的 INSERT 要由写入方重写。
预防:攒批或 async_insert 减少写入次数(lab 上 20 次并发同步 INSERT 涨 21,async_insert 只涨 1);把 log_max_index 加进巡检。
运维 · 出处:ClickHouse 误删表和分区之后的恢复;ZooKeeper 顺序节点计数器溢出导致复制表无法写入
17
1
part 写完不再改,没有 UPSERT。改数据按 part 出新版本,修数据按分区整体替换:执行前能验、执行后能回滚。
2
这条链路只能做到至少一次。入口的去重窗口是尽力而为,正确性靠存储层按业务键折叠、读带 FINAL(方案)。
3
物化视图是插入触发器,看不到合并。报表只能最终一致,要能按桶对账、能按天重算,重算要过 part_log 的闸。
4
三副本多主异步。核对一律 clusterAllReplicas 问遍三个副本,换分区带 alter_sync = 2,一批写没写进去以 part_log 为准。
5
前兆在 Keeper 在途请求、复制队列和 INSERT 耗时尾部,不在错误率。part_log 和 query_log 只留 4 天,先把保留期拉长。
现状:ReplicatedMergeTree 明细表,托管 sink 至少一次写入,聚合表按 create_time 的窗口增量累加、不按键去重。
方案(lab 验过,还没在现有集群上执行):换成 ReplacingMergeTree 加 FINAL、调大去重窗口、物化视图加对账重算、清理和换引擎的 runbook。
小结
18
博客 ClickHouse 系列,按架构篇末尾的阅读顺序
存和查
ClickHouse 交易明细系统的整体架构 · ClickHouse 的写入路径 · ClickHouse 的查询路径和 FINAL 的代价
报表
用物化视图做 ClickHouse 交易报表 · ClickHouse 报表的对账和重算
重复行,从哪来、怎么防
ClickHouse 里的重复行来自 Kafka Connect 超时重投 · ClickHouse 的块级去重窗口 · ClickHouse 重复行的四个入口和 sink exactlyOnce 的覆盖范围 · ClickHouse 明细表的去重方案 · 在线把明细表换成 ReplicatedReplacingMergeTree
重复行,已经写进去了
清理 ClickHouse 重复行的 REPLACE PARTITION runbook · 清理 ClickHouse 重复行时行数校验拦不住的情形 · ClickHouse 轻量删除清重复行的代价 · 用只读权限评审 ClickHouse 补数方案
其他异常
ClickHouse 数据少了的排查顺序 · ClickHouse 数字对不上的排查顺序 · ClickHouse 的 Keeper 故障和 part 过多 · ClickHouse 误删表和分区之后的恢复 · ZooKeeper 顺序节点计数器溢出导致 ClickHouse 复制表无法写入
clickhouse-lab(github.com/meirongdev/clickhouse-lab):ClickHouse 25.3.14.14 三副本加 Kafka 3.7.0 和 connector v1.3.9 的本地实验台,experiments/ 下的脚本各自带断言,results/ 是实跑输出;本 deck 里标「实验 NN」的数字都出自这里。它能验机制,验不了真实量级、对象存储和托管层的行为。
一手资料:ClickHouse 团队 2024 年 VLDB 论文「ClickHouse - Lightning Fast Analytics for Everyone」(存储第 3 章、查询第 4 章)· 官方文档 MergeTree、Replication、Deduplicating inserts on retries、Incremental materialized view、ALTER … PARTITION · ClickHouse Kafka Connect Sink 文档与 v1.3.9 源码、Kafka 3.7 的 WorkerSinkTask · Aiven for ClickHouse 的服务架构、tiered storage、system tables 文档。
参考
19