订阅更新

MySQL Binlog采集突然延迟5分钟——Canal卡住了怎么排查?

数据采集手册 第 1 题 约 10 分钟读完

本文目录
  1. 先说结论
  2. 业务场景:早高峰GMV看板停摆
  3. 根因一:上游Binlog突然激增
  4. 根因二:下游消费/写入慢导致反压
  5. 根因三:Canal自身GC或单parser瓶颈
  6. Canal vs Maxwell vs Debezium vs Flink CDC
  7. 面试高频追问
  8. 实战踩坑:Canal重启后回到一周前的position
  9. 总结

先说结论

延迟的根因就三类:① 上游MySQL Binlog产量激增(批量update或大事务);② Canal自身解析或下游写入跟不上;③ 网络/GC/磁盘IO卡顿。

排查必走三步:先看Canal admin的delay和position,再去MySQL show master status对比,最后看Canal Server日志的GC和异常堆栈。

生产兜底方案:一定要做HA双节点+Kafka做缓冲,Canal直连下游一旦慢就反压回MySQL会更危险。

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

Canal采集Binlog延迟,是几乎每个做CDC同步的团队都会撞上的问题。之前做用户支付实时大盘的时候,早高峰一上来就延迟报警,5分钟数据卡住不动,下游Hologres看板的成交GMV直接停在原地。这道题面试官想听的不是"重启Canal",而是你有没有一套排查思路,能从源头到下游一层一层定位。

业务场景:早高峰GMV看板停摆

当时的链路是 MySQL(订单库)→ Canal → Kafka → Flink → Hologres,业务高峰QPS大概5000,订单表binlog每秒约2MB。那天上午9点05分,运营群里有人喊:"GMV不动了!"打开Canal admin一看,delay直接飙到280秒并且还在涨。

# 1) Canal admin metrics接口看delay(毫秒)和当前消费位点
curl http://canal-admin:8089/api/v1/canal/canalCluster/instances
 
# 2) MySQL端查最新Binlog位点,跟Canal的position对比
mysql> SHOW MASTER STATUS;
+------------------+-----------+
| File             | Position  |
+------------------+-----------+
| mysql-bin.001234 | 587326441 |  -- 最新位点
+------------------+-----------+
 
# 3) Canal当前消费到哪:差异越大说明落后越多
# meta.dat里记录的是 mysql-bin.001231 / 12387645 → 落后了3个文件
 
# 4) Canal Server GC日志
tail -f /home/admin/canal-server/logs/canal/canal_stdout.log | grep -E 'Full GC|OutOfMemory'

一对位点立刻看出来:Canal落后了将近3个Binlog文件。再去看Canal日志,发现parser线程没有异常但下游sink到Kafka的batch一直在retry,原来Kafka某个broker磁盘报警限速了,把Canal反压住了。

根因一:上游Binlog突然激增

最常见的场景是业务方半夜跑批,一条SQL update一百万行,瞬间产生几个GB的Binlog。Canal单parser线程拉不过来就开始堆积。判断方法很直接:看MySQL的Binlog文件增长速度。

# 看最近1小时Binlog文件大小变化(每个mysql-bin文件默认1G)
ls -lh /var/lib/mysql/mysql-bin.* | tail -20
 
# 平时1小时1-2个文件,激增时可能1小时10+个文件
# 进一步查是哪张表:用mysqlbinlog解析
mysqlbinlog --base64-output=DECODE-ROWS -v mysql-bin.001234 \
  | grep -E '^### (UPDATE|INSERT|DELETE)' | awk '{print $3}' \
  | sort | uniq -c | sort -rn | head
 
# 输出示例:某张表占了90%的binlog量
# 380000 `order_db`.`order_item`
#   2100 `order_db`.`order_main`

应对:跟业务约定大批量update必须分批小事务,避免一条SQL刷百万行;Canal侧在instance.properties把parser的ringbuffer调大(canal.instance.memory.buffer.size从16384调到65536)。

根因二:下游消费/写入慢导致反压

Canal本身只是搬运工,真正瓶颈往往在下游Kafka或者直连的MQ上。我们这次就是Kafka broker磁盘满80%触发限速。

# canal到kafka的关键参数(instance.properties)
canal.mq.flatMessage = true              # 是否JSON扁平化(影响吞吐)
canal.mq.canalBatchSize = 50             # 一次拉多少条binlog
canal.mq.parallelThreadSize = 8          # 并行写kafka的线程数(默认1,要调)
canal.mq.partitionsNum = 12              # topic分区数对应
canal.mq.partitionHash = .*\\..*:$pk$  # 按主键hash保证同主键有序
 
