Canal分库分表同步:group-instance.xml配置与实战

发布时间:2026/10/13 11:07:03
Canal分库分表同步:group-instance.xml配置与实战
1. 为什么分库分表场景下要单独聊 group-instance.xml分库分表这件事只要业务量稍微往上走一走基本都绕不开。订单表按用户 ID 拆成 8 个库、每个库再拆 16 张表这种配置在很多中型系统里已经是标配。数据拆开之后最直接的问题就来了数据同步怎么做缓存怎么刷新异构数据源怎么保持一致Canal 就是在这个环节里被大量使用的工具。它伪装成 MySQL 的从库读取 binlog把变更事件投递出来下游可以接消息队列、可以写 ES、可以刷 Redis。单库单表的时候一个 instance 配一个库简单直接。但一旦进入分库分表一个逻辑表散落在几十个物理库表里如果还按单实例的思路去配运维成本会直接爆炸。group-instance.xml就是 Canal 为这种多库多表场景准备的一把钥匙。它的核心作用是把多个物理实例instance组合成一个逻辑分组让下游消费者感知到的是一个统一的数据源而不是几十个零散的 instance。这篇文章我会把 group-instance.xml 的定位、配置结构、实操步骤、踩坑经验完整拆一遍适合已经在用 Canal 做单库同步、准备往分库分表场景迁移的开发和运维同学参考。如果你还没接触过 Canal建议先把单实例跑通再来看这篇不然容易一头雾水。2. group-instance.xml 到底解决了什么问题2.1 分库分表下 Canal 的原始困境先还原一下没有 group-instance 之前的场景。假设订单表 order 被拆成了 4 个库db0、db1、db2、db3每个库里有 order_0 到 order_3 四张表。Canal 要同步这些数据最朴素的做法是配 4 个 instance每个 instance 对应一个库各自订阅自己的 binlog。这样做的直接后果有三个。第一下游消费者要同时连 4 个 Canal 客户端自己去做合并和路由业务代码里到处是这个库连哪个 instance的判断逻辑。第二每个 instance 都要独立维护位点position一旦某个 instance 挂了重启位点管理、重复消费、顺序性问题全都要单独处理。第三运维层面 4 个 instance 就是 4 份配置、4 份日志、4 个监控项库再拆多一点比如 16 个库运维直接崩溃。更麻烦的是顺序性。同一个订单的变更可能因为分片规则落在不同库如果下游要保证同一业务主键的顺序多 instance 各自投递根本没法保证。这不是 Canal 的锅是架构本身的问题但 group-instance 提供了另一种解法。2.2 group-instance 的核心思路group-instance.xml 的思路其实很朴素既然多个物理库本质上是同一份逻辑数据那就把它们在 Canal 内部先合起来对外只暴露一个 instance 名字。下游消费者连的还是那个熟悉的 instance但背后 Canal 帮你把多个物理库的 binlog 都读进来统一投递。这里有个关键点要理解清楚group-instance 并不是把数据真正物理合并它做的是逻辑分组。每个物理库依然有自己独立的 binlog 读取、独立的位点管理只是在投递层做了一个聚合。所以它解决的是消费端感知的问题不是数据存储的问题。注意group-instance 解决的是多库订阅的统一入口问题它不会帮你做分片路由也不会帮你做数据合并。下游拿到的还是原始的库表变更事件只是这些事件来自同一个逻辑 instance。2.3 和普通 instance 的本质区别普通 instance 的配置里canal.instance.master.address指向一个具体的 MySQL 地址一个 instance 对应一个数据源。group-instance 则是在 instance 之上再包一层通过canal.instance.detecting之类的机制去管理多个子 instance。从配置文件的角度看普通 instance 用的是instance.propertiesgroup-instance 用的是group-instance.xml。前者是 properties 格式后者是 Spring XML 格式这个差异很多人第一次接触会懵。XML 里可以定义多个property节点也可以引用外部的 properties 文件灵活性更高但也更容易配错。从消费端看普通 instance 你连examplegroup-instance 你连的是 group 的名字比如order-group。对客户端来说连接方式、订阅方式基本没变变的只是背后的数据来源从一个库变成了多个库。3. group-instance.xml 的配置结构拆解3.1 文件位置与加载机制group-instance.xml 放在 Canal 的conf目录下具体路径是conf/{group-instance-name}/group-instance.xml。比如你定义一个叫order-group的分组那目录结构就是conf/ order-group/ group-instance.xml instance.properties # 可选用于覆盖默认配置Canal Server 启动时会扫描 conf 目录下的所有子目录每个子目录如果包含instance.properties或group-instance.xml就会被识别为一个 instance 或 group instance。这里有个容易踩的坑如果同一个目录下同时存在instance.properties和group-instance.xmlCanal 会优先按 group-instance 处理但配置项会互相覆盖行为很难预测。所以建议一个目录只放一种配置文件。加载顺序上Canal 先读group-instance.xml里的 Spring bean 定义再读同目录下的instance.properties做属性覆盖。这个机制意味着你可以把公共配置放在 XML 里把环境相关的配置比如数据库地址、账号密码放在 properties 里方便不同环境切换。3.2 核心配置项逐条说明group-instance.xml 里最关键的几个配置项我按重要性排一下配置项作用典型值canal.instance.master.address主库地址group 模式下通常指向第一个库127.0.0.1:3306canal.instance.dbUsername数据库账号canalcanal.instance.dbPassword数据库密码canalcanal.instance.filter.regex过滤规则决定订阅哪些库表.\..canal.instance.connectionCharset连接字符集UTF-8canal.instance.detecting.enable是否开启多库探测truecanal.instance.detecting.sql探测 SQL用于发现分库show databases这里重点说detecting相关的配置。group-instance 之所以能管理多个库靠的就是 detecting 机制。开启后Canal 会定期执行detecting.sql根据返回结果动态发现需要订阅的库。比如你配了show databases返回里所有匹配filter.regex的库都会被纳入订阅范围。提示detecting 机制适合库数量动态变化的场景比如按业务线分库、按租户分库。如果库是固定的直接写死地址列表反而更稳定避免探测带来的额外开销。3.3 filter.regex 的写法与陷阱filter.regex是 group-instance 里最容易配错的一项。它的格式是库名正则.表名正则中间用点号分隔。比如order_db\\..*表示订阅 order_db 库下的所有表.*\\..*表示订阅所有库所有表。分库分表场景下库名通常有规律比如order_db_0到order_db_7那正则可以写成order_db_\\d\\..*。表名如果是order_0到order_15可以写成order_db_\\d\\.order_\\d。这里有个坑Java 正则里反斜杠要转义所以在 properties 文件里写\\d在 XML 里写\\d或者用 CDATA 包裹。很多人第一次配的时候只写了一个反斜杠结果正则匹配不上排查半天。另一个坑是 filter.regex 的匹配是全匹配不是包含匹配。也就是说order_db不会匹配order_db_0必须写成order_db.*或者order_db_\\d。这个和很多人的直觉相反务必注意。4. 分库分表场景下的完整实操步骤4.1 环境准备与前置检查动手之前先把几个前置条件确认清楚。第一MySQL 必须开启 binlog且格式为 ROW。show variables like binlog_format返回必须是 ROWMIXED 和 STATEMENT 都不行因为 Canal 解析 ROW 格式的 binlog 才能拿到完整的行变更数据。第二binlog 的binlog_row_image建议设为 FULL这样 update 事件里能拿到变更前后的完整字段下游处理更方便。第三Canal 账号要有 REPLICATION SLAVE 和 REPLICATION CLIENT 权限。授权语句大概是CREATE USER canal% IDENTIFIED BY canal; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO canal%; FLUSH PRIVILEGES;第四确认所有分库的 server_id 不冲突。Canal 伪装成从库每个 instance 会占用一个 server_id如果多个 instance 的 server_id 重复MySQL 会拒绝连接。group-instance 模式下每个子 instance 的 server_id 由 Canal 自动分配但建议在配置里显式指定避免冲突。4.2 编写 group-instance.xml下面是一个分库分表场景的完整配置示例假设订单表拆成 4 个库每个库 4 张表?xml version1.0 encodingUTF-8? beans xmlnshttp://www.springframework.org/schema/beans xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd bean classcom.alibaba.otter.canal.instance.spring.support.PropertyPlaceholderConfigurer lazy-initfalse property nameignoreResourceNotFound valuetrue / property namesystemPropertiesModeName valueSYSTEM_PROPERTIES_MODE_OVERRIDE / property namelocationNames list valueclasspath:canal.properties/value valueclasspath:instance.properties/value /list /property /bean bean idinstance classcom.alibaba.otter.canal.instance.spring.CanalInstanceWithSpring property namedestination valueorder-group / property nameeventParser bean classcom.alibaba.otter.canal.parse.inbound.mysql.MysqlEventParser property namedestination valueorder-group / property nameslaveId value${canal.instance.mysql.slaveId} / property namefilterQueryDcl valuefalse / property namefilterQueryDml valuefalse / property namefilterQueryDdl valuefalse / property namefilterRows valuefalse / property nameconnectionCharset valueUTF-8 / property namedetectingEnable valuetrue / property namedetectingSQL valueshow databases / property namemasterAddress value${canal.instance.master.address} / property namedbUsername value${canal.instance.dbUsername} / property namedbPassword value${canal.instance.dbPassword} / property namefilter value${canal.instance.filter.regex} / /bean /property /bean /beans这个配置里detectingEnable设为 truedetectingSQL用show databases配合filter.regex就能自动发现所有匹配的库。slaveId从 properties 里读方便不同环境用不同的值。4.3 配套 instance.properties 的写法同目录下的instance.properties负责提供具体参数canal.instance.master.address127.0.0.1:3306 canal.instance.dbUsernamecanal canal.instance.dbPasswordcanal canal.instance.mysql.slaveId1234 canal.instance.filter.regexorder_db_\\d\\..* canal.instance.connectionCharsetUTF-8 canal.instance.tsdb.enabletrue canal.instance.tsdb.dir${canal.home}/conf/order-group/tsdbfilter.regex这里写的是order_db_\\d\\..*匹配 order_db_0 到 order_db_9 的所有表。如果你只想订阅 order 相关的表可以改成order_db_\\d\\.order_\\d。tsdb.enable建议开启它会把表结构变更记录到本地下游解析 binlog 时能拿到正确的字段类型。分库分表场景下多个库的表结构可能不完全一致tsdb 能帮你兜底。4.4 启动与验证配置写完后启动 Canal Server。启动日志里会看到类似这样的输出start CannalInstance for order-group detecting databases: [order_db_0, order_db_1, order_db_2, order_db_3] subscribe binlog from order_db_0 subscribe binlog from order_db_1 subscribe binlog from order_db_2 subscribe binlog from order_db_3看到 detecting 列出了所有分库说明配置生效了。接下来用客户端连上去验证CanalConnector connector CanalConnectors.newSingleConnector( new InetSocketAddress(127.0.0.1, 11111), order-group, , ); connector.connect(); connector.subscribe(order_db_\\d\\..*); Message message connector.get(100);如果能在 message 里看到来自不同库的变更事件说明 group-instance 已经正常工作。这里注意客户端订阅的 filter 要和服务端的 filter 配合客户端可以进一步缩小范围但不能超出服务端的范围。5. 常见问题与排查技巧实录5.1 位点管理混乱导致重复消费group-instance 模式下每个子 instance 有独立的位点但对外暴露的是一个逻辑 instance。如果某个子 instance 重启它的位点可能回退导致重复投递。这个问题在多库场景下特别隐蔽因为下游看到的是同一个 instance很难判断是哪个库重复了。排查思路先看 Canal 日志里每个子 instance 的位点变化对比 MySQL 的show master status确认位点是否正常推进。如果发现某个库的位点卡住检查该库的 binlog 是否被清理、网络是否稳定、账号权限是否正常。解决方式开启 tsdb 记录位点定期备份位点文件。Canal 的位点默认存在conf/{instance}/meta.dat建议把这个文件纳入监控位点长时间不更新就告警。5.2 filter.regex 匹配不上这是最高频的问题。表现是 Canal 启动正常但收不到任何数据。排查步骤确认filter.regex的转义是否正确Java 正则里\d要写成\\d。确认库名表名是否真的匹配可以用show databases和show tables手动核对。确认客户端订阅的 filter 是否和服务端一致客户端 filter 不能超出服务端范围。确认 binlog 格式是 ROWshow variables like binlog_format检查。提示filter.regex 是全匹配order_db不会匹配order_db_0。这个坑我见过太多次建议配置完后用在线正则工具先验证一遍。5.3 多库顺序性问题分库分表下同一个业务主键的变更可能落在不同库group-instance 虽然统一了入口但投递顺序不保证。比如订单创建在 db0订单支付在 db1下游可能先收到支付事件再收到创建事件。这个问题没有银弹。如果业务强依赖顺序有两个方向一是下游按业务主键做缓冲和排序二是把同一业务主键的变更路由到同一个库这需要改分片规则。Canal 层面能做的有限主要是保证单个库内的顺序跨库顺序需要业务侧配合。5.4 常见问题速查表问题现象可能原因排查方向启动后无数据filter.regex 不匹配检查正则转义和库表名部分库无数据detecting 未发现该库检查 detecting.sql 和权限重复消费位点回退检查 meta.dat 和 binlog 保留时间连接被拒slaveId 冲突检查各 instance 的 slaveId字段类型错误tsdb 未开启开启 tsdb 并重建内存溢出单批数据量过大调整 canal.instance.transaction.size6. 性能调优与生产环境经验6.1 批量拉取与内存控制group-instance 模式下多个库的数据会汇聚到一个投递通道单批数据量容易变大。canal.instance.transaction.size控制单次事务的最大条数默认 1024分库分表场景建议调到 2048 或 4096但不要太大否则内存压力大。canal.instance.memory.buffer.size控制内存缓冲大小默认 16384单位是字节。如果下游消费慢缓冲会堆积建议根据下游吞吐调整。我一般会把它设成 32768配合监控观察堆积情况。6.2 并行度与线程模型Canal 的解析线程数由canal.instance.parser.parallel控制默认是 CPU 核数。分库分表下多个库的 binlog 可以并行解析但投递是串行的。如果发现解析是瓶颈可以适当调大这个值如果投递是瓶颈调大没用得从下游消费能力入手。还有个参数canal.instance.parser.parallelBufferSize控制并行解析的缓冲队列大小默认 256。库多的时候建议调到 512 或 1024避免队列满导致解析阻塞。6.3 监控指标与告警配置生产环境必须监控的几个指标位点延迟当前位点和 MySQL 最新位点的差值超过阈值告警。投递速率每秒投递的事件数突降说明有问题。内存使用缓冲队列的使用率超过 80% 告警。子 instance 状态每个子 instance 是否存活挂了要立即知道。Canal 自带 JMX 指标可以接 Prometheus 或者自己写脚本采集。我一般会在 Canal 外面包一层健康检查定期调connector.getWithoutAck(1)看是否有数据返回没返回就告警。注意group-instance 的子 instance 状态在原生监控里不够直观建议自己写脚本解析日志把每个子 instance 的位点单独打点这样出问题能快速定位到具体是哪个库。7. 几个我踩过的坑和实操心得第一个坑是 detecting 机制的延迟。detecting.sql默认是定时执行的间隔由canal.instance.detecting.interval.time控制默认 5 秒。如果新加了一个分库最多要等 5 秒才能被发现。这个延迟在扩容场景下会有影响建议把间隔调小到 1 秒或者干脆用固定地址列表。第二个坑是 filter.regex 的优先级。服务端的 filter 和客户端的 filter 是取交集的如果服务端配了order_db_\\d\\..*客户端配了.*\\..*实际生效的是服务端的范围。很多人以为客户端能扩大范围其实不能。第三个坑是 tsdb 的清理。tsdb 会记录所有表结构变更时间长了文件会很大。建议定期清理或者设置canal.instance.tsdb.spring.xml里的过期时间。我见过一个案例tsdb 文件涨到几十 G把磁盘撑爆了。第四个坑是 slaveId 的分配。group-instance 下每个子 instance 需要一个唯一的 slaveId如果配置里写死了一个值多个子 instance 会冲突。正确做法是用${canal.instance.mysql.slaveId}配合自动递增或者手动给每个库分配不同的值。最后一个心得分库分表场景下Canal 的配置复杂度会指数级上升建议先用一两个库跑通再逐步扩展。不要一上来就配几十个库出了问题根本没法排查。另外group-instance 虽然好用但它不是万能的如果业务对顺序性要求极高或者分片规则经常变可能需要考虑其他方案比如在应用层做双写或者用专门的数据同步中间件。这个内容后续还可以这样扩展一是结合具体消息队列比如 Kafka、RocketMQ讲下游消费的最佳实践二是讲 group-instance 和 Canal Admin 的集成三是讲多机房场景下的位点同步。这些方向我后续会单独写感兴趣可以先关注。