☰
ClickHouse ON CLUSTER 删表卡住?分布式DDL故障排查与恢复指南
2026/10/12 3:35:49 网站建设 项目流程

写 ClickHouse 的人,十有八九都躲不过 ON CLUSTER 删表这个坎。平时一条DROP TABLE IF EXISTS xxx ON CLUSTER 'cluster_name'敲下去,几秒钟就返回,感觉比本地删表还省心。但一旦赶上 ZooKeeper 抖动、某个副本悄悄挂了,这条命令就会变成一把悬在头上的刀——要么卡住不动,要么报一堆看不懂的错误,更麻烦的是集群里有的节点表没了、有的节点表还在,整个元数据状态变成一锅粥。

这篇文章就把这事彻底讲明白。我会从分布式 DDL 的调度原理讲起,沿着一次真实故障的排查链路走一遍,最后给出几种不同场景下的恢复手段和日常预防参数。不管你是刚接触 ClickHouse 的初学者,还是已经被 ON CLUSTER 折磨过的运维老手,这篇都能帮你省下几个熬夜排查的晚上。

1. ON CLUSTER 删表的调度流程与依赖

1.1 分布式 DDL 的调度链路

先明确一个容易被忽略的事实:ClickHouse 里的ON CLUSTER并不是直接把一条 DDL 广播给所有节点执行,而是把这条语句当作一个任务,先写到 ZooKeeper(或者 ClickHouse Keeper,下面统一叫 ZK)的某个队列路径下,然后由集群内每个节点上的DistributedDDLWorker后台线程去拉取并执行。

这条队列路径一般是/clickhouse/task_queue/ddl。当你在任意一个节点执行DROP TABLE ON CLUSTER时,该节点会生成一个带唯一标识的 DDL 任务节点,写入这个队列,这个节点本身扮演的就是“协调发起者”的角色。其他节点上的后台线程会发现队列里有新任务,于是各自把它拉下来,在本机执行对应的 SQL。

这里有个关键点:虽然语句是在一个节点上发起的,但任务对所有节点是公平可见的。只要 ZK 正常、节点和 ZK 之间的会话没断,理论上所有存活节点都会各自执行一次删表操作。这个机制的好处是天然并发,坏处是——只要有一个节点和 ZK 会话出问题,它就不会执行这个任务,于是整个环境就变得不一致。

1.2 本地副本删除时依赖哪些 ZK 路径

DROP TABLE在语义上分成两步。第一步是从元数据里把表定义干掉,第二步是删掉本地数据目录里的物理文件。如果是ReplicatedMergeTree系列的表引擎,还会涉及和 ZK 的交互,因为副本注册信息是存在 ZK 里的。

具体涉及的关键路径大致有:

  • /clickhouse/tables/{shard}/{uuid}或旧版本里的/clickhouse/tables/{database}/{table},存放的是表级别的副本注册信息,包括每个副本的元数据版本、日志指针、活跃状态等。
  • /clickhouse/task_queue/ddl,就是前面说的分布式 DDL 任务的队列目录。
  • /clickhouse/session,节点与 ZK 建立会话的临时节点路径,会话过期的话这个路径下会有残留,也会影响副本状态判定。

一个 ReplicatedMergeTree 表在删表时,会先从 ZK 里删除自己这个副本对应的注册节点,然后清理本地的 metadata 文件、WAL、数据目录等。如果这些步骤中任何一环在 ZK 会话层面失败,那么本地文件可能删了一半,ZK 里的注册信息也可能还在,那一半的状态就非常难受。

1.3 DDL 任务超时与返回机制

distributed_ddl_task_timeout这个参数控制的是发起点等待集群内其他节点执行 DDL 任务的超时时间。默认值是 180 秒。这个参数不是“只要超时就失败”,而是超时后发起点会主动放弃等待,只返回当前已经收到响应的节点列表。

