URL
date
AI summary
slug
status
tags
summary
type
昨天在测试Canal解析本地binlog文件的时候,发现解析不出来,明明记得前段时间测试的时候还是能解析出来的。反正最近也在看Canal的源代码,就通过源码来破案吧。

问题原因

这里先上结论,节省各位看官的时间,枯燥的源代码分析的工作就交给我吧。
原因竟然是因为binlog文件太小了为什么binlog文件小就会有问题呢?
首先我们要知道,Canal处理binlog默认采用的是并行(paraller)模式,是基于生产者-消费者的模式来设计的,使用了Disruptor作为Buffer。生产消费过程如下:生产者(Parser线程)负责读取binlog并publish到Disruptor,消费者负责消费Disruptor里的事件(binlog event),主要是解析binlog,然后输出人类可读的日志。
而binlog文件太小,会导致生产者很快就把binlog全部读到Disruptor中了。此时生产者任务完成,于是就走到了一段设计上用来处理异常流程的代码分支。这段代码会做一些reset操作以便重新消费,而这些reset操作里就错误地把消费者给停掉了。而此时Disruptor 里的event可能只消费了一部分,并且其实消费者总共有三个环节(SimpleParserStageDmlParserStageSinkStoreStage),所以当停掉消费者的时候,一条binlog event都还没消费到最后一个环节。所以你就会发现,控制台始终无法输出我们期望看到的日志。(你可以试着在SimpleParserStageDmlParserStageSinkStoreStage 里加些日志来帮助你观测)
这也解释了为什么前段时间测试的时候是好的,因为之前用的是线上真实的binlog文件,文件比较大。不过大文件就没问题了吗?
其实在读到末尾的时候也会碰到这个问题,也就是可能读不完全。只是有输出,问题看起来就比较隐蔽。

关键代码段

结论说完了,下面贴一些佐证的代码片段
我们先来看看,生产者读完binlog文件之后“误入的”代码段:
notion image
影响我们读取binlog的就是 multiStageCoprocessor.stop()这一行。
notion image
并且,这里全部停止掉之后,由于running变量还是为true,那么会在Sleep一段时间之后,再次去读取binlog文件。但是还是会再一次进入上述的流程,造成死循环。

解决方案

知道问题原因之后,我们对症下药就行了。以下提供N种思路帮助你输出binlog日志
  1. 在”误入的“代码段第一行打上断点,这样可以一直断点到消费者把binlog event都消费完
  1. 可以把误入的“代码段里最后的sleep逻辑前置,先sleep,然后再去重置 + 停消费者
  1. 重载afterDump(ErosaConnection connection) 方法,在里面人为的sleep一段时间
  1. 不使用并行模式消费,非并行模式下,读到的binlog event当前线程会直接解析并添加到transactionBuffer里
    1. final LocalBinlogEventParser controller = new LocalBinlogEventParser(); controller.setParallel(false);

思考

为什么会有这个问题?

你有没有想过,这里的设计为什么会有这个问题
我猜想可能有2个原因:
  1. 是代码复用,直接连接MySQL和读取binlog文件复用了代码,虽然主流程大致相同,但是像一些异常场景可能考虑得不太一样。比如binlog文件就是一次性读取,读完就结束了。而对于消费mysql实时产生的binlog来说,正常情况下是一直不会退出的。
  1. 代码块逻辑太多,处理场景不明确。比如”误入的“代码段,我都没有办法很明确的能得知会走入这段代码的场景,因为上面既又paraller模式又有非paraller模式的流程,包括dump过程也是包含了第一次发送一些初始化命令和dump命令以及后续的读binlog的流程,就是从代码层面也很难看出到底是正常还是异常场景。所以没法针对性的处理。
这两个点,在我们日常开发过程中也比较常见。比如为了复用代码,或者是为了抽象设计,经常会陷入一些比较尴尬的境地或者踩到一些意想不到的问题。但是也不能因噎废食,良好的抽象设计不管是对扩展性还是可读性都有很大的裨益。

如果连接mysql消费binlog的走到这一段代码是不是也会有问题?

仔细想想,好像这并不是读取binlog文件特有的问题吧?如果连接mysql消费binlog,并且走到了这段代码逻辑,是不是也会有问题。因为这里也并没有等消费者把Disruptor RingBuffer里的event都消费完。是不是也可能会丢消息?
我的猜想应该是肯定的。但是还是因为流程分支众多,不好判定到底是因为什么原因会走到这段代码。
但是如果从另外一个角度分析。我们假设已经走到这段代码了,重新开始读取binlog,我们看看起始点位是哪里,如果说是之前读到的最后的位置,那就真的丢event了。还好起始点位是根据client的消费点位取的,只要client没有消费到,即使之前的RingBuffer里的event丢了,重新消费的时候还是能从mysql的binlog里拉出来,所以对于整体来说,还是保证了binlog的准确性。
 
刷一张亿级表带来的思考带你了解MySQL binlog event