☰
Canal实时同步实战:MySQL binlog到目标库的部署原理与避坑指南
2026/10/8 3:17:14 网站建设 项目流程

做过几年后端,团队业务库越来越大,遇到最频繁的一个问题就是:客户要在大屏上看到分钟级甚至秒级的数据,而原来的订单库和报表库中间还是定时脚本在搬运数据。每次业务量一上来,定时任务延迟、漏数据、目标库压力各种问题就全冒出来了。后来彻底换了思路,用 Canal 订阅 MySQL binlog 做实时同步,算是把这个老大难问题解决了。整个过程涉及 MySQL 侧开启 binlog、创建复制账号,再到部署 Canal Server、配置 adapter 写入目标库,很多细节不自己踩一遍根本发现不了。这篇文章就把我从零开始跑通“MySQL 到其它库实时同步”的完整过程,以及中间踩过的坑,原原本本分享出来。

1. 为什么选了 Canal:从轮询脚本到 CDC 的思路转变

1.1 最初定时任务的瓶颈在哪里

早几年做小规模项目时,实现跨库同步最直觉的方案就是写个定时任务,每隔几秒或几十秒去源库执行一次SELECT ... WHERE update_time > ?,然后把增量数据搬进目标库。这种方式在小表、低并发场景下确实能用,但它有几个天然天花板:

  • 高频轮询对源库压力很大。每多一个同步任务,就多一串 SELECT 在跑,业务高峰期源库 CPU 和 IO 很容易被这些“顺手查询”拖垮。
  • 增量判断字段依赖业务表结构。如果业务表没有update_time,或者更新这列时没维护好,漏数据是必然的,而且很难发现。
  • 数据量大之后,区间扫描越来越慢,同步间隔被迫拉长,实时性无从谈起。
  • 对 UPDATE、DELETE 这类变更,轮询往往只拿到“当前结果”,拿不到变更前后的完整现场,很难做到精确同步和审计。

这些痛点说白了,是方案本身的问题——你在用“读业务表”的方式模拟“读日志”,注定是绕远路。

1.2 把目光转到 binlog 上

MySQL 本身在主从复制时靠的就是 binlog,从库可以实时收到主库的每一个变更事件。既然 MySQL 已经提供了这个“日志级”的数据通道,那同步工具完全可以复用它:自己不主动查业务表,而是伪装成 MySQL 的从库,等源库把 binlog 推过来,再解析成结构化数据发给下游。

这个方向在业界有个统一的名字,叫 CDC(Change Data Capture),中文常叫变更数据捕获。Canal 就是阿里开源的一整套 CDC 实现,专门做 MySQL binlog 的解析和分发。它的优势主要在这几点:

  • 数据实时性高。解析的是 binlog 事件,理论上延迟能到毫秒级,日常稳定跑也能做到几百毫秒以内。
  • 对源库侵入小。Canal 连接源库时只走复制协议,不额外查询业务表,权限也只需要复制相关权限,比在业务库执行高频 SELECT 干净得多。
  • 自带丰富适配器。官方支持 MySQL、PostgreSQL、Elasticsearch、HBase 等目标端,也能对接 Kafka、RocketMQ,基本覆盖了常见同步场景。

如果用一句话总结我对 Canal 的理解:它把 MySQL 的 binlog 变成了一条可编程的数据管道,源库只需开启 binlog、给一个复制账号,剩下的事情由 Canal 帮你解析、过滤、分发。

2. 伪装从库:Canal 与 binlog 之间的协议原理

2.1 Canal 到底怎么拿到 binlog 的

要弄懂 Canal 的配置,必须先知道它和 MySQL 之间是怎么通信的。MySQL 主从复制的大致流程是这样的:从库启动一个 IO 线程,连上主库后发送 dump 请求;主库收到请求后,开启一个 dump 线程,把 binlog 事件不断发送给从库;从库的 IO 线程把事件写进自己的 relay log,再由 SQL 线程重放。

