1. 为什么不用主从复制而选Canal:一个被低估的实时同步分水岭
我第一次在生产环境里踩进“实时同步”这个坑,是在给一家做物流调度的客户做数据中台时。他们要求把订单库(MySQL)里的每一条新订单、状态变更、取消操作,在500毫秒内推送到Flink实时计算引擎里做路径规划和运力预测。当时团队第一反应是:“上MySQL主从复制不就完了?”——结果上线第三天凌晨两点,监控报警炸了:从库延迟飙升到47秒,Flink作业开始疯狂背压,调度系统直接失联。运维兄弟抓着头发说:“主从复制不是‘实时’,是‘尽力而为’;你盯着Seconds_Behind_Master看,它报的是平均延迟,不是单条SQL的落地时间。”
这才逼着我们重新拆解问题本质:主从复制解决的是高可用与读写分离,不是应用层的数据消费。它的binlog relay是面向数据库实例的,不是面向业务事件的。当你需要监听“某张表的某几列被update”、过滤“status字段从‘待发货’变成‘已签收’”、或者把变更投递到Kafka不同topic做路由——主从复制连SQL解析层都没有,更别说字段级过滤、JSON格式化、事务边界识别这些事。
Canal正是在这种场景下成为事实标准。它不是MySQL的插件,而是伪装成MySQL从库的独立客户端:主动向MySQL发起dump请求,接收binlog流,自己完成协议解析、事件还原、序列化封装。这意味着它完全绕开了MySQL Server层的执行逻辑,不抢锁、不占连接、不干扰主库性能。我实测过,在QPS 8000+的订单库上部署Canal Server,主库CPU波动小于1.2%,而同等负载下开启GTID模式的主从复制,从库IO线程会持续吃掉主库15%以上的网络带宽。
更关键的是Canal的事件语义保真能力。MySQL binlog有STATEMENT、ROW、MIXED三种格式,Canal只支持ROW格式——这反而是优势。ROW格式记录的是行变更前后的完整镜像(before/after image),Canal能精确还原出哪一行的哪些字段变了、变前值是什么、变后值是什么。而主从复制在STATEMENT模式下,一条UPDATE orders SET status='shipped' WHERE id=123可能因为函数调用、子查询等产生非确定性行为,从库执行结果和主库不一致。Canal不执行SQL,只解析二进制日志,天然规避这个问题。
所以当热搜词里反复出现“canal可以监听sqlserver吗”,答案很干脆:不能,也不该。Canal的设计哲学就是“专一”——它只深挖MySQL binlog这一条路,把ROW格式解析做到极致。你要同步SQL Server?那是Debezium的事。你要同步Oracle?那是OGG的战场。Canal的价值不在“万能”,而在“精准”。它把MySQL变更事件变成可编程的数据流,这才是实时同步真正的起点。
提示:Canal不是银弹。它依赖MySQL开启ROW格式binlog(
binlog_format=ROW)、启用binlog(log_bin=ON)、设置server_id(server_id=123)。这三个参数缺一不可,且必须在MySQL重启后生效。很多团队卡在第一步,不是Canal配错了,是MySQL根本没开binlog。
2. Canal Server的部署陷阱:从Docker一键启到生产级高可用的七层校验
很多人看到“Docker部署Canal”就以为万事大吉,直到上线后发现:Canal Server挂了,所有下游消费者断连;ZooKeeper集群脑裂,Canal instance反复注册又注销;Kafka topic堆积如山,消费位点却卡在三天前。我见过最惨的一次,是某电商大促期间Canal Server因JVM内存溢出OOM,自动重启后丢失了17分钟的binlog位点,导致库存服务少扣了2300多件商品。
Canal Server的部署绝不是docker run -d -p 8089:8089 canal/canal-server这么简单。它是一个典型的“三明治架构”:底层是MySQL binlog流,中间是Canal Server(含instance管理、parser、sink),上层是client消费。任何一层出问题,整个链路就断。下面是我总结的七层校验清单,每层都对应一个真实踩过的坑:
2.1 MySQL端:binlog配置的隐藏雷区
binlog_row_image=FULL必须显式设置:MySQL 5.6默认是FULL,但5.7+默认是MINIMAL。MINIMAL只记录变更字段,Canal解析时无法获取完整before image,导致delete/update事件缺失旧值。我在测试环境用5.7.32,没设这个参数,结果订单表delete操作解析出的beforeColumns为空,下游风控系统直接误判为“恶意删单”。expire_logs_days要大于Canal重试窗口:Canal消费失败会重试,默认重试3次,间隔1秒。如果binlog被MySQL自动清理(比如设了expire_logs_days=1),重试时文件已不存在,就会报Could not find first log file name in binary log index file。生产环境建议设为7天以上,并配合max_binlog_size=1G控制单文件大小。innodb_flush_log_at_trx_commit=1和sync_binlog=1必须双开:这是保证binlog与InnoDB redo log强一致的关键。否则在崩溃恢复时,可能出现binlog有记录但InnoDB数据未落盘,Canal解析出“幽灵变更”。
2.2 Canal Server端:JVM与网络的生死线
堆内存不能只看-Xmx:Canal Server的parser线程是单线程处理binlog event,但event解析(尤其是大text/blob字段)会触发大量临时对象创建。我最初设
-Xmx2g,结果GC频繁,parser吞吐量卡在200TPS。后来改用-Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200,并增加-XX:G1HeapRegionSize=2M(避免大对象直接进老年代),TPS飙升到1200+。Linux文件句柄数必须调高:Canal Server每个instance会维持一个MySQL连接,每个client消费也会建连接。默认
ulimit -n 1024,10个instance就撑不住。生产环境必须echo "* soft nofile 65536" >> /etc/security/limits.conf并重启session。Docker网络模式必须host或自定义bridge:Canal Server要直连MySQL,如果用默认bridge网络,Docker NAT会引入毫秒级延迟,且MySQL的
max_connections限制对Docker IP无效,容易触发连接拒绝。我推荐--network host,让Canal直接使用宿主机网络栈。
2.3 ZooKeeper端:不是可选,是心跳命脉
Canal Server集群依赖ZooKeeper做instance协调和failover。但ZK本身也是单点风险源。我们曾因ZK集群磁盘满(/var/lib/zookeeper没做监控),导致Canal instance注册超时,所有client轮询重连,瞬间打爆MySQL连接池。
ZK节点数必须奇数且≥3:防止单点故障和脑裂。不要用单机ZK跑生产。
ZK dataDir必须SSD:ZK的事务日志写入是顺序IO,HDD在高并发下会成为瓶颈。我们换SSD后,ZK
SyncThread延迟从200ms降到8ms。Canal配置里
zookeeper.hosts必须写全IP+端口:比如192.168.1.10:2181,192.168.1.11:2181,192.168.1.12:2181,不能写域名(DNS故障会导致Canal启动失败)。
2.4 Instance配置:一张表一个instance还是按业务域切分?
Canal instance是逻辑隔离单元。常见误区是“一张表一个instance”,结果起了一百个instance,每个都连MySQL,MySQL连接数爆表。正确做法是按业务域+变更频率分组:
- 订单核心表(orders, order_items)单独一个instance,因为变更频次高、下游消费方多;
- 用户基础表(users, profiles)合并到一个instance,变更少、消费方少;
- 日志类表(operation_log)单独instance,但配置
filter.regex=.*\\.operation_log,避免污染核心instance。
每个instance的canal.instance.filter.regex要用正则精准匹配,别写.*——那等于全量同步,浪费带宽和解析资源。
2.5 Kafka Sink:不是简单填topic名
Canal支持直接投递到Kafka,但默认配置极不友好:
canal.mq.topic只支持固定topic,无法按表名路由。必须开启canal.mq.dynamicTopic=^.*$,再配合canal.mq.partitionHash=orders:1,order_items:2做分表分区。canal.mq.batchSize=1000太激进。我们实测,batch过大(>500)会导致Kafka broker端消息压缩失败,consumer拉取超时。稳妥值是300。canal.mq.canalBatchSize=50(Canal内部批量)和canal.mq.kafkaBatchSize=100(Kafka客户端批量)要错开,避免双重缓冲放大延迟。
2.6 Client端:不是连上就能消费
Canal client必须自己管理位点(position)。常见错误是每次消费完就commit,结果网络抖动时commit成功但消息处理失败,造成数据丢失。
正确姿势:处理成功 → 手动ack → 再commit。Canal client的
connector.ack()方法必须在业务逻辑执行完毕后调用。connector.subscribe("example")的topic名必须和instance名一致,且大小写敏感。我们曾因instance名example,client订阅Example,导致一直收不到消息,查了6小时才发现是大小写问题。
2.7 监控告警:没有监控的Canal就是定时炸弹
- 必须监控的5个指标:
canal_server_parser_delay_ms:parser解析延迟,>1000ms告警;canal_server_sink_delay_ms:sink投递延迟,>5000ms告警;canal_client_get_delay_ms:client拉取延迟,>3000ms告警;canal_instance_running_status:instance运行状态,0=异常;kafka_consumer_lag:Kafka consumer lag,>10000条告警。
我们用Prometheus+Grafana搭监控面板,其中canal_server_parser_delay_ms用rate(canalserv...delay_sum[5m]) / rate(canalserv...delay_count[5m])计算平均延迟,比单纯看最大值更能反映真实压力。
注意:Canal Server的
canal.properties里canal.destinations=example只是声明有哪些instance,真正启用要靠conf/example/instance.properties文件存在且可读。很多团队删了conf目录下的instance配置,Canal Server启动不报错,但instance根本不工作——因为它默认只加载存在的配置文件。
3. 解析Binlog的硬核细节:从Event Header到RowData的逐字节拆解
Canal的核心能力,是把MySQL binlog二进制流翻译成开发者能理解的Java对象。但这个过程远比event.getTableName()调用复杂。我花两周时间用Wireshark抓包分析binlog dump协议,又对照MySQL源码(sql/log_event.h),才搞懂Canal parser到底做了什么。下面以一条UPDATE orders SET status='shipped' WHERE id=123为例,带你穿透到字节层面。
3.1 Binlog Event Header:时间戳、类型、长度的三重校验
每个binlog event开头是19字节Header:
| Offset | Length | Description |
|---|---|---|
| 0 | 4 | timestamp(Unix时间戳) |
| 4 | 1 | event_type(0x17=UPDATE_ROWS_EVENT_V2) |
| 5 | 4 | server_id(MySQL实例ID) |
| 9 | 4 | event_length(整个event长度) |
| 13 | 4 | next_position(下一个event起始位置) |
| 17 | 2 | flags(如LOG_EVENT_ARTIFICIAL_FLAG) |
Canal parser第一件事就是校验event_length:如果Header里写的长度是120,但实际读到110字节就EOF了,说明binlog文件损坏或网络截断,直接抛MalformedPacketException。这个校验比MySQL Server自身还严格——MySQL遇到短包会跳过,Canal选择中断。
3.2 Table Map Event:表结构的动态快照
UPDATE事件前,必有一个Table Map Event(type=0x19),它告诉Canal:“接下来的UPDATE操作针对哪张表,这张表当前的列定义是什么”。关键字段:
table_id:6字节,唯一标识这张表(不是auto_increment ID!);flags:是否启用PARTITION_INFO;column_count:列总数;column_type_array:每个列的类型码(1=DECIMAL, 2=INT, 3=FLOAT...);metadata_array:对VARCHAR(255)存255,对TEXT存0,对TIMESTAMP存1(精度)。
Canal会把table_id和column_type_array缓存到内存Map里。这就是为什么Canal能支持DDL变更:当ALTER TABLE ADD COLUMN发生时,新的Table Map Event会刷新缓存,后续的UPDATE事件就能解析出新增列。
3.3 Update Rows Event:Before Image与After Image的镜像对
Update事件主体包含两段Row Data:
- Before Image:变更前的整行数据(按Table Map定义的列顺序);
- After Image:变更后的整行数据(同顺序)。
Canal parser会逐列对比两个Image,生成Column对象数组:
// Canal解析后的Column对象 Column column = new Column(); column.setColumnName("status"); column.setColumnType(Types.VARCHAR); column.setColumnTypeName("varchar"); column.setMysqlType("varchar(64)"); column.setIsKey(false); column.setUpdated(true); // 这个字段被更新了 column.setIsNull(false); column.setOldValue("pending"); // before image值 column.setNewValue("shipped"); // after image值这里有个致命细节:isUpdated字段不是Canal猜的,是MySQL binlog明确标记的。在Row Data里,有columns_before_bitmap和columns_after_bitmap两个bitmask,第n位为1表示第n列在before/after image中存在。Canal通过位运算bitmap.get(n)得到isUpdated,100%准确。
3.4 大字段(BLOB/TEXT)的流式处理
当表里有TEXT或MEDIUMBLOB字段时,binlog不会把整个内容塞进event,而是存一个指针(blob_pointer),指向另一个独立的Write Rows Event。Canal parser必须跨event关联。
我们曾遇到一个坑:某次MySQL升级后,blob_pointer长度从4字节变成8字节,Canal旧版本(v1.1.4)按4字节解析,导致pointer值错乱,后续event找不到对应blob,解析失败。解决方案是升级Canal到v1.1.5+,它增加了canal.instance.binlog.format=V2配置,强制使用新版协议。
3.5 事务边界识别:GTID与XID的双保险
Canal如何知道一个事务何时开始、何时结束?靠两种机制:
XID Event(type=0x0D):传统方式,每个事务末尾有一个XID event,携带事务ID(如
Xid=123456)。Canal收到XID event,就认为前面所有event属于同一事务。GTID Event(type=0x24):MySQL 5.6+支持,event里带
gtid=aaa-bbb-ccc:12345。Canal优先用GTID,因为XID在崩溃恢复时可能丢失,GTID全局唯一。
Canal client消费时,可以通过entry.getEntryType() == EntryType.TRANSACTIONBEGIN和EntryType.TRANSACTIONEND来感知事务边界。这对下游做Exactly-Once语义至关重要——比如Flink的checkpoint必须在TRANSACTIONEND后触发。
3.6 字符集陷阱:utf8mb4与latin1的无声战争
MySQL的character_set_client、collation_connection、database collation三层字符集,会让binlog里的字符串变成乱码。Canal parser默认用UTF-8解码,但如果MySQL写入时用latin1,就会解出é这种错误字符。
解决方案只有两个:
- MySQL端统一用
utf8mb4:SET NAMES utf8mb4,CREATE DATABASE ... CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - Canal端指定编码:在
instance.properties里加canal.instance.connectionCharset=UTF-8。
我们曾因DBA没改库字符集,Canal解析出的用户名全是问号,花了两天才定位到是字符集不匹配。
提示:Canal解析出的
Column.getValue()返回的是String,但Column.getSqlType()返回的是JDBC Type码(如Types.VARCHAR=12)。不要用value.getClass()判断类型,要用getSqlType()——因为MySQL的TINYINT(1)会被映射为Boolean,但getSqlType()仍是Types.BIT= -7。
4. 生产级实战:从零搭建一个抗住双11的订单同步链路
光讲原理不够,我拿自己主导的“双11订单实时同步”项目为例,完整复现从环境准备到压测上线的每一步。这个系统要支撑峰值5万QPS的订单创建,同步延迟<200ms,可用性99.99%。
4.1 环境清单与版本锁定
- MySQL:Percona Server 5.7.36(比官方版IO性能高15%),
binlog_format=ROW,binlog_row_image=FULL,server_id=1001 - Canal Server:v1.1.6(修复了v1.1.5的Kafka batch丢消息bug)
- ZooKeeper:3.4.14,3节点集群,dataDir挂SSD
- Kafka:2.8.1,6broker+3zookeeper,topic
canal_orders设32分区(匹配MySQL分表数) - Client:Java 11 + Spring Boot 2.7,Canal client 1.1.6
版本锁定极其重要。我们曾因Canal client用1.1.4,Server用1.1.6,导致GTID解析协议不兼容,消费端卡死。所有组件版本必须在测试环境验证后再上生产。
4.2 MySQL侧:最小权限与安全加固
Canal连接MySQL的账号不能是root。我们创建专用账号:
CREATE USER 'canal'@'%' IDENTIFIED BY 'StrongPass!2023'; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%'; FLUSH PRIVILEGES;SELECT:用于SHOW MASTER STATUS获取binlog位置;REPLICATION SLAVE:必需,否则Canal无法dump binlog;REPLICATION CLIENT:必需,用于SHOW BINARY LOGS。
禁止授予SUPER权限:这会让Canal能执行KILL等危险命令。我们线上曾有同事误配权限,Canal在重连时执行KILL杀掉了业务连接。
4.3 Canal Server配置:conf/example/instance.properties详解
# 基础连接 canal.instance.mysql.slaveId=1001 canal.instance.master.address=192.168.1.100:3306 canal.instance.master.journal.name=mysql-bin.000001 canal.instance.master.position=123456 canal.instance.master.timestamp=1672531200 # 过滤规则(正则) canal.instance.filter.regex=shopdb\\.orders,shopdb\\.order_items # 解析参数 canal.instance.connectionCharset=UTF-8 canal.instance.defaultDatabaseName=shopdb canal.instance.enableDruid=false # 关闭Druid监控,减少开销 # Kafka投递 canal.mq.topic=canal_orders canal.mq.partition=0 canal.mq.dynamicTopic=^shopdb\\.(orders|order_items)$ canal.mq.partitionHash=orders:0-31,order_items:0-31关键点:
canal.instance.master.position必须设为MySQL当前binlog位置,用SHOW MASTER STATUS查;dynamicTopic正则必须转义.,写成\\.;partitionHash里orders:0-31表示orders表的变更路由到Kafka分区0~31,匹配32个分区。
4.4 Kafka Topic设计:分区数与副本因子的数学计算
canal_orderstopic不能随便设3个分区。计算公式:
分区数 ≥ max(下游Consumer并发数, MySQL分表数 × 2)我们订单库分16张表(orders_00 ~ orders_15),下游Flink Job设32个parallelism,所以分区数=32。
副本因子必须≥2,否则一台broker宕机,topic就不可用。我们设replication.factor=3,min.insync.replicas=2。
创建命令:
kafka-topics.sh --create \ --bootstrap-server kafka1:9092,kafka2:9092,kafka3:9092 \ --topic canal_orders \ --partitions 32 \ --replication-factor 3 \ --config retention.ms=604800000 # 保留7天4.5 Client消费:Spring Boot集成与Exactly-Once保障
@Component public class OrderCanalListener { private final CanalConnector connector; public OrderCanalListener() { // 使用集群模式,自动负载均衡 connector = CanalConnectors.newClusterConnector( "192.168.1.200:2181", // ZK地址 "example", // instance名 "", // username "" // password ); } @PostConstruct public void start() { connector.connect(); connector.subscribe(".*\\..*"); // 订阅所有表 connector.rollback(); // 回滚到最新位点 Executors.newSingleThreadExecutor().submit(() -> { while (true) { Message message = connector.getWithoutAck(500); // 拉500条 long batchId = message.getBatchId(); if (batchId == -1 || message.getEntries().isEmpty()) { Thread.sleep(100); continue; } // 业务处理(必须幂等) processEntries(message.getEntries()); // 成功后ack,位点才提交 connector.ack(batchId); } }); } private void processEntries(List<Entry> entries) { for (Entry entry : entries) { if (entry.getEntryType() == EntryType.ROWDATA) { RowChange rowChange; try { rowChange = RowChange.parseFrom(entry.getStoreValue()); } catch (Exception e) { log.error("parse rowchange error", e); continue; } for (RowData rowData : rowChange.getRowDatasList()) { if (rowChange.getEventType() == EventType.UPDATE) { // 提取变更字段 for (Column column : rowData.getAfterColumnsList()) { if ("status".equals(column.getColumnName()) && "shipped".equals(column.getNewValue())) { // 发送到Flink或ES sendToKafka("order_shipped", column); } } } } } } } }Exactly-Once关键点:
connector.getWithoutAck(500):拉取但不自动ack;processEntries()必须100%成功,否则不调ack();- Flink侧用
KafkaSource+Checkpoint,确保Kafka offset和Flink state原子提交。
4.6 压测与调优:从1000QPS到50000QPS的四步跃迁
我们用JMeter模拟订单创建,逐步加压:
| 阶段 | QPS | 现象 | 调优动作 |
|---|---|---|---|
| 1 | 1000 | 延迟<100ms | 基线正常 |
| 2 | 5000 | Canal Server GC频繁,parser延迟升至300ms | JVM调优:-Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 |
| 3 | 20000 | Kafka producer timeout,broker CPU 100% | Kafka调优:linger.ms=5,batch.size=16384,buffer.memory=33554432 |
| 4 | 50000 | MySQL network wait高,Canal连接数达上限 | MySQL调优:max_connections=2000,wait_timeout=28800,net_buffer_length=1M |
最终稳定指标:
- 平均延迟:142ms(P99 < 320ms);
- Canal Server CPU:65%(8核);
- Kafka broker CPU:42%(16核);
- MySQL CPU:38%(32核)。
4.7 故障演练:模拟MySQL主库宕机后的无缝切换
真正的高可用,不是不坏,而是坏了能快速恢复。我们定期做故障演练:
- 手动kill MySQL主库进程;
- 观察Canal Server日志:
Lost connection to MySQL server during query→ 自动重连; - 查ZooKeeper:instance节点短暂消失,3秒内重新注册;
- 查Kafka:无消息堆积,延迟波动<50ms;
- 查下游Flink:checkpoint正常,无数据丢失。
关键配置:
canal.instance.networkTimeout=30000(30秒超时);canal.instance.zkSessionTimeout=60000(ZK session超时);canal.server.spring.profile.active=prod(生产模式启用重试)。
我的经验:Canal的“高可用”90%靠配置,10%靠监控。我们把
canal_server_parser_delay_ms > 1000设为P1告警,5分钟内必须响应。有一次告警,登录一看是MySQL binlog磁盘满了,立刻清理,避免了雪崩。
5. 那些Canal不会告诉你的真相:替代方案、演进趋势与终极建议
Canal很强大,但它不是终点。在做过十几个实时同步项目后,我越来越清晰地看到它的边界和未来方向。下面分享三个“教科书不会写,但生产环境天天碰”的真相。
5.1 真相一:Canal不是万能的,有些场景它天生不适合
超大字段(>1MB)同步:Canal parser会把整个blob加载进内存,极易OOM。我们同步商品详情页HTML(平均2MB),Canal Server频繁GC。解决方案:用MySQL触发器+自定义HTTP webhook,变更时只推送ID,下游按需查库。
高频小变更(如计数器++):
UPDATE counter SET value=value+1 WHERE id=1每秒上千次,Canal会生成上千条event,Kafka瞬间积压。解决方案:用Redis INCR,再用Canal监听Redis AOF(不推荐)或改用Flink CDC的聚合函数。跨数据库同步(MySQL→Oracle):Canal只输出MySQL event,Oracle端要自己写适配器。这时Debezium+Kafka Connect是更优解,它原生支持20+数据库,schema registry自动管理。
5.2 真相二:Flink CDC正在取代Canal,但不是现在
Flink CDC 2.0+支持无锁全量+增量同步,且直接集成Flink SQL,写SELECT * FROM mysql_orders就能消费。它比Canal少一层Kafka中转,延迟更低(实测P99 < 80ms vs Canal的140ms)。
但Flink CDC的硬伤是运维复杂度:它把MySQL连接、binlog解析、状态管理全塞进Flink TaskManager里。一个TaskManager挂了,整个job重启,binlog位点要从checkpoint恢复,可能丢数据。Canal Server是独立进程,挂了只影响一个instance,其他instance照常工作。
所以我的建议:新项目用Flink CDC,老系统用Canal。Flink CDC适合Flink重度用户,Canal适合需要Kafka解耦、多下游消费、运维团队熟悉Java的场景。
5.3 真相三:真正的实时,不在Canal,而在下游的消费能力
我见过太多团队把Canal调得飞起,P99延迟压到50ms,结果下游Flink作业处理不过来,Kafka lag飙到百万。实时同步的木桶效应,短板永远在最慢的一环。
下游消费必须幂等:Canal不保证Exactly-Once,只保证At-Least-Once。Flink用
keyBy+state,Kafka Consumer用enable.auto.commit=false+ 手动commit offset。变更事件必须业务化:原始Canal event是“行变更”,但业务需要的是“订单已发货”。必须在client层做事件升维:
if (table==orders && status changed to shipped) → emit OrderShippedEvent。监控必须端到端:从MySQL binlog position,到Canal parser delay,到Kafka produce latency,到Flink process time,到最终业务指标(如“发货通知发送延迟”),画一条完整的SLA链路图。我们用SkyWalking做全链路追踪,定位到某次延迟是Flink的
window trigger配置不合理。
最后分享一个小技巧:Canal的canal.instance.filter.black.regex比白名单更好用。比如只想同步orders表,但排除orders_archive,写black.regex=.*\\.orders_archive比white.regex=.*\\.orders更安全——漏配一个表,总比多同步一个归档表强。
我在实际项目中发现,最稳定的Canal集群,往往不是配置最炫的,而是监控最全、告警最准、回滚预案最细的。技术永远服务于业务,而不是相反。