1. 先聊清楚:这套方案到底解决什么问题
1.1 为什么要把MySQL的数据搬到ES
先说个我实际遇到的场景。某个后台系统的订单表,业务跑了两三年以后单表过了千万级,用户在前台按订单号、客户姓名、手机号、商品名称搜历史订单,赶上一个奇怪的关键词,一条深分页的模糊查询能直接把数据库CPU打到80%以上。加索引?like查询一旦带了前导通配符,常规B+ Tree索引直接失效,只能硬扫。后来我们决定上ElasticSearch,用倒排索引来扛这种任意字段组合检索的场景。
这个诉求本身不新鲜:MySQL擅长事务性写入,ES擅长全文检索和复杂过滤聚合。问题是数据不能靠人肉搬运,也不能让业务系统在每次增删改的时候顺手往ES里也写一份。业务方代码里双写,等于把同步逻辑和一个高可用中间件绑定到业务事务里面,一方失败,两边就出现脏数据,而且排查成本极高。
所以,基于binlog做增量数据同步就成了最稳妥的路子。把MySQL的变更日志实时采集出来,翻译成下游能听懂的消息,再交给ES消费写入。这条路子不需要业务代码做任何改造,对线上系统零侵入,这就是Canal这类CDC(Change Data Capture,变更数据捕获)组件存在的意义。
1.2 中间为什么非要加一个Kafka
很多人在初期会有一个疑问:Canal不是可以直接把数据推到ES吗?为什么中间还要再堆一个Kafka,平白多了一套要运维的东西。
直接推当然能做,Canal本身确实有输出到ES或者直接HTTP回调的能力,但我在实际落地时几乎一定会选择中间加消息队列。原因有几条:
第一是削峰填谷。电商大促或者业务高峰的时候,数据库的变更量会突然暴涨,而ES集群的写入能力是有上限的。Canal直接推ES,ES扛不住的时候Canal也被拖死,甚至会倒逼binlog拉取积压。中间加一个Kafka,相当于给整条链路装了一个巨大的缓冲池,ES慢一点没关系,消息先落在Kafka里面,消费端慢慢追。
第二是一对多的分发。一份binlog变更往往不只是给ES用。同步到数仓做统计分析、刷新缓存、构建下游宽表,这些都是常见的需求。只要Canal把消息投递到Kafka,任何一个下游都可以通过独立的消费组去消费同一份数据,互不干扰。
第三是可回溯和容错。Kafka消息有保留时间,消费端哪怕崩了一整天,只要offset没丢,起来之后从上次位置继续消费就能把数据补齐。这个能力在跨团队协作场景下非常实用。
用个生活化的类比:Canal像一个专职盯梢的观察员,只负责把看到的“变化”记录下来;Kafka就像一个蓄水池,既承接上游的水量,也按需放水给下游多个工厂。没有这个池子,上游稍微抖一下,下游就要跟着抖。
1.3 整条链路长什么样
我落地这套方案之后,经常见人问架构长什么样。用文字描述就是:
MySQL开启binlog且格式为ROW,Canal伪装成一个MySQL从库去订阅binlog,把解析后的增量变更事件投递到Kafka的指定Topic,自研的消费端服务从Kafka拉取消息,经过解析、字段转换、幂等处理后,通过ES的Bulk接口批量写入索引。
把链路各环节拆开看,职责非常清晰:
| 环节 | 组件 | 核心职责 |
|---|---|---|
| 数据源 | MySQL | 业务数据的真实来源,开启binlog作为变更依据 |
| 变更采集 | Canal | 伪装成MySQL从库,订阅binlog并解析成结构化消息 |
| 缓冲传输 | Kafka | 削峰、隔离、消息保留、一对多分发 |
| 消息消费 | 自研同步服务 | 拉取消息,解析事件类型,构造ES文档,批量写入 |
| 目标存储 | ElasticSearch | 提供倒排索引与聚合检索能力 |
这篇文章接下来的内容,就是从环境准备、Canal配置、消息格式解析、消费端代码,到生产环境常见故障排查的一条龙落地记录。想看具体代码的可以直接跳到第4节,但我强烈建议先把前面的配置讲透,因为后面90%的坑都在配置和消息格式上。
2. 动手前的环境准备:这三样东西一个都别漏
2.1 MySQL端:不把binlog开对,后面全是白搭
很多人装好Canal之后,启动日志一切正常,但数据库改了数据就是收不到消息。第一个排查点永远是MySQL的binlog配置。Canal工作的前提是MySQL开启了binlog,并且格式必须是ROW。
为什么必须是ROW?这要从binlog的三种格式说起。STATEMENT格式记录的是原始SQL语句,比如UPDATE orders SET status=2 WHERE id=1001。对Canal来说,它只能拿到这句SQL,没法知道这条记录更新之前是什么值、更新之后受影响的具体行有哪些,更没办法直接推算出一个完整的JSON消息。而ROW格式记录的是每一行变更的前后镜像,Canal才能把变化拆成一个个结构化事件。MIXED格式呢,某些场景下会退化成STATEMENT,同样不可靠。
另外还有一个容易被忽略的参数binlog_row_image。这个参数决定了ROW模式下记录的字段范围。生产环境必须设置成FULL,否则默认的MINIMAL模式只记录发生变化的字段和主键,消费端拿不到整行数据,后面做ES全量覆盖写入的时候就会遇到大麻烦。
建议的MySQL配置如下:
[mysqld] server-id = 1 log-bin = mysql-bin binlog_format = ROW binlog_row_image = FULL expire_logs_days = 7 max_binlog_size = 256M其中server-id必须设置,而且不能和Canal伪装的从库ID冲突,否则MySQL主从复制协议会直接报错。改完配置记得重启MySQL实例才能生效。
然后创建一个专用的Canal账号,不要用业务账号去订阅binlog,权限最小化是原则:
CREATE USER 'canal'@'%' IDENTIFIED BY 'canal_pass'; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%'; FLUSH PRIVILEGES;这几个权限的用途:SELECT权限是让Canal读取表结构做字段映射,REPLICATION SLAVE权限是让它以从库身份拉取binlog日志流,REPLICATION CLIENT权限是让它能执行SHOW MASTER STATUS这类命令定位当前位点。没有这些权限,Canal会一直处于连接失败的状态,日志报错也容易误导人。
配置完可以用下面两条SQL自查:
SHOW VARIABLES LIKE 'binlog_format'; SHOW VARIABLES LIKE 'binlog_row_image';2.2 Kafka和Topic的基本设计
Kafka在这一条链路里的定位是缓冲和分发,Topic的规划直接决定了消费端代码的复杂度。
我的建议是按照业务线来建Topic,而不是一个表一个Topic。比如订单库建一个canal-order,商品库建一个canal-product。好处是消费者只需要订阅有限的几个Topic,而且同一个Topic里可以承载多个表的消息,消息体内自带database和table字段,消费端可以内部路由。
另外一点,一个实例的Canal配置里,可以让每个instance(任务)分别对应不同的Topic。比如订单库的instance产生的消息进canal-order,商品库的instance进canal-product。这样互不影响,某个Topic积压了也能独立调优。
Topic的分区数怎么定?我给一个通用的粗算思路:先预估业务峰值时刻每秒产生的binlog变更消息量,比如每秒5000条;再估算单分区的消费能力,按一个消费者每秒能处理2000~5000条来估,当然这个数字取决于消费端逻辑重不重。要留出弹性,那么分区数设为3到5个比较合适。如果只同步一个索引且数据量不大,也可以直接从2到3个分区起步,后续不够再加分区。但要注意,Kafka不推荐一个Topic只有1个分区,那样消费并发能力就被锁死了,Canal投递消息的性能也会受限。
消息保留时间建议设置成3到7天。同步链路偶尔出问题,运维希望的通常是能直接回放消息修复,而不是重新全量导数据,保留时间长一点,处理问题的余地就大很多。
2.3 ES索引的mapping真的要先建好
我见过不少团队,Canal跑起来了,消息也进来了,才发现ES索引字段类型全是text,数字比较、范围查询通通不对,然后又要重建索引。这种事一定要在同步开始之前就避免。
ES的mapping一旦创建,大部分字段类型是无法直接修改的。在Canal消息进入消费端之前,就要先设计好索引结构。比如订单索引,金额用scaled_float或double,状态用keyword,订单时间用date,商品名称用text并配ik_max_word分词器。注意不要把所有字段都设成text,否则你之后做精确过滤、聚合统计、排序都会很痛苦。
分片数的估算给个原则:单个分片的数据量建议控制在20~50GB以内,按最终一年的数据量反推分片数。副本数按读多写少的需求来,一般1个副本就够了。
索引别名也建议加上。线上需要调整mapping或者滚动索引时,可以平滑切换,不用停同步服务。这块准备工作做好,后面才能专心调Canal和消费端。
3. Canal配置要点:从全局到实例
3.1 canal.properties里影响链路的关键项
Canal部署好之后,第一个要改的是conf/canal.properties这个全局配置文件。里面接近上百个配置项,但真正决定Kafka投递行为的就是几个。
# 指定模式为kafka canal.serverMode = kafka # Kafka连接地址,集群就写多个,逗号分隔 canal.mq.servers = kafka1:9092,kafka2:9092,kafka3:9092 # 是否投递扁平JSON消息,必须为true canal.mq.flatMessage = true # 设置投递到Kafka的默认Topic,实例配置可以覆盖 canal.mq.defaultTopic = canal-default # 并行发送线程数 canal.mq.parallelThreadSize = 16canal.mq.flatMessage这个配置尤其重要。它为false时,Canal投递给Kafka的是Protobuf序列化的二进制字节流,消费者要用Canal提供的客户端库反序列化才能读到内容,调试成本很高。为true时,Canal会直接把事件转成可读的JSON数组投出去。我建议所有场景都开flatMessage,除非你的下游就是Canal的官方client在做二次开发。
3.2 instance.properties:把同步目标锁死
全局配置之外,Canal的每一个同步任务对应一个instance目录,默认在conf/example下。真正的同步范围配置都在instance.properties里面。
# MySQL实例地址 canal.instance.master.address = 10.0.0.10:3306 # 上面创建的canal账号 canal.instance.dbUsername = canal canal.instance.dbPassword = canal_pass canal.instance.connectionCharset = UTF-8 # 要订阅的库表,格式是 库名\\.表名 canal.instance.filter.regex = mall\\.orders,mall\\.users # 黑名单,排除不需要的表 canal.instance.filter.black.regex = mall\\.t_temp.*,mall\\.t_log.* # 这个实例投递到的Topic,覆盖全局默认值 canal.mq.topic = canal-order这里正则的写法是新手最容易翻车的地方。mall\\.orders中的两个反斜杠是必须的,因为整个配置值会被Canal用正则表达式去解析,而正则中匹配一个点号.必须转义成\\.。很多人在这个字符串上反复出问题,漏一个反斜杠,就发现Canal一条消息都不报。
正则支持通配符。mall\\..*表示mall库下所有表;.*\\..*表示所有库所有表。黑名单的优先级高于白名单,也就是正则匹配的表会先被过滤掉。比如你白名单里配了mall\\..*,黑名单里写mall\\.t_temp.*,那么mall库下所有以t_temp开头的表都会被排除。
3.3 启动Canal并验证消息流出
启动和验证的流程很简单,但每个细节都值得确认一次。
进入Canal安装目录,执行:
bin/startup.sh然后看日志确认没报错:
tail -f logs/canal/canal.log tail -f logs/example/example.logexample.log中如果出现connected to字样,说明Canal已经成功连接到了MySQL。如果出现权限错误或者连接超时,先回头检查Canal账号权限和MySQL实例地址。
验证链路通不通,最快的方式是开一个Kafka终端消费者,订阅Canal投递的那个Topic:
kafka-console-consumer.sh --bootstrap-server kafka1:9092 --topic canal-order --from-beginning然后去数据库里手动执行一条UPDATE或者INSERT,如果终端能看到JSON消息输出,说明从binlog采集到Kafka投递这一段已经通了。这是我个人习惯的验证顺序:先验MySQL到Kafka,再写消费端,这样出了问题不会被上下游互相干扰的假象误导。
4. 消费端把消息吃进去并同步到ES
4.1 先看懂Canal投过来的JSON
在动手写代码前,一定要先看懂Canal的flatMessage格式。它投递到Kafka的是一条JSON数组,数组里的每个元素代表一个变更事件。这里以一个订单表的UPDATE为例:
[ { "id": 560, "database": "mall", "table": "orders", "isDdl": false, "type": "UPDATE", "data": [ { "id": "1001", "order_no": "ORD20240501001", "status": "2", "pay_time": "2024-05-01 12:30:00", "total_amount": "199.00", "update_time": "2024-05-01 12:30:05" } ], "old": [ { "status": "1", "pay_time": null } ] } ]几个关键字段的含义:
type:事件类型,常见的有INSERT、UPDATE、DELETE,还有CREATE、ALTER这些DDL类型。data:变更后的数据数组。注意数组里每个字段的值在Canal的JSON里全是字符串,因为binlog里面存的就是字符串表达。old:变更前的数据数组,只包含发生变化的列。对于UPDATE事件,data里是完整的更新后镜像,old里是发生变化的字段的旧值。isDdl:是否为DDL语句,同步到ES的一般直接过滤掉。database和table:标明这条消息来自哪个库哪张表,消费端可以根据这个字段做表到索引的路由。
为什么前面反复强调binlog_row_image=FULL?就是因为FULL模式下,data里才是整行的完整快照,消费者拿data直接就能拼出一个完整的ES文档。如果是MINIMAL模式,data里可能只包含变化的列和主键,那消费端要做字段合并才能写ES,复杂度和出错率都会上去。
还有一个细节值得注意:Canal投递的消息是一次性投递一个数组,这个数组可能包含同一个事务里的多个事件。消费端处理时,不要只取message[0],要遍历整个集合。
4.2 三类事件的同步逻辑怎么定
消费端最核心的就是事件类型的分流逻辑,我按三种类型分别处理:
INSERT事件。data[0]就是新增记录的全部字段,直接构造ES文档并指定主键,用IndexRequest写入索引。
UPDATE事件。这是最需要抠细节的类型。绝大多数情况下主键没有变化,那就直接用data[0]整行覆盖写入ES文档。之所以用整行覆盖而不是用UpdateRequest做部分字段更新,是因为data里本身就是FULL镜像,包含所有字段,覆盖写入既简单又能保证ES文档和MySQL的记录完全一致。用UpdateRequest做部分更新,万一漏字段或者字段名映射错了,ES里就会残留旧值,而且这类问题很难一眼看出来。
但有一种边界情况:UPDATE事件里data和old的主键字段值不一样,说明这条记录的主键被修改了。MySQL主键变更虽然在业务规范里应该杜绝,但真碰到了,消费端必须做两步操作:先按old里的旧主键删除ES里的旧文档,再按data里的新主键写入新文档。不然ES里会同时存在新旧两条数据。
DELETE事件。MySQL删除一条记录时,Canal投递的data里是删除前的整行快照,消费端直接从data[0]里取主键字段,调用DeleteRequest删除ES文档。
DDL事件,比如ALTER TABLE、CREATE TABLE,直接忽略。如果业务上确实有关联需求,可以加日志留痕,但默认不要影响主流程。
4.3 核心代码:解析、转换、批量写入
下面这段是我实际用过的消费端核心流程,基于Spring Boot加Spring Kafka,ES客户端以ElasticSearch 7.x为例。代码做了简化,但保留了主干结构。
@Component @Slf4j public class CanalEsSyncConsumer { private static final ObjectMapper MAPPER = new ObjectMapper(); @Resource private RestHighLevelClient esClient; @KafkaListener(topics = "canal-order", groupId = "es-sync-order-group") public void onMessage(String message) { try { List<CanalMessage> eventList = MAPPER.readValue(message, new TypeReference<List<CanalMessage>>() {}); BulkRequest bulkRequest = new BulkRequest(); for (CanalMessage event : eventList) { if (event.getIsDdl()) { continue; } if (event.getData() == null || event.getData().isEmpty()) { continue; } buildBulkRequest(bulkRequest, event); } if (!bulkRequest.requests().isEmpty()) { BulkResponse response = esClient.bulk(bulkRequest, RequestOptions.DEFAULT); if (response.hasFailures()) { log.error("ES bulk写入存在失败项: {}", response.buildFailureMessage()); // 这里根据业务需要决定抛异常回滚消费位点,还是走补偿表 } } } catch (Exception e) { log.error("处理Canal消息失败", e); // 抛异常触发Kafka重试 throw new RuntimeException(e); } } private void buildBulkRequest(BulkRequest bulkRequest, CanalMessage event) { Map<String, Object> data = event.getData().get(0); String primaryKey = String.valueOf(data.get("id")); String indexName = routeIndex(event.getTable()); switch (event.getType()) { case "INSERT": case "UPDATE": if (isPrimaryKeyChanged(event)) { // 主键变更:先删旧文档,再写新文档 Map<String, Object> old = event.getOld().get(0); String oldId = String.valueOf(old.get("id")); bulkRequest.add(new DeleteRequest(indexName).id(oldId)); } IndexRequest indexRequest = new IndexRequest(indexName) .id(primaryKey) .source(data); bulkRequest.add(indexRequest); break; case "DELETE": bulkRequest.add(new DeleteRequest(indexName).id(primaryKey)); break; default: break; } } private String routeIndex(String table) { if ("orders".equals(table)) { return "orders_index"; } return "default_index"; } private boolean isPrimaryKeyChanged(CanalMessage event) { if (event.getOld() == null || event.getOld().isEmpty()) { return false; } Map<String, Object> old = event.getOld().get(0); Map<String, Object> data = event.getData().get(0); Object oldId = old.get("id"); Object newId = data.get("id"); return oldId != null && newId != null && !oldId.toString().equals(newId.toString()); } }这段代码里有几个点要重点说明。
Bulk批量提交是ES写入的核心手段,比一条一条写吞吐高出一个数量级。批量大小建议控制在500到1000条之间,或者按总字节数控制在5MB左右。开太小了,网络往返开销占比高;开太大了,单次bulk耗时会变长,一旦出差错整个批次的重试成本很高。
消费端抛异常触发Kafka重试时,默认的Spring Kafka机制会导致同一个批次反复消费。所以消费逻辑必须幂等。ES的index写入天然幂等,重复执行结果一致,这块可以放心。
CanalMessage这个对象我直接用一个POJO承载,字段包括type、database、table、isDdl、data、old,data和old用List<Map<String, Object>>表示。这种写法比硬编码字段名灵活,后续表结构加字段,消费端代码都不用改。
4.4 顺序性、幂等性与并发注意
这里要单独把顺序性拎出来讲,因为它直接决定数据对不对。
Kafka只能保证单个分区内的消息是有序的。如果同一行数据的主键操作分散到了不同分区,消费端并行处理时,完全可能出现旧消息后到、覆盖新数据的情况。举个例子:一个订单先是UPDATE把status改成2,紧接着又UPDATE改成3,如果这两条消息被投到不同分区,消费端并发执行,结果可能是status为2的旧消息最后写入,ES里永远停在2。
Canal提供了一个配置来按主键hash投递分区,非常关键:
# 表示mall库orders表按id字段hash,决定投递到哪个分区 canal.mq.partitionHash = mall.orders:id这样同一个订单id的所有变更消息都会进入同一个分区,消费端只要保证每个分区单线程消费,顺序就保住了。如果还需要再拆分线程提高吞吐,可以在消费端内部按主键再做一次hash分桶,把同一主键的消息分到同一个本地内存队列里,由对应的单线程处理。本质上就是做一个两级分区,一级由Kafka保证,一级由消费端内部自建。
幂等性其实要分两层看。ES层面的index写入天然幂等,重复执行没问题;但UpdateRequest配合doc做部分更新就不是严格幂等的,必须谨慎。我的个人习惯是:能走index全量覆盖就不走update,流程简单得多。
还有消费者实例数。多个消费者实例属于同一个groupId订阅同一个Topic时,Kafka会把分区分摊给这些实例。消费者实例数不要超过分区数,否则多出来的实例一直是空闲状态,还会引起无意义的rebalance。这个点排查消息积压时经常遇到。
5. 上线后我踩过的坑:排查与调优实录
5.1 数据库改了数据,Kafka却一直没消息
这个问题排在所有问题的第一位,遇到概率最高。从下往上排查,我一般按这个顺序过一遍:
先查MySQL的binlog配置。SHOW VARIABLES LIKE 'binlog_format',很多库默认是MIXED或STATEMENT,Canal采集不到可靠的行级数据,表现为日志显示连接正常但没有任何消息。这个检查是零成本的,建议第一时间做。
再查instance里的filter.regex正则。最常见的翻车点是漏了反斜杠。mall\\.orders和mall.orders就差一个反斜杠,前者正常匹配,后者正则里的点号会匹配任意字符,看起来像配置了对,实际上匹配到的表和预期完全不同,甚至直接什么都没匹配上。
然后查Canal账号权限。如果没有REPLICATION SLAVE权限,Canal拉取binlog时会报Access denied,但有些版本的Canal日志里只显示一条连接异常,很容易被忽略。
最后查黑名单。如果某个表被filter.black.regex命中了,Canal会静默过滤掉,日志不报任何错误。
5.2 UPDATE之后ES里还是旧数据
这个问题的优先级也很高。数据改了,消息也收到了,但ES里查出来还是旧值。我的排查路径有这几条:
消费端程序是不是把UPDATE事件漏掉了。检查代码里switch的case分支,很多人的case只写了INSERT和DELETE,忘了UPDATE。Canal投递的type字段值是大写的,匹配时大小写敏感,也是一个隐藏雷点。
消费组是不是有多个。如果启动了多个消费端服务实例,但groupId配置不一致,就会出现两个服务同时消费同一个Topic的情况,其中一份逻辑是有问题的旧版代码,两边互相覆盖,数据看起来就忽新忽旧。
还有一类罕见但真实存在的问题:MySQL里记录更新了,但update_time在业务代码里没有更新,看起来像没同步。这种其实不是同步链路的问题,是源端数据本身就没变。
如果确认消息和代码都没问题,再查ES的refresh机制。ES默认refresh间隔是1秒,写入后立即检索可能查不到最新数据。这个不算故障,但第一次接触ES的人经常会误判。
5.3 主键变更和无主键表的边界情况
ES文档必须有唯一的_id,这个_id我们通常直接取MySQL主键。一旦MySQL表没有主键,Canal解析的时候会直接报错,因为binlog事件里没有主键可以追踪行变化。这不是消费端能绕过去的,规范上就是要求业务表必须有主键。
主键变更的问题在5.3节的代码里已经给出了处理方式。但我想补充一个真实场景:很多业务不做真实删除,而是软删除加状态标记,然后又重新插入一条相同业务含义的记录,主键却是新的。这种情况下ES里旧文档还在,新文档也写进来了,就会出现重复记录。针对这类业务,消费端可以考虑在软删除事件里也做一次ES删除,或者约定一个逻辑主键来做去重。具体怎么设计取决于业务对数据一致性的要求。
5.4 Kafka消息积压与同步延迟过高
消息积压的表现是Kafka的consumer lag越来越高,ES的新数据迟迟不出现。先不要急着加机器,按这几个维度排查:
消费者实例数是否少于分区数。如果8个分区但只有2个消费者实例在跑,那最多只有2个分区被消费,剩下6个分区的消息完全停滞。要么加消费者实例,要么给消费逻辑提速。
单批次消息解析和写入是否超时。有些消费端设计里,对每条消息都做一次ES请求,效率极低。一定要走Bulk批量写入。Bulk批次调大能显著降低平均耗时。
ES集群的写入瓶颈。分片数太少,单分片写入成为瓶颈;refresh_interval如果设置成1秒,写入压力大时会产生大量小Lucene分段合并,拖慢整库的写入性能。可以在索引设置里适当调大refresh间隔,比如30秒,换取更高的写吞吐。
消费端本身的HASH分桶线程设计不合理,也会导致某几个分桶堆积。这要看代码里是否有热点键的问题,比如某个用户的订单量特别大,hash到同一个桶,其他桶空闲,这个桶一直堆积。
5.5 延迟监控和日常运维习惯
同步链路没有监控等于裸奔。我采用的方案是在ES文档里额外写入一个sync_time字段,值是消费端的系统时间。业务表里本来就有update_time(如果源表没有,这条链路建议推着业务方补上),对比ES文档的sync_time和MySQL的update_time,两者差值就是这条链路的真实同步延迟。
Kafka侧最直接的监控指标是consumer lag,也就是log_end_offset减去current_offset。这个值可以接入Prometheus和Grafana,配上告警阈值。有一个自定义告警的经验值:如果某个Topic的lag持续超过1万条并且没有下降趋势,就该人工介入了。
日常运维里最实用的习惯是保留Canal的位点信息。Canal的meta信息记录在conf/example/meta.dat里,包含上一次消费的binlog文件名和位点。如果观察Kafka消息没问题但数据对不上,可以把这个位点文件和MySQL的SHOW MASTER STATUS结果对比,能快速判断Canal是从哪个位置开始采集的。
还有一个小技巧:Canal的安装目录升级时,建议先把旧meta.dat文件备份,升级完如果Canal异常,备份的位点文件可以用于快速回退,避免重新从某个时间点开始同步导致大数据量的补数。
最后再补一个很多团队忽略的点。整条链路的配置项,包括MySQL的binlog参数、Canal的filter正则、Topic分区数、消费端groupId,都建议固化到配置管理平台或者至少写进项目的README里。我见过最多次的返工,都是因为某个环境用了旧的配置但没人知道,排查了大半天才发现是配置漂移造成的。这套组件本身很成熟,真正让开发头疼的从来都是环境差异和配置不一致。