
做过几年后端团队业务库越来越大遇到最频繁的一个问题就是客户要在大屏上看到分钟级甚至秒级的数据而原来的订单库和报表库中间还是定时脚本在搬运数据。每次业务量一上来定时任务延迟、漏数据、目标库压力各种问题就全冒出来了。后来彻底换了思路用 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 推过来再解析成结构化数据发给下游。这个方向在业界有个统一的名字叫 CDCChange 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 amount100 WHERE id1这种格式体积小但在跨环境同步时可能出现函数、时间、自增列等不可控因素很难精确还原“每一行到底变成了什么”。ROW 模式记录的是行级变更镜像包含每一行的前后值比如 id 为 1 的订单金额从 80 改成 100binlog 里就记录得非常清楚。Canal 解析 ROW 模式能拿到字段级 old 和 new 数据下游做同步、审计、搜索更新都很方便。所以实例配置里尽量显式把binlog_formatROW固定住不要依赖默认值。顺手再检查一个参数叫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.iniLinux 下一般是/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_formatROW和binlog_row_imageFULL是给 Canal 解析打底原因上面已经说过。expire_logs_days控制 binlog 保留天数。MySQL 8.0 里这参数改成了binlog_expire_logs_seconds8.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是 ONbinlog_format是 ROW。还可以用SHOW MASTER STATUS;查看当前正在写的 binlog 文件和位点这个文件位点信息后面排错时经常要用。3.2 创建 Canal 专用复制账号Canal 需要用专门的账号去连接源库不建议复用 root 或高权限账号。原因是权限越小误操作范围越小而且 Canal 正常工作只需要三个权限SELECT、REPLICATION SLAVE、REPLICATION CLIENT。具体建账号和授权的 SQL 如下CREATE USER canal% IDENTIFIED BY Canal2024; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO canal%; FLUSH PRIVILEGES;这里解释一下为什么需要这三个权限SELECTCanal 在部分场景下需要读取表结构信息来做字段映射所以需要读权限。REPLICATION SLAVE核心权限允许 Canal 以从库身份向主库请求 binlog。REPLICATION CLIENT允许 Canal 查看主库状态比如SHOW MASTER STATUS用来定位位点。如果你的源库是 MySQL 8.0要注意认证插件问题。MySQL 8.0 默认的caching_sha2_password认证方式在部分 Canal 版本下会因为公钥交换或协议兼容问题导致连接失败。常见处理办法是显式指定账号用mysql_native_passwordCREATE USER canal% IDENTIFIED WITH mysql_native_password BY Canal2024;如果已经建了账号也可以改ALTER USER canal% IDENTIFIED WITH mysql_native_password BY Canal2024;3.3 binlog 保留时长与 MySQL 安装中的常见坑说完授权我必须把 binlog 保留时长单独拿出来强调一遍。Canal 启动后会记住自己同步到哪个位点下次启动从位点继续拉取。如果源库 binlog 因为过期被清理而 Canal 的位点还停在老位置它重新连接时会发现自己要读的文件已经不存在了这时候除了重建同步位点没有别的办法意味着历史数据要重新同步。所以初期调试阶段binlog 保留时间尽量放宽一点。我在测试环境直接设了expire_logs_days15上线稳定后再改回 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.propertiesCanal 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 Canal2024 # 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/数据库名?useUnicodetruecharacterEncodingutf-8 username: canal password: Canal2024 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_passwordCanal 日志正常但目标库没数据过滤正则写错了或黑名单误伤检查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 做一次全量初始化目标表主键建好再开增量。这样即使中途出问题最多重跑全量不会出现基线数据和增量数据互相覆盖的混乱状态。同步链路看起来复杂但把“全量打底、增量追平”这个顺序理顺了整个方案就站稳了。