Canal 做的事情很有意思,它把自己模拟成一个从库,向 MySQL 发送同样的 dump 请求,拿到 binlog 事件后不重放,而是直接解析成结构化消息。所以很多时候大家把 Canal 称为“伪装从库”,就是这个原因。也正因为复用的是成熟的主从复制协议,Canal 拿到的事件内容天然和从库是一致的,不会出现“读业务表读到一半数据变了”这种不一致问题。

2.2 binlog 格式和事件结构

Canal 能正确解析事件,前提是源库 binlog 格式要设置成 ROW 模式。这是很关键的一个点。

MySQL 的 binlog 格式主要有三种:STATEMENT、ROW、MIXED。STATEMENT 记录的是执行过的 SQL 语句,比如UPDATE orders SET amount=100 WHERE id=1,这种格式体积小,但在跨环境同步时可能出现函数、时间、自增列等不可控因素,很难精确还原“每一行到底变成了什么”。ROW 模式记录的是行级变更镜像,包含每一行的前后值,比如 id 为 1 的订单金额从 80 改成 100,binlog 里就记录得非常清楚。Canal 解析 ROW 模式能拿到字段级 old 和 new 数据,下游做同步、审计、搜索更新都很方便。

所以实例配置里,尽量显式把binlog_format=ROW固定住,不要依赖默认值。顺手再检查一个参数叫binlog_row_image,我建议保持FULL。默认情况下 MySQL 5.7 之后这个值是 FULL,也就是记录整行的完整镜像;如果改成 MINIMAL,只记录主键和发生变化的列,Canal 解析出的数据可能不完整,下游需要回查源表,那又回到了“额外查询”的老路上,不值得。

2.3 从“拿事件”到“发数据”的模块划分

Canal 拿到 binlog 原始事件后,内部还要经过解析器和过滤器的处理,最终形成一行行结构化的变更数据。如果只是简单理解,可以把它想象成一条流水线:

  • 源库 binlog 事件进入 Canal Server。
  • Canal 根据不同表、不同操作类型,过滤出你需要关注的变更。
  • 解析后的数据被包装成统一格式,可以定义推送到 MQ,或者由 adapter 直接消费并写入目标端。

理解了这条流水线,你就能明白为什么配置里要同时设置“连接源库的地址账号”和“订阅过滤规则”,这俩确实是不一样的维度。很多人部署失败,就是把这两类配置混在一起了,后面第 4 节我会具体讲。

3. MySQL 侧开启 binlog 与复制账号授权的完整步骤

3.1 调整 my.cnf 并验证 binlog 生效

这一步是所有工作开始前的基础。如果你在 Windows 上用解压版 MySQL,配置文件通常是my.ini,Linux 下一般是/etc/my.cnf。无论是哪种,在[mysqld]段下准备加这些参数:

[mysqld] server-id = 100 log-bin = mysql-bin binlog_format = ROW binlog_row_image = FULL expire_logs_days = 7 max_binlog_size = 256M

这几个参数的含义逐个说清楚:

  • server-id必须有,它是 MySQL 实例在复制拓扑里的唯一标识。没有它,MySQL 启动时可能直接报错;如果和已有从库冲突,主从关系会错乱。建议整个机房统一规划,不要随手写。
  • log-bin指定 binlog 文件前缀,开启 binlog。文件通常会生成在数据目录下,文件名类似mysql-bin.000001。
  • binlog_format=ROW和binlog_row_image=FULL是给 Canal 解析打底,原因上面已经说过。
  • expire_logs_days控制 binlog 保留天数。MySQL 8.0 里这参数改成了binlog_expire_logs_seconds,8.0 环境按秒配置。保留太短会有位点断档风险,太长又占磁盘,初期调试阶段我建议 7 天起步。
  • max_binlog_size控制单个 binlog 文件大小,超过后会滚动生成新文件。256M 是常见值,具体看写入量。

改完配置后重启 MySQL。如果起不来,优先检查server-id是否设置、配置文件路径是否正确。在 Linux 上可以用这条确认 binlog 是否开启:

mysql -uroot -p -e "SHOW VARIABLES LIKE 'log_bin'; SHOW VARIABLES LIKE 'binlog_format';"

正常情况会看到log_bin是 ON,binlog_format是 ROW。还可以用SHOW MASTER STATUS;查看当前正在写的 binlog 文件和位点,这个文件位点信息后面排错时经常要用。

3.2 创建 Canal 专用复制账号

Canal 需要用专门的账号去连接源库,不建议复用 root 或高权限账号。原因是权限越小,误操作范围越小;而且 Canal 正常工作只需要三个权限:SELECT、REPLICATION SLAVE、REPLICATION CLIENT。具体建账号和授权的 SQL 如下:

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

这里解释一下为什么需要这三个权限:

  • SELECT:Canal 在部分场景下需要读取表结构信息来做字段映射,所以需要读权限。
  • REPLICATION SLAVE:核心权限,允许 Canal 以从库身份向主库请求 binlog。
  • REPLICATION CLIENT:允许 Canal 查看主库状态,比如SHOW MASTER STATUS,用来定位位点。

如果你的源库是 MySQL 8.0,要注意认证插件问题。MySQL 8.0 默认的caching_sha2_password认证方式,在部分 Canal 版本下会因为公钥交换或协议兼容问题导致连接失败。常见处理办法是显式指定账号用mysql_native_password:

CREATE USER 'canal'@'%' IDENTIFIED WITH mysql_native_password BY 'Canal@2024';

如果已经建了账号,也可以改:

ALTER USER 'canal'@'%' IDENTIFIED WITH mysql_native_password BY 'Canal@2024';

3.3 binlog 保留时长与 MySQL 安装中的常见坑

说完授权,我必须把 binlog 保留时长单独拿出来强调一遍。Canal 启动后会记住自己同步到哪个位点,下次启动从位点继续拉取。如果源库 binlog 因为过期被清理,而 Canal 的位点还停在老位置,它重新连接时会发现自己要读的文件已经不存在了,这时候除了重建同步位点没有别的办法,意味着历史数据要重新同步。

所以初期调试阶段,binlog 保留时间尽量放宽一点。我在测试环境直接设了expire_logs_days=15,上线稳定后再改回 7。如果要更保险,binlog 还可以定期备份到别的机器或对象存储,不过那是另一个体系的话题了。

还有个小知识点,如果你是完全从零部署 MySQL 环境,Windows 解压版最常见的坑是忘记初始化数据目录,启动时报data directory is empty。先执行mysqld --initialize-insecure,再启动服务;Docker 方式通常是挂载配置文件和数据卷时路径写错,导致容器起来了但配置没生效。无论哪种安装方式,最后都用SHOW VARIABLES LIKE 'log_bin';验证一下,别想当然以为参数写上就一定生效了。

4. 部署 Canal Server 与实例配置的关键点

4.1 先从官方架构认识两个不同的组件

在配置之前,建议先弄清楚 Canal 的两个组件分工:

  • Canal Server(也叫 canal deployer):负责连接 MySQL、解析 binlog、维护同步位点,同时对外提供 TCP 或 MQ 输出能力。
  • Canal Adapter(也叫 canal adapter):是独立进程或模块,负责消费 Canal Server 的数据,再写入具体目标端,比如 MySQL、Elasticsearch、HBase 等。

你可以把 Canal Server 想成一个“管道中枢”,把 Adapter 想成“末端水龙头”。如果目标端是消息队列,那通常只需要 Canal Server 直接对接 MQ,不需要 Adapter;如果目标端是普通数据库,就需要 Adapter 按照映射规则把数据落库。

4.2 单机版目录与核心配置文件

Canal 官方 Release 下载压缩包解压后,目录结构大概是:

  • bin:启动脚本
  • conf:配置文件目录
  • conf/canal.properties:Canal Server 全局配置
  • conf/example/instance.properties:单个同步实例配置,一个 instance 对应一组源库连接信息
  • lib和plugin:依赖与插件目录