新版本里还有一个distributed_ddl_output_mode参数,用来控制返回结果的展示方式。比如none表示不等待响应直接返回,throw表示只要有一个节点报错就抛出异常。实际使用中很多人会遇到“命令执行了,但是报错说某节点没响应”,这大概率就是这个超时机制在起作用——任务还在 ZK 队列里没被执行,但发起点已经不想等了。

理解了这条链路,下面看故障现象就清楚多了。大概率不是 SQL 语法的问题,而是这个分布式调度链路里某个环节断了。

2. 故障现象与根因库

2.1 常见的故障表现分类

根据我遇到过的情况,DROP TABLE ON CLUSTER的故障大致能分成三类。

第一类是卡住不动。命令敲下去之后一直不返回,既不报错也不成功。这种一般有两种可能:要么发起点连不上 ZK,要么 ZK 里的 DDL 队列任务在等待某个不可用节点的响应,而那个节点已经联系不上了。

第二类是直接报错,错误信息五花八门。最常见的有Code: 342. DB::Exception: The replica is not active、All replicas are lost、Cannot drop table because it is in readonly state等等。这些错误翻译成人话就是:这个表有副本,但是副本的 ZK 会话已经断了,或者副本自己都觉得自己已经不健康了。

第三类最隐蔽,命令返回成功,但集群状态不对。有的节点表删了,有的节点表还在,甚至有的节点上的表变成了只读状态。这种问题最坑的地方在于,你以为删完了,实际上某个分片的数据还在磁盘上占着空间,后续重新建表还可能因为元数据残留而失败。

2.2 根因库速查表

结合实践经验,我把常见的根因整理成一个速查表,排查的时候可以先对照一下。

故障现象核心根因关键排查点
命令卡住不返回ZK 会话异常,或者某个节点失联system.replicas、ZK 节点状态
报 replica is not active某个副本的 ZK 会话过期system.replicas.is_active字段
部分节点成功部分失败失败节点当时和 ZK 断连DDL 队列残留任务
表变成 readonly元数据和 ZK 状态不一致system.replicas的 readonly 字段
重新建表失败本地 metadata 残留检查 metadata 目录
一直显示 deleted 状态DDL 任务已标记删除但未清理ZK 任务队列残留

2.3 为什么不同节点状态会不一致

ClickHouse 的集群一致性并不是“强同步”的,它依赖 ZK 这个外部协调者来达成最终一致。每个节点都有自己的本地元数据,ZK 里存的是集群维度的公共状态。两者之间靠会话和心跳维持同步。

当某个节点的 ZK 会话超时后,这个节点上的副本会被标记为readonly,它不再接收写入,也不再参与副本同步。但它本地的元数据文件并不会自动消失。这个时候你发起一个DROP TABLE ON CLUSTER,健康节点正常执行删除,但这个断连节点不响应 DDL 任务,于是整个集群的元数据就不一致了。

更麻烦的是,如果这个会话长期没有恢复,ZK 里的副本注册信息也会变成失联状态。此时即使你手动在这个节点上执行本地DROP TABLE,也会因为 ZK 状态校验不过而报错。这就是为什么很多人最后只能选择停掉节点,手动清理元数据文件才能恢复。

3. 完整排查实录:从故障到定位

3.1 一次典型的 Drop On Cluster 卡住故障

之前一个业务团队遇到的情况非常有代表性。某天他们在例行清理过期分区时,执行了一条:

DROP TABLE IF EXISTS ods_order_temp ON CLUSTER 'cluster_01';

命令敲下去之后,终端直接卡住,Ctrl+C 都救不回来。过了好几分钟才返回了一个类似这样的报错:

Received exception from server (Version 23.8.1): Code: 159. DB::Exception: Timeout exceeded while waiting for the DDL task to be executed on servers.

这个报错翻译过来就是:发起节点已经把 DDL 任务写进 ZK 了,也等了一段时间,但有一些服务器没有在超时时间内确认执行完成。

当时我第一反应是去看system.replicas,因为删表卡住的本质往往是副本状态不健康,而不是 SQL 本身的问题。

