4、Canal入门:Canal是什么、Canal工作原理、Canal的安装与启动、Canal连接MySQL测试

好,咱们进入第四章。前面几章我们把CDC的概念、场景和整体架构都聊透了。从这一章开始,要动真格的了——上手第一个核心组件:Canal。

Canal这东西,说白了就是阿里巴巴开源的一个MySQL binlog解析工具。我最早接触它是在2018年,那时候公司要做订单数据的实时同步到Elasticsearch,调研了一圈,最后选了Canal。嗯,这一用就是好几年,踩过坑,也填过坑,今天把这些经验都揉碎了讲给你。

4.1 Canal是什么

Canal,全称是「Canal Alibaba」,中文意思是「运河」。名字挺形象——它就像一条数据运河,把MySQL的数据源源不断地运送到下游系统。

它的核心能力就一句话:伪装成MySQL的从库,拉取主库的binlog,然后解析成你可以消费的数据格式。

你想想看,MySQL主从复制大家都知道吧?主库写binlog,从库拉binlog然后回放。Canal干的活,本质上跟从库一模一样。只不过从库把binlog拿来写数据,Canal把binlog拿来给你做业务处理。

核心定位: Canal是一个中间件,它不存储数据,只做数据的搬运和解析。它支持的数据格式包括JSON、Avro、Protobuf等,最常用的是JSON。

我个人习惯把Canal放在数据链路的最前端,紧挨着MySQL。它后面可以接Kafka、RocketMQ、Elasticsearch,甚至直接写文件。灵活性非常高。

4.2 Canal工作原理

理解Canal的工作原理,其实就理解了MySQL主从复制的精髓。我画了一张图,你看完就明白了。

Canal工作原理流程图 Canal工作原理流程图 MySQL 主库 写入 binlog 拉取 binlog Canal Server 伪装为 MySQL Slave 解析 binlog 解析引擎 INSERT/UPDATE/DELETE 输出事件 事件输出 JSON / Avro / Protobuf 投递 Kafka 消息队列 Elasticsearch 搜索引擎 RocketMQ 消息队列 自定义 HTTP / 文件 💡 核心:Canal 伪装成 MySQL Slave,通过 dump 协议拉取 binlog,解析后投递给下游。

整个流程其实就三步:

  1. 伪装:Canal把自己伪装成一个MySQL从库,向主库发送dump请求。
  2. 拉取:主库把binlog推送给Canal,Canal接收并缓存。
  3. 解析:Canal把binlog的二进制格式解析成结构化事件,比如INSERT、UPDATE、DELETE。

这里有个关键点——binlog的格式。MySQL的binlog有三种格式:STATEMENT、ROW、MIXED。Canal要求MySQL必须开启ROW模式,因为只有ROW模式才能记录每一行数据的变化细节。STATEMENT模式只记录SQL语句,解析出来你根本不知道具体改了哪些数据。

注意: 如果你的MySQL binlog格式是STATEMENT或MIXED,Canal无法正常工作。我曾经接手过一个项目,对方说Canal连不上,我一看binlog_format还是STATEMENT……改完重启就好了。

4.3 Canal的安装与启动

安装Canal其实不复杂,但有几个前置条件必须满足。我按步骤来,你跟着做就行。

4.3.1 前置条件

组件 版本要求 说明
MySQL 5.7+ 或 8.0+ 必须开启binlog,格式为ROW
JDK 1.8+ Canal基于Java开发
操作系统 Linux / macOS / Windows 推荐Linux,生产环境稳定

4.3.2 MySQL配置

首先,确保你的MySQL已经开启了binlog。检查一下my.cnf(或my.ini)配置文件:

[mysqld]
# 开启binlog
log-bin=mysql-bin
# binlog格式必须为ROW
binlog-format=ROW
# 服务器ID,主从复制需要唯一
server-id=1
# 需要同步的数据库(可选,不配置则同步所有)
binlog-do-db=test_db

配置完重启MySQL,然后登录验证:

mysql> SHOW VARIABLES LIKE 'log_bin';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| log_bin       | ON    |
+---------------+-------+

mysql> SHOW VARIABLES LIKE 'binlog_format';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| binlog_format | ROW   |
+---------------+-------+

嗯,看到ON和ROW就对了。

4.3.3 创建Canal用户

Canal需要连接MySQL,所以得给它一个账号。我个人习惯单独创建一个用户,权限最小化:

mysql> CREATE USER 'canal'@'%' IDENTIFIED BY 'canal123';
mysql> GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
mysql> FLUSH PRIVILEGES;

这里注意,REPLICATION SLAVE和REPLICATION CLIENT是必须的,少了任何一个Canal都拉不到binlog。SELECT权限是为了读取表结构信息。

4.3.4 下载与安装Canal

去Canal的GitHub Releases页面下载最新稳定版。我写这篇文章时,最新稳定版是1.1.7。下载后解压:

# 下载
wget https://github.com/alibaba/canal/releases/download/canal-1.1.7/canal.deployer-1.1.7.tar.gz

# 解压
tar -zxvf canal.deployer-1.1.7.tar.gz -C /usr/local/canal

解压后的目录结构是这样的:

/usr/local/canal/
├── bin/          # 启动脚本
├── conf/         # 配置文件
├── lib/          # 依赖jar包
├── logs/         # 日志目录
└── plugin/       # 插件目录

4.3.5 配置Canal

核心配置文件在conf/canal.properties和conf/example/instance.properties。前者是全局配置,后者是实例配置。我们主要改实例配置:

# conf/example/instance.properties

# MySQL地址
canal.instance.master.address=127.0.0.1:3306

# 数据库账号
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal123