如果你采用 Docker 方式部署,原理是一样的,只是把配置文件通过 volume 挂载进容器而已。我自己的经验是:如果只是验证功能,直接跑官方 Docker 镜像最省事;如果要长期维护,还是二进制方式更直观,方便看目录里生成的 meta 数据。

4.3 instance.properties 中最容易搞混的几个参数

打开conf/example/instance.properties,重点配置这些参数:

# Canal 连接源库 MySQL 的地址和账号 source.address = 192.168.1.10:3306 source.dbUsername = canal source.dbPassword = Canal@2024 # Canal 伪装成从库使用的 slaveId canal.instance.mysql.slaveId = 103 # 订阅规则 canal.instance.filter.regex = 数据库名\\..* canal.instance.filter.black.regex = mysql\\.slave_.*

这里有一个特别容易犯的错:把目标库的地址填到source.address里。我第一次部署时就干过这事,Canal 一直报连接拒绝,但目标库那边却多了大量无效连接,查了好久才发现是填反了。你要时刻记住,这个 source 配置永远指的是“源 MySQL”,不是“目标库”。

canal.instance.mysql.slaveId是伪装从库的编号,必须和当前 MySQL 拓扑里已有的 slaveId 不冲突。如果这台源库已经有两个从库,ID 分别占了 101、102,那 Canal 就设 103。一旦冲突,MySQL 会把 Canal 和真正的从库当成同一个复制源,导致其中一个连接被踢掉,影响线上复制。

4.4 订阅过滤规则的正则写法

过滤规则是很多人出问题的重灾区。canal.instance.filter.regex默认是.*\\..*,匹配所有库的所有表。实际使用中常用写法有这么几类:

  • 监听单库所有表:数据库名\\..*
  • 监听多库所有表:库1\\..*|库2\\..*
  • 只监听某张表:数据库名\\.表名

在 properties 文件里,正则中的点需要转义,所以通常看到的是双反斜杠写法。有些新手会在这里写单反斜杠,导致匹配不到任何表,表现为“Canal 日志正常但目标库收不到数据”。另一个容易忽略的问题是库名大小写。Linux 下 MySQL 的库名和表名区分大小写,正则同样区分,配置前先确认实际库名。

canal.instance.filter.black.regex则是黑名单,常用来过滤系统表或临时表。如果某些库始终同步不过来,先看黑名单是不是误伤了你需要的库。

5. 事件流转与目标库适配:从 MySQL 到其它库

5.1 直接使用 Canal Adapter 写目标库

最轻量的链路是:Canal Server 解析完事件后,由 Adapter 直接消费并写目标数据库。这种方式的好处是架构简单,不需要额外引入消息队列。

Adapter 的配置分两层。第一层在conf/application.yml,声明 Canal Server 地址和源库数据源;第二层是每个目标端的映射文件,比如同步 MySQL 时通常有对应表的映射配置。

canal.conf: canalServerHost: 127.0.0.1:11111 srcDataSources: defaultDS: url: jdbc:mysql://源库IP:3306/数据库名?useUnicode=true&characterEncoding=utf-8 username: canal password: Canal@2024 canalAdapters: - instance: example groups: - groupId: g1 outerAdapters: - name: logger

版本不同,这个文件的具体字段可能略有变化,但核心思路都是一样的:告诉 Adapter “你从哪个 Canal Server 拿数据、源库长什么样、最终往哪写”。我建议第一次跑通时先把outerAdapters配成logger,这样 Canal 解析出的数据会打印在日志里,方便确认整条链路是通的,再加真正的目标端。

当目标库也是 MySQL 时,Adapter 会按映射 SQL 把 INSERT、UPDATE、DELETE 落到目标表。需要注意幂等性设计:如果源库发生了重复投递或位点回退,目标库要有主键或唯一键约束,尽量保证重复写入不会造成脏数据。最怕的就是目标表没有主键,一条数据被写了两遍,后面排查想死了。