SELECT database, table, is_readonly, is_session_expired, zookeeper_exception, replica_is_active FROM system.replicas WHERE database = 'default';

结果非常直观:其中某个分片的一块副本is_session_expired = 1,replica_is_active = 0。也就是说,这个副本和 ZK 之间的会话已经过期了,它完全不知道自己应该执行什么任务。

3.2 顺着 DDL 队列查无用功

确认副本不健康之后,还需要确认 DDL 任务本身的状态。ClickHouse 在较新版本里提供了一个内部表system.distributed_ddl_queue,可以直接查到 DDL 任务的执行情况。

SELECT query, host, status, create_time, cluster FROM system.distributed_ddl_queue ORDER BY create_time DESC LIMIT 10;

从结果里能看到这条DROP TABLE任务的状态是in_process,也就是还在等待中。而正常执行完的任务状态应该是finished。

另外一步值得做的操作是去 ZK 里直接看 DDL 任务队列目录。ClickHouse 提供了system.zookeeper表,可以像查普通表一样查询 ZK 节点内容:

SELECT name, value, num_children FROM system.zookeeper WHERE path = '/clickhouse/task_queue/ddl' LIMIT 20;

这里能看到所有待执行的 DDL 任务节点。正常来说这个目录应该是空的,或者只有极少数正在执行的任务。如果你发现里面堆积了大量历史任务,说明之前有多次删表/建表操作失败过,残留的任务一直没被清理。

3.3 定位到问题副本并验证

确认问题副本后,下一步要判断这个副本还有没有救。当时我直接在一个健康的副本上执行:

SYSTEM SYNC REPLICA ods_order_temp ON CLUSTER 'cluster_01';

结果也是超时,说明不只是当前 DDL 卡住,而是这个分片内部的副本同步链路已经断了。

再回头看system.replicas里的zookeeper_exception字段,里面会出现类似这样的报错:

All connection attempts to ZooKeeper failed

这个字段是排查 ZK 会话类问题的金钥匙。一旦出现这个值,基本可以断定该节点和 ZK 之间的网络链路或者 ZK 自身的会话管理出了状况。

这个时候如果你不死心,想直接在问题节点上执行本地删表:

DROP TABLE IF EXISTS default.ods_order_temp;

大概率也会报错。因为在 ZK 的副本注册信息里,当前节点可能已经不是 active 状态了,本地删表操作过不了校验。

3.4 复盘当时为什么没更早发现

这个案例到最后虽然救回来了,但复盘时发现一个很明显的问题:这个不健康的副本其实已经异常存在了一段时间,如果平时有巡检system.replicas的习惯,早就应该看到is_readonly = 1或者zookeeper_exception不为空。但因为这个表平时读取压力不大,业务侧也没发现异常,直到删表时才踩中。

所以我现在做任何 ON CLUSTER 操作之前,都会先看一眼集群整体副本健康度。与其等 DDL 卡住再去救,不如在动手前就把不健康的副本排除掉。这个习惯帮我避了好几次坑。

4. 分场景恢复方案与强制清理

4.1 场景一:单副本会话过期,表本身可以重建

如果只是会话过期,且表的数据已经不是很重要,最简单的方式是先把不健康的副本清理掉。ClickHouse 提供了SYSTEM DROP REPLICA之类的命令,可以删除本地这副本在 ZK 里的注册信息。

SYSTEM DROP REPLICA 'replica_name' FROM TABLE default.ods_order_temp;

这个命令的作用是告诉 ZK:这个副本我放弃了,请移除它的注册信息。之后再回到这个节点上执行普通建表语句,让副本重新拉取元数据。

注意一个细节:replica_name不是随便填的,它是该节点配置的macros里的replica值。可以用这条 SQL 查到:

SELECT * FROM system.macros;

确认replica的取值后,再执行SYSTEM DROP REPLICA。

4.2 场景二:表数据不重要,直接走强制清理

