
写 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;命令敲下去之后终端直接卡住CtrlC 都救不回来。过了好几分钟才返回了一个类似这样的报错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 1replica_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-stringlocalhost: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_timeout60/distributed_ddl_task_timeout distributed_ddl_output_modethrow/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 的副本数持续 0ZK 连接状态zookeeper_exception非空次数持续 0DDL 队列积压/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 队列答案往往就在那里。