基于Canal的MySQL到Elasticsearch数据同步实战与避坑指南
先说一下我这边为什么会折腾这个东西。之前接了一个业务项目业务方要求把MySQL里的订单和商品数据同步到Elasticsearch后面统一叫ES里做搜索和聚合实时性要求很高数据变更后几秒钟内要能查到。最开始的方案很朴素业务代码里双写写MySQL的同时写ES结果交互链路一长两边经常对不上还得靠定时任务每天全量对账光维护就够头疼的。后面我换成了canal同步MySQL数据到ES这套方案监听binlog增量变化业务代码零侵入MySQL主库写完变更事件自己就会消费到ES里。这套方案我前后跑了小半年从部署、全量初始化、增量订阅到各种坑基本都踩了一遍这篇就把完整的实操过程、配置思路和排障心得整理出来给同样想用canal同步MySQL数据到ES的读者一个可以直接抄作业的参考。1. 为什么选canal这套方案主流同步路径对比与取舍先别急着上配置同步方案选型这一步很关键选错了后面返工成本极高。我梳理下来MySQL数据同步到ES的主流路径大概有四条每条的适用场景和坑都不同。1.1 业务双写与定时任务方案的痛点第一条路是业务双写在业务代码里凡是涉及订单表、商品表变更的地方同时更新MySQL和ES。这种方式在小规模、表数量少、团队协作简单的时候能用但痛点很明显。首先是事务问题MySQL写成功了ES写失败怎么办回滚MySQL吗很多场景根本没法回滚只能靠补偿任务。其次是代码侵入每张表、每个写接口都要改一遍业务代码里到处是ES索引更新的逻辑随着调用方增多压根维护不住。最后是实时性依赖业务成功返回一旦某个写操作失败被重试ES里可能已经写了脏数据。第二条路是定时全量同步写个定时任务每隔几分钟把MySQL数据全量捞一遍再刷进ES。这种方式适合数据量小、实时性要求不高的场景但数据量一旦上了百万级全量扫描对MySQL主库压力非常大而且同步的频率上不去无法做到秒级。早期我接手一个旧系统用的就是这种方案订单表三百万行每次全量同步耗时几分钟ES里的数据最少延迟五分钟起步搜索场景还能凑合一旦业务方要求查实时库存和最新订单状态这套方案基本就废了。1.2 基于binlog的增量订阅为什么更合适第三条路是引入消息队列做解耦业务代码把变更事件发到消息队列再由消费端写入ES。这个方案比双写干净一些但还是在业务代码里埋了发消息的逻辑而且消息队列本身也是额外的组件运维成本不低。同时事件的可靠性依赖业务对消息的成功发送一旦漏发问题很难排查。第四条路就是based on binlog的增量订阅方案也就是canal干的活。canal的核心思路是把自己伪装成MySQL的从节点向MySQL主库请求binlog把数据库的插入、更新、删除事件解析出来然后通过adapter或者自定义客户端消费到目标端。这个方案的好处是业务代码零侵入不需要改任何业务逻辑数据链路天然跨过业务层只要MySQL产生binlog就会触发同步实时性可到秒级甚至毫秒级不额外引入消息中间件canal组件本身够轻量如果单机部署只需要一个Java进程加配置。1.3 canal同步MySQL到ES的整体链路整体链路很清晰MySQL主库开启binlogcanal server伪装成从库连接主库并解析binlogcanal client或canal adapter拿到解析后的数据变更事件按配置的映射关系写入ES索引。MySQL binlog → canal server解析 → canal adapter消费与映射 → Elasticsearch这套链路我拆解成四个核心部分MySQL端binlog配置、canal deployer部署、全量初始化方案、adapter映射配置。下面逐一展开。2. 部署前必做的MySQL配置与canal环境准备2.1 MySQL开启binlog这一步不能省canal工作的前提是MySQL已经开启binlog且binlog格式必须设置为ROW。如果MySQL当前没开binlog需要修改配置文件并重启MySQL实例这一步通常需要和DBA确认因为重启主库实例会影响线上连接。MySQL端关键配置项[mysqld] # 开启binlog日志 log-binmysql-bin # binlog格式必须使用ROWcanal才能拿到每行数据变更前后的完整字段 binlog_formatROW # 确保binlog中包含完整的前镜像和后镜像删除操作才能拿到被删行的所有字段 binlog_row_imageFULL # 每个binlog文件大小默认即可 max_binlog_size128M # binlog保留天数根据磁盘容量和数据恢复需求设置 expire_logs_days7需要注意有部分云数据库默认不开启binlog或者binlog_row_image不是FULL需要工单申请开启。我踩过一次坑MySQL开启了binlog但binlog_row_image是MINIMAL删除数据事件里只带主键不带其他字段ES索引里其他字段的删除逻辑只能靠主键关联MySQL再查一次才能补全非常麻烦。所以这一步务必确认是FULL。权限方面canal连接MySQL需要单独的账号。给它授权SELECT、REPLICATION SLAVE和REPLICATION CLIENT权限即可CREATE USER canal% IDENTIFIED BY canal_passwd; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO canal%; FLUSH PRIVILEGES;这里SELECT权限是canal在特殊场景下回查数据用的比如某些binlog事件信息不完整时需要通过主库补查虽然实际同步主要靠binlog但权限尽量给全避免后续未知报错。2.2 canal deployer安装与实例配置canal分为deployer和adapter两部分。deployer负责连接MySQL和解析binlogadapter负责把解析出来的数据写入目标端。这两部分可以部署在同一个节点也可以分开部署。生产环境我建议至少用三台机器或者三个容器分别跑deployer的实例配合ZooKeeper做高可用。我这里只讲单机部署链路高可用集群在文章最后简单提一句。部署时先从官方仓库下载对应版本安装包解压后结构大致如下canal.deployer/ ├── bin/ # 启动停止脚本 ├── conf/ │ ├── canal.properties # canal server级别的全局配置 │ └── example/ │ └── instance.properties # 单个数据同步实例配置example是默认实例名 └── lib/ # 依赖jar包核心配置集中在canal.properties里的这几项# 当前节点角色单机直接填1 canal.registerMode tcp # server模式固定就行 canal.serverMode tcp # 实例列表多个实例用逗号分隔 canal.destinations example # 一批事件的最大字节数 canal.instance.memory.batch.mode MEMSIZE其实单机跑通的话这些默认配置基本不用改重点改的是conf/example/instance.properties。实例配置里需要指定MySQL连接信息、监听过滤规则和binlog位点配置# MySQL主库地址改成你自己的 canal.instance.master.address127.0.0.1:3306 canal.instance.dbUsernamecanal canal.instance.dbPasswordcanal_passwd canal.instance.connectionCharsetUTF-8 # 要监听的库表支持正则表达式 # 这里的规则表示监听test库下所有的表 canal.instance.filter.regextest\\..* # 也可以指定多库多表例如test\\..*,shop\\.(order|goods) canal.instance.filter.black.regex # 同步位点存储位置单机用memory即可 canal.instance.global.modememory # 也可以指定为zookeeper用于多节点高可用 # canal.instance.global.modezookeeper # canal.instance.zookeeper.host127.0.0.1:2181canal.instance.filter.regex这个配置我多说一句第一次配置时很容易被反斜杠转义搞晕test\\..*表示匹配test库的所有表。如果只想同步某一张表写成test\\.order即可。正则匹配规则是canal基于Java正则的写完之后建议先用grep确认配置文件里没有多余空格或者注释行被错误打开否则实例启动会直接失败。2.3 启动deployer与验证连接状态配置完成后先运行bin目录下的启动脚本./startup.sh启动后去logs目录看日志重点看两个文件logs/canal/canal.logserver级别的日志看是否完成启动logs/example/example.log实例日志看是否成功连上MySQL并dump binlog实例启动成功后日志里会出现类似dump address /127.0.0.1:3306 has been dump successfully的提示这说明canal已经成功连接MySQL并开始拉取binlog。此时再到MySQL主库执行一条insert操作实例日志会立刻出现对应表的DML事件解析信息。需要注意canal第一次启动默认是从MySQL当前最新的binlog位点开始消费的也就是只能捕获启动之后的新增变更。如果ES里需要老数据就得先做全量初始化这一点下面专门讲。3. 数据同步链路设计全量初始化与增量订阅衔接3.1 为什么必须先做全量初始化canal本质上是增量订阅它只同步启动之后产生的binlog事件启动之前MySQL里已有的存量数据它是看不见的。所以整套同步链路的第一步必须把存量数据先灌入ES然后才能放心地让canal持续增量同步。全量初始化有几种做法我分别说下优劣。方法一是直接用mysqldump导出数据再写脚本批量写入ES。这种方式简单直接但需要自己处理类型映射和批量写入逻辑而且数据量大时对MySQL和ES都有压力。方法二是用canal自带的适配器在adapter的配置里允许通过http接口触发全量同步它会自己从MySQL拉数据并写入ES比较省事。方法三是用中间表或时间字段分段捞从一个update_time字段判断把历史数据分批同步。这个适合不想停业务、且业务表有统一的修改时间字段的场景。我实际推荐的做法是先停掉canal的增量消费或者让增量消费处于暂停状态完成存量全量导入再把canal实例的消费位点对齐到全量导出的起点时间之后继续消费。这样能尽量避免数据重叠或漏数据。具体来说我曾经在某个项目里用过的简单方案是记录一个时间点T1用SELECT把业务表按主键分批捞出来批量写入ES等全量写完再启动canal增量同步。这里有个隐蔽问题如果在T1到增量真正启动之间业务表发生了新的变更这一段的变更如果没被全量捞到就会出现窗口期数据缺失。严谨一点的做法是一边让canal的binlog消费位点保持在T1时刻全量导入完成后再让canal从T1继续消费这样T1之后的新变更会以增量形式补偿进ES不会丢数据。实际实现时可以将canal的canal.instance.master.journal.name和canal.instance.master.position手动设置为T1时刻对应的binlog文件名和位点。3.2 增量同步的批量消费与ack机制增量同步启动后canal client或adapter会持续拉取解析后的事件。canal在消费机制上采用的是批量拉取确认ack的模型客户端每次从canal server拉取一批事件处理完成后发送ack确认如果客户端处理失败可以发送rollbackcanal会从上次ack的位置重新推送这批数据。这个机制保证了数据不丢但也带来一个问题如果没有正确实现ack逻辑客户端进程一重启可能会重复消费已经写入ES的数据。比如一批数据里有一部分写入ES成功另一部分失败如果整体回滚之前成功写入的这部分又会重新消费一遍如果不做幂等处理ES里就会产生重复或脏数据。在实际代码里我简化的adapter客户端伪逻辑是这样while (running) { Message message connector.getWithoutAck(batchSize); long batchId message.getId(); ListCanalEntry.Entry entries message.getEntries(); if (batchId -1 || entries.isEmpty()) { // 没有数据稍等片刻 Thread.sleep(100); continue; } try { // 解析并写入ES这一步必须保证整体成功 processEntries(entries); connector.ack(batchId); // 确认消费完成 } catch (Exception e) { logger.error(process batch error, rollback, e); connector.rollback(batchId); // 回滚让canal重新推送 } }这个逻辑看起来简单但唯一需要谨慎的是processEntries这一层要保证整体成功。如果批次内部分事件写ES成功、部分失败一旦回滚重推成功的那部分也会再写一次这时候ES端的写入必须做幂等。好在写入ES时可以用业务主键作为_id同一主键重复写入在ES里本质是覆盖更新所以重复消费的副作用基本可控。3.3 表结构到ES索引映射从MySQL表到index mapping同步最核心的工作其实是表结构到ES索引映射的设计。MySQL的表是一对一的固定字段结构ES的索引则可以支持嵌套、数组、keyword、text等多种类型映射设计直接影响搜索效果和写入稳定性。以一张商品表为例CREATE TABLE goods ( id bigint(20) NOT NULL AUTO_INCREMENT, goods_name varchar(128) DEFAULT NULL, category_id bigint(20) DEFAULT NULL, price decimal(10,2) DEFAULT NULL, status int(11) DEFAULT NULL, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB;对应ES的索引mapping我一般遵循以下几条经验主键字段直接映射为ES文档的_id保证写入幂等需要精确匹配的字段状态值、分类ID用keyword类型需要分词搜索的字段商品名用text类型并指定中文分词器金额字段最好用scaled_float或double避免使用text导致无法范围查询时间字段统一映射为date类型并严格按照yyyy-MM-dd HH:mm:ss或strict_date_optional_time格式写入否则ES解析时间字段会报错。这部分映射关系在canal adapter里可以通过配置完成也可以在adapter的client代码里自己控制。如果用的是canal adapter的es模式配置文件里会有一份索引映射的规则我在下一节详细讲。4. 适配器接入canal adapter同步到ES的配置实操4.1 配置adapter的数据源与ES连接canal官方提供adapter组件它内置了多种目标端的写入能力其中就包括Elasticsearch。adapter启动后会订阅canal server的实例通过配置的映射规则把数据自动写入ES省去自己写客户端的功夫。先解压canal.adapter修改application.yml里关于数据源和ES连接的配置canal: conf: # 对应deployer里配置的实例名 canalAdapters: - instance: example groups: - groupId: g1 outerAdapters: - name: logger - name: es7 hosts: http://127.0.0.1:9200 properties: cluster.name: my-elasticsearch # 连接ES时用的账号密码如果ES开启了安全认证 security.auth: elastic:elastic_passwd这里s7是ES 7.x版本的适配器key如果你用的是ES 8.x需要确认对应适配器的兼容性。早期我用ES 7.4搭配adapter整体比较稳定切换到ES 8.x后适配器版本不匹配REST API的兼容层有些字段变了花了不少时间排查。建议先确认adapter版本对ES大版本的兼容支持再决定。4.2 映射规则文件的核心配置adapter在conf目录下会有一个映射目录每个实例对应一个配置文件比如order.yml或goods.yml。以同步goods表到goods_index索引为例dataSourceKey: defaultDS destination: example groupId: g1 esMapping: _index: goods_index _type: _doc _id: id sql: SELECT id, goods_name, category_id, price, status, create_time, update_time FROM goods commitBatch: 3000 etlCondition: where update_time {0} mapping: id: id goods_name: goodsName category_id: categoryId price: price status: status create_time: createTime update_time: updateTime逐项拆解_index目标ES索引名_id指定文档ID对应的MySQL字段名这里用主键id能保证同一个主键的变更在ES里覆盖更新而不是新增sqladapter执行ETL和全量初始化时用的查询SQL字段顺序会作为后续映射的上下文字段mappingMySQL字段到ES字段名的映射关系。注意canal adapter的es同步模式里mapping的左侧是SQL查询结果里的字段名右侧是ES索引里的字段名如果两边一致也要写出来commitBatch批量提交的条数根据数据量调整太高会导致ES写入压力大太低吞吐又起不来。这里有一个很关键的点adapter的es同步逻辑其实是先通过SQL查询出当前主键对应的整行数据再整体覆盖写入ES。这就意味着不管binlog事件是插入、更新还是删除adapter的处理方式是如果是删除事件根据_id执行delete请求如果是插入或更新事件则执行SQL查询该主键的最新整行数据再整体写入ES。所以配置里的SQL字段必须覆盖ES索引里需要的所有字段。4.3 同步细节更新、删除与字段变更的处理插入和更新事件adapter处理方式一样拿binlog事件里的主键ID按照配置的SQL重新查MySQL把最新整行数据取出来写入ES。这个设计的好处是即使binlog的ROW事件里只包含变更字段ES里最终写入的也是完整的一行数据不会因为字段缺失导致索引字段为空。删除事件则走ES的delete接口。但这里有个容易忽略的问题如果删除业务表的记录时MySQL的binlog事件里主键字段必须存在。使用FULL格式的binlog image可以确保删除事件里携带全字段数据但adapter其实只需要主键。之前遇到过一种情况业务方在某个历史版本中直接删除了数据库记录没有通过应用层删除canal推送的删除事件主键是对的但ES里对应文档删除后关联的二级缓存没清干净搜索端还残留旧数据。这块需要业务侧自行处理缓存失效逻辑。另外映射时要特别注意MySQL字段类型与ES类型的对应。比如MySQL的decimal(10,2)在ES里如果用keyword接收写入时会变成字符串后续如果要范围过滤、按金额排序搜索就会出问题。我一般的做法是MySQL的数值类型字段在SQL查询里用CAST转成ES期望的数值类型例如CAST(price AS DECIMAL(10,2))或者让映射字段在ES里定义为scaled_float。简而言之binlog解析出来的是MySQL的原始类型最终写入ES的JSON格式取决于adapter的转换逻辑和映射定义建议在正式接入前用几条真实数据做一次端到端验证。5. 生产环境实战性能调优与高频踩坑现场5.1 写入延迟从秒级到毫秒级的调优canal同步MySQL数据到ES数据从MySQL binlog产生到ES可搜索中间经过canal解析和adapter处理。默认配置下端到端延迟通常在一秒左右这是符合预期的毕竟canal是批量消费机制不会每条事件都单独写一次ES。如果发现延迟超过三秒甚至分钟级优先排查以下几点canal server到MySQL主库的读取速率查看example.log里是否有blocking字样的日志如果出现说明deployer解析binlog的速度跟不上binlog的产生速度简单粗暴的解决办法是调大instance的批量消费字节数canal.instance.memory.batch.size4096 cannal.instance.memory.buffer.size16384同时检查deployer所在机器的磁盘IO和网络binlog解析是连续读盘操作磁盘IO高会直接影响解析速度。adapter批量提交设置commitBatch设置的太小比如100数据量大时会导致ES写入太频繁反而拖慢整体写入性能。我这里的经验值中等数据量单表日增百万行以内设3000到5000比较合适。ES端bulk写入队列adapter写入ES走的是bulk接口如果ES出现写入拒绝或circuit_breaker报错说明ES节点写入压力太大。此时去检查ES集群的堆内存和磁盘阈值及时扩容或调整bulk并发参数。延迟调优最直观的验证方式在MySQL里做一条update在ES端通过文档的_seq_no或者更新时间字段观测到可见时延。如果压测后发现瓶颈不在canal而在ES端不要盲目调大canal的batch先把ES的写入吞吐提上来才是正解。5.2 字段映射不当引发的写入失败这是一类我在接入过程中遇到最多的问题基本集中在类型不匹配上。比如MySQL的datetime字段adapter在生成JSON时会输出类似2024-06-01 12:00:00的字符串ES mapping里如果对应字段是date类型且没有指定formatES会自动尝试解析这种格式一般是能成功的。但如果ES mapping里字段被定义成了keyword写入也会成功但后续你想按时间范围搜索就傻眼了keyword根本没法做ranger查询。更常见的是字段长度超出ES keyword限制。ES 7.x默认keyword类型最大长度是32766字节MySQL里如果有一个varchar(50000)字段映射成keyword后超长文本写入会直接报错。解决办法是这类长文本在ES mapping中定义为text类型开启分词即可。还有一个不显眼但容易踩的坑字段名大小写不一致。MySQL字段默认是不区分大小写的但是ES映射的字段名是区分大小写的。如果SQL查询里写的是IDmapping右侧写的是idadapter在做字段映射时会找不到对应字段导致ES里该文档的部分字段为空或者整条写入失败。这个需要在配置完映射规则后用测试数据立刻验证不能等上线后才发现。5.3 幂等性与顺序性保障前面提到canal的消费模型是批量拉取加ack确认这会导致重复消费。尽管我们将业务主键映射为ES文档_id可以覆盖更新但如果binlog事件本身携带的字段是旧值重复消费时会用旧值覆盖新值。这种问题通常发生在adapter处理慢、消费位点回退、或者网络抖动导致ack丢失的场景。举个例子同一行数据先执行更新A把status设置为2再执行更新B把status设置为3。如果canal推送了A和B两个事件adapter在处理A时执行SQL查询结果查到的是最新的status3随后的B事件也会查到status3两次写入ES都是status3没问题。但如果A事件处理极慢消耗了很长时间等A的SQL终于查到MySQL时B已经又改成4了A先把status4写入紧接着B事件再查询一次写入status4最终结果也对。 因为每次都是拿最新全量行覆盖写入所以顺序问题在这个模式下被弱化了。真正的顺序性隐患出现在删除事件上。如果一个主键在短时间内经历了更新和删除adapter处理删除时MySQL里已经查不到这一行了它会执行ES delete。但如果删除事件处理失败被回滚重推后MySQL里仍然查不到该行ES里的delete会再次执行通常会得到not_found响应不影响最终结果但会有大量重复日志。如果这些事件走的是自定义客户端而不是adapter模式则必须在客户端逻辑中自行保证同一主键事件的顺序消费。最简单的方式是按主键哈希路由到单线程处理避免同一行的多个binlog事件被并发消费。注意canal默认推送事件是按binlog顺序排列的但在多线程消费时如果事件被拆分处理顺序可能被打乱。一定要避免同一条主键的记录被多个线程同时更新到ES否则会出现最终值不一致。最稳妥的方案是单线程消费或者在客户端内对主键做哈希后按分区串行处理。5.4 DDL变更对同步链路的影响线上业务调整表结构时会执行ALTER TABLE这类DDL事件canal也能捕获但adapter的默认处理往往不够完善。我遇到过一个场景业务方给商品表新增了一个字段执行完ALTER TABLE之后adapter日志开始报字段找不到的错误原因是映射配置里的SQL还是旧的字段列表adapter执行SELECT时查询的字段已经不存在了导致所有变更事件都失败。这要分两步解决一是调整adapter映射SQL把新字段补上二是在ES索引的mapping里增加对应字段如果索引定义比较严格的话。因此DDL变更必须建立变更流程不能直接在生产库上执行alter。如果有多个下游依赖需要先在测试环境验证DDL对canal链路的影响再在业务低峰期执行。6. 常见问题与排障速查表下面把我在实际运维中积累的典型故障和排查方法整理成一个速查表每一条都对应真实场景方便读者在出问题时快速定位。现象可能原因排查方法与解决思路canal实例启动失败提示连接MySQL失败账号权限不足或网络不通确认canal账号有SELECT、REPLICATION权限用mysql客户端从canal机器手动连接主库验证日志提示dump address has been dump successfully后没有任何事件实例过滤器正则配置错误或者库里没有实际变更检查filter.regex匹配规则在MySQL里手动执行一条insert触发事件ES里新增数据正常但更新数据没生效binlog事件里没携带变更字段adapter查不到最新数据检查binlog_row_image是否为FULL确认adapter SQL能正常查出更新后的数据ES删除操作不生效删除事件处理失败或者主键字段没映射正确查看adapter日志是否报错在ES里用主键手动测试deleteES写入报错mapper [xxx] of different type映射字段类型与ES index mapping不匹配查看ES mapping定义修正adapter映射SQL或mapping类型延迟持续增大MySQL写入量太大canal解析或ES写入达到瓶颈分别监控canal日志、adapter线程池、ES写入队列定位瓶颈组件重复消费导致ES里偶发旧数据覆盖新数据客户端ack/rollback机制设计不合理检查客户端是否使用了getWithoutAck方式处理失败后正确的回滚逻辑用主键_id覆盖写入adapter执行Etl时全量数据量过大导致ES批量写入超时全量同步线程数过高或者批量过大降低commitBatch控制全量导入的并发线程手动分批执行在做排障时有一个习惯值得养成所有同步问题先看日志再看数据最后看配置。canal的日志其实写得很清楚尤其example.log里会打印每一条解析后的entry类型和表名adapter日志会打印写入ES的实际请求和响应。多数问题通过日志就能定位不需要乱猜。还有一个排查技巧canal自带的管理接口可以从canal.properties里配置的端口访问通过管理接口能看到当前实例的消费位点、延迟、客户端连接数等信息。结合MySQL主库的SHOW MASTER STATUS里的binlog位点可以快速判断canal是不是已经积压了大量未消费的binlog。如果二者位点差距持续拉大说明消费链路某处出了瓶颈直接查看adapter写入ES的耗时和错误日志即可。7. 初始化快照生产实践数据一致性验证与回滚预案全量同步和增量同步衔接时最大的风险是数据不一致。我只讲我自己用过的稳妥流程。在正式对线上库做全量初始化前先在一个临时表或者Shadow环境做一次小规模试验这能帮你提早发现映射配置、SQL语句、ES mapping的问题。试验完成后再在正式环境按下面步骤操作记录启动时间点T0把canal实例的消费位点设置到T0对应的binlog位置让canal暂时不消费只等待配置好adapter的映射规则手动触发adapter的全量ETL功能它会通过配置的SQL把存量数据写入ES全量ETL完成后立即启动canal增量消费因为它的位点在T0T0之后发生的变更都会被补推写一个比对脚本从MySQL按主键随机抽1000条数据和ES对比检查字段全量一致。这里最容易出的问题是第4步和第5步之间可能有窗口期。如果全量ETL本身耗时很长比如几千万行数据导了几个小时那T0到ETL结束之间的新变更如果已经通过canal补推到ES而ETL又把这些数据重复写了一次就会出现短暂的数据回退。稳妥的做法是全量ETL期间暂停canal增量消费等ETL全部结束再启动让增量从T0开始追平。对于数据一致性校验不要只比对主键是否存在还要比对关键业务字段。因为ES中常见的问题不是数据不存在而是部分字段因为映射问题为空或类型错误。校验脚本要覆盖数值、时间、文本几个类型字段数量不需要太多但每个字段类型的代表性必须有。一旦在校验中发现某张表数据不一致回滚预案是直接从MySQL中按主键分批重新拉取该表全部数据用adapter或脚本强制覆盖写入ES覆盖完成后再重新启动增量消费。如果没有完整的覆盖写入能力最坏的情况就是全量重导所以初始化完成后保留一份ETL用的SQL配置很重要后续补数、对账都能复用。8. 从单机到多活canal高可用与规模扩展的一点经验最后顺便说一句生产化的问题。单机canal在数据量小、没有高可用要求时完全够用。但一旦业务规模上来比如Store主库每秒写入几千条记录ES索引几十个单机deployer往往撑不住需要做高可用部署。canal的高可用依托ZooKeeper多个deployer节点组成集群同一个实例比如example只能在其中一个节点运行其他节点作为standby当主节点宕机后自动切换。这是官方支持的模式配置上需要在canal.properties打开canal.zkServer127.0.0.1:2181 canal.instance.global.modezookeeper同时多实例的横向扩展也有两种路径。一种是按业务库拆分成多个canal实例每个实例负责一组表同一台机器可以跑多个instance各自独立消费。另一种是同一个实例的消费端使用多个client通过groupId实现客户端集群消费类似于消息队列的消费组。我用过的组合是第一台机器跑4个deployer实例实例分别对应A库、B库、C库、D库adapter配置多个group或分组消费。这样的好处是某一张表数据量暴涨导致同步延迟时只影响对应实例不会拖垮所有同步链路。高可用方案会带来额外的运维成本尤其是ZooKeeper和集群状态监控需要额外部署。如果你的团队运维能力有限建议先把单机方案跑稳把监控报警补齐再逐步演进。很多时候问题的瓶颈不在canal本身而在数据模型和ES索引设计这些基础的东西不夯实上再多机器也白搭。这套方案跑下来最大的感受是canal同步MySQL数据到ES真正决定成败的往往不是canal本身的搭建而是数据映射和链路容错。配置好binlog、设计好索引mapping、做好全量增量衔接、想清楚重复消费下的幂等这四个点打通了整个同步链路就很顺。我在实际使用中发现adapter的SQL mapping模式虽然灵活但遇到复杂宽表和多表关联时还是得自己写消费段逻辑不要硬套官方adapter。如果你刚开始接触建议先拿一张简单表把端到端链路跑通确认日志里事件解析、ES写入都正常再逐步扩展到核心业务表。同步数据没有一劳永逸的方案保持对监控日志的敏感度才是长期稳定运行的关键。