1. 为什么"全库全表同步"是个看起来简单、做起来全是细节的活
刚接触数据同步这块的时候,我一度觉得"监听某个库的所有表"是最没技术含量的需求——不就是配个实例地址、填个库名、把表过滤关掉,然后启动就完事了吗?真上手跑起来才发现,坑全藏在"所有表"这三个字里。新上线的表没被监听到、DDL 变更之后同步链路悄悄断掉、大事务把内存打爆、位点回滚之后数据重复写入……这些问题不会在 demo 阶段暴露,只会在业务量涨起来之后集中爆发。
这篇内容聊的就是围绕canal 监听同步指定数据库、所有表这个场景,把从环境准备、实例配置、位点管理到故障排查的完整链路讲透。canal 的本质是伪装成 MySQL 从库(slave),通过解析 binlog 拿到数据变更事件,再投递给下游消费方。理解这一点非常关键,因为后面所有的配置项、限制、坑,几乎都能从"它是个假从库"这个身份推导出来。
适合谁看?如果你正在做异构数据同步、缓存与数据库一致性、数仓 ODS 层接入、搜索索引实时构建这类工作,并且需要覆盖一整个库而不是某几张表,那这篇基本能帮你少走大半弯路。我会把每个关键选择背后的理由讲清楚,参数怎么算、为什么这么设,而不是甩一份配置文件让你抄。
先说一个反直觉的结论:"同步所有表"最难的不是同步本身,而是"所有"这个集合会动态变化。业务随时可能建新表,你的同步任务能不能自动覆盖到,取决于你对 canal 过滤机制和位点机制的理解深度。下面逐层拆。
2. canal 的工作机制:先搞懂它到底在"听"什么
2.1 伪装成从库的 binlog 订阅者
MySQL 的主从复制里,从库会向主库发送 dump 请求,主库把 binlog 推给从库。canal 干的就是这件事——它把自己注册成一个 slave,拿到 binlog 流之后解析成结构化的数据变更事件。所以你在配置里填的canal.instance.master.address就是主库地址,canal.instance.dbUsername就是那个"从库账号"。
这个身份带来两个直接后果。第一,主库必须开启 binlog 且格式为 ROW。STATEMENT 格式只记录 SQL 语句,canal 拿不到变更前后的行数据,没法做可靠同步;MIXED 格式在某些语句下会退化成 STATEMENT,同样不可靠。第二,这个账号需要 REPLICATION SLAVE 和 REPLICATION CLIENT 权限,否则连 dump 请求都发不出去。
-- 主库上确认 binlog 配置 SHOW VARIABLES LIKE 'log_bin'; -- 必须是 ON SHOW VARIABLES LIKE 'binlog_format'; -- 必须是 ROW SHOW VARIABLES LIKE 'binlog_row_image'; -- 建议 FULL SHOW VARIABLES LIKE 'server_id'; -- 必须非 0 且唯一binlog_row_image这个参数特别容易被忽略。它默认是 FULL,记录变更前后的完整行;如果被设成 MINIMAL,只记录被改动的列,canal 解析出来的事件里就缺列,下游写入时可能把没变的字段覆盖成 NULL。我见过一次线上事故就是运维为了省 binlog 空间把它调成了 MINIMAL,结果同步过去的订单表大量字段被清空。
2.2 位点:canal 的"记忆"
canal 靠 binlog 文件名加偏移量(比如mysql-bin.000123:4567)来记录自己消费到哪了。这个位点存在conf/{instance}/meta.dat里,重启后会从这里继续。理解位点机制是排查"数据重复"和"数据丢失"两类问题的钥匙。
- 位点落后于实际消费位置:重启后会重复消费,下游必须做幂等。
- 位点超前或损坏:会丢数据,且很难恢复,只能从备份重来。
所以生产环境里,位点文件一定要纳入备份,并且下游消费端必须实现幂等写入。指望 canal 精确一次(exactly-once)是不现实的,它保证的是至少一次(at-least-once)。
2.3 全库监听时,过滤规则怎么写
canal 的过滤靠canal.instance.filter.regex这个参数,格式是库名.表名的正则,多个用逗号分隔。想监听某个库的所有表,写法是:
# 监听 testdb 库下所有表 canal.instance.filter.regex=testdb\\..*注意这里是双反斜杠,因为 properties 文件里反斜杠是转义字符,写单反斜杠会被吃掉。这个细节坑过不少人——配置看着没问题,启动后一条数据都收不到,排查半天才发现是转义。
几个常用的过滤写法对照:
| 需求 | 配置写法 | 说明 |
|---|---|---|
| 指定库所有表 | testdb\\..* | 本文核心场景 |
| 指定库排除某表 | testdb\\..*,testdb\\.log_table | 排除靠后写,但实际是白名单逻辑,需谨慎 |
| 多库所有表 | db1\\..*,db2\\..* | 逗号分隔 |
| 所有库所有表 | .*\\..* | 慎用,量极大 |
这里有个关键认知:filter.regex是白名单机制,匹配上的才会被同步。它是在 canal 解析 binlog 之后、投递之前做的过滤,也就是说被过滤掉的表,binlog 照样被读取和解析,只是不往下游发。这意味着即使你只同步一个库,主库上其他库的写入也会消耗 canal 的解析资源。如果主库实例上跑了很多库,建议用独立的数据库账号配合canal.instance.filter.black.regex或者干脆给 canal 单独一个实例,减少无效解析。
3. 实例配置逐项拆解:每个参数为什么这么设
3.1 核心连接参数
一份监听指定库所有表的最小可用配置大概长这样:
# 实例名称,对应 conf 下的目录名 canal.instance.mysql.slaveId=1234 canal.instance.master.address=10.0.0.10:3306 canal.instance.dbUsername=canal_user canal.instance.dbPassword=your_password canal.instance.connectionCharset=UTF-8 # 过滤:指定库所有表 canal.instance.filter.regex=testdb\\..* # 位点起点 canal.instance.master.journal.name= canal.instance.master.position= canal.instance.master.timestamp=slaveId必须全局唯一。如果多个 canal 实例连同一个主库,slaveId 重复会导致主库把其中一个踢掉,表现为同步时断时续。我一般用"实例编号 + 固定基数"的方式分配,比如 1000 起步递增,避免和真实从库的 server_id 撞车。
connectionCharset建议显式设成 UTF-8。虽然默认值通常没问题,但主库如果用的是 latin1 或者 utf8mb4,字符集不一致会导致中文乱码,而且这种乱码往往在特定字符上才出现,排查起来很折磨。
3.2 位点起点的三种选择
journal.name、position、timestamp这三个是位点起点,留空表示从当前最新位点开始,也就是只同步启动之后的变更。这个默认行为要特别注意:
- 如果你希望同步历史全量数据,canal 本身做不到,它只处理增量。全量得靠其他工具(比如 DataX、mysqldump)先灌一遍,再用 canal 接增量。
- 如果你填了
timestamp,canal 会去找那个时间点对应的 binlog 位点。但主库 binlog 如果已经过期清理了,就会报错找不到。
提示:生产环境首次启动前,务必确认主库
expire_logs_days或binlog_expire_logs_seconds的保留时长,确保你要的起点位点还在。binlog 被清理后,canal 无法从中间恢复,只能重新全量。
3.3 大事务与内存控制
全库监听时,最怕的就是某个大事务——比如批量更新几十万行、或者一个大表的 DDL。canal 解析这类事务时会把整个事务的事件缓存在内存里,事务越大内存占用越高,极端情况直接 OOM。
# 单个事务允许缓存的最大字节数,超过会拆分成多个事务投递 canal.instance.memory.buffer.size=16384 # 事务超时时间,毫秒 canal.instance.memory.buffer.memunit=1024memory.buffer.size的单位是 memunit 的倍数,默认 16384 * 1024 约 16MB。这个值不是越大越好——设太大,单个大事务能过,但内存峰值高;设太小,大事务被拆分,下游可能拿到不完整的事务边界。我的经验是先按默认值跑,观察日志里有没有 "transaction size exceed" 之类的告警,再针对性调整。如果业务里确实有大批量操作,更好的做法是从源头拆小事务,而不是无限加大 canal 的缓冲。
4. 全表同步绕不开的三个硬骨头
4.1 新表上线后同步不到,问题出在哪
这是"所有表"场景下最高频的问题。业务新建了一张表,往里面写数据,但下游死活收不到。原因通常有两个:
第一,过滤规则没覆盖。如果你写的是testdb\\..*,理论上新表应该被覆盖。但如果你之前为了排除某张表写成了枚举式白名单(比如testdb\\.order,testdb\\.user),那新表自然不在列。所以全库同步一定要用通配写法,不要枚举。
第二,canal 对 DDL 的处理有延迟或异常。canal 解析到 CREATE TABLE 这类 DDL 时,会更新自己维护的表结构元数据。如果 DDL 语句里有 canal 不认识的语法(比如某些新版本特性、或者用了特殊字符),解析可能失败,导致后续这张表的 DML 事件无法正确解析。排查方法是看 canal 日志里有没有DDL parse error或table meta not found之类的关键字。
# 查看 canal 日志中的 DDL 解析情况 grep -i "ddl" logs/example/example.log | tail -50 grep -i "meta" logs/example/example.log | tail -50遇到 DDL 解析失败,一个实用的兜底手段是手动触发元数据刷新:在 canal 的 admin 端口调用 reload 接口,或者重启实例让它重新拉取表结构。但重启会中断同步,所以更稳妥的是在业务侧规范 DDL 语法,避免使用过于冷门的特性。
4.2 DDL 变更导致同步链路断裂的完整排查链路
我遇到过一次典型故障:某张表加了个字段,加完之后这张表的同步就停了,其他表正常。排查过程记录如下,这个链路可以复现:
- 确认现象:下游只有这一张表没数据,其他表正常,说明 canal 进程本身没挂,问题定位到单表。
- 看 canal 日志:发现
table meta相关的报错,提示字段数量不匹配。 - 确认根因:canal 缓存的表结构还是旧的(加字段前),但 binlog 里的事件已经带了新字段,解析时列数对不上,直接抛异常。
- 验证:手动在 canal 里触发元数据刷新,或者等它下次遇到该表的 DDL 时自动更新。
- 修复:重启实例强制刷新元数据,同步恢复。
- 预防:在业务 DDL 流程里加入"通知同步链路"的环节,或者配置 canal 定期刷新元数据。
这个链路的价值在于:它把"单表同步中断"和"整体同步中断"区分开了。整体中断通常是进程、网络、权限问题;单表中断八成是元数据或过滤规则问题。先做这个二分,能省掉大量无效排查。
4.3 位点回滚与数据重复的幂等设计
前面说过 canal 是至少一次投递。网络抖动、下游消费失败重试、实例重启,都可能导致同一批数据被投递多次。全库同步时数据量大,重复的概率更高。
下游必须做幂等,常见方案:
- 基于主键的 upsert:写入时用
INSERT ... ON DUPLICATE KEY UPDATE或等价语义,重复数据覆盖而不是报错。 - 基于版本号或时间戳:每条记录带一个单调递增的版本,写入时比较版本,旧版本丢弃。
- 基于唯一键去重:在中间层用消息 ID 或 binlog 位点做去重表。
我个人最推荐第一种,简单可靠,对业务侵入小。但要注意,如果表没有主键或唯一键,upsert 就无从谈起。MySQL 允许无主键表,但这类表在 canal 同步场景下是灾难——既没法幂等,也没法保证顺序。所以全库同步前,务必检查所有表都有主键。
-- 找出库里没有主键的表 SELECT t.table_name FROM information_schema.tables t LEFT JOIN information_schema.table_constraints c ON t.table_name = c.table_name AND c.constraint_type = 'PRIMARY KEY' WHERE t.table_schema = 'testdb' AND t.table_type = 'BASE TABLE' AND c.constraint_name IS NULL;这条 SQL 建议在接入前跑一遍,把无主键的表列出来,要么加主键,要么单独处理。
5. 从单实例到生产可用:性能与稳定性调优
5.1 批量拉取与网络参数
canal 从主库拉 binlog 是批量进行的,批量大小影响吞吐和延迟。相关参数:
# 每次拉取的最大 binlog 字节数 canal.instance.network.receiveBufferSize=16384 canal.instance.network.sendBufferSize=16384 # 拉取超时 canal.instance.network.soTimeout=30000receiveBufferSize和sendBufferSize默认 16KB,在高吞吐场景下偏小,可以适当调大到 64KB 或 128KB。但别盲目调太大,TCP 缓冲区过大会增加内存占用,且对延迟敏感的场景反而不利。我的做法是先用默认值压测,看网络是否是瓶颈,再决定是否调整。
5.2 并行消费与顺序保证的权衡
canal 本身对单个实例是单线程解析 binlog 的,保证事件顺序。但下游消费可以并行,这就带来一个矛盾:并行能提吞吐,但会打乱顺序。
对于全库同步,不同表之间其实没有严格的顺序依赖,可以按表并行。但同一张表内的变更必须保序,否则会出现"先删后插"变成"先插后删"这种逻辑错乱。所以并行策略应该是按表哈希分区,同一张表固定路由到同一个消费线程。
如果下游是消息队列,可以用表名做分区键;如果是直接写库,可以按表分多个消费者组。这个设计在表数量多、单表写入不频繁的场景下效果最好。
5.3 监控指标:哪些数字必须盯
生产环境跑 canal,这几个指标必须纳入监控:
| 指标 | 含义 | 告警阈值建议 |
|---|---|---|
| 位点延迟 | 当前消费位点与主库最新位点的差距 | 超过 30 秒告警 |
| 内存占用 | canal 进程堆内存 | 超过堆上限 80% 告警 |
| 下游投递失败率 | 投递失败次数占比 | 持续大于 0 告警 |
| 主从连接状态 | 与主库的连接是否正常 | 断开立即告警 |
| binlog 解析错误 | DDL/DML 解析异常次数 | 出现即告警 |
位点延迟是最核心的指标。它直接反映同步是否跟得上业务写入。如果延迟持续增长,说明 canal 处理速度跟不上主库写入速度,要么加资源,要么优化下游消费。
6. 几个我踩过的坑和对应的处理经验
6.1 主库账号权限给多了反而出问题
一开始图省事,给 canal 用的账号直接开了 ALL PRIVILEGES。后来发现这个账号在某些操作下会触发额外的元数据查询,甚至影响主库性能。正确做法是最小权限:
CREATE USER 'canal_user'@'%' IDENTIFIED BY 'your_password'; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal_user'@'%'; FLUSH PRIVILEGES;只需要 SELECT(读表结构)、REPLICATION SLAVE(发 dump 请求)、REPLICATION CLIENT(查位点信息)这三个。给多了没好处,还增加安全风险。
6.2 时间戳起点选错导致同步了不该同步的数据
有次为了补数据,把起点设成了三天前的时间戳。结果 canal 从三天前开始重放,把这三天的所有变更又投递了一遍,下游没做幂等,直接产生大量重复数据。教训是:用时间戳起点前,先确认下游的幂等能力,或者先在一个隔离环境验证。时间戳起点是把双刃剑,补数据方便,但重放范围控制不好就是事故。
6.3 表名大小写敏感问题
Linux 下 MySQL 默认表名大小写敏感,Windows 下不敏感。如果你的过滤规则写的是testdb\\.Order,而实际表名是order,在 Linux 上就匹配不到。全库同步用通配符能规避这个问题,但如果需要精确排除某张表,一定要注意大小写和主库的lower_case_table_names设置保持一致。
6.4 网络抖动后的自动重连
canal 和主库之间的连接如果断了,默认会重试。但重试期间位点不动,恢复后会从断点继续,这部分是安全的。需要注意的是重试间隔和最大重试次数,如果主库长时间不可用,canal 可能放弃重连。相关配置在canal.properties里,建议把重试次数设大一些,配合监控告警,确保人工能及时介入。
7. 关于全库全表同步,我个人的几条实操建议
跑了一段时间的全库同步之后,我最大的体会是:"所有表"这个需求,本质上是在管理一个动态集合,而不是配置一个静态任务。表会增会减,结构会变,数据量会涨,任何静态的配置都迟早会过时。所以真正要投入精力的地方,是建立一套能感知变化、能自动适应、出问题能快速定位的机制。
具体来说,我会坚持做这几件事:接入前用 SQL 扫一遍无主键表并处理掉;过滤规则永远用通配符而不是枚举;下游消费端无条件实现幂等;位点文件和配置纳入版本管理;位点延迟和解析错误纳入监控。这几条看起来都是小事,但每一条背后都对应着我踩过的真实坑。
还有一个容易被忽视的点:定期演练恢复流程。canal 挂了怎么快速拉起、位点损坏怎么从备份恢复、主库 binlog 被清理了怎么办——这些预案不能只写在文档里,要真的演练过。我见过太多团队平时跑得好好的,一出故障手忙脚乱,就是因为恢复流程从来没实操过。
最后分享一个小技巧:如果下游消费能力有限,而主库写入又很猛,可以在 canal 和下游之间加一层缓冲(比如消息队列),让 canal 只管快速拉取和投递,下游按自己的节奏消费。这样即使下游短暂卡顿,也不会反压到 canal 导致位点延迟飙升。这个架构在数据量大的场景下几乎是标配,值得一开始就考虑进去。