1. 技术背景与核心原理
1.1 数据同步的真实痛点
做了几年后端开发和数据平台建设,我几乎每年都会遇到“数据要搬家”的需求。最典型的是业务库和检索库之间的同步:MySQL 里存着订单、用户、商品,Elasticsearch 里要建索引支撑搜索,Redis 里要做缓存加速读取。早期最简单粗暴的方案就是业务代码里双写,写入 MySQL 之后顺手调用接口更新 ES 和 Redis。可一旦某个下游调用超时、失败了,数据就出现不一致,轻则需要定时任务对账补偿,重则直接污染线上数据。
还有一种常见做法是定时批量扫描增量字段,比如按 update_time 每五分钟拉一次。这个方案能解决一部分场景,但时效性差、对数据库有额外查询压力,而且遇到删除操作、DDL 变更就很难处理。真正干净利落的做法,是监听数据库的 Binlog,让变更事件自动推给下游。Canal 这套数据同步工具,就是把“监听 Binlog 并分发变更”这个能力做成了通用基础设施。
Canal 我所知最早源自于内部业务流程的同步需求,后来开源出来形成了独立的社区项目。它可以直连 MySQL 主库或者从库的 Binlog,伪装成从节点拉取日志,再以 JSON 等格式推送给客户端。基于它你几乎能实现所有实时同步场景:异构数据迁移、缓存刷新、搜索索引更新、数据仓库实时入湖、甚至触发后续业务逻辑。只要你需要把 MySQL 中的增量变化实时地送给别的系统,Canal 就值得你花半小时了解一下。
1.2 Canal 的工作原理:伪装成从库的“日志搬运工”
很多人刚接触 Canal 时会被一堆概念绕晕:Binlog、Slave、GTID、位点、Canal Server、Canal Client、Adapter。其实它的原理一句话就能概括:Canal 把自己伪装成一个 MySQL 从节点,向 MySQL 主节点发起正常的复制协议请求,然后主节点就会源源不断地把 Binlog 日志推送给它。
常规的 MySQL 主从复制中,从服务器上有两个关键线程:I/O 线程负责接收主库的 Binlog 并写入本地的 Relay Log,SQL 线程负责回放 Relay Log。Canal 相当于只实现了 I/O 线程那部分,它拿到 Binlog 原始字节流后并不回放,而是解析成结构化的事件对象,再通过自己的协议或消息队列推给业务系统。MySQL 官方有一套 Replication 协议,Canal Server 在启动时通过一个配置好的 dump 伪装的 Slave ID 向主库注册,之后主库就把它当成普通从库对待。
Binlog 本身有三种格式:STATEMENT、ROW、MIXED。Canal 强制依赖 ROW 格式,因为只有行级日志里才完整记录了每行数据变更前后的值,STATEMENT 记录的是 SQL 语句,解析难度大且无法可靠还原字段级变化。所以配置数据库时,必须把 binlog-format 设置成 row。GTID 模式不是必须的,但我建议启用,因为 GTID 天然解决位点定位和主从切换后的同步位置问题,可以简化后续维护。
Canal 的整个链路由三部分组成:源头是 MySQL 的 Binlog,中间是 Canal Server,下游是客户端。Server 负责连接数据库和解析日志,它可以对接内置的客户端协议、Kafka、RocketMQ 或 DirectMemory 等模式。客户端拿到的是 Canal 封装好的数据对象,里面包含库名、表名、事件类型、变更前镜像、变更后镜像、执行时间等字段,用起来非常直观。
1.3 为什么选择 Canal 而不是其他方案
市面上数据同步工具并不少,比如 Debezium、Flink CDC、Maxwell 等。很多人会纠结选哪个。我的看法是:选择 Canal 不是因为它在所有维度都是最强,而是它在 MySQL 同步这个场景下最有针对性,且周边生态完整。
| 工具 | 定位 | 消息输出 | 学习成本 | 适用场景 |
|---|---|---|---|---|
| Canal | MySQL 增量同步 | TCP 协议、Kafka、RocketMQ | 中 | MySQL 为中心的异构同步、消息驱动 |
| Debezium | 多种数据库 CDC | Kafka Connect | 高 | 全流程 Kafka 生态、多库支持 |
| Flink CDC | 全增量一体化计算 | Flink 流式处理 | 较高 | 实时计算、动态变更、数据湖 |
| Maxwell | MySQL 轻量 CDC | Kafka、Kinesis、RabbitMQ | 低 | 简单 JSON 同步、轻量需求 |
Canal 的优势主要在这几方面。第一,它在 MySQL 复制协议上深耕得足够久,对 Binlog 各种边界情况处理得比较成熟,比如大事务拆分、位点持久化、DDL 事件携带等。第二,它自带 Client SDK,对不熟悉 Kafka 的团队非常友好,拿过一条数据直接开始消费,不需要额外搭一套消息集群。第三,它的 Server 和 Adapter 支持配置化接入常见目标端,比如同步到关系型数据库、Elasticsearch、HBase、Redis 等,有些团队甚至纯配置就能完成一个同步任务。
当然也需要客观认识它的边界。Canal 的源头只支持 MySQL 和部分兼容 MySQL 协议的数据库,不像 Debezium 那样横跨多种数据库。如果你们的数据源是 PostgreSQL 或 MongoDB,就另选方案。另外,Canal 本身不具备计算和转换能力,复杂的字段映射、多表关联、数据过滤可以在客户端完成,也可以配合 Flink CDC 这类计算引擎使用。总之,在纯 MySQL 增量同步领域,Canal 是一个性价比极高的选择。
2. 环境准备与部署实战
2.1 前置条件与版本搭配
动手部署之前,先把环境理清楚。Canal 对数据库版本、操作系统、Java 版本都有要求,这些基础条件没准备好,后面排查问题会很痛苦。
Canal Server 基于 Java 开发,1.1.x 系列要求 JDK 8 及以上,官方推荐 1.8 环境。我实际试过在 JDK 11 上运行也没有问题,但建议生产环境保持与官方一致,避免某些依赖库在更高版本上出现兼容性差异。操作系统建议用 Linux 64 位,CentOS 7 或 Ubuntu 18.04 以上都行。Windows 环境虽然也能跑,但生产环境不建议,毕竟后续作为常驻服务,nohup、systemd 等管理方式在 Linux 下更顺手。
数据库版本方面,Canal 支持的 MySQL 版本横跨 5.1.x 到 8.x,但不同版本在连接参数、认证插件上有个别差异。MySQL 8.0 默认的 caching_sha2_password 认证插件在 Canal 的旧版本上会连接失败,解决方法是把 canal 用户的认证方式改为 mysql_native_password,或者升级到新版驱动。如果你的数据库是 5.7,就没有这个烦恼,这也是很多团队仍然停留在 5.7 的原因之一。
Canal 的版本选择也有讲究。1.1.5 是很多教程里最常见的版本,功能稳定、资料最多。1.1.6 之后加入了不少增强,比如更完善的分区支持、配置热加载等。建议新项目直接使用 1.1.7 或更新版本,同时注意下载对应 release 包时要选择 canal.deployer-x.x.x.tar.gz 作为服务端,canal.adapter-x.x.x.tar.gz 作为适配器,两者功能不同,别下错。
2.2 MySQL 端开启 Binlog 的完整步骤
Binlog 默认通常是关闭的,所以第一步是确认并修改 MySQL 配置。这里直接给出我惯用的配置,兼容 MySQL 5.7 和 8.0。修改 my.cnf:
[mysqld] log-bin=mysql-bin binlog-format=ROW server-id=1 binlog_row_image=FULL gtid_mode=ON enforce_gtid_consistency=ON其中 server-id 必须设置,而且在使用 Canal 时,建议不要和 Canal 配置里的 slaveId 重复,否则 MySQL 会认为主从 ID 冲突,导致复制中断。binlog_row_image=FULL 意思是记录所有列的新旧值,如果设置为 MINIMAL,会只记录被修改的列,解析出来的数据不完整,同步到下游可能缺字段。
修改完配置后重启 MySQL,然后登录数据库检查:
SHOW VARIABLES LIKE 'log_bin'; SHOW VARIABLES LIKE 'binlog_format'; SHOW BINARY LOGS;如果 log_bin 为 ON,binlog_format 为 ROW,且能看到 mysql-bin.000001 之类的日志文件,说明 Binlog 已经正常开启。
接下来创建 Canal 专用的同步账号。这个账号需要 REPLICATION SLAVE 和 REPLICATION CLIENT 权限。前者用于拉取 Binlog,后者用于查询主库状态和位点。SQL 如下:
CREATE USER 'canal'@'%' IDENTIFIED BY 'canal_pass'; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%'; FLUSH PRIVILEGES;如果你希望 Canal 能解析到表结构,最好再给 canal 用户授对应库表的 SELECT 权限,否则某些场景下解析表结构会报错。生产环境里我一般直接授予 SELECT 权限,它虽然是只读,但也要做好网络隔离,避免同步账号被任意主机连接。
2.3 Canal Server 部署与配置
部署时先把安装包解压到指定目录,我通常放在 /opt/canal/。解压 canal.deployer-1.1.7.tar.gz 后目录结构如下:
/opt/canal/ ├── bin/ ├── conf/ ├── lib/ └── logs/核心配置在 conf/canal.properties 和 conf/example/instance.properties 这两个文件。canal.properties 控制整个服务端的行为,比如端口号、ZooKeeper 地址、目标输出模式。instance.properties 控制单个数据同步实例连接哪个 MySQL、监听哪些库表。
先看 canal.properties 里的关键配置。默认端口是 11111,这个端口是 Canal Server 与 Client SDK 直接通信用的。
canal.ip = 0.0.0.0 canal.port = 11111 canal.metric.pull.port = 11112 canal.admin.manager = ;如果你想让多个 Canal Server 做高可用,需要配置 ZooKeeper 集群,把 canal.zkServers 指向 ZK 地址,实现 failover。如果只是单机测试,这一项留空即可。
然后编辑 conf/example/instance.properties,这是单个同步实例的配置。我常用的模板如下:
canal.instance.mysql.slaveId=20 canal.instance.master.address=127.0.0.1:3306 canal.instance.master.journal.name= canal.instance.master.position= canal.instance.dbUsername=canal canal.instance.dbPassword=canal_pass canal.instance.connectionCharset=UTF-8 canal.instance.tsdb.enable=true canal.instance.gtidon=false canal.instance.filter.regex=test\\..* canal.instance.filter.black.regex=说明一下几个容易踩坑的项。canal.instance.master.slaveId 不能和 MySQL 的 server-id 冲突,也不要和别的 Canal 实例重复。journal.name 和 position 默认留空即可,Canal 会自动从当前 Binlog 的位点开始解析,或者按需求手动指定从某个时间或位点开始。filter.regex 是库表过滤的正则表达式,格式是 库名\..,反斜杠需要转义。我要监听 test 库所有表,就写成 test\..;要监听多库则用竖线分隔,比如 db1\..|db2\..。
配置完成后启动服务:
sh /opt/canal/bin/startup.sh查看日志确认启动是否成功:
tail -f /opt/canal/logs/example/example.log正常情况下会输出 “start the canal client ... successfully” 之类的日志。同时可以查看 logs/canal/canal.log 确认服务端整体状态。如果连接失败,常见原因是端口被防火墙拦截,或者 MySQL 账号权限不对,后面问题排查章节会详细说。
2.4 Admin 控制台的搭建与使用
很多同学第一次接触 Canal 都是纯命令行管理,修改一次配置要改文件再重启。如果你需要管理多套 Canal 实例,可以考虑搭一套 Canal Admin。它相当于 Canal 的控制台,可以在界面上创建实例、动态修改配置、查看运行状态和位点。
Admin 和 Server 是两个独立运行的服务。从 canal.admin-1.1.7.tar.gz 解压后,在 conf/application.yml 里配置管理端口和数据库连接。Admin 需要一个 MySQL 数据库来存储配置和监控数据,建库语句在 conf/canal_manager.sql 里。
启动 Admin 后访问默认端口 8089,登录默认账号 admin/123456(第一次登录后建议立刻改密码)。在“集群管理”里添加 Canal Server,然后在“实例管理”里创建 instance,选择所属集群并填入 MySQL 连接信息。之后 Server 的控制权就交给了 Admin,每次实例配置变更后可以选择“重载”,热更新而不用完全重启。这个功能在实际运维中很省事,尤其是同步链路出问题需要快速切换位点时,界面操作比改文件再重启要快得多。
3. 客户端接入与数据消费实战
3.1 Canal 消息格式深入解读
数据从 Binlog 解析出来后,Canal 会封装为统一的协议对象。当你用 Client SDK 接收时,拿到的是基于 Google Protobuf 序列化后的数据,再转换成 Java 对象。理解这条消息的结构,是开发数据同步逻辑的关键。
一条典型的 Canal 消息包含以下层次:
- CanalMsg:最外层的消息对象,包含多个 Entry。
- Entry:对应一个数据库变更事件,比如一次 INSERT、一次 UPDATE、一次 DELETE。
- Entry 内部包含 header 和 storeValue。header 里有数据库名、表名、事件类型、执行时间戳、GTID 等信息。
- storeValue 是序列化后的 RowChange 对象。RowChange 里包含一行行 RowData,每行 RowData 包含 beforeColumns 和 afterColumns 两个列表,列表里每个 Column 对象包含字段名、字段值、是否主键、是否变更等信息。
用一张表来对照协议中的核心字段,平时排查问题时很管用:
| 字段 | 含义 | 典型值 |
|---|---|---|
| entryHeader.schemaName | 库名 | test |
| entryHeader.tableName | 表名 | orders |
| entryHeader.eventType | 事件类型 | INSERT / UPDATE / DELETE |
| entryHeader.executeTime | 执行的毫秒时间戳 | 1714000000000 |
| rowData.beforeColumns | 变更前字段列表 | UPDATE 时非空,INSERT 为空 |
| rowData.afterColumns | 变更后字段列表 | INSERT / UPDATE 非空,DELETE 为空 |
| column.isPrimary | 是否主键 | id 字段为 true |
| column.updated | 该字段是否发生更新 | UPDATE 事件下有用的标记 |
事件类型除了常见的增删改,还有 CREATE、ALTER、TRUNCATE、QUERY、ERASE 等。对于 DDL 语句,Canal 会以 CREATE 或 ALTER 类型的事件发送,此时 storeValue 只包含 DDL 对应的 SQL 语句。针对 DDL 的处理,后面会专门讲。
一个容易忽略的细节是:一条 SQL 影响多行数据时,一个 Entry 里会包含多个 RowData。批量 INSERT 和批量 UPDATE 很常见,如果你在客户端按单条数据处理逻辑逐行消费,没有问题;但如果你需要判断事务边界,就要注意 Entry 级别的分组。
3.2 使用 Client SDK 编写消费程序
官方 SDK 是 canal.client 包,它提供了一种类似拉模式订阅消息的方式。下面这个示例演示了最简单的消费逻辑。先引入依赖,以 Maven 为例:
<dependency> <groupId>com.alibaba.otter</groupId> <artifactId>canal.client</artifactId> <version>1.1.7</version> </dependency>然后编写 main 方法创建连接、订阅消息并解析。注意我这里用了简化后的逻辑,实际项目中会有线程池和异常恢复机制。
public class CanalClientDemo { public static void main(String[] args) { CanalConnector connector = CanalConnectors.newSingleConnector( new InetSocketAddress("127.0.0.1", 11111), "example", "", "" ); connector.connect(); connector.subscribe("test\\..*"); connector.rollback(); int batchSize = 1000; while (true) { Message message = connector.getWithoutAck(batchSize); long batchId = message.getId(); if (batchId == -1 || message.getEntries().isEmpty()) { try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } continue; } processEntries(message.getEntries()); connector.ack(batchId); } connector.disconnect(); } private static void processEntries(List<Entry> entries) { for (Entry entry : entries) { if (entry.getEntryType() != EntryType.ROWDATA) { continue; } RowChange rowChange; try { rowChange = RowChange.parseFrom(entry.getStoreValue()); } catch (InvalidProtocolBufferException e) { throw new RuntimeException("解析 storeValue 失败", e); } System.out.printf("schema=%s, table=%s, event=%s%n", entry.getHeader().getSchemaName(), entry.getHeader().getTableName(), entry.getHeader().getEventType()); for (RowData rowData : rowChange.getRowDatasList()) { for (Column column : rowData.getAfterColumnsList()) { System.out.println("字段:" + column.getName() + " 值:" + column.getValue()); } } } } }这段代码里有两个关键方法:getWithoutAck 表示获取数据但不确认,ack 表示确认已处理成功。如果处理过程中发生异常,不调用 ack 而调用 rollback,Canal Server 会重新发送该批次数据,这是 Canal 实现可靠消费的核心机制。我在生产代码中会把 ack 放在 try-catch 的 finally 外边,只有业务处理成功才 ack。
3.3 同步到 MySQL 和 Elasticsearch 的典型流程
最经典的同步场景就是把 A 库的变更实时同步到 B 库,或者把 MySQL 数据同步到 Elasticsearch 用于搜索。前者常用于库表拆分后的数据打通,后者则非常普遍。
同步到 MySQL 的逻辑其实是把 Canal 的事件转换成目标库的 SQL。对于 INSERT 事件生成 INSERT SQL,UPDATE 事件生成 UPDATE SQL,DELETE 事件生成 DELETE SQL。需要注意的是,Canal 给出的字段值是字符串类型,目标库字段类型是数字或者日期时,必须做转换,否则 MySQL 虽然会隐式转换,但遇到特殊格式可能出错。
我通常会在程序里维护一张表结构映射表。比如源表 orders 的字段 order_id 要映射到目标表的 order_id,同时去掉一些冗余字段;源表 status 如果值为 1,想映射成目标表的 status_name 为“已支付”,这就需要写转换函数。Canal 本身不做这些转换,所以客户端处理是必要的。
同步到 Elasticsearch 的流程也类似。接收到事件后,构造 Elasticsearch 的 index 请求,以主键作为文档 ID,使用 update API 保证幂等。对于 DELETE 事件,调用 delete API。下面是一个通过 RestHighLevelClient 写入的逻辑片段:
private void syncToEs(Entry entry, RowData rowData) { String key = getPrimaryKey(rowData.getAfterColumnsList()); String index = "orders_index"; Map<String, Object> doc = new HashMap<>(); for (Column column : rowData.getAfterColumnsList()) { doc.put(column.getName(), column.getValue()); } if (entry.getHeader().getEventType() == EventType.DELETE) { esClient.delete(new DeleteRequest(index, key), RequestOptions.DEFAULT); } else { IndexRequest request = new IndexRequest(index).id(key).source(doc); esClient.index(request, RequestOptions.DEFAULT); } }这里要注意:如果源表没有主键或者主键不是单列,在用 Canal 同步时你会很痛苦。因为 Binlog 里的主键可以用来唯一标识一行,在 ES 和缓存中都需要这个标识。建议所有需要同步的重要业务表都设计无业务含义的自增主键或 UUID 主键。
另外还有一条重要经验:删除操作在 Binlog 的 ROW 格式下只包含 beforeColumns 字段,afterColumns 为空。如果你的同步程序只解析 afterColumns,DELETE 事件就会拿不到任何字段值。正确做法是在 DELETE 时从 beforeColumns 里取主键值,用于确定要删除的文档 ID。
3.4 顺序性与幂等性:数据同步绕不开的两个课题
实时同步链路里,最常见也最难解决的就是顺序错乱和重复消费。理解这两个问题,才能写出真正可靠的同步程序。
先说顺序性。Canal 默认保证同一个库表的 Binlog 事件按写入 Binlog 的顺序投递,只要客户端是单线程消费,顺序就是有保证的。但当并发消费时,不同线程之间的执行先后不可控。比如一条 UPDATE 把订单状态从“待支付”改成“已支付”,如果线程 A 处理慢,线程 B 先执行了后面的“发货”逻辑,下游就会看到状态回退。
解决办法通常有三种。第一种最简单,按表路由,把同一张表的改动路由到同一个线程。第二种是给消息附加递增序号,下游检测到序号倒退就拒绝或丢弃。第三种是对于严格要求顺序的场景不并发消费,单线程处理。我在实际项目中一般采取“分桶”:以表名或者主键哈希为 key,将消息分发到固定数量的队列中,保证同一个 key 的消息始终进入同一个队列,再由队列对应的消费者串行处理。
再说幂等性。Canal 消费失败重试后,同一批消息可能会重复投递。下游必须接受重复数据不产生重复影响。数据库同步时,INSERT 可以使用 INSERT ON DUPLICATE KEY UPDATE,UPDATE 自然幂等,DELETE 重复执行也不会报错。同步到 Redis 时,直接 SET 即可,天然幂等。同步到 Elasticsearch 时,用 index API 指定同一文档 ID 覆盖写。这些手段都能保证重复消费不出事。
4. 常见问题排查与避坑实录
4.1 启动失败和权限问题的典型原因
Canal 启动不了,大部分原因集中在连接 MySQL 失败、认证失败、位点异常这三类。日志是排查的第一依据,我强烈建议把 logs/example/example.log 和 logs/canal/canal.log 都看一遍,重点搜 “ERROR” 和 “Exception”。
连接失败时日志通常会出现:
Caused by: java.io.IOException: connect 127.0.0.1:3306这明显是网络不通或者端口没开放。检查 MySQL 是否监听在目标 IP,使用 telnet 测试连通性,查看服务器防火墙规则。另外要注意 MySQL 的 bind-address 如果配置为 127.0.0.1,远程 Canal Server 自然连不上,需要改成 0.0.0.0 或者内网 IP。
认证失败一般长这样:
Caused by: java.sql.SQLException: Access denied for user 'canal'@'...'解决办法是重新执行权限授予 SQL,并且执行 FLUSH PRIVILEGES。MySQL 8.0 场景下,如果错误信息里出现 “Authentication plugin 'caching_sha2_password' cannot be loaded”,需要执行:
ALTER USER 'canal'@'%' IDENTIFIED WITH mysql_native_password BY 'canal_pass';另一个容易忽略的问题是 Canal 连接 MySQL 时,如果有一个旧的 Canal 或旧从库仍在运行,且它们的 server-id 冲突,MySQL 会中断复制连接。日志表现为 “A slave with the same server_uuid/server_id as this slave has connected to the master”。此时修改 canal.instance.mysql.slaveId 成一个更大的值即可,比如 100。
4.2 Binlog 保留时间与位点丢失
Canal 是靠记录 Binlog 位点来断点续传的。如果某个位点的 Binlog 文件已经被 MySQL 删除,Canal 将无法继续解析,启动时会报错并最终停止。这是实时同步最怕的情况。
要避免位点丢失,第一件事是检查 MySQL 的 Binlog 保留时间。默认情况下 Binlog 可能只保留几天,但你的同步下游如果停机维护几天,恢复时位点早就没了。修改 MySQL 配置:
expire_logs_days=14 binlog_expire_logs_seconds=1209600MySQL 5.7 用 expire_logs_days,8.0 推荐 binlog_expire_logs_seconds。我习惯设成 7 天以上,具体取决于你业务允许的最大停机时间。
即使 Binlog 在同一台机器上,如果 Canal 记录位点的 meta.dat 文件被删除,也会导致位点丢失。Canal 的位置信息默认保存在 conf/example/meta.dat 文件里。如果 MySQL 发生过主从切换或者数据恢复,Binlog 文件号和位点对不上,Canal 会在启动时尝试基于 GTID 定位,这再次说明了开启 GTID 的价值。没开 GTID 的极端情况下,只能重置为从当前最新位点开始,但这样会丢失历史变更数据,建议先备份重要数据。
4.3 大事务和 DDL 事件的处理策略
大事务是 Canal 同步链路中一个比较头疼的隐患。所谓大事务,就是一次性更新了十万行、百万行数据的 SQL。在行级 Binlog 中,事务里的每行变化都会生成一条事件,Canal 解析时会把整个事务的事件作为一个批次推送给客户端。如果客户端逐行调用下游 API,很容易造成慢消费和消息积压。
解决思路有几个层面。Canal Server 端可以通过配置内存缓冲区大小和 batch 大小控制单次拉取数量。客户端拉取消息后,如果发现一个 Entry 内的 RowData 数量很大,可以先对变更做聚合,比如将多条同主键的 UPDATE 合并成最新一条。更激进的做法是,对于统计类数据直接忽略明细变更,只触发一次全量重算,具体要看业务容忍度。
DDL 事件导致同步中断是另一个常见问题。当执行 ALTER TABLE 添加字段后,Canal 解析出的表结构信息会更新。如果下游目标端的表结构没有同步更新,后续插入的新字段数据会写入失败。处理方式有两种:一是 DDL 变更走审批流程,业务侧停掉同步任务,人工更新目标表结构后再恢复位点;二是开发一个 DDL 监听模块,解析 ALTER 语句,自动在目标库执行对应的 DDL。第二种方式风险较高,建议只在测试环境自动化,生产环境仍然以人工确认后执行为主。
4.4 性能调优与运行监控
Canal 本身的性能瓶颈一般不在解析,而在下游消费速度。曾经遇到过这样一个场景:Canal Server 的解析延迟正常,但消费程序处理单条消息耗时超过 100ms,导致消息积压,延迟从秒级涨到分钟级。排查后发现是每次消费都建立一次 Elasticsearch 连接。优化方式很简单:改为批量请求,一次批量写入 1000 条,延迟立刻降下来。
如果 Canal Server 的 CPU 偏高,多半是开启了大字段解析或者大事务频繁。可以调整 canal.properties 中的内存缓冲:
canal.memory.buffer.size = 16384 canal.memory.buffer.memunit = 1024 canal.instance.memory.batch.mode = MFS还可以关闭不需要的字段或表,通过 filter.regex 过滤掉日志表、临时表,减少无意义解析。
监控方面,Canal 暴露了 metrics 接口,默认端口 11112。可以通过 HTTP 拉取 JSON 指标:
curl http://127.0.0.1:11112/metrics返回数据中包含当前的延迟时间、累积处理条数、异常次数等指标。建议把该接口接入现有的监控告警系统,设置延迟超过阈值时告警。如果没有现成监控,至少写个定时脚本采集 key 指标到日志文件,方便事后复盘。
4.5 高频问题速查表
把这几年来踩过和帮别人排查过的典型问题整理成一张表,方便你直接对照:
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| Canal 启动后立即退出,日志无异常 | 内存不足或 JDK 版本不对 | 检查机器内存和 Java 环境 |
| 日志报 Access denied | 账号或权限不正确 | 重新 GRANT 并 FLUSH PRIVILEGES |
| 连不上 MySQL | bind-address、防火墙配置 | 修改 MySQL 监听地址或放行端口 |
| 能启动但收不到数据 | filter.regex 配置错误 | 确认正则写法,如 test\..* |
| 收到数据但中文乱码 | connectionCharset 不对 | canal.instance.connectionCharset=UTF-8 |
| 消费端重启后数据重复 | 没有正确 ack 或落库没有幂等 | 确保业务成功后再 ack,下游操作幂等 |
| 主从切换后 Canal 同步错乱 | 位点没有正确重置 | 使用 GTID,或手动设置 journal/position |
| UPDATE 只解析到新值 | binlog_row_image=FULL 未设置 | 修改配置并重启 MySQL |
| DDL 后消费失败 | 目标表结构没有同步变更 | 人工同步 DDL 或自动 DDL 任务 |
| 消息积压严重 | 下游耗时过长 | 批量化、增加分桶、优化索引写入 |
5. 从项目出发的落地建议
Canal 虽然部署简单,但真正跑在生产环境,还是有不少需要提前设计的点。我从自己的项目经验里总结了三条实用建议,给准备接入的团队参考。
第一,同步链路一定要有“全量+增量”的能力。Canal 擅长增量同步,但首次使用时,目标端通常需要一份历史数据。很多团队先做一次全量导入,再启动 Canal 增量同步,中间会产生时间差,导致少数据。稳妥的做法是先启动 Canal 但不消费,记录一个起始位点;然后做全量导入;导入完成后,把 Canal 配置成从记录的位点开始消费增量。这样全量和增量之间无缝衔接,不会遗漏变更。
第二,尽量让下游具备幂等能力。哪怕你确定 Canal 不会重复投递,也难免遇到网络超时后客户端重试。最省心的方式是所有写入操作都设计成覆盖写。比如同步到 Redis 用 SET,同步到数据库用 UPSERT,同步到 ES 用固定文档 ID。这样即便重复消费,也只是多执行一次无影响的覆盖。
第三,为同步任务建立独立的监控大盘。Canal 自身的指标虽然能用,但客户端消费状态同样需要监控。我们会在消费程序里埋点,记录每张表的消费延迟、失败条数、重试次数。当某张表的消费失败数量递增时,告警日志能直接定位到表名,大大缩短故障发现时间。
6. 写在最后的经验心得
最后再分享一点我个人的经验。刚开始用 Canal 时,我犯过一个低级错误:把 filter.regex 写成了 test.,而不是 test..,结果收不到任何数据。后来查文档才知道,这里的正则表达式要求库名和表名之间用两个反斜杠加一个点分隔,左边库名、右边表名的模式,比如 test\.user 就是监听 test 库的 user 表。这种细节不实际操作一次,真的很难发现。
Canal 这个工具的价值,不在于它有多炫酷,而在于它把 MySQL 增量同步这个很底层、很容易出错的事情封装成了可以直接复用的能力。你可以非常快速地把 MySQL 的变更与 Elasticsearch、Redis、Kafka、数仓等系统联动起来,让数据流动变得可控、可观测。
如果你正在做数据同步方案,建议先从小范围的单表同步开始跑通,验证位点、幂等、顺序这些基础能力,再逐步扩展到复杂的多表和多目标场景。数据同步不像业务功能那样上线见效快,但一旦它稳定运行,整个系统的数据一致性都会因此受益。