5.2 可扩展链路:先送 Kafka 再做分发

如果后续不只有一个目标端,或者想对接数据平台、实时计算框架,那比较推荐先把 Canal 事件送到 Kafka 或 RocketMQ,让下游自己消费。Canal Server 在canal.properties里配置 MQ 相关参数,比如kafka.bootstrap.servers、topic 命名规则、分区策略等。

进入 MQ 后,每条变更消息大致是这样的 JSON 结构:

{ "database": "shop", "table": "orders", "type": "INSERT", "ts": 1710000000000, "data": { "id": 1, "amount": 99.90, "status": 0 }, "old": null }
  • type表示操作类型:INSERT、UPDATE、DELETE。
  • data表示变更后的行数据。
  • old只在 UPDATE 时出现,表示被覆盖的旧字段。
  • ts是事件时间戳,做延迟监控时经常用到。

如果是 UPDATE 操作,典型结构像这样:

{ "database": "shop", "table": "orders", "type": "UPDATE", "ts": 1710000100000, "data": { "id": 1, "amount": 199.90, "status": 1 }, "old": { "amount": 99.90, "status": 0 } }

看到这个结构之后你会发现,Canal 已经把“哪张表、哪行、哪个字段从什么变成什么”都解析好了,下游消费代码只需要处理 JSON,不必再去源库回查。这也是我觉得 Canal 最讨喜的地方——它把最累的一部分脏活干完了。

5.3 同步到 Elasticsearch 的映射配置

搜一下“MySQL 同步到 ES”,绝大多数场景是业务需要组合搜索,而 ES 的索引结构和 MySQL 表结构并不完全一致,所以需要可以自定义的映射关系。Canal Adapter 对 ES 的支持就是通过一套映射配置完成的。

一个简化版的映射配置长这样:

esMapping: _index: orders_idx _id: id sql: select id, amount, status from shop.orders

这里的sql字段很有意思,Adapter 会拿这个 SQL 去源库一次性查全量数据,后续增量事件再按主键找到对应文档更新。也就是说,全量初始化由这个 SQL 完成,增量更新由 binlog 事件完成,两者用主键对上即可。使用 ES 目标端时,有几条经验值得记一下:

  • _id尽量选择和源表主键一致,否则重复同步时会产生重复文档。
  • 字段类型要提前在 ES index mapping 里定义好,不要让 ES 自动推断数字和日期类型,否则同步过程中类型冲突会写不进去。
  • 如果同步的字段有 date 类型,务必先解决时区问题,不然会出现文档里的时间和源库不一致。

5.4 链路到底怎么选

我自己的选型结论是这么几条:

  • 业务简单、只有一个目标库:直接用 Adapter 直连,不要上 MQ,省维护。
  • 有多个下游系统或要做实时计算:先走 Kafka 准没错,Canal 到 MQ 之间的稳定性远比你后面临时加接口靠谱。
  • 目标端是 ES:先把全量 SQL 写好、主键选好,再开增量,不然基线数据和增量数据对不上会很痛苦。
  • 目标端是不同数据库类型:SQL 语法和字段类型都有差异,Adapter 不一定能完美转化,复杂场景最终还是要写一个消费程序自己做转换。

6. 实战排错与调优:延迟、位点、时区那些坑

6.1 最常碰到的几个报错和排查路径

接口类的问题其实不多,因为 Canal 设计得还算清楚,但下面这几个坑我几乎每次上线都会遇到。整理成表格,方便排查:

现象常见原因处理方式
Canal 启动报connection refusedsource.address填错,填成了目标库或 IP 端口不对确认 source 配置是源库地址,源库端口可达,防火墙放行 3306
启动报Access denied for user 'canal'授权没配完整,少了REPLICATION SLAVE或REPLICATION CLIENT重新执行授权 SQL,确认账号密码
日志出现Could not find first log file name in binary log index位点对应的 binlog 已被清理检查expire_logs_days,必要时重建位点
MySQL 8.0 连接失败caching_sha2_password认证插件不兼容改账号认证方式为mysql_native_password
Canal 日志正常但目标库没数据过滤正则写错了,或黑名单误伤检查filter.regex,先在 logger 模式验证是否真的收到事件
目标端写入失败字段类型不匹配、目标表缺字段核对映射 SQL 和目标表结构,先跑全量 SQL 是否报错

