1. Canal 的 ZooKeeper 节点数据损坏了,先别急着重装
Canal 的 ZooKeeper 节点数据损坏,指的是/otter/canal/destinations/{instance}下的 znode 被误删、内容被写乱,或者 ZK 事务日志异常导致节点数据读不出来。表现出来就是 Canal Server 进程还在,但 instance 起不来,日志里刷KeeperException.NoNodeException,或者同步任务卡在某个位点不动。这套东西适合正在用 Canal 做 MySQL binlog 订阅、又用 ZooKeeper 做集群元数据存储的运维和开发同学。
我先把结论放前面:ZK 里的 Canal 元数据不是不可再生资源,绝大多数情况下都能从 Canal 内存、Canal 日志、MySQL binlog 三个地方把位点找回来。真正危险的不是节点丢了,而是你随手用set命令往/cursor里塞了一个错误的位点,导致下游数据重复或者直接跳过一段 binlog。
Canal 在集群模式下默认用ZooKeeperMetaManager管理元数据,核心就三类信息:消费位点 cursor、过滤规则 filter、客户端标识 client。ZK 在这里的角色像一个公共公告板,每个 instance 有自己的区域,Active Server 抢到/running这个临时节点后才往里写进度。公告板上的纸条被撕了,Server 就不知道从哪继续。
下面按“先诊断、再定位、后重建、最后验证”的顺序走一遍,命令都可以直接复制。
2. 前置准备:确认 ZK 健康与 Canal 版本
动手恢复之前,必须先确认一件事:ZK 集群本身是好的,坏的只是 Canal 相关的那几个 znode。如果 ZK 集群自己脑裂或者磁盘挂了,那要先修 ZK,别在应用层瞎折腾。
2.1 用四字命令看 ZK 状态
ZooKeeper 提供了一组四字命令,最常用的是ruok、stat、mntr、cons。它们通过 2181 端口直接发,不需要 zkCli。
# 探活,正常返回 imok echo ruok | nc zk1 2181 # 看角色和连接数,确认谁是 leader 谁是 follower echo stat | nc zk1 2181 # 看关键指标:延迟、znode 数量、watch 数量 echo mntr | nc zk1 2181 | grep -E "zk_avg_latency|zk_znode_count|zk_watch_count|zk_server_state" # 看当前所有客户端连接,确认 Canal Server 有没有连上 echo cons | nc zk1 2181mntr里重点看zk_znode_count,如果这个数比平时骤降,说明确实有节点被删了。cons里如果看不到 Canal Server 的 IP,说明 Canal 根本没连上 ZK,那问题可能出在网络或 ACL,不是节点损坏。
2.2 确认 Canal 版本和 MetaManager 实现
不同版本的 Canal 对 ZK 的重试逻辑不一样。1.1.7 之前对短暂网络抖动容忍度低,1.1.8 优化了重试。先确认版本:
# 看 Canal 安装目录下的版本 cat $CANAL_HOME/conf/canal.properties | grep -i "canal.instance.global.mode" # 集群模式下应该是 zookeeper如果canal.instance.global.mode=zookeeper,说明用的是 ZK 元数据。如果是spring或memory,那 ZK 里的节点损坏跟你的 instance 启动没关系,别搞错方向。
注意:单机模式下 Canal 用
meta.dat本地文件存位点,改 ZK 是无效的。先确认模式再动手。
3. 可复制配置:定位损坏节点并重建 znode
这一步是核心。先看清楚 ZK 里 Canal 的目录结构长什么样,再决定从哪恢复。
3.1 用 zkCli 检查节点结构
# 连接 ZK 集群 zkCli.sh -server zk1:2181,zk2:2181,zk3:2181 # 列出所有 instance ls /otter/canal/destinations # 假设 instance 叫 product_sync,看它的子节点 ls /otter/canal/destinations/product_sync # 正常应该有:running cluster cursor filter client_xxx properties # 如果 cursor 或 cluster 不见了,就是损坏点逐个 get 看内容:
get /otter/canal/destinations/product_sync/running # 期望:{"active":"192.168.1.101:11111","ip":"192.168.1.101","port":11111} get /otter/canal/destinations/product_sync/cursor # 期望:{"journalName":"mysql-bin.000015","position":456789,"timestamp":1713823200000,"gtid":"","serverId":1} get /otter/canal/destinations/product_sync/cluster # 期望:["192.168.1.101:11111","192.168.1.102:11111"]如果get报NoNodeException,说明节点丢了。如果返回的内容是乱码或者 JSON 解析不了,说明数据错乱。
3.2 策略一:从 Canal 内存 dump 回写(首选)
只要 Active Server 进程没重启,它内存里还留着最新位点。Canal 提供了一个dump管理命令,强制把内存位点写回 ZK。
# 找到 Active Server 的 IP 和端口,从 running 节点或进程看 jps | grep CanalLauncher # 发送 dump 命令 echo "dump" | nc 192.168.1.101 11111执行后去 Canal 日志确认:
tail -f $CANAL_HOME/logs/canal/canal.log | grep -i "write cursor" # 期望看到:ZookeeperMetaManager write cursor to zk successfully这个方案最快,但前提是 Active Server 还活着。如果 Server 已经重启,内存位点丢了,走策略二。
3.3 策略二:从 Canal 日志提取位点重建
Canal 每消费一批事件都会在 instance 日志里打印位点。日志路径是logs/{instance_name}/{instance_name}.log。
# 从日志尾部往前找最新的位点 tail -n 2000 $CANAL_HOME/logs/product_sync/product_sync.log | grep -E "cursor:\[" | tail -5 # 典型输出: # pipelineId:1 cursor:[mysql-bin.000015,456789,1713823200000,]拿到位点后,用 zkCli 手动重建节点。注意顺序:先建父节点,再建子节点。
# 在 zkCli 里执行 create /otter/canal/destinations/product_sync "" create /otter/canal/destinations/product_sync/cluster '["192.168.1.101:11111","192.168.1.102:11111"]' create /otter/canal/destinations/product_sync/cursor '{"journalName":"mysql-bin.000015","position":456789,"timestamp":1713823200000,"gtid":"","serverId":1}' create /otter/canal/destinations/product_sync/filter '.*\\..*'serverId必须和 MySQL 主库的server_id一致,否则 Canal 拉 binlog 会报错。filter填你原来的订阅规则,不确定就填.*\\..*表示全库全表。
3.4 策略三:从 MySQL binlog 推断兜底位点
如果 Canal 日志也没了,只能从 MySQL 本身推断。这是最不精确但最安全的方法,代价是可能重放一部分数据。
-- 在 MySQL 主库执行,看当前 binlog 位置 SHOW MASTER STATUS; -- 输出示例: -- File: mysql-bin.000020 Position: 123456 -- 看上一个 binlog 文件的末尾,选一个保守起点 SHOW BINLOG EVENTS IN 'mysql-bin.000019' LIMIT 1 OFFSET 999999999;选一个比当前位点更早的位置,比如上一个文件的末尾,然后按策略二的方式写进/cursor。下游系统必须支持幂等,比如 ES 用主键 Upsert,否则会重复。
4. 验证请求:确认 Canal 恢复拉取 binlog
节点重建完不代表完事,必须验证 Canal 真的能重新拉 binlog 并同步。
4.1 重启 Canal Server 并观察日志
# 重启所有 Canal Server $CANAL_HOME/bin/stop.sh $CANAL_HOME/bin/startup.sh # 实时看 instance 日志 tail -f $CANAL_HOME/logs/product_sync/product_sync.log正常启动的日志顺序应该是:加载 instance 配置 → 连接 ZK → 读取 cursor → 连接 MySQL → 开始拉 binlog。看到类似下面的行就说明通了:
start successful..... subscribe successfully, clientId=10014.2 用 Canal 客户端或 admin 端口验证位点推进
# 通过 admin 端口看 instance 状态 echo "list" | nc 192.168.1.101 11112 # 看具体 instance 的位点 echo "cursor product_sync" | nc 192.168.1.101 11112如果位点数字在持续增长,说明 binlog 在正常拉取。再往 MySQL 写一条测试数据,看下游 ES 或 ClickHouse 有没有同步过去。
4.3 检查 ZK 节点是否被正常刷新
# 回到 zkCli get /otter/canal/destinations/product_sync/cursor # 位点应该比刚才重建时更新了,说明 Canal 在正常回写如果位点一直不动,说明 Canal 卡住了,去日志里找NoNodeException或ConnectionLossException。
5. 本篇常见错排查
恢复过程中最容易踩的坑,我列几个高频的。
报KeeperException.NoNodeException但节点明明存在:多半是 ACL 问题。Canal 用的 ZK 账号没有读权限。用getAcl /otter/canal/destinations/product_sync看权限,必要时用超级用户重新授权。
重建后 Canal 启动报serverId mismatch:/cursor里的serverId和 MySQL 的server_id不一致。去 MySQL 执行SHOW VARIABLES LIKE 'server_id',把值改成一致。
位点写进去但 Canal 不消费:检查/filter节点。如果 filter 是空字符串或者格式不对,Canal 会认为没有表需要订阅,自然不拉数据。filter 格式是库名.表名,多个用逗号分隔,全匹配用.*\\..*。
ZK 里节点建了但重启后又被删:/running是临时节点,Server 断开后 ZK 会自动清理,这是正常的。但/cursor、/cluster、/filter是持久节点,不该被自动删。如果持久节点也消失,检查是不是有人配了错误的 cleanup 策略,或者 ZK 的autopurge配置误伤。
Canal 日志刷ConnectionLossException但 ZK 是好的:看 Canal Server 到 ZK 的网络和防火墙,以及 ZK 的maxClientCnxns是不是被打满了。echo cons | nc zk1 2181 | wc -l看连接数。
提示:恢复操作前先对 ZK 做一次快照备份。ZK 的
dataDir下snapshot.*和log.*文件复制一份,出问题还能回滚。
6. 恢复之后:把元数据备份和监控补上
节点恢复只是救火,真正省心的是提前把备份和监控做起来。Canal 的 ZK 元数据量很小,定期导出成本极低。
#!/bin/bash # backup_canal_zk.sh INSTANCE="product_sync" ZK_HOST="zk1:2181" DATE=$(date +%Y%m%d_%H%M%S) BACKUP_DIR="/backup/canal_zk/$DATE" mkdir -p $BACKUP_DIR for node in cursor filter cluster; do zkCli.sh -server $ZK_HOST get /otter/canal/destinations/$INSTANCE/$node > $BACKUP_DIR/$node.txt 2>&1 done echo "backup done: $BACKUP_DIR"配合 crontab 每小时跑一次,保留最近 48 小时。监控方面,重点盯三个指标:Canal 到 ZK 的连接状态、canal_instance_delay消费延迟、/cursor节点是否存在。任何一个异常就告警。
如果你在恢复过程中需要反复验证模型对 Canal 配置和 ZK 命令的理解,可以用 TaoToken 模型对话 快速核对命令参数;接入相关的 API Key 在 API Keys 管理页 获取,接入文档在 TaoToken 接入文档。如果是长期跑 Canal 集群运维、需要把排障脚本和 Agent 结合起来的场景,Coding Plan 更适合做持续性的编码和自动化任务。
最后提醒一句:往/cursor写位点之前,一定先确认下游系统的幂等能力。位点写早了会丢数据,写晚了会重复,两者都比节点丢失更难收拾。