如果表已经没有任何保留价值,也没必要费劲修复副本,直接走强制清理流程。

步骤一,把所有节点的 ClickHouse 进程停掉。这一步是为了避免在清理过程中有后台线程悄悄改 ZK 或本地元数据。

步骤二,找到 ClickHouse 的数据目录(默认是/var/lib/clickhouse),在metadata/目录下找到对应的数据库文件。比如default库的元数据路径是/var/lib/clickhouse/metadata/default.sql。打开这个文件,搜到ods_order_temp这张表对应的CREATE TABLE语句,手动删掉。

步骤三,在metadata/{database}/目录下,可能会有该表的独立元数据文件,一并删掉。同时检查data/{database}/目录下有没有对应的物理数据目录,有的话也一起删除。

步骤四,再去看 ZK 里该表对应的路径是否还有残留。用之前的system.zookeeper查询方式,先找:

SELECT * FROM system.zookeeper WHERE path = '/clickhouse/tables';

找到对应表路径后,确认没有其他副本还在使用的情况下,可以直接删掉这个 ZK 节点。不过这一步要非常谨慎,最好在确认所有节点都已经停掉之后再操作,否则容易引发其他副本的状态混乱。

4.3 场景三:DDL 队列残留导致新 DDL 永远卡住

还有一种情况:某次 DDL 超时后,那个任务节点一直残留在 ZK 的 DDL 队列里,导致后续所有 ON CLUSTER 操作都要排队,甚至直接卡住。

这种问题的解法相对简单,直接清掉这个残留任务节点。查询/clickhouse/task_queue/ddl,找到那个查询对应的节点名称,在 ZK 客户端里删除它就行。

用 clickhouse-client 操作的话,需要借助system.zookeeper找到精确路径,然后调用zookeeper删除接口。较新版本里 ClickHouse 提供了一个zk命令,可以直接操作:

clickhouse-keeper-client --connection-string=localhost:9181

进入客户端后执行:

rmdir /clickhouse/task_queue/ddl/ddl_query_id_xxx

但说实话,我一般不太推荐在生产环境直接对着 ZK 目录删东西。更稳的方式是确认 DDL 任务确实已经没用了,再重启下出问题的节点。ClickHouse 启动时如果发现 DDL 队列里有尚未完成的任务,会尝试重新执行或标记失败并清理。多数情况下重启节点就能把脏队列带出来。

4.4 场景四:表已经只读但还有历史数据要保留

如果表里还有需要保留的数据,不能直接丢弃,那就得先想办法恢复副本的读写状态。

第一步先排查 ZK 链路,看看zookeeper_exception是否已经恢复。如果 ZK 连接恢复了,直接在只读副本上执行:

SYSTEM RESTORE REPLICA;

这个命令会尝试用本地的元数据状态去和 ZK 里的信息对齐,重建会话状态。执行完后再看system.replicas,如果is_readonly变成 0,说明副本恢复了。

如果执行SYSTEM RESTORE REPLICA失败,或者报错说 ZK 路径不存在,那说明 ZK 里的表注册信息已经被清理过了。这种情况想保留数据,只能先停掉节点,然后把本地的data目录复制出来,再重新建表导入数据。

5. 参数调优与日常预防建议

5.1 调整 DDL 超时与输出模式

经验不足的时候,很多人会直接把distributed_ddl_task_timeout调大,觉得等久一点总能成功。实际上这个策略是错的:调大超时只是让卡住的时间更长,并不能解决问题本身。

我的建议是把这个值调到一个“可接受的等待上限”,比如 60 秒。同时把distributed_ddl_output_mode设置为throw,这样只要集群内有节点反馈执行异常,发起点会第一时间把错误抛出来,而不是默默等超时。

<profile> <distributed_ddl_task_timeout>60</distributed_ddl_task_timeout> <distributed_ddl_output_mode>throw</distributed_ddl_output_mode> </profile>

这样设置之后,删表操作如果遇到问题,会很快暴露出来,而不是卡到地老天荒。

