canal要做高可用,真不只是多起两个进程那么简单。很多团队把canal部署成单机,数据同步链路一旦断掉,靠人肉盯日志、手动重启,哪天半夜出了故障,才意识到问题有多被动。这篇我来完整拆解一套Linux环境下canal HA的安装配置方案,从原理到落地一步步说清楚,尽量把我在实际部署中踩过的坑也带出来。
这套方案适合谁?如果你正在做MySQL到消息队列、搜索引擎、异构数据库的增量同步,或者任何依赖binlog变更捕获的场景,只要同步链路的SLA有要求,就可以直接参考。我会以两台canal server + 一套ZooKeeper集群为例,讲清楚高可用选主、位点续传、故障切换这些核心机制,并给出完整的配置文件。
1. 为什么canal HA是数据同步链路的刚需
先聊一个实际痛点。很多团队的canal部署模式是“一台机器跑一个instance”,数据同步链路如下图这种形态:MySQL主库产生binlog,canal伪装成从库拉取,再把解析后的增量数据推给下游。这台canal一旦出问题,下游收不到数据,整个消费链路全部阻塞。
单机的麻烦在于几个短板上线后就会被放大。第一,进程挂掉后依赖人工发现、人工重启,夜间故障的响应时间基本按小时算;第二,canal默认把消费位点放在内存或单机文件里,机器损坏或者目录被误删,位点丢了就只能从头拉,下游要承受重复数据甚至是数据错位;第三,日常升级、换配置、调整JVM参数,都意味着停机窗口。业务方每次听到“要重启canal”都会问一句:影响多久?这个问题的答案往往是“看在不在夜里”。
HA模式解决的就是这些问题。多部署一个canal实例,通过ZooKeeper做分布式协调,同一时刻只让一个实例服务,另一个实时待命。服务实例故障后,备机自动顶上,并且从ZooKeeper中读取上次的消费位点继续拉取增量数据。整个过程业务侧能感知到的只是几十秒的数据延迟,链路不会断。
这套机制对下游消费端是透明的。消费端连接的是canal集群,不必关心当前哪个节点在服务。这里有一个前提要提前讲清楚:HA依赖ZooKeeper选主和位点持久化,也就意味着你必须先搭好一套ZooKeeper集群,再把canal切到ZooKeeper模式下运行。如果只是两台canal放在那里,自己读写自己的文件,那不叫HA,叫“双份麻烦”。
另外提醒一句,canal HA不改变数据语义。canal在发生故障切换时会从持久化位点继续消费,位点之后的数据可能重复投递给下游,消费端最好做幂等处理。Kafka、RocketMQ这类消息队列本身支持幂等键,ES、MySQL这类存储则建议用唯一键去重。
2. 一张图看懂canal HA的工作原理:选主、接管、续传
很多人装完HA能跑通,但对它背后的逻辑一头雾水,出故障时判断就非常被动。我把canal HA拆成三个核心环节,逐个展开。
2.1 canal怎么“伪装”成MySQL从库
canal之所以能拿到MySQL的增量数据,是因为它模拟了一个MySQL Slave实例的交互过程。它向MySQL主库发送dump协议,告诉主库“我是一个从库,把binlog从某个位点开始发给我”。主库收到了这个请求,就会持续把binlog二进制日志推送过来。
这里面有两个技术前提,需要提前确认。第一,MySQL主库必须开启binlog,并且binlog_format必须为ROW。因为只有ROW模式下,binlog中才包含了每一行数据的完整变更内容,canal才能解析出增删改的具体记录。第二,用于连接MySQL的账号必须具备REPLICATION SLAVE和REPLICATION CLIENT权限,否则dump阶段直接报权限错误。这两条我在后面环境准备部分会给出具体配置。
canal把收到的binlog解析成结构化增量事件后,再以TCP或者消息队列的方式推送给消费端。解析过程包括了事务边界识别、表结构映射、字段类型转换,这也是canal相比直接读binlog日志更省心的原因。
2.2 ZooKeeper在canal HA里的三个用处
ZooKeeper在整套方案里承担了三件事。第一件事是选主。每个canal实例启动后会往ZooKeeper的某个节点下注册自己,并尝试创建一个临时节点,谁创建成功谁就是当前服务的active节点。第二件事是监听。standby实例会监听active节点,一旦发现临时节点消失,立刻重新发起创建,谁先创建成功谁接管。第三件事是保存位点。在HA模式下,canal每次处理完一批binlog,会把解析到的位置信息写入ZooKeeper,这样active挂掉之后,新的active能够从上次的位置继续拉取。
ZooKeeper临时节点有一个特性,就是会话超时自动删除。active实例和ZooKeeper之间有心跳机制,如果active实例宕机、断网或者JVM假死,都会导致会话超时,ZooKeeper就会把这个临时节点删掉。这正是故障切换的触发信号。
这里要提一个实际心得:ZooKeeper集群最好独立部署,不要和canal混在一台机器上。虽然混部也能跑,但zk集群本身如果节点挂掉过多导致选不出leader,canal的HA机制就会失效。生产环境建议zk至少三节点,且单独规划机器。如果机器紧张,至少也要让zk的存储目录落在独立的数据盘上,别和系统日志抢IO。
2.3 一次完整的故障切换过程
假设两台canal实例分别部署在两台Linux服务器上,一台是active,另一台是standby。它们的destination名称一致,连接到同一个ZooKeeper集群。正常情况下,active实例在ZooKeeper里持有一个running临时节点,standby实例一直watch这个节点。
当active实例宕机后,会发生下面的过程:
- active与ZooKeeper之间的心跳中断,等到会话超时阈值,ZooKeeper删除active持有的临时节点。
- standby收到节点变化通知,立即发起临时节点创建。
- standby创建成功,成为新的active。
- standby从ZooKeeper读取持久化的消费位点,建立MySQL dump连接,从这个位点继续拉取binlog。
- 新的active将自己注册的节点信息更新,同时对外提供服务。
整个过程的时间成本主要是ZooKeeper会话超时等待,通常配置在几十秒级别。也就是说,故障自动恢复的时间窗口在几十秒内。如果你在配置里把ZooKeeper的会话超时调得很长,切换时间会成比例增长,这一点在故障演练时要心里有数。
这里有一个重点容易被忽略:canal实例本身的进程还活着,但和下游之间的连接断了,或者GC卡死,这种情况ZooKeeper可能短时间内不会判定会话超时,切换就不会触发。所以监控不能只看canal进程在不在,还要监控它和zk的会话、下游消费延迟这些指标。
3. 环境准备:先把MySQL、ZooKeeper、JDK这几个地基打好
开始配置canal之前,环境准备工作量不小。我按我的部署习惯整理了一套顺序,建议按这个顺序来做。
3.1 服务器规划与JDK安装
我拿一套典型的生产配置举例,两个canal节点加三个ZooKeeper节点。表格如下:
| 主机 | IP | 角色 | 关键端口 |
|---|---|---|---|
| node1 | 10.0.1.11 | canal + ZooKeeper | 2181, 11111 |
| node2 | 10.0.1.12 | canal + ZooKeeper | 2181, 11111 |
| node3 | 10.0.1.13 | ZooKeeper | 2181 |
| mysql | 10.0.1.20 | MySQL主库 | 3306 |
之前说过生产环境建议zk独立部署,但如果资源有限,像上面这样把zk和canal混部在两台机器上也是可行的,前提是端口不冲突。canal默认端口为11111,zk默认2181,这个组合不冲突。但一定要注意另一个默认端口:canal的admin接口在部分版本会占用8080端口,如果机器上已经有其他web服务,需要调整。
JDK版本建议使用JDK8或JDK11。canal是Java技术栈,依赖JDK运行,安装方式很简单。直接用系统包管理器安装openjdk就可以了。
yum install -y java-1.8.0-openjdk-devel java -version安装完成后确认JAVA_HOME环境变量。canal的启动脚本bin/startup.sh会读取JAVA_HOME,如果没设置,启动就会报错找不到java命令。建议直接在/etc/profile里写入:
export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk export PATH=$JAVA_HOME/bin:$PATH然后执行source /etc/profile让配置生效。
3.2 MySQL端开启binlog和创建授权账号
canal拉取增量数据的前提是MySQL主库已经开启了binlog,且格式是ROW。打开MySQL配置文件my.cnf,在[mysqld]段落下加入:
[mysqld] server_id = 1001 log-bin = mysql-bin binlog_format = ROW binlog_row_image = FULL expire_logs_days = 7 max_binlog_size = 256M这几个参数逐个解释一下。server_id在主从架构里要求全局唯一,canal伪装成从库时,server_id也必须另配一个并且不能和其他从库重复。binlog_format是ROW这件事很重要,STATEMENT格式下binlog里只有SQL语句,canal没法解析出变更了哪些行的数据。binlog_row_image设置为FULL,让binlog记录完整的前后镜像,否则某些场景解析出来的数据不完整。expire_logs_days是binlog保留天数,根据你的同步链路重要程度来定,太短可能canal故障期间binlog已经被清理,恢复时追不回增量。
配置完之后重启MySQL生效。
然后创建一个专门给canal用的账号。注意账号的主机范围,如果canal和MySQL不在同一台机器,不能用localhost,要允许远端连接。
CREATE USER 'canal'@'%' IDENTIFIED BY 'canal123'; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%'; FLUSH PRIVILEGES;创建完账号后,用一个简单命令验证一下能不能正常拉取binlog信息:
mysql -h 10.0.1.20 -ucanal -pcanal123 -e "SHOW MASTER STATUS;"如果能看到类似下面这样的输出,说明binlog已开启,且账号有权限读取:
File Position Binlog_Do_DB Binlog_Ignore_DB mysql-bin.000012 154 null null另外,如果你的MySQL主库已经开启了GTID模式,建议在canal的instance配置里开启GTID对齐开关。开启GTID后,位点匹配会更精准,HA切换时不容易出现位点回退问题。GTID模式配置如下:
[mysqld] gtid_mode = on enforce_gtid_consistency = true开启了GTID后,binlog文件里每个事务都会有唯一的GTID标识,后续在canal配置里把canal.instance.gtidon设为true就行。
3.3 ZooKeeper集群部署
ZooKeeper选主和位点持久化都依赖它,必须保证集群本身可靠。这里以三节点为例,生产环境强烈建议至少三个节点,两个节点的zk集群可能出现脑裂时选不出leader的极端情况。
在每一台机器上下载解压ZooKeeper。我这里用zookeeper-3.6.x的部署为例:
tar -zxf apache-zookeeper-3.6.3-bin.tar.gz -C /opt/ cd /opt/apache-zookeeper-3.6.3-bin/conf cp zoo_sample.cfg zoo.cfg修改zoo.cfg:
tickTime=2000 initLimit=10 syncLimit=5 dataDir=/data/zookeeper clientPort=2181 server.1=10.0.1.11:2888:3888 server.2=10.0.1.12:2888:3888 server.3=10.0.1.13:2888:3888dataDir建议放到独立的数据盘目录,不建议和系统目录混用。然后每台机器创建dataDir目录,并在其中创建一个myid文件,内容为各自的序号:
mkdir -p /data/zookeeper echo "1" > /data/zookeeper/myid # 第一台机器 echo "2" > /data/zookeeper/myid # 第二台 echo "3" > /data/zookeeper/myid # 第三台随后在三台机器上依次启动:
cd /opt/apache-zookeeper-3.6.3-bin bin/zkServer.sh start检查启动状态:
bin/zkServer.sh status正常的三节点集群,一个节点会显示leader,另外两个显示follower。如果全部显示standalone,说明集群没配好,检查myid和zoo.cfg中的IP列表。
最后测试一下,确保canal节点所在的机器能访问zk集群的2181端口。用下面的命令验证:
yum install -y nc nc -vz 10.0.1.11 21814. canal server高可用部署实战:从下载到配置一步步来
环境准备好后,进入正式部署环节。canal的项目发布包分为deployer和adapter两部分,其中deployer是核心服务端,负责拉取binlog并推送给下游;adapter是官方提供的消费适配器,可以直接对接Kafka、RocketMQ、ES、MySQL等。本节先讲deployer,因为HA的核心链路在它这里。
4.1 下载并规划canal目录
当前稳定版本以1.1.x为主流,我以canal.deployer-1.1.6版本为例子,下载方式就是去官方GitHub Releases页找到对应tar.gz包。这里只讲包内结构,具体文件名可以对着release页面认。
mkdir -p /opt/canal tar -zxf canal.deployer-1.1.6.tar.gz -C /opt/canal/ cd /opt/canal解压后的目录结构如下:
| 目录/文件 | 作用 |
|---|---|
| bin/startup.sh | 启动脚本,会自动加载JAVA_HOME |
| bin/stop.sh | 停止脚本 |
| conf/canal.properties | canal server全局配置 |
| conf/example/instance.properties | 单个instance的数据源配置 |
| conf/spring/*.xml | 不同模式的Spring装配文件 |
| lib/ | 依赖jar包 |
| logs/ | 运行日志目录 |
这个包本身是给单机用的,部署成HA时我们需要准备两份,分别放在两台Linux服务器上。我建议直接用同样的路径,避免后续维护时路径不一致带来的混淆。
4.2 canal.properties核心参数配置
canal.properties是全局配置文件,重点改动以下几个参数。
canal.id = 1 canal.ip = 10.0.1.11 canal.port = 11111 canal.register.ip = 10.0.1.11 canal.register.port = 11111 canal.zkServers = 10.0.1.11:2181,10.0.1.12:2181,10.0.1.13:2181 canal.instance.global.spring.xml = classpath:spring/zk-instance.xml canal.instance.global.lazy = false canal.destinations = example这里有几个点,一定要讲透。
第一,canal.ip和canal.register.ip的区别。canal.ip是服务本身监听的地址,如果机器有多块网卡,需要显式指定,避免canal把内网和公网地址搞混。canal.register.ip是向所在集群注册节点时对外暴露的地址,消费端会通过这个IP连接canal。两台canal机器的register.ip分别填自己机器的内网IP。
第二,canal.zkServers是整套HA的命脉。这里写的是zk集群的地址列表,canal实例启动后会连接这些地址,完成节点注册和位点持久化。如果不配置这一项,canal会退化为单机模式,HA无从谈起。
第三,canal.instance.global.spring.xml是HA切换的关键开关。默认值通常是classpath:spring/memory-instance.xml,这种模式下canal把元数据和位点存内存,进程重启后位点丢失。要实现HA,必须改成classpath:spring/zk-instance.xml,让canal把位点和元数据持久化到ZooKeeper中。改完这个配置后,canal启动时才会去注册zk节点并创建持久化位点。
第四,canal.destinations是instance名称列表,多个instance可以用逗号分隔。每台canal机器上只要配置的destination名称一致,它们就自动组成一个HA组。比如我上面写的example,两台机器都要有这个同名destination。
另外有一个参数建议打开,canal.instance.global.lazy=false。这是让canal在启动时就初始化instance并抢占zk节点。如果设为true,实例启动时会延迟初始化,备机可能要等真正发起连接时才去抢占节点,不利于自动切换。
4.3 instance.properties数据源与位点配置
接下来进入每台机器上的conf/example/instance.properties,这是单个数据同步任务的核心配置。以我用的MySQL主库10.0.1.20为例:
canal.instance.mysql.slaveId = 654321 canal.instance.master.address = 10.0.1.20:3306 canal.instance.dbUsername = canal canal.instance.dbPassword = canal123 canal.instance.connectionCharset = UTF-8 canal.instance.filter.regex = .*\\\\.testdb..* canal.instance.gtidon = true逐个来看。
canal.instance.mysql.slaveId注意两点。第一,不能和MySQL真实从库的server_id冲突;第二,两台canal机器的这个值不能相同。因为canal伪装成MySQL的一个从库,这个ID在MySQL视角里必须是一个唯一的从库标识。如果两台canal配了同一个slaveId,MySQL会认为是同一个从库在重复连接,可能出现连接互相踢掉的问题。
canal.instance.master.address是MySQL主库地址和端口。注意这里填的是主库地址。虽然canal HA部署在多个节点,但所有canal节点拉的是同一个主库的binlog。
canal.instance.filter.regex是过滤规则,格式为正则表达式。上面的写法表示只监听testdb库所有表。实际生产环境,如果你只想同步某几张表,可以更精确地写成:
canal.instance.filter.regex = testdb\\.(t_user|t_order)这里有个容易踩坑的细节:在properties文件里写正则,反斜杠需要双写。因为properties文件本身会做一层转义解析,Java字符串又会做一层,最后正则引擎收到的才是真正的反斜杠。所以你看上面的写法是四个反斜杠对应一个点号,四个反斜杠在properties解析后变成两个,Java字符串再解析后变成一个,这样正则的.才是匹配点号。
canal.instance.gtidon设为true,表示连接MySQL时使用GTID作为位点标识。前提是MySQL主库已经开启了GTID模式。这个开关配合zk-instance.xml,切换后的位点恢复准确度更高。
配置完毕之后,再把这份instance.properties文件复制到另一台canal服务器的相同路径下。记住要把slaveId改掉,把master.address保持同一个主库地址,以及把过滤规则检查一遍。十次有九次问题出在“两台机器配置不一致”上。
4.4 启动canal并观察节点注册情况
在两台Linux机器上分别启动canal:
cd /opt/canal sh bin/startup.sh启动后立刻看日志:
tail -f logs/canal/canal.log tail -f logs/example/example.log正常情况下,一台机器的日志里会出现类似“register to zk”的字样,另一台机器则会等待并注册为备机。canal.log里如果出现“canal is running with [destination = example]”这样的信息,说明该实例成功启动。
再去ZooKeeper里确认节点状态。进入一台zk机器执行:
bin/zkCli.sh -server 10.0.1.11:2181 ls /otter/canal/destinations/example应该能看到类似cluster这样的节点,再往下看running节点的内容:
get /otter/canal/destinations/example/running能读到当前active节点的IP,就说明选主成功。再用:
ls /otter/canal/destinations/example/cluster能看到两台机器的地址都在,说明它们已经组成一个HA组。
启动时还要检查端口是否被占用。canal的netty服务默认监听11111端口,如果看到端口被占用,一般有两种可能:一是另一个canal实例已经起来了,二是之前有残留的java进程没有清理干净。用netstat排查:
netstat -tlnp | grep 11111确认监听地址是0.0.0.0还是具体IP,如果只想让本机访问,可以不用管;如果要让远端消费端连接,需要确保安全组或者防火墙放行该端口。
5. 消费端如何对接canal高可用集群
canal server的HA实现了,消费端也要跟上。不少人在这一步踩坑,认为server高可用之后,消费端什么都不用改,结果故障切换后消费端连不上,整个链路还是断的。
5.1 TCP模式下消费端的ZooKeeper感知能力
canal提供了两种主要消费方式,一种是原生TCP客户端,一种是MQ消息队列。TCP模式下,消费端需要主动获取当前active实例的地址。canal官方的Java客户端支持通过ZooKeeper自动感知当前master,启动时只需要配置zk地址。
以Java客户端为例,地址字符串是这么写的:
String destination = "example"; CanalConnector connector = CanalConnectors.newSingleConnector( new InetSocketAddress("10.0.1.11", 11111), destination, "", "" );但这种方式写死了某个canal实例的地址,HA切换后消费端就会连不上。正确做法是使用基于ZooKeeper的连接器:
String zkServers = "10.0.1.11:2181,10.0.1.12:2181,10.0.1.13:2181"; CanalConnector connector = CanalConnectors.newClusterConnector(zkServers, destination, "", "");newClusterConnector构造方法接收的是zk地址列表,客户端内部会去zk中查找当前active实例并与之建立连接。当active切换时,客户端会感知到running节点的变化,自动重连到新的active节点。这个机制对不熟悉canal的团队来说属于“隐藏知识点”,如果你现在用的是newSingleConnector,务必检查一下。
上面的示例严格来说不够准确的地方在于,CanalConnectors的API不同版本之间方法名有变化,但核心思路都一样:基于zk的cluster connector代替写死的single connector。具体方法名请以你引入的client版本Javadoc为准。
5.2 adapter模式如何跟上HA
如果你用的是官方adapter包,也就是canal.adapter那个组件,它在tcp模式下默认支持通过zk感知master。application.yml里按如下方式配置:
canal.conf: mode: tcp canalServerHost: 10.0.1.11:11111 zookeeperHosts: 10.0.1.11:2181,10.0.1.12:2181,10.0.1.13:2181这里canalServerHost填写任意一台canal的地址,adapter在启动时如果发现配置了zookeeperHosts,会优先通过zk获取当前active实例,并动态维护连接。当active切换之后,adapter会自动重连新的active。如果你不配置zookeeperHosts,adapter就只会连接写死的canalServerHost,切换后链路必断。
如果你是通过Kafka或RocketMQ的方式消费,那canal server会先把增量数据投递到MQ,消费端只跟MQ打交道,canal本身的HA对消费端完全透明。这种情况下,canal HA只需要保证投递端持续有数据写入MQ即可。
不管哪种消费方式,消费端都要做一件事:幂等。HA切换后,新的active实例是从zk中记录的位点继续拉取,但ZK位点已经记录到哪个binlog位置,取决于canal的flush时机。极端情况下,下游可能收到重复的增量记录。对MQ消费场景,建议在消费逻辑里用业务主键做去重;对写入MySQL/ES的场景,建议用唯一键或版本号保证幂等。
6. 高可用切换演练:亲手把active节点kill掉看结果
配置完成后不要直接上线,先做一次故障演练。这一步能帮你确认HA是否真的生效,也能让你心里有底,知道切换的实际耗时是多少。我按照一个标准的演练流程来演示。
6.1 确认当前active节点
先登录zk确认当前running节点在哪个机器上:
bin/zkCli.sh -server 10.0.1.11:2181 get /otter/canal/destinations/example/running假设返回的IP是10.0.1.11,说明当前active是node1。再登录node1,检查canal的日志和进程:
cat logs/example/example.log | tail -n 20 ps -ef | grep canal | grep -v grepOK,node1是active,node2是standby。为了确认数据链路的状态,可以同时往MySQL测试表里插入几行数据,然后看消费端是否收到。
6.2 主动kill掉active节点
直接杀掉node1上的canal进程:
ps -ef | grep canal | grep -v grep | awk '{print $2}' | xargs kill -9不要用stop.sh,因为stop.sh是优雅停机,会先把节点从zk摘除,这样standby立刻接管,无法验证“进程突然死掉”这个场景。真正要模拟的是进程异常退出,所以用kill -9。
杀掉之后立刻登录node2,观察日志变化:
tail -f logs/example/example.log同时登录zk,观察running节点的变化:
get /otter/canal/destinations/example/running正常情况下,几秒到几十秒之后,running节点的IP会变成10.0.1.12。切换的耗时取决于zk会话超时时间,如果你的zk配置没有特调,一般会看到30秒到40秒左右。zk配置中tickTime、sessionTimeout这些参数,会影响这个时间窗口的长短。
node2接管后,立刻往MySQL测试表再插入一条数据,观察消费端是否能继续收到增量数据。如果消费端在30秒延迟后继续正常消费,说明切换完全成功。
6.3 恢复原active节点,验证不会出现双活
node1进程被kill之后,如果我们要把node1重新拉起来,直接执行startup.sh。启动后观察日志,这个节点不会立即抢占active,而是会进入standby状态。因为此时node2已经持有了running临时节点。
重点来了,此时一定要去zk里看running节点,确认它还是node2的IP,而不是被node1抢回去。如果node1启动后把running抢走了,说明配置里存在冲突,大概率是zk-instance.xml没有配置好,或者两个节点的destination名称不一致。正常情况下,不应该出现两个节点同时处于active状态,那就意味着消息被同时消费两次。
检查完原节点恢复为standby后,可以再杀掉node2做反向切换演练。这个步骤建议做一次,因为灾后恢复场景和首次故障场景的细节略有不同,跑通一遍心里才踏实。
6.4 切换指标记录
我给一个切换演练的记录表,大家照着填,评审时直接给这个表就行:
| 观测项 | 第一次切换 | 恢复后切换 |
|---|---|---|
| 触发方式 | kill -9 node1 | kill -9 node2 |
| zk会话超时配置 | 默认约30秒 | 默认约30秒 |
| 实际切换耗时 | 约28秒 | 约31秒 |
| 消费端感知延迟 | 约31秒 | 约34秒 |
| 是否出现重复消费 | 出现1条 | 出现0条 |
| 数据是否丢失 | 否 | 否 |
从表格看到,重复消费问题在第二次演练中没有出现,但这不表示永远不会出现。消费端必须坚持幂等原则,一旦流量高峰时zk写入延迟,极端情况下重复消费概率会上升。
7. 实际部署中绕不开的坑和排查思路
canal HA部署一次就能顺利跑通的情况,我几乎没有遇到过。下面这些坑是高频问题,不少都是我亲手踩过又花时间排查的,大家提前对照。
7.1 MySQL连接报错:账号权限和密码加密方式
启动之后example.log里报类似Communications link failure或者Access denied for user 'canal'@'...',百分之八十是账号权限问题。先去MySQL里执行:
SELECT user, host FROM mysql.user WHERE user='canal';确认账号的host允许canal所在机器的IP连接。如果用的是MySQL 8.0,还要确认默认认证插件是否为caching_sha2_password。canal老版本对这种加密方式适配有缺陷,建议创建账号时指定mysql_native_password:
CREATE USER 'canal'@'%' IDENTIFIED WITH mysql_native_password BY 'canal123'; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%'; FLUSH PRIVILEGES;MySQL 5.7没有这个问题,MySQL 8.0一定要先确认。
7.2 ZooKeeper会话频繁断开
zk连接正常,但canal日志里反复出现Session expired、Connection loss之类的报错,可能的原因有三个。第一是zk集群本身不稳定,三节点之间时钟偏差过大。ZooKeeper对节点间时钟偏差很敏感,建议配置NTP时间同步。第二是zk的jvm堆内存给得太小,GC频繁导致处理请求变慢,导致会话超时。第三是canal节点和zk节点之间的网络有丢包,用ping和nc多测一下。
还有一个容易被忽略的点:不要使用zk的默认dataDir路径,也就是zk解压目录下的data目录。这个目录通常在系统盘,系统日志和临时文件一多,IO压力上去,zk的写性能会波动,直接导致会话超时。把dataDir挪到独立数据盘,是一项低成本高收益的改动。
7.3 位点冲突导致的重复消费或丢数据
HA切换后,如果新active实例拉取位点比旧active实际消费位点要早,下游就会收到重复数据。要确认位点信息,可以去zk里查看实例的位点节点路径。这里提醒一点,位点节点会存放在zk节点下面,路径包含destination名称,切换前最好先确认位点是否有持续更新。
如果发现位点没有更新,检查canal.properties里的instance.global.spring.xml是否真的改成了zk-instance.xml。如果你用默认的memory模式,在zk里能看到节点注册,但看不到位点持久化,切换后新active会从当前binlog最新位置开始拉,这就会丢数据。
位点冲突问题还有一种隐蔽场景:同一套配置跑了多套zk环境。比如测试环境、预发环境的zk地址写混了,两台canal实例分别注册到不同的zk集群,看似都在运行,实际上各拉各的位点。这种问题排查起来最折磨人,配置上线前建议三个人互查一遍zk地址。
7.4 端口冲突和网卡IP识别错误
canal的netty端口是11111,如果你在同一台机器上部署多套canal实例,端口必须错开。在canal.properties里修改canal.port时,要注意自定义端口必须在1024以上,并且要同步修改防火墙规则。
关于canal.ip和register.ip,我再强调一遍。如果Linux机器配置了多块网卡,canal可能会识别出错误的IP,导致消费端连接失败。此时必须在canal.properties里显式指定canal.ip为内网IP,同时让register.ip保持通信可达。
7.5 升级canal版本时的特殊注意
canal版本升级时,官方release包之间的instance配置项可能有差异。升级前先备份conf目录,然后对比新旧canal.properties,把不认识的参数挨个查一遍。不要直接复制旧配置覆盖新配置,有些参数在新版本里改了语义,沿用旧值可能导致启动报错或者行为不一致。
升级顺序上,建议先升级standby节点,确认standby能正常注册并观察到active节点后才升级active节点。这样最坏情况下也不会造成链路长时间中断。
我在实际部署里的体会是,canal HA本身配置量不大,真正的复杂度全在依赖环境和不一致上。MySQL的binlog参数、zk的稳定、两台实例的配置一致性,任何一个环节没对齐,切换时都会露馅。配置完成后一定要做一次完整的故障演练,不要等线上出了故障才开始研究zk会话怎么排查。
最后分享一个小技巧:每隔一段时间去zk里看一眼running节点的持有者,结合canal的运行日志做一次快照,记录当前active节点是谁。等到故障发生时,你手里有一张“正常状态基准表”,排查时间能省下一大截。