# 需要订阅的数据库和表
# 格式:dbName.tableName,支持正则
canal.instance.filter.regex=test_db\\..*

# 解析后投递的目标
# tcp: 直接TCP输出(默认)
# kafka: 投递到Kafka
# rocketmq: 投递到RocketMQ
canal.serverMode=tcp

这里有个坑——正则表达式中的反斜杠要转义。我第一次配置时写成了test_db\..*,结果Canal启动后什么数据都抓不到,查了半天日志才发现是正则写错了。

4.3.6 启动Canal

启动很简单,执行脚本就行:

# 启动
/usr/local/canal/bin/startup.sh

# 查看日志
tail -f /usr/local/canal/logs/canal/canal.log

看到类似这样的日志,说明启动成功:

2024-01-15 10:30:00.123 [main] INFO  c.a.o.c.CanalLauncher - ## start the canal server.
2024-01-15 10:30:01.456 [main] INFO  c.a.o.c.CanalLauncher - ## the canal server is running now ......

小技巧: 启动后可以用jps命令查看Java进程,如果看到CanalLauncher,说明Canal已经跑起来了。

4.4 Canal连接MySQL测试

安装启动都搞定了,接下来验证一下Canal能不能正常拉取数据。Canal自带了一个简单的客户端示例,我们可以用它来测试。

4.4.1 编写测试客户端

创建一个Maven项目,引入Canal客户端依赖:

<dependency>
    <groupId>com.alibaba.otter</groupId>
    <artifactId>canal.client</artifactId>
    <version>1.1.7</version>
</dependency>

然后写一个简单的Java类:

import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.protocol.Message;
import com.alibaba.otter.canal.protocol.CanalEntry.Entry;
import com.alibaba.otter.canal.protocol.CanalEntry.EntryType;
import com.alibaba.otter.canal.protocol.CanalEntry.RowChange;
import com.alibaba.otter.canal.protocol.CanalEntry.RowData;

import java.net.InetSocketAddress;
import java.util.List;

public class CanalClientTest {
    public static void main(String[] args) {
        // 1. 创建连接
        CanalConnector connector = CanalConnectors.newSingleConnector(
            new InetSocketAddress("127.0.0.1", 11111),
            "example",  // instance名称
            "",         // 用户名(默认空)
            ""          // 密码(默认空)
        );

        // 2. 连接并订阅
        connector.connect();
        connector.subscribe("test_db\\..*");

        // 3. 循环拉取数据
        while (true) {
            // 每次拉取100条,超时时间1秒
            Message message = connector.getWithoutAck(100, 1000);
            long batchId = message.getId();
            int size = message.getEntries().size();

            if (batchId == -1 || size == 0) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                continue;
            }

            // 4. 解析数据
            for (Entry entry : message.getEntries()) {
                if (entry.getEntryType() == EntryType.ROWDATA) {
                    RowChange rowChange;
                    try {
                        rowChange = RowChange.parseFrom(entry.getStoreValue());
                    } catch (Exception e) {
                        throw new RuntimeException("解析失败", e);
                    }

                    // 获取事件类型
                    String eventType = rowChange.getEventType().name();
                    System.out.println("事件类型: " + eventType);

                    // 遍历行数据
                    for (RowData rowData : rowChange.getRowDatasList()) {
                        if (eventType.equals("INSERT") || eventType.equals("UPDATE")) {
                            System.out.println("新增数据: " + rowData.getAfterColumnsList());
                        }
                        if (eventType.equals("DELETE") || eventType.equals("UPDATE")) {
                            System.out.println("旧数据: " + rowData.getBeforeColumnsList());
                        }
                    }
                }
            }

            // 5. 确认消费
            connector.ack(batchId);
        }
    }
}

4.4.2 运行测试

运行这个客户端,然后在MySQL里执行一些操作:

mysql> USE test_db;
mysql> CREATE TABLE user (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50),
    age INT
);

mysql> INSERT INTO user(name, age) VALUES('张三', 25);
mysql> UPDATE user SET age = 26 WHERE name = '张三';
mysql> DELETE FROM user WHERE name = '张三';

你会看到客户端控制台输出类似这样的内容:

事件类型: INSERT
新增数据: [id:1, name:张三, age:25]

事件类型: UPDATE
新增数据: [id:1, name:张三, age:26]
旧数据: [id:1, name:张三, age:25]

事件类型: DELETE
旧数据: [id:1, name:张三, age:26]

看到这个输出,说明Canal已经成功抓取到了MySQL的binlog变化。恭喜,你的第一个CDC链路跑通了!

验证要点:

  • INSERT事件:只有afterColumnsList有数据
  • UPDATE事件:beforeColumnsList和afterColumnsList都有数据
  • DELETE事件:只有beforeColumnsList有数据

4.5 常见问题与避坑

最后,分享几个我在项目中遇到过的坑,你遇到了可以直接对照排查。

问题1:Canal启动后日志报错「No suitable driver」

原因:MySQL驱动版本不匹配。Canal 1.1.7默认带的驱动是5.x,如果你连的是MySQL 8.0,需要手动替换驱动为8.x版本。去lib目录下把mysql-connector-java-5.x.jar换成mysql-connector-java-8.x.jar。

问题2:Canal连接MySQL报错「Access denied for user」

原因:用户权限不足。检查一下REPLICATION SLAVE和REPLICATION CLIENT权限是否已经授予。我曾经遇到过只给了SELECT权限,结果Canal一直连不上。

问题3:Canal能启动但抓不到数据

原因:最常见的是binlog格式不是ROW,或者正则表达式写错了。先检查MySQL的binlog_format,再检查instance.properties里的canal.instance.filter.regex。我建议先用test_db\\..*这种最简单的正则测试,通了再细化。