5.2 删表前的副本健康检查三件套

在我自己的运维习惯里,只要涉及 ON CLUSTER 的写操作,尤其是删表这种破坏性操作,动手前必须先过三关。

第一关,看system.replicas,确认所有相关表的副本都处于 active 状态。

SELECT database, table, count() AS total_replicas, sum(replica_is_active) AS active_replicas FROM system.replicas GROUP BY database, table HAVING total_replicas != active_replicas;

这条 SQL 会直接把有非活跃副本的表列出来。只要结果是空的,说明副本层面是健康的。

第二关,看 ZK 的 DDL 队列是否干净。

SELECT count() FROM system.zookeeper WHERE path = '/clickhouse/task_queue/ddl';

这个数量最好常年为 0,偶尔有 1 到 2 个正在执行中的任务也正常。如果积压了几十个,就先排查历史故障原因。

第三关,确认 ZK 本身状态正常。如果用的是 ZooKeeper 集群,直接看监控里的节点连接数、延迟、leader 状态。如果用的是 ClickHouse Keeper,可以用内置的system.keeper相关表检查。

5.3 监控与告警:在故障之前接住它

其实大部分 DROP TABLE ON CLUSTER 故障都是可以提前发现的。关键在于监控里有没有盯住下面几个指标:

监控指标推荐监控内容阈值建议
副本活跃度replica_is_active为 0 的副本数持续 0
ZK 连接状态zookeeper_exception非空次数持续 0
DDL 队列积压/clickhouse/task_queue/ddl子节点数超过 5 告警
只读表数量is_readonly为 1 的表数量持续 0

这些指标在 ClickHouse 的 prometheus 导出接口里基本都有暴露,接入 Grafana 就能做成监控大盘。比起事后救火,提前盯住这几个指标能避免绝大多数麻烦。

5.4 规避性的运维规范建议

最后说几条我在实际生产环境里总结出来的运维规范,每条都踩过坑,写出来给大家避雷。

第一个规范:生产环境禁止直接对 ON CLUSTER 的 DROP 操作不加思考地执行。尤其是大表,先确认这张表是否有下游依赖,是否有备份,是否真的不需要了。可以用SHOW CREATE TABLE看一下表的引擎类型和集群定义,心里先有个底。

第二个规范:执行删除前先备份元数据。最简单的办法是先执行一次SHOW CREATE TABLE,把结果保存到本地。万一删表过程中元数据残留导致建表失败,有这条语句就能快速手工重建。

第三个规范:ZooKeeper 或 ClickHouse Keeper 的会话超时相关参数不要随便改。调大了会导致副本故障发现变慢,调小了又会造成误判。默认值通常是合理的,除非你有充分的理由,否则不要动它。

第四个规范:千万不要在集群内单个节点上直接执行不带 ON CLUSTER 的DROP TABLE。这在 ReplicatedMergeTree 表上往往不会成功,就算侥幸成功,也会立刻被其他副本把元数据同步回来,造成状态混乱。

最后再分享一个实操小技巧

如果你发现某个 ON CLUSTER 语句卡住了,但又不确定到底卡在哪个节点上,可以在执行新语句之前先手动清理一下本地 DDL 队列里的历史任务。

SYSTEM FLUSH DISTRIBUTED DDL;

这命令会把本地已经完成但还没上报的 DDL 任务状态刷新到 ZK,能让后续状态查询更准确。虽然它不能直接解决卡住的问题,但能让你的排查视野更干净。

我个人的体感是,ClickHouse 的 ON CLUSTER 机制本身设计得并不复杂,真正复杂的是它依赖的外部协作者——ZK。大部分故障都不是 ClickHouse 自身坏了,而是它和 ZK 之间的关系出了问题。所以下次再遇到 Drop Table On Cluster 的故障,先别急着怀疑 SQL 写错了,静下心来查一查副本状态和 ZK 队列,答案往往就在那里。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询