你可能发现,很多问题看起来是 Canal 的问题,实际是源库或目标库配置的问题。所以我的习惯是:先把 Adapter 的outerAdapters配成logger跑 10 分钟,确认 Canal Server 确实能解析出事件,再往下加目标端。这样能快速把问题隔离在“源库侧”还是“目标侧”。

6.2 大事务和延迟问题怎么调

Canal 的解析和传输都是近实时的,但如果源库一次更新了几百万行,情况就不一样了。一个大事务在 binlog 里会产生海量事件,Canal 解析传输需要时间,目标库同步写入需要时间,整个链路的延迟都会瞬间飙升。遇到这种场景,最彻底的解决办法是让业务拆分大事务,但现实往往改不动,所以要从 Canal 侧做些缓冲和限流。

Canal 的内存存储模块本质是个有界队列。canal.properties里有关 memory storage 的 buffer size 可以调大,让它在目标写入慢的时候多缓存一阵,别轻易反压到解析线程。同时可以调整每次推送的 batch 大小,适当增大 batch 有助于批量写入目标库,减少 IO 次数。目标端如果是 MySQL,最好用 adapter 的批量模式,而不是每次事件都单独执行一条 SQL。

这里要认清一个事实:Canal 不会凭空消失延迟,它只是把压力往队列或目标端转移。如果目标库本身索引缺失、磁盘慢、连接数小,调哪里都没用。排查延迟时我第一个看的不是 Canal 参数,而是目标库的慢查询和 IO 等待,往往瞬间定位问题。

6.3 时钟时区问题不能留到上线后

binlog 里的时间字段同步到其它库,时区问题几乎是必踩的。MySQL 的DATETIME类型本身不带时区,Canal 解析后输出的时间戳也可能按服务端时区转换。如果源库和目标库不在同一时区,或者使用 ES 时默认 UTC,你会发现数据相差 8 个小时。

我的建议是提前统一约定:

  • 源库、Canal Server、目标库所在机器的时区都设置一致,最好是 UTC 或 Asia/Shanghai,不要混用。
  • 如果目标端 ES 的 date 字段使用 UTC 索引,业务查询层再转本地时区,而不是在同步链路上来回换算。
  • 如果必须转换,宁可写在下游消费程序里显式处理,也不要依赖数据库隐式转换,因为隐式转换最难排查。

6.4 日常监控看什么

线上跑起来之后,我强烈建议至少盯住这几个指标:

  • 同步位点推进。Canal 的位点如果在某个时间点后不再变化,说明解析或传输停了。
  • 内存缓冲水位。缓冲区长期接近上限,说明下游消费速度跟不上,要么加消费并发,要么目标库写不进去。
  • 目标库写入延迟。从 binlog 事件的时间戳到目标库实际落库的时间差,这是最直观的用户体验指标。
  • 源库 binlog 是否还有保留空间。磁盘满会让 MySQL 直接出问题,位点断档比同步延迟更可怕。

Canal 社区有 prometheus 插件,可以把指标接入监控系统。如果集群规模不大,自己写脚本定期查位点差异也够用。重点是别等用户发现数据不同步才去看,主动监控能省掉太多半夜工单。

最后分享一个我在生产环境跑 Canal 过程中的习惯:新增大表同步前,先关掉 Adapter 的增量消费,用映射 SQL 做一次全量初始化,目标表主键建好,再开增量。这样即使中途出问题,最多重跑全量,不会出现基线数据和增量数据互相覆盖的混乱状态。同步链路看起来复杂,但把“全量打底、增量追平”这个顺序理顺了,整个方案就站稳了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询