本文将从 Canal 的使用场景、工作原理和架构开始介绍,然后总结了计划使用canal前必须要知道的 15 个关键问题。
Canal 介绍
canal [kə'næl],译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费。
我这里主要基于 canal 实现以下三个场景:
- 数据库的实时备份(等保要求数据库实时异地备份,可以基于canal实现)。
- 将相关业务表数据写入 es 中存储,实现高级搜索(分词后搜索)。
- 某些特殊的业务场景,例如我公众号上一篇文章《利用 Canal 基于 binlog 处理跨天数据,再也不怕客户投诉账单和流水对不上了》
工作原理
MySQL 主备复制原理
- MySQL master 将数据变更写入二进制日志( binary log, 其中记录叫做二进制日志事件binary log
events,可以通过 show binlog events 进行查看) - MySQL slave 将 master 的 binary log events 拷贝到它的中继日志(relay log)
- MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据
canal 工作原理
- canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议。
- MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal )。
- canal 解析 binary log 对象(原始为 byte 流)。
当前的 canal 支持源端 MySQL 版本包括 5.1.x , 5.5.x , 5.6.x , 5.7.x , 8.0.x 。
Canal 架构
1、server代表一个canal运行实例,对应于一个jvm。
2、instance对应于一个数据队列(用来操作不同的 MySQL 实例)。
instance 模块
- eventParser (数据源接入,模拟slave协议和master进行交互,协议解析)
- eventSink (Parser和Store链接器,进行数据过滤,加工,分发的工作)
- eventStore (数据存储)
- metaManager (增量订阅&消费信息管理器)
canal 使用总结
配置管理
开源版本的 canal 基于本地spring xml的配置管理方式,其原理是将整个配置抽象为两部分:
xxxx-instance.xml:canal组件的配置定义,可以在多个instance配置中共享(默认在 ${CANAL_HOME}/conf/spring/ 目录下)。
xxxx.properties:每个instance都有一个独立的配置,连接的 mysql 地址、账号、密码可能会不同。
[canal@imzcy canal]$ ll conf/spring/
total 60
-rwxrwxrwx 1 canal canal 2192 Nov 16 2022 base-instance.xml
-rwxrwxrwx 1 canal canal 11154 Sep 11 10:52 default-instance.xml
-rwxrwxrwx 1 canal canal 10682 Sep 11 10:52 file-instance.xml
-rwxrwxrwx 1 canal canal 15722 Apr 21 2023 group-instance.xml
-rwxrwxrwx 1 canal canal 10192 Sep 11 10:51 memory-instance.xml
drwxr-xr-x 4 canal canal 4096 Feb 16 18:27 tsdb
[canal@imzcy canal]$
properties配置分为两部分:
- canal.properties:(系统根配置文件)。
- instance.properties:(instance级别的配置文件,每个instance一份)。
位点组件
先了解一下canal如何维护一份增量订阅&消费的关系信息:
- 解析位点(parse模块会记录,上一次解析binlog到了什么位置,对应组件为:CanalLogPositionManager)。
- 消费位点(canal server在接收了客户端的ack后,就会记录客户端提交的最后位点,对应的组件为:CanalMetaManager)。
对应的两个位点组件,目前都有几种实现:
- mixed:file-instance.xml 中使用,先写内存,定时刷新数据到本地文件中。
- zookeeper
- period:default-instance.xml中使用,集合了zookeeper+memory模式,先写内存,定时刷新数据到zookeeper上。
- memory:memory-instance.xml中使用。
默认位点信息存储
canal 默认情况下使用 file-instance.xml
[canal@imzcy conf]$ cat canal.properties |grep global.spring.xml |grep -v ^#
canal.instance.global.spring.xml = classpath:spring/file-instance.xml
[canal@imzcy conf]$
[canal@imzcy conf]$ cat spring/file-instance.xml |grep metaManager
<property name="metaManager">
<ref bean="metaManager" />
<bean id="metaManager" class="com.alibaba.otter.canal.meta.FileMixedMetaManager">
<constructor-arg ref="metaManager"/>
[canal@imzcy conf]$
将位点信息保存到每个 instance 目录下的 meta.dat 文件中
[canal@imzcy canal]$ cat conf/zcy_test/meta.dat |jq .
{
"clientDatas": [
{
"clientIdentity": {
"clientId": 1001,
"destination": "zcy_test",
"filter": ""
},
"cursor": {
"identity": {
"slaveId": -1,
"sourceAddress": {
"address": "localhost",
"port": 3306
}
},
"postion": {
"gtid": "",
"included": false,
"journalName": "mysql-bin.000012",
"position": 5332,
"serverId": 1,
"timestamp": 1708169799000
}
}
}
],
"destination": "zcy_test"
}
[canal@imzcy canal]$
工作模式和端口
canal 默认 serverMode 为 tcp 模式,可选为 tcp, kafka, rocketMQ, rabbitMQ, pulsarMQ 。
[canal@imzcy conf]$ cat canal.properties |grep serverMode
canal.serverMode = tcp
[canal@imzcy conf]$
默认会监听如下三个端口,其中 11111 为工作于 tcp 模式时才会监听的端口,11112 为 prometheus 拉取监控数据时请求的端口,11110 为 canal admin 监听的端口。
[canal@imzcy conf]$ cat canal.properties |grep 'port ='
canal.port = 11111
canal.metrics.pull.port = 11112
canal.admin.port = 11110
[canal@imzcy conf]$
多实例
根据上面 canal 的架构我们知道,一个 canal server 中,可以运行多个 instance 用于连接处理多个 mysql 实例的数据。方法是直接在 ${CANAL_HOME}/conf/ 目录下基于名为 example 的示例 instance 复制出多个即可(目录名将作为 instance 的名称)。
[canal@imzcy conf]$ cp -rp example/ test
[canal@imzcy conf]$ ll test/
total 4
-rwxrwxrwx 1 canal canal 2224 Apr 21 2023 instance.properties
[canal@imzcy conf]$
动态加载 instance 配置
canal 默认情况下启用了自动扫描功能,自动将 ${CANAL_HOME}/conf/ 下所有包含 instance.properties 文件的子目录都作为一个 instance 来启动(创建空目录不或不包含 instance.properties 文件的目录不会被匹配上)。
[canal@imzcy conf]$ cat canal.properties |grep scan
# auto scan instance dir add/remove and start/stop instance
canal.auto.scan = true
canal.auto.scan.interval = 5
[canal@imzcy conf]$
canal server 运行过程中,也会本剧上面 interval 定义的间隔时间(秒)来进行扫描,如果发现有包含 instance.properties 文件的目录新增则启动新的 instance,如果发现目录被删除则停止对应的 instance,发现对应目录下 instance.properties 文件被更改则重启 instance。
修改 example目录下 instance.properties 文件配置
2024-02-17 16:29:33.825 [canal-instance-scan-0] INFO com.alibaba.otter.canal.deployer.CanalController - auto notify stop example successful.
2024-02-17 16:29:34.513 [canal-instance-scan-0] INFO com.alibaba.otter.canal.deployer.CanalController - auto notify start example successful.
2024-02-17 16:29:34.513 [canal-instance-scan-0] INFO com.alibaba.otter.canal.deployer.CanalController - auto notify reload example successful
添加名为 test 的 instance(添加test目录)
2024-02-17 16:57:25.869 [canal-instance-scan-0] INFO com.alibaba.otter.canal.deployer.CanalController - auto notify start test successful
删除名为 test 的 instance(删除 test 目录)
2024-02-17 16:59:25.914 [canal-instance-scan-0] INFO com.alibaba.otter.canal.deployer.CanalController - auto notify stop test successful.
正在运行的 canal server 如果要添加新的 instance ,最好是在 conf 目录外先创建好对应的目录,调整好 instance.properties 文件配置后,直接将目录移动到 canal conf 目录下(避免自动扫描将未调整配置的instance启动引起报错等)。
手动管理 instance 配置
如果不希望动态管理 instance 配置,也可以禁用掉自动扫描,采用手动管理的方式。
[canal@imzcy conf]$ cat canal.properties |grep 'scan\|destinations'
######### destinations #############
canal.destinations = example,test
# auto scan instance dir add/remove and start/stop instance
canal.auto.scan = false
[canal@imzcy conf]$
如上所示,将 canal.auto.scan 参数值设置为 false 来禁用自动扫描;通过 canal.destinations 参数手动指定(英文逗号分隔多个 instance 名称,即文件夹名称)要启用的 instance,conf目录下即使有多个instance配置,只要没有在这里显示指定,对应 instance 就不会被启动。
确认 slaveId 是否会重复
如果配置多个 instance 连接同一个 mysql 时,不确定指定的 slaveId 是否被使用,可以到 mysql 上查询一下。
mysql> show slave hosts;
+-----------+-----------+-------+-----------+--------------------------------------+
| Server_id | Host | Port | Master_id | Slave_UUID |
+-----------+-----------+-------+-----------+--------------------------------------+
| 12 | 127.0.0.1 | 40780 | 1 | 0223f366-cd88-11ee-8e07-000c29a86b73 |
| 22 | 127.0.0.1 | 40972 | 1 | 215bb2fa-cda5-11ee-8e07-000c29a86b73 |
+-----------+-----------+-------+-----------+--------------------------------------+
2 rows in set (0.00 sec)
mysql>
如上所示,当前 12、22 这两个已经被使用了。
instance 首次启动时使用的 binlog 位置
在实例的 instance.properties 配置文件中可以指定首次启动时 binlog 开始的位置,有两种方式。
- 一种是直接指定 binlog 文件和 position
[canal@imzcy conf]$ vi zcy_test/instance.properties
# position info
canal.instance.master.address=127.0.0.1:3306
canal.instance.master.journal.name=mysql-bin.000012
canal.instance.master.position=5952
[canal@imzcy conf]$
- 另一种是直接指定时间戳,canal 会自动查找指定时间戳对应 binlog 位点
[canal@imzcy conf]$ vi zcy_test/instance.properties
# position info
canal.instance.master.address=127.0.0.1:3306
canal.instance.master.timestamp=1708165800000
[canal@imzcy conf]$
如果不指定任何位点信息,只提供 mysql 实例的连接地址,那么则会从当前位点开始:
mysql> show master status;
+------------------+----------+--------------+------------------+-------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+------------------+----------+--------------+------------------+-------------------+
| mysql-bin.000012 | 5952 | | | |
+------------------+----------+--------------+------------------+-------------------+
1 row in set (0.02 sec)
mysql>
mq的顺序性
将 binlog 数据发送到 mq(例如 kafka)时,必须要考虑的一个问题就是数据的顺序性。假设 topic 有三个分区,那么 canal 在写入数据的时候会分散写入到三个分区中,但是 kafka 只能保证数据在一个分区内的顺序性,当客户端连接到 kafka 多个分区消费数据的时候,就会导致数据不会像在mysql执行时的顺序一样被处理。在 mysql 里某条记录先更新了某个 number 字段值为 50 然后又更新为 20 ,从 kafka 消费后顺序变了就有可能先执行更新为 20 的条目 ,再更新为 50 ,这样最终数据就不对了。
上面的问题 canal 官方也进行了解答,canal的消费顺序性主要取决于用户使用的MQ数据的路由方式:单topic单分区,单topic多分区、多topic单分区、多topic多分区。
- 单topic单分区可以保证和 binlog 一样的顺序性,缺点就是性能比较慢。
- 多topic单分区可以保证表级别的顺序性,例如每个表数据写入到单独的 topic 中。
- 如果有多分区的情况,就会出现业务消费先后顺序的问题(需要特殊处理),这里目前不考虑使用此方式。
正则匹配
在 canal 实例的配置文件 instance.properties 中,canal.instance.filter.regex 指定要处理连接到 mysql 实例中哪些表的数据。多个正则表达式使用英文逗号(,)分隔,转义字符使用双反斜杠(\)。
所有表: .* or .*\\..*
imzcy 库中的所有表:imzcy\\..*
imzcy 库中的账户表和订单表:imzcy\\.kernel_account, imzcy\\.kernel_order
imzcy 库中以 test 开头的表: imzcy\\.test.*
imzcy 库中名为 test1 的这一个表:imzcy.test1
多种规则的组合:canal\\..*, mysql.test1, mysql.test2 (以英文逗号作为分隔符)
动态 topic
在 canal 实例的配置文件 instance.properties 中,canal.mq.dynamicTopic 指定 canal 工作在 kafka 模式时对应表的数据应该写入到哪个 topic 中(多个配置之间以英文逗号隔开)。
test\\.test 指定匹配的单表,发送到以test_test为名字的topic上。
test\\..* 匹配test库下所有表,针对匹配的表会发送到各自表名的topic上。
.*\\..* 匹配所有表,每个表都会发送到各自表名的topic上(库名和表名以下划线连接,例如 testdb 下的 kernel_order 表匹配后写入的 topic 名称为 testdb_kernel_order)。
同时为满足更大的灵活性,canal 允许对匹配条件的规则指定发送的topic名字,配置格式:topicName:schema 或 topicName:schema.table 。
test:test\\.test 指定匹配的单表,发送到以test为名字的topic上。
test:test 匹配对应的库,test 库的所有表都会发送到名为test的topic下。
test0:test,test1:test1\\.test1 指定多个表达式,会将test库的表都发送到test0的topic下,test1\\.test1的表发送到对应的test1的topic下,其余的表发送到默认的canal.mq.topic值。
启动和停止 canal
目录 ${CANAL_HOME}/bin/ 下存放了操作 canal 的相关脚本
[canal@imzcy canal]$ ll bin/
total 16
-rwxr-xr-x 1 canal canal 226 Nov 16 2022 restart.sh
-rwxr-xr-x 1 canal canal 1244 Nov 16 2022 startup.bat
-rwxr-xr-x 1 canal canal 3805 Oct 8 14:38 startup.sh
-rwxr-xr-x 1 canal canal 1356 Nov 16 2022 stop.sh
[canal@imzcy canal]$
执行 startup.sh 脚本即可启动 canal server
[canal@imzcy canal]$ sh bin/startup.sh
cd to /usr/local/canal/bin for workaround relative path
LOG CONFIGURATION : /usr/local/canal/bin/../conf/logback.xml
canal conf : /usr/local/canal/bin/../conf/canal.properties
CLASSPATH :/usr/local/canal/bin/../conf:/usr/local/canal/bin/../lib/zstd-jni-1.5.2-5.jar:/usr/local/canal/bin/../lib/zookeeper-jute-3.5.6.jar:/usr/local/canal/bin/../lib/zookeeper-3.5.6.jar:/usr/local/canal/bin/../lib/zkclient-0.10.jar:/usr/local/canal/bin/../lib/spring-tx-5.3.9.jar:/usr/local/canal/bin/../lib/spring-orm-5.3.9.jar:/usr/local/canal/bin/../lib/spring-jdbc-5.3.9.jar:/usr/local/canal/bin/../lib/spring-jcl-5.3.9.jar:/usr/local/canal/bin/../lib/spring-expression-5.3.9.jar:/usr/local/canal/bin/../lib/spring-core-5.3.9.jar:/usr/local/canal/bin/../lib/spring-context-5.3.9.jar:/usr/local/canal/bin/../lib/spring-beans-5.3.9.jar:/usr/local/canal/bin/../lib/spring-aop-5.3.9.jar:/usr/local/canal/bin/../lib/slf4j-api-1.7.12.jar:/usr/local/canal/bin/../lib/simpleclient_pushgateway-0.4.0.jar:/usr/local/canal/bin/../lib/simpleclient_httpserver-0.4.0.jar:/usr/local/canal/bin/../lib/simpleclient_hotspot-0.4.0.jar:/usr/local/canal/bin/../lib/simpleclient_common-0.4.0.jar:/usr/local/canal/bin/../lib/simpleclient-0.4.0.jar:/usr/local/canal/bin/../lib/protobuf-java-3.6.1.jar:/usr/local/canal/bin/../lib/oro-2.0.8.jar:/usr/local/canal/bin/../lib/netty-all-4.1.68.Final.jar:/usr/local/canal/bin/../lib/netty-3.2.10.Final.jar:/usr/local/canal/bin/../lib/mysql-connector-java-5.1.48.jar:/usr/local/canal/bin/../lib/mybatis-spring-2.0.4.jar:/usr/local/canal/bin/../lib/mybatis-3.5.6.jar:/usr/local/canal/bin/../lib/logback-core-1.2.8.jar:/usr/local/canal/bin/../lib/logback-classic-1.2.8.jar:/usr/local/canal/bin/../lib/jsr305-3.0.2.jar:/usr/local/canal/bin/../lib/joda-time-2.9.4.jar:/usr/local/canal/bin/../lib/jctools-core-2.1.2.jar:/usr/local/canal/bin/../lib/jcl-over-slf4j-1.7.12.jar:/usr/local/canal/bin/../lib/javax.annotation-api-1.3.2.jar:/usr/local/canal/bin/../lib/j2objc-annotations-1.1.jar:/usr/local/canal/bin/../lib/httpcore-4.4.13.jar:/usr/local/canal/bin/../lib/httpclient-4.5.13.jar:/usr/local/canal/bin/../lib/h2-2.1.210.jar:/usr/local/canal/bin/../lib/guava-22.0.jar:/usr/local/canal/bin/../lib/fastjson2-2.0.31.jar:/usr/local/canal/bin/../lib/error_prone_annotations-2.0.18.jar:/usr/local/canal/bin/../lib/druid-1.2.17.jar:/usr/local/canal/bin/../lib/disruptor-3.4.2.jar:/usr/local/canal/bin/../lib/connector.core-1.1.7.jar:/usr/local/canal/bin/../lib/commons-logging-1.2.jar:/usr/local/canal/bin/../lib/commons-lang3-3.7.jar:/usr/local/canal/bin/../lib/commons-lang-2.6.jar:/usr/local/canal/bin/../lib/commons-io-2.4.jar:/usr/local/canal/bin/../lib/commons-compress-1.22.jar:/usr/local/canal/bin/../lib/commons-collections-3.2.2.jar:/usr/local/canal/bin/../lib/commons-codec-1.9.jar:/usr/local/canal/bin/../lib/commons-beanutils-1.9.4.jar:/usr/local/canal/bin/../lib/canal.store-1.1.7.jar:/usr/local/canal/bin/../lib/canal.sink-1.1.7.jar:/usr/local/canal/bin/../lib/canal.server-1.1.7.jar:/usr/local/canal/bin/../lib/canal.protocol-1.1.7.jar:/usr/local/canal/bin/../lib/canal.prometheus-1.1.7.jar:/usr/local/canal/bin/../lib/canal.parse.driver-1.1.7.jar:/usr/local/canal/bin/../lib/canal.parse.dbsync-1.1.7.jar:/usr/local/canal/bin/../lib/canal.parse-1.1.7.jar:/usr/local/canal/bin/../lib/canal.meta-1.1.7.jar:/usr/local/canal/bin/../lib/canal.instance.spring-1.1.7.jar:/usr/local/canal/bin/../lib/canal.instance.manager-1.1.7.jar:/usr/local/canal/bin/../lib/canal.instance.core-1.1.7.jar:/usr/local/canal/bin/../lib/canal.filter-1.1.7.jar:/usr/local/canal/bin/../lib/canal.deployer-1.1.7.jar:/usr/local/canal/bin/../lib/canal.common-1.1.7.jar:/usr/local/canal/bin/../lib/aviator-2.2.1.jar:/usr/local/canal/bin/../lib/audience-annotations-0.5.0.jar:/usr/local/canal/bin/../lib/animal-sniffer-annotations-1.14.jar:/usr/local/java/lib/dt.jar:/usr/local/java/lib/tools.jar
cd to /usr/local/canal for continue
[canal@imzcy canal]$
启动脚本会在 bin 目录下创建一个名为 canal.pid 的文件,后续 stop.sh 脚本会根据此文件中 pid 来停止 canal server 。
[canal@imzcy canal]$ cat bin/canal.pid
8959
[canal@imzcy canal]$
停止后 canal.pid 文件就会被自动删除
[canal@imzcy canal]$ sh bin/stop.sh
imzcy: stopping canal 8959 ...
Oook! cost:1
[canal@imzcy canal]$
[canal@imzcy canal]$ ll bin/
total 16
-rwxr-xr-x 1 canal canal 226 Nov 16 2022 restart.sh
-rwxr-xr-x 1 canal canal 1244 Nov 16 2022 startup.bat
-rwxr-xr-x 1 canal canal 3805 Oct 8 14:38 startup.sh
-rwxr-xr-x 1 canal canal 1356 Nov 16 2022 stop.sh
[canal@imzcy canal]$
如果canal server 启动后被异常结束,那么当再次执行 startup.sh 脚本启动时会因为已存在 canal.pid 文件而报错需要先执行 stop.sh 再启动。
[canal@imzcy canal]$ sh bin/startup.sh
found canal.pid , Please run stop.sh first ,then startup.sh
[canal@imzcy canal]$
HA
为了保证 canal 服务的高可用,可以以 HA 模式部署 canal (需要依赖外部 zookeeper 实现注册发现以及存储位点信息等)。
相比之前单节点部署 canal,只需要调整如下配置:
[canal@imzcy conf]$ vi canal.properties
canal.zkServers = 192.168.13.212:2181,192.168.13.212:2182,192.168.13.212:2183
canal.instance.global.spring.xml = classpath:spring/default-instance.xml
[canal@imzcy conf]$
第2行指定 zookeeper 集群地址,第3行指定使用 default-instance.xml 配置(这样才会将位点信息存储到 zookeeper ,否则默认使用 file-instance.xml 配置会存储在本地)。
另外需要注意的是,部署到多台服务器上的 canal 做 HA 的时候,除了连接同一个 zookeeper 地址,做 HA 的 instance 名称(目录名)也必须一样,并且需要注意 slaveId 不要重复。
HA 模式部署 canal 后,多个服务器上部署的 instance 同时只会有一个在运行,正在运行的节点挂掉后,其他节点会接替继续工作。
监控
canal 原生支持prometheus监控,只需要新增一个 job 指定 canal.properties 配置文件中 canal.metrics.pull.port 的端口即可(默认为11112)。
- job_name: 'canal'
static_configs:
- targets: ['192.168.2.103:11112']
需要注意的是,如果 HA 模式部署的 canal,存在多个节点时,每个节点都要加到 targets 里。
相关参考:
本文中关于 canal 介绍、工作原理、架构描述及相关配图来自 canal 官方:https://github.com/alibaba/canal/wiki/Home
本文采用 知识共享署名4.0 国际许可协议进行许可。
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名。
如果您的问题未解决,欢迎微信扫描右侧二维码与我联系。