最近好几个做后端的朋友都在问同一个事:数据库里现成的数据,怎么才能同步到 Elasticsearch 里做搜索。业务量一上来,MySQL 上的 LIKE 查询慢到怀疑人生,想要毫秒级搜索结果,就必须把数据搬到 ES 里。但 ES 不能直接读 MySQL,两个库之间的数据同步就成了绕不开的环节。我最早接触这个需求是在做一个商品检索模块时,试过自己写定时任务、用消息队列异步双写,最后都因为各种坑回到了 Canal。今天就把这套方案从头到尾拆一遍,重点是步骤、参数和那些文档里不写、但实测会踩的坑。
Canal 本质上是个伪装成 MySQL 从库的中间件,它订阅 MySQL 的 binlog,把数据变更事件解析成结构化的消息,然后推到下游。下游可以接消息队列,也可以直接接 ES。用它的好处是:对业务代码零侵入,不需要在每个写入 MySQL 的地方手动去同步 ES,所有增删改操作都能被自动捕获。这东西很适合那些已经有历史数据、又不想大规模改动现有代码的团队。
这篇东西不玩虚的,直接按一条完整的同步链路走:从为什么选 Canal、binlog 原理、部署配置,到全量增量协同、常见问题排查,最后给一些性能优化和个人经验。不管你是第一次搭,还是已经搭完了发现同步老延迟,应该都能找到点有用的东西。
1. 为什么需要把 MySQL 数据同步到 Elasticsearch
1.1 业务场景:关系型数据库做搜索的痛点
先说说最典型的触发点。很多业务系统最开始都用 MySQL 存储核心数据,比如商品表、文章表、订单表。数据量小的时候,一个WHERE name LIKE '%关键词%'就能应付。可一旦数据量到了几百万、上千万,MySQL 的索引机制对这种模糊查询就力不从心了。MySQL 的 B+ 树索引对前缀匹配有效,但对包含匹配、分词搜索、多字段联合过滤,优化空间很小。随便一次搜索请求就可能拖慢整个数据库。
这时候 Elasticsearch 的优势就出来了——倒排索引天生就是为全文检索设计的。但问题在于:MySQL 是业务系统的数据源,所有写入操作都在这里发生;ES 只是检索层,数据必须从 MySQL 过来。如果手动在业务逻辑里双写 MySQL 和 ES,代码侵入太强,而且很容易出现两边数据不一致。一旦某个环节失败,MySQL 写成功了 ES 却没更新,整个检索模块就处于脏数据状态。
定时任务全量同步也是常见方案,但效果很差。全量同步的间隔最低也是分钟级,数据实时性根本谈不上。而且全量同步时,如果表数据发生了变更,还要处理冲突,逻辑很容易失控。对于时效性要求比较高的系统,比如电商库存、订单状态、内容发布,分钟级延迟完全不能接受。
Canal 提供的正是实时同步能力。它通过解析 MySQL 的 binlog 拿到每一次变更,基本可以做到秒级甚至毫秒级的数据同步。业务代码不需要改一行,只要数据库开启了 binlog,Canal 就能自动把数据的增删改同步到 ES 里。这也是我最终从定时任务切到 Canal 的核心原因。
1.2 同步方案选型:为什么选择 Canal 而不是其他
除了 Canal,市面上还有几种常见方案。每种方案都有自己的适用场景,不存在绝对的好坏。我按实际体验说说它们之间的优劣,方便你选型时少走弯路。
第一种是应用层双写。业务代码在操作 MySQL 后,再写一次 ES。这个方案最简单直接,但要改所有写入路径,后续每次新增写入点都得记得同步。而且无法保证原子性,MySQL 事务提交后 ES 写失败就是脏数据。除非业务极其简单、写入点很少,否则不推荐。
第二种是消息队列异步同步。业务写入 MySQL 的同时,发送一条 MQ 消息,另一个消费者将数据写入 ES。这个方案比双写好一点,把写 ES 的节奏与业务解耦了。但仍然需要业务代码入侵,而且需要处理消息丢失、重复消费、乱序等问题。如果团队本来就有 MQ 基础设施,可以考虑。
第三种是基于 Binlog 的 CDC 方案,代表就是 Canal。它从数据库层面捕获变更,业务代码零改造成本。能保证 MySQL 中的每一次事务提交顺序被准确捕获,天然解决乱序问题。配合消息队列缓存或直接投递下游,实时性非常高。而且 Canal 本身兼容多种输出方式,单独部署在应用集群外,运维隔离很好。
还有一种类似工具,比如 Debezium,也做得不错。它基于 Kafka Connect 生态,功能很强大。如果你的技术栈已经是 Kafka 为主,Debezium 是个好选择。但 Canal 在国内生态里使用广泛,很多公司都有成熟实践,社区资料丰富,遇到问题容易搜到解决方案。我选择 Canal 除了技术原因,还有团队可维护性的考虑——一个大家都见过的工具,远比一个炫酷但没人会排障的工具更实用。
2. Canal 的工作原理与核心概念
2.1 Canal 的定位:伪装成 MySQL 从库,解析 Binlog
Canal 的核心思路其实很聪明。它把自己伪装成 MySQL 的一个从库(Replica),向 MySQL 主库发送复制请求。MySQL 的 binlog 是主从复制的基石,主库将每次变更写入 binlog,从库拉取并重放。Canal 做的事情,就是替代从库去拉取 binlog,但并不要把数据落地到本地存储,而是解析成一种结构化的变更事件,再按需求推给下游。
这个过程涉及一个关键角色:binlog dump 线程。MySQL 主库上有专门线程负责向从库输出 binlog 内容。Canal 在启动时,会向主库发送COM_REGISTER_SLAVE请求,然后发送COM_BINLOG_DUMP指令,从指定位置开始读取 binlog。这个位置由 Canal 的位点信息(LogPosition)控制,它记录了自己消费到哪个 binlog 文件和偏移量。
正因为 Canal 是模拟从库,它需要主库的复制权限。创建的账号必须至少有SELECT、REPLICATION SLAVE、REPLICATION CLIENT权限。如果权限不够,启动时就会报错,这个后面在配置部分会具体讲。
Canal 解析 binlog 之后,得到的数据结构是一组事件。每个事件里包含库名、表名、事件类型(INSERT、UPDATE、DELETE)、变更前镜像、变更后镜像。这些事件又被封装成CanalEntry.Entry这样的协议对象。如果你的下游是 ES Adapter,它可以直接消费;如果接 MQ,则会将数据序列化后发到 Topic 里。
2.2 Binlog 的三种格式:为什么只推荐 Row 模式
搞清楚 binlog 格式,是理解 Canal 的前提。MySQL 的 binlog 有三种格式:STATEMENT、ROW、MIXED。
STATEMENT 格式记录的是 SQL 语句本身,比如UPDATE user SET age=18 WHERE id=1。这种格式日志量小,但有个致命问题:某些非确定性 SQL,比如使用了NOW()、UUID()的函数,在从库重放时结果可能和主库不一致。Canal 解析这种格式也很难还原每行数据的具体变化,因为语句本身并不包含变更前后的值。
ROW 格式记录的是每一行数据的变化,比如在 update 前记录旧行,在 update 后记录新行。这种格式日志量大,但信息最完整,能精确到字段级别。Canal 拿到这种格式,根本不需要执行 SQL,直接解析出前后数据,就能知道某一行从什么值变成了什么值。这正是 ES 同步所需要的:删掉旧值、写入新值。
MIXED 格式是两者混用。MySQL 会根据 SQL 的类型动态选择适合的格式。对于多数情况,它也能工作,但 Canal 官方文档明确推荐使用 ROW 格式。因为 MIXED 在遇到某些语句时仍可能记录为 STATEMENT,导致解析结果不完整。如果你在生产环境配了 MIXED,建议尽快改掉。
配置位置在 MySQL 的 my.cnf 或 my.ini 中,设置项是binlog_format=ROW。除此之外,binlog_row_image=FULL也很关键。它控制在 ROW 格式下记录镜像的方式。默认是 FULL,即记录所有列;如果改成 MINIMAL,只记录变更列和主键列,日志量会变小,但 Canal 可能拿不到完整的字段值。为了保证同步到 ES 的数据完整性,我一般都保持 FULL。
2.3 Canal 的架构:Server、Instance、Adapter 之间的关系
Canal 官方提供了两种常见使用模式:一是单独部署 Canal Server,将数据投递到 MQ 或者通过 TCP 给下游;二是在 Server 上直接挂 Adapter,用 Adapter 对接不同的存储,比如 ES、HBase、Kafka。我这里重点讲带 Adapter 的架构,因为这是将数据同步到 ES 最直接的方式。
Canal Server 是一个 Java 进程,它内部可以运行多个 Instance。每个 Instance 对应一个 MySQL 数据源,也就是一组 binlog 的订阅任务。你可以只订阅某一个库,也可以订阅多个库,通过正则匹配表名来决定哪些表的数据需要被解析。
Canal Adapter 是独立于 Server 的组件,它通过 TCP 连接 Canal Server,获取变更事件,然后按照配置好的映射规则,把事件转换成对 ES 的 REST API 请求。Adapter 的可执行模块中,canal-adapter-launcher是启动入口,但实际它并不是靠这个名字运行,而是引用了 launcher 模块来加载 adapter 的装配。日常操作中,直接使用bin/start.sh启动即可。
如果你不想用 Adapter,也可以让 Canal Server 把数据发到 Kafka 或 RocketMQ,然后自己写个消费者去消费,再写入 ES。这种模式更灵活,适合需要对数据做复杂清洗的场景。如果数据格式不需要二次加工,直接上 Adapter 最简单,因为它内置了字段映射和类型转换的机制。
我在实际部署中,将 Canal Server 和 Adapter 放在同一台机器上,但进程独立。这样网络拓扑简单,排障方便。如果数据量极大,建议把 Canal Server 和 Adapter 分开部署,避免单机负载过高。
3. 搭建同步链路:从零到一的具体实操
3.1 环境准备:版本选择与部署方式
开始部署前,先把版本说清楚。不同版本的 Canal 对 MySQL 和 ES 的兼容性有差异,版本不对会带来很多莫名其妙的问题。我用的是目前社区里比较稳定的一套组合:
- MySQL:5.7.x 或 8.0.x,必须开启 binlog
- Elasticsearch:7.x 系列,考虑适配性与稳定性,7.x 是比较广泛的选择
- Canal:1.1.6 或 1.1.7,这两个版本对 ES 7 支持已经相当完善
- JDK:Canal Server 和 Adapter 需要 JDK 8 以上
Canal 的部署方式有两种:直接下载安装包,或者使用 Docker。我建议你先用安装包试跑通流程,因为它更直观,方便调试。Docker 方式用于后续的运维环境更省心,但调试起来不如本地直接看日志方便。
下载 Canal 压缩包后,解压到/opt/canal目录。结构大致如下:
canal.deployer/ # Canal Server canal.adapter/ # Canal Adapter这两个目录需要分别下载对应的压缩包。Canal Server 负责解析 binlog,Canal Adapter 负责将数据写入 ES。启动顺序是先启动 Server,再启动 Adapter,因为 Adapter 需要连接 Server 拉取事件。
3.2 配置 MySQL:开启 Binlog 与创建专用账号
MySQL 那端要做两件事:开启 binlog 和创建专属账号。
开启 binlog,需要修改 MySQL 配置文件。在[mysqld]段中添加以下内容:
server-id=1 log_bin=mysql-bin binlog_format=ROW binlog_row_image=FULL expire_logs_days=7server-id必须设置,否则 MySQL 无法使用 binlog。log_bin设定 binlog 文件前缀。binlog_format=ROW是关键,前面已经解释过。binlog_row_image=FULL保证 Canal 能拿到完整行数据。expire_logs_days=7是因为 binlog 会不断膨胀,不及时清理会占用大量磁盘空间,这里保留七天作为安全窗口。在 MySQL 8.0 中,这个参数改为了binlog_expire_logs_seconds,但expire_logs_days仍可用。
修改完配置后,重启 MySQL 使配置生效。验证 binlog 是否开启,执行:
SHOW VARIABLES LIKE 'log_bin'; SHOW VARIABLES LIKE 'binlog_format';如果返回ON和ROW,就说明配置正确。
创建专用账号。不建议在 Canal 配置里使用 root 账号,权限太宽,且不利于审计。执行:
CREATE USER 'canal'@'%' IDENTIFIED BY 'canal_password'; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%'; FLUSH PRIVILEGES;REPLICATION SLAVE使 Canal 能像从库一样拉取 binlog,REPLICATION CLIENT用于获取主库状态信息。SELECT权限在某些场景下用于查询表结构(尤其是解析 DDL 事件时)。这样配置后,Canal 可以在不接触业务数据的情况下工作。
3.3 配置 Canal Server:关键参数解析
Canal Server 的配置文件位于conf/canal.properties。这个文件是全局参数,影响所有 Instance。需要关注以下几个:
canal.port=11111 canal.destinations=example canal.instance.global.spring.xml=classpath:spring/default-instance.xmlcanal.port是 Canal Server 对外提供 TCP 服务的端口,Adapter 需要通过这个端口连接。canal.destinations指定 Instance 名称,默认是example。如果你要创建多个同步任务,可以用逗号分隔多个名字,比如example,order_db。
接着看conf/example/instance.properties,这是单个 Instance 的配置。核心内容如下:
canal.instance.mysql.slaveId=1234 canal.instance.master.address=127.0.0.1:3306 canal.instance.dbUsername=canal canal.instance.dbPassword=canal_password canal.instance.connectionCharset=UTF-8 canal.instance.filter.regex=test\\..*slaveId是 Canal 模拟从库时使用的 ID,不能和 MySQL 集群中已有的从库 ID 冲突。master.address是 MySQL 主库地址。dbUsername和dbPassword对应前面创建的账号。connectionCharset按数据库实际编码配置,一般是 UTF-8。
filter.regex是最容易写错的参数。它支持正则表达式匹配表名,格式是数据库名\\..*。注意,在 properties 文件中,反斜杠需要转义,所以写test\\..*。如果要同时订阅多个库,用竖线分隔:db1\\..*|db2\\..*。如果表名是精确匹配,还可以写具体的表名,比如test\\.user。默认配置订阅所有库,但在生产环境,强烈建议缩小范围,减少无谓的资源消耗。
修改完配置后,启动 Canal Server:
cd bin ./startup.sh启动后查看logs/canal/canal.log,如果出现类似“canal server is running now”的字样,就说明启动成功了。接着查看logs/example/example.log,这个文件记录了 Instance 的连接状态。看到"start successful"或者"binlog position"相关日志,说明已经连接到 MySQL。
3.4 配置 Canal Adapter:将 Binlog 事件转成 ES 索引操作
Adapter 是这套链路中配置最繁琐的部分。它有两层配置:一层是全局配置,一层是具体针对 ES 的映射配置。
全局配置文件是conf/application.yml,它决定了 Adapter 如何连接 Canal Server。核心内容如下:
canal: server: servers: 127.0.0.1:11111 destinations: - example spring: application: name: canal-adaptercanal.server.servers指向 Canal Server 的 TCP 端口。canal.destinations与 Canal Server 中的canal.destinations对应。启动多个 Instance 时,这里也要列全。
ES 映射配置在conf/es目录下,每条同步链路一个 yml 文件。文件名可以自定义,比如user.yml。配置文件结构如下:
dataSourceKey: defaultDS destination: example esMapping: _index: user_index _id: id sql: "SELECT id, name, age, email, create_time FROM user WHERE id = ?" etlCondition: "where create_time >= {etl_start} and create_time <= {etl_end}" commitBatch: 3000这里的dataSourceKey指的是conf/application.yml中配置的数据源 key,需要在 Adapter 的application.yml里额外定义数据源连接。如果表在 MySQL 的 test 库下,数据源配置中要确保连接库名正确。
sql字段非常关键。它定义了你需要同步到 ES 的字段集合。Canal Adapter 会执行这条 SQL 去 MySQL 中查询整行数据,然后将查询结果匹配到 ES 的索引字段。WHERE id = ?中的问号会被 Canal 自动填充为 binlog 中的主键值。因此,sql中的?位置必须对应表的主键。
_index是 ES 中索引名称,_id是文档 ID,一般用主键。etlCondition用于全量同步时按时间范围抽取数据,增量同步时可以不配置。
配置完成后,启动 Adapter:
cd bin ./startup.sh观察logs/adapter/adapter.log。如果出现类似“canal-adapter start successful”的日志,并且连接 Canal Server 成功,就说明链路已经跑通。这时在 MySQL 中执行一条插入语句,再去 ES 中查询对应索引,如果能查到数据,说明基础设施已经正常工作了。
4. 核心细节:数据映射、增量同步与全量同步的取舍
4.1 索引设计:MySQL 字段与 ES 类型的映射关系
同步链路搭好,只是第一步。真正让人头疼的是字段类型映射。MySQL 和 ES 的类型体系并不一一对应,一个不小心就会导致搜索行为不符合预期。
常见映射规则,我整理了一个参考表:
| MySQL 类型 | ES 类型 | 说明 |
|---|---|---|
| INT / BIGINT | long | 整数类型,直接映射 |
| VARCHAR / TEXT | text 或 keyword | 需要全文搜索的用 text,需要精确匹配、排序、聚合的用 keyword |
| DATETIME / TIMESTAMP | date | 注意时区问题 |
| DECIMAL | double 或 scaled_float | 大金额用 scaled_float,避免浮点误差 |
| TINYINT(1) | boolean | 如果需要布尔语义,可以转换 |
这里最容易踩坑的是 Varchar。如果源字段是昵称、名称这类需要整体匹配的内容,像按用户名精确查询时,ES 默认会将字符串解析为 text 和 keyword 两个子字段。如果你在创建索引时没有显式指定 mapping,ES 7 起默认策略是文本字段映射为 text 并附加一个 keyword 子字段。但 Adapter 自动创建索引时未必会按这个策略处理,所以最保险的方式是在 ES 里先手动创建索引和 mapping,再让 Adapter 去写入。
我自己遇到过这么一个问题:把一个描述字段同步成 text 类型后,原本只需要按完整短语查询,但分词器将短语拆碎了,导致搜索出来一堆不相关的结果。后来我将该字段改成了 keyword,只在确实需要全文检索的字段上用 text,并配上合适的 ik 分词器,才恢复正常。
另一个常被忽略的是时间字段的时区。ES 的 date 类型默认使用 UTC 存储,而 MySQL 的 DATETIME 往往存储的是本地时间。如果两边不带时区信息,同步后查询出来的时间会偏差 8 小时。解决办法有两种:一是在 SQL 映射时用DATE_FORMAT(create_time, '%Y-%m-%dT%H:%i:%s+08:00')之类的格式把时区显式带上;二是调整 Adapter 的时区配置,或者直接在 ES mapping 中将format设置为yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis,配合 Java 虚拟机时区设置。但最省心的还是统一约定:MySQL 和应用层都按 UTC 存储,展示层再按当前时区转换,这样任何同步组件都不会出现时间错位。
4.2 全量同步与增量同步如何配合
Canal 的核心是增量同步,它从 binlog 中捕获的是实时变更。但业务系统已有的历史数据,Canal 是无法感知的,因为 binlog 早在开启订阅之前就已经过期或被清理了。所以一次完整的接入,必须做一次全量同步,将存量数据搬迁到 ES,然后再启动增量同步,让后续变更持续生效。
全量同步的实现,在 Canal Adapter 中内置了一个 ETL 功能。它并不依赖 binlog,而是通过执行配置在esMapping.sql中的查询来抽取数据。你可以通过 HTTP 接口触发全量同步任务,Canal Adapter 会启动一个线程,用分批查询的方式将数据写入 ES。
触发命令类似于:
curl -X POST http://127.0.0.1:8081/etl/{task_name}.yml这里的{task_name}就是你的映射配置文件名。默认端口是 8081,可以在application.yml中调整。当执行这个接口后,Adapter 会根据etlCondition的设置按批抽取数据。如果你的表数据量是百万级,这个接口会默认使用commitBatch作为每批写入条数,一次性提交到 ES。一般来说,直接跑就行,它内部有批处理,不会压垮 ES。
不过,更稳妥的做法是使用数据迁移工具先完成全量,比如用 Logstash 的 jdbc input 插件。它能控制抽取速度,还支持并行。为什么推荐 Logstash?因为 Canal Adapter 的 ETL 本身只是为了保证链路完整性而附带的,它在断点续跑、任务重试等机制上比较弱。如果你要迁移的是千万级大表,我更建议先跑 Logstash,确定数据一致后再开启 Canal 增量。
全量与增量的衔接点在于位点。全量同步跑完后,ES 中的数据和 MySQL 在某一个时间点是一致的。这个时间点之后,开启 Canal 订阅,Canal 会从当前 binlog 位置开始消费。如果全量同步期间还有业务写入,就会出现重复。解决办法是:在业务低峰期,先停掉写入,做全量,然后立刻开启 Canal。如果无法停写,则需要用快照技术,或者在全量迁移日志中记录时间边界,将边界后的增量数据重新回放一次。
4.3 主键冲突、更新删除动作的处理
同步链路的核心是让 ES 文档的生命周期与 MySQL 行数据的生命周期保持一致。增量的增和改没什么大问题,关键在于删除和更新时对旧数据的处理。
对于 INSERT 和 UPDATE,Canal Adapter 的默认行为是向 ES 发起 index 请求。在 ES 中,index 不是单纯的插入,而是一种 upsert 操作——如果文档 ID 已存在,就覆盖更新。这种设计天然就解决了一部分幂等性问题。即使 binlog 事件被重复推送,只要文档 ID 不变,最终结果仍然一致。
但 DELETE 就需要特别注意。binlog 中的 DELETE 事件只包含删除行的主键值,Adapter 会据此在 ES 中执行 delete 操作。如果你的 Adapter 配置正确,这套流程是自动的。可一旦你的 SQL 映射中写了多表关联查询,情况就复杂了。比如同步的字段来自两张表联合查询,删除子表记录时,父表的数据其实没变,但如果你的删除操作也映射到了子表,就可能导致父文档被误删。解决这类问题的前提是搞清楚业务中哪些删除需要影响 ES 文档,然后针对性地配置过滤规则。
更新操作还有一个隐蔽问题:如果更新的字段不在 SQL 映射的字段列表中,比如你只同步了name和age,但业务更新了address字段,Canal Adapter 依然会产生一次对 ES 的更新请求。表面上是无用的写操作,但如果这个请求的总量很大,就会造成无谓的写入压力。最好的做法是在数据源表上做精简,只订阅真正需要同步到 ES 的字段变更。不过这需要依赖 canal 的过滤能力,目前 Adapter 并不支持字段级别的细粒度过滤,所以实际中我更倾向于在业务侧尽量避免无意义的简单字段频繁更新,或者通过版本号字段来降低 ES 中的写频率。
5. 常见问题与排查实录
5.1 连接失败与权限问题
启动 Canal Server 时报“Access denied for user 'canal'”,十有八九是权限没配好。回 MySQL 里重新执行一遍 GRANT 语句,然后 FLUSH PRIVILEGES。注意,如果 canal 账号是在开启 binlog 之前创建的,并且没给 REPLICATION 权限,也会失败。最好按前面章节的配置完整执行一次。
另一种连接失败是网络不通导致的。如果 MySQL 和 Canal 不在同一台机器,需要检查 MySQL 的 bind-address 是否允许外部 IP 访问,以及防火墙是否放行了 3306 端口。Canal 的日志中会明确提示连接超时还是拒绝访问,注意区分。
Canal Server 启动成功,但 Adapter 连接不上 Canal Server,请确认canal.properties中的canal.port是否正确,以及 Adapter 的application.yml中server.servers的 IP 和端口是否一致。还有一个容易忽略的细节:canal.destinations必须匹配,否则 Adapter 即使连接成功,也找不到对应的实例。
5.2 数据不同步与延迟飙升
链路都通了,但 MySQL 中更新了数据,ES 里迟迟不见变化。这是最让人抓狂的问题。先别瞎猜,按下面的顺序检查。
先看 Canal Server 日志,确认 binlog 是否在持续消费。如果日志停留在某个位点不动,说明 Canal 没有收到新的 binlog 变更。原因可能是 binlog 已经过期被清理,也可能是 Canal 持有的位点太老,导致 MySQL 无法提供该位置的 binlog。这种情况常发生在你停了好几天没启动 Canal,重新启动时,binlog 已经被清理了。解决方法是重置位点,让 Canal 从当前 binlog 位置开始消费,但这样会丢失停止期间的数据变更,需要配合全量同步来补齐。
再检查 Adapter 日志,确认它是否接收到了 Canal 推送的事件。如果 Adapter 收到了事件,但 ES 中没有变化,看是否出现了字段映射错误。比如 MySQL 中字段名和 ES 中字段名不一致,Adapter 可能忽略掉异常事件,或者写入失败。Adapter 的日志中通常会打印出失败原因和堆栈信息,根据堆栈去排查。
延迟飙升,通常是因为单条同步链路处理能力不足。Canal 的消费模型是串行处理,如果某条 SQL 映射查询特别慢,比如关联了多张大表,那么后续事件就会积压。这种场景要么优化 SQL,要么把数据同步拆成多条链路,按表粒度分开订阅。
5.3 解析错误与字段映射错位
有时候 MySQL 表结构发生变化,比如新增了一个字段,但 ES 索引中没有对应的字段。Adapter 写入时不会自动补字段,它会按照配置的 SQL 去查询数据。如果 SQL 没有变化,那新增字段不会同步;如果希望同步,需要修改映射配置文件中的 SQL,加字段,同时也要在 ES 的索引 mapping 中增加该字段。修改映射后,要重启 Adapter 才能生效。
字段映射错位还有个常见诱因:Adater 的 SQL 查询结果列顺序和 ES mapping 字段顺序不对应。虽然 Adapter 一般通过字段名匹配,但如果 MySQL 的查询结果中存在同名字段,或者使用了别名,可能导致取错值。建议在 SQL 中显式使用别名,避免歧义。
如果出现date parse error类似的异常,基本就是时间格式问题。前面 4.1 章节提过时区,这里再次强调,ES 对日期的解析很严格,格式不对就直接抛异常。检查 MySQL 的日期字段格式和 ES mapping 的 format 是否匹配。
5.4 幂等性与重复数据
Canal 的位点机制基于 MySQL 主从同步协议。在某些异常场景下,比如 Canal 崩溃、网络闪断,binlog 可能被重复拉取,导致 Adapter 收到重复事件。重复事件最常见的影响是:重复执行 update 没有大问题,因为 ES 的 index 是幂等的;但重复执行 delete 可能删掉一条已经被新数据替代的文档。
要解决重复问题,需要理解 Canal 的消费确认机制。Canal Server 消费 binlog 后,会定期更新位点。如果 Adapter 处理事件时进程挂掉,位点可能没有及时更新,重启后 Canal 会从旧的位点重新推送事件。这属于 at-least-once 语义。ES 侧的关键在于,将文档的版本信息(如 MySQL 中的 update_time)一并同步到 ES,并在写入时利用 ES 的乐观并发控制,通过外部版本号字段来保证旧版本不会覆盖新版本。
但从我的实践看,大多数场景下,只要不依赖 ES 做精确计数或者账务类业务,重复事件造成的影响是可控的。ES 本来就是搜索和展示层面,最终一致性已经够用。如果你实在需要对账,建议在同步链路上加一层 Kafka,利用消费者的 commit 机制配合去重表来实现精确一次。
6. 经验技巧与优化方向
6.1 避免脑裂的消费位点管理
Canal 支持集群模式,可以通过 ZooKeeper 或者 Canal 自带的 server 间协调机制来管理多个实例的位点。如果你单机部署,位点存储在本地meta.dat中。默认情况下,Canal 会每隔一段时间自动持久化位点。
我最开始做的时候,直接单机部署,没太注意位点文件。后来因为日志盘满,Canal Server 被强制重启,才发现位点文件损坏,导致数据从最开始重新消费了一遍。从那次之后,我养成了一个习惯:部署机器上定期备份meta.dat,或者在配置一个 CHANNEL 时,手动记下启动时的 binlog 位点,方便出问题时恢复。
如果是多 Canal Server 部署,务必配置好 ZooKeeper,让主备自动切换。否则一旦主节点宕机,备节点无法接管,同步就会中断。ZooKeeper 的方式虽然引入了额外组件,但收益远大于维护成本。前提是你们的运维环境能接受 ZooKeeper 部署。
6.2 性能调优:攒批、并行与 MQ 缓冲
Canal Adapter 写入 ES 时,默认按照commitBatch参数批量提交。这个值默认是 3000,意思是攒够 3000 条再向 ES 发起 bulk 请求。如果你对实时性要求没那么极端,可以提高这个值到 5000 甚至 10000,减少网络交互次数,ES 的写入吞吐会明显提升。
但批量越大,单次请求的响应时间越长,可能导致 ES 的 bulk 队列堆积。要结合 ES 集群的写入能力来调。如果 ES 的索引有副本,写入开销倍增,建议降低commitBatch同时加并行。Adapter 通常按表粒度串行,如果要并行写入同一张表的数据,需要用 Canal 的 MQ 模式,将数据投递到多个分区,然后写多个消费实例。
MQ 模式是我在数据量上到一定规模之后的推荐方案。Canal Server 将 binlog 事件发到 Kafka 的 topic 中,每个分区对应一组有序事件。然后你用多个消费者进程,每个进程负责几个分区,各自消费并写入 ES。这样并行度完全由消费端控制,而且 Kafka 天然解决了削峰填谷的问题。不过引入 MQ 会增加运维复杂度,建议在单 Adapter 扛不住时再考虑。
6.3 日常维护与监控
同步链路看起来简单,但长期运行中,还是要做好监控。我目前会关注几个核心指标:Canal Server 的连接状态、当前消费位点与 MySQL binlog 最新位点之间的滞后量、Adapter 的写入失败数量、ES 集群的 bulk 拒绝数量。
滞后量是核心。Canal 的canal.properties中有一个参数,可以通过输出 metrics 来获取当前的消费进度。如果你遇到了延迟,先看这个指标,判断是 Canal 拉取慢还是 Adapter 写入慢。一般的经验是,Canal 拉取 binlog 的速度非常快,瓶颈多半在 Adapter 或 ES 侧。
日志监控也别落下。Adapter 的日志默认是 info 级别,在排障时可以将级别调到 debug,查看每个事件的执行细节。但生产环境不建议长时间开启 debug,日志量非常大,磁盘会迅速被打满。
另外,建议定期核对 ES 中的文档总数和 MySQL 中对应表的行数是否一致。可以通过透出 SQL 统计总数,再和 ES 的_count接口对比。如果差距持续变大,说明有事件丢失。这种情况下,最直接的办法是重新跑一次全量同步,再观察增量。
我个人在实际操作中的体会是,Canal 这套链路并不是一次配置就永久无忧。它更像一个需要定期拧一拧螺丝的机器,版本升级、MySQL 参数调整、ES 索引调整,都可能导致行为变化。所以一定要保留好每一个版本的配置文件,标注清楚改了什么、为什么改。踩过的坑越多,越发觉得“可追溯”比“自动化”更重要。如果你正在设计数据同步方案,别急着堆技术组件,先把 MySQL 的 binlog 机制和 ES 的写入模型吃透,同步的方案自然就清晰了。