# Kafka producer侧
canal.mq.producer.batch.size = 524288   # 512KB攒批
canal.mq.producer.linger.ms = 100       # 攒100ms再发
canal.mq.producer.compression.type = lz4 # 必开压缩

适用场景:链路是Canal→Kafka→Flink/Spark的标准CDC流转。

不适用:如果Canal直连业务系统(比如直接写ES),那要在业务系统侧调,而不是Canal侧。

根因三:Canal自身GC或单parser瓶颈

Canal Server是Java进程,默认堆内存1G,扛大事务的时候很容易Full GC。后来把堆调到4G、G1GC,问题大幅缓解。还有一个隐藏坑:一个instance对应一个MySQL库,但只有一个parser线程,单库流量超过50MB/s基本就是上限。

# canal启动脚本调JVM
JAVA_OPTS="-server -Xms4g -Xmx4g -Xmn2g -XX:+UseG1GC \
  -XX:MaxGCPauseMillis=200 \
  -XX:+PrintGCDetails -Xloggc:/home/admin/logs/gc.log"
 
# 单库流量超50MB/s的解法:拆instance,按表分组
# instance-1 监听 order_main 表
# instance-2 监听 order_item, order_log 表
# 每个instance独立parser线程,物理隔离

Canal vs Maxwell vs Debezium vs Flink CDC

维度CanalMaxwellDebeziumFlink CDC
开发方阿里ZendeskRed Hat阿里(基于Debezium)
输出格式Protobuf/JSONJSONJSON/AvroFlink内部RowData
HA能力Zookeeper主备弱(需自建)Kafka Connect集群Flink Checkpoint
全量+增量需配合DataX支持bootstrap支持snapshot原生一体化
运维复杂度中等低高(依赖KafkaConnect)低(一个Flink作业)
我的选择传统团队首选小团队快速上手云原生K8s团队已有Flink栈优先

面试高频追问

追问一:Canal的HA是怎么做的?主备切换会丢数据吗?

Canal用Zookeeper做主备选举,instance同一时刻只有一个active节点在拉Binlog,position也写在ZK里。主节点挂了备节点接管,从ZK拿到最后的position继续拉。理论上不丢数据,但有可能重复——因为position commit是异步的,切换时可能回退几条。所以下游一定要做幂等,靠主键去重,不能依赖Canal保证Exactly-Once。

追问二:Canal的position记在哪几个地方?哪个最权威?

有三个地方:① meta.dat(本地文件,Canal重启用);② Zookeeper(HA场景下用,最权威);③ 下游消费者ack回来的位点。生产上以ZK为准,meta.dat只做兜底,避免单机重启丢ZK的更新。

追问三:怎么监控Canal的健康状态?告警怎么配?

我配了三个告警维度:① delay > 60秒(Prometheus拉Canal的JMX metrics);② Canal Server进程存活(agent探活);③ position停滞(连续3分钟position不变就告警,说明卡死了)。第三个最关键,因为有时候Canal进程在但parser线程死锁了,前两个都正常但实际数据没在动。

实战踩坑:Canal重启后回到一周前的position

有一次发布升级Canal,重启完发现下游Kafka里疯狂涌入老数据,Hologres里的订单成交时间都是7天前的。立刻排查发现是同事手抖把instance.properties里的canal.instance.master.journal.name改回了旧值(本来是从ZK读最新的,但配置文件里写死会覆盖)。Canal启动时优先级是:配置文件 > meta.dat > ZK,所以又从一周前重新拉了一遍。

# 错误配置:写死了起始位点
canal.instance.master.journal.name = mysql-bin.001100
canal.instance.master.position = 0
canal.instance.master.timestamp = 
 
# 正确做法:留空让Canal从ZK/meta.dat读
canal.instance.master.journal.name = 
canal.instance.master.position = 
canal.instance.master.timestamp = 
# 这样首次启动从master.timestamp(如果设了)开始,
# 之后都从ZK里恢复,不会回退

解决方案:后来在CI流程里加了个静态检查:禁止把journal.name和position写成具体值上线,否则PR直接reject。这种坑遇到一次就够了。

总结

Canal延迟排查的标准动作:先对位点(Canal vs MySQL master),再看下游sink是否反压(Kafka/MQ),最后看Canal自身GC和parser堆积。生产上一定要做HA、加监控、Kafka做缓冲、下游做幂等。面试时按"现象→对位点→分根因→给方案"这个顺序讲,比直接背"调大buffer"分高得多。

下一篇聊一个选型大题:上百张MySQL表要同步到数仓,DataX、Canal、Flink CDC到底怎么选?

你们生产环境用的是哪种CDC方案?踩过哪些坑?评论区聊聊~

关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长

刷题进度保存在当前浏览器