再学Canal

一、Canal简介

canal 是用于解析 MySQL 增量日志,提供增量数据订阅。

日志增量订阅的业务包括:
1)数据库实时备份
2)索引构建和实时维护(拆分异构索引、倒排索引等)
3)数据库更新后, cache 实时刷新
4)带业务逻辑的增量数据处理

1.1、Canal官方架构图

在这里插入图片描述

1.3、Canal原理

Canal的原理基于MySQL主备复制原理
在这里插入图片描述
1)MySQL master 将数据变更写入二进制日志( binary log, 其中记录叫做二进制日志事件binary log events,可以通过 show binlog events 进行查看)
2)MySQL slave 将 master 的 binary log events 拷贝到它的中继日志(relay log)
3)MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据

canal 工作原理
1)canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议
2)MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal Server)
3)canal 解析 binary log 对象(原始为 byte 流)

1.3、Canal的客户端多语言支持方式

方式1:多语言客户端

Canal支持 Server-Client 模式
Canal提供了多语言的客户端,用于读取Canal解析后的增量日志。

方式2:借助于MQ的多语言客户端

Canal也支持把增量日志的变更投递到MQ,借助于MQ的多语言客户端来实现多语言消费Canal。

二、Canal Server架构设计详解

2.1、MySQL的binlog简介

1)mysql的binlog是多文件存储
2)通过 binlog filename + binlog position 定位一个LogEvent

mysql的binlog数据格式,按照生成的方式,主要分为:statement-based、row-based、mixed。

mysql> show variables like 'binlog_format';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| binlog_format | ROW   |
+---------------+-------+
1 row in set (0.00 sec)

目前canal支持所有模式的增量订阅(但配合同步时,因为statement只有sql,没有数据,无法获取原始的变更日志,所以一般建议为ROW模式)

2.2、Canal Server的架构图

在这里插入图片描述
1、server代表一个canal运行实例,对应于一个JVM
2、instance对应于一个数据队列 (1个server对应n个instance)

对于每个instance,包含的模块:
1)eventParser :数据源接入,模拟slave协议和master进行交互,协议解析
2)eventSink :Parser和Store链接器,进行数据过滤,加工,分发的工作
3)eventStore :数据存储,供client消费
4)metaManager :增量订阅&消费信息管理器

2.2.1、EventParser设计

在这里插入图片描述
Parser执行步骤如下:
1)Connection前先获取上一次解析成功的位置 (如果第一次启动,则获取初始指定的位置或者是当前数据库的binlog位点)
2)Connection建立链接,发送BINLOG_DUMP指令
3)MySQL开始推送Binaly Log给Canal
4)Canal接收到binlog后,通过parser进行协议解析:补充一些特定信息,如补充字段名字,字段类型,主键信息,unsigned类型处理
5)传递给EventSink模块进行数据存储,是一个串行阻塞操作,直到存储成功,再处理下一条
6)存储成功后,定时记录Binaly Log位置

总结:Parser模块从MySQL拉取增量日志时,也是要传递上一次拉取成功的offset作为参数的。

MySQL binlog的发送接收流程:
在这里插入图片描述
Step1:通过HandShake协议进行Client和DB的握手认证
Step2:握手成功以后,Client对DB发送show master status命令,此命令中回带回当前最新binlog存储在哪个文件,以及对应哪个偏移量。如果想从当前开始接收binglog,则在后面发送binlog dump命令的时候用这两个值就好。
Step3:发送show global variables like 'binlog_checksum’命令,这是由于binlog event发送回来的时候需要,在最后获取event内容的时候,会增加4个额外字节做校验用。mysql5.6.5以后的版本中binlog_checksum=crc32,而低版本都是binlog_checksum=none。如果不想校验,可以使用set命令设置set binlog_checksum=none
Step4:向MySQL发送Dump命令

Dump命令包图:
在这里插入图片描述
如上图所示,在报文中塞入binlogPosition和binlogFileName即可让master从相应的位置发送binlog event。

一旦发送了BinlogDump命令,master就会在数据库有变化的源源不断的推送binlog event到client。
binlog的类型有三种:
1)Statement:每一条会修改数据的sql都会记录在binlog中。
2)Row:不记录sql语句上下文相关信息,仅保存哪条记录被修改。
3)Mixedlevel:以上两种Level的混合。

2.2.2、EventSink设计

在这里插入图片描述
1、数据过滤:支持通配符的过滤模式,表名,字段内容等
2、数据路由/分发:解决1:n (1个parser对应多个store的模式)
3、数据归并:解决n:1 (多个parser对应1个store)
4、数据加工:在进入store之前进行额外的处理,比如join

2.2.2.1、数据1:n业务

为了合理的利用数据库资源, 一般常见的业务都是按照schema进行隔离,然后在mysql上层或者dao这一层面上,进行一个数据源路由,屏蔽数据库物理位置对开发的影响,阿里系主要是通过cobar/tddl来解决数据源路由问题。

所以,一般一个数据库实例上,会部署多个schema,每个schema会有由1个或者多个业务方关注

总结:parser和store为1:n,对于nebula而言,可以是一个storage的wal对应多个业务消费,即多套store,每个业务的过滤规则都不同。

2.2.2.2、数据n:1业务

同样,当一个业务的数据规模达到一定的量级后,必然会涉及到水平拆分和垂直拆分的问题,针对这些拆分的数据需要处理时,就需要链接多个store进行处理,消费的位点就会变成多份,而且数据消费的进度无法得到尽可能有序的保证。

所以,在一定业务场景下,需要将拆分后的增量数据进行归并处理,比如按照时间戳/全局id进行排序归并.

总结:如果数据库数据量非常大,经历了拆库拆表后,那么为了提高消费速度,可以由多个parser并行处理,但这样会导致数据乱序,所以后续再重新做归并排序。

2.2.3、EventStore设计

1、目前仅实现了Memory内存模式,后续计划增加本地file存储,mixed混合模式
2、借鉴了Disruptor的RingBuffer的实现思路
在这里插入图片描述
要注意的是,如果生成速度大于消费速度时,以上图为例,如果生产者想要写入 Ring Buffer 中序号 3 占据的节点,因为它是 Ring Buffer 当前游标的下一个节点。但是 ProducerBarrier 明白现在不能写入,因为有一个消费者正在占用它。所以,ProducerBarrier 停下来自旋 (spins),等待,直到那个消费者离开。

定义了3个cursor:
1)Put : Sink模块进行数据存储的最后一次写入位置
2)Get : 数据订阅获取的最后一次提取位置
3)Ack : 数据消费成功的最后一次消费位置

2.2.4、Instance设计

在这里插入图片描述
可以通过配置文件或者console这两种方式二选一在设置一个server启动几个instance。
instance代表了一个实际运行的数据队列,包括了EventPaser,EventSink,EventStore等组件。

2.2.5、Server设计

在这里插入图片描述
server代表了一个canal的运行实例,为了方便组件化使用,特意抽象了Embeded(嵌入式) / Netty(网络访问)的两种实现:
1)Embeded(嵌入式):对latency和可用性都有比较高的要求,自己又能hold住分布式的相关技术(比如failover)-- 例如,可以把Canal Server跟数据库部署在相同的服务器上,但当Canal Server的instance实例故障时,需要自己包装failover,如果可以一个instance对应一个套增量日志,那么当instance故障时,可以通过运维命令自动拉起。
2)Netty(RPC框架):基于netty封装了一层网络协议,由canal server保证其可用性,采用的pull模型,当然latency会稍微打点折扣,不过这个也视情况而定。(阿里系的notify和metaq,典型的push/pull模型,目前也逐步的在向pull模型靠拢,push在数据量大的时候会有一些问题)。

2.2.6、增量订阅设计

在这里插入图片描述
具体的协议格式,可参见:https://github.com/alibaba/canal/blob/master/protocol/src/main/java/com/alibaba/otter/canal/protocol/CanalProtocol.proto

get/ack/rollback协议介绍:
1)Message getWithoutAck(int batchSize),允许指定batchSize,一次可以获取多条,每次返回的对象为Message,包含的内容为:
a. batch id 唯一标识
b. entries 具体的数据对象,对应的数据对象格式:https://github.com/alibaba/canal/blob/master/protocol/src/main/java/com/alibaba/otter/canal/protocol/EntryProtocol.proto

2)void rollback(long batchId),顾命思议,回滚上次的get请求,重新获取数据。基于get获取的batchId进行提交,避免误操作

3)void ack(long batchId),顾命思议,确认已经消费成功,通知server删除数据。基于get获取的batchId进行提交,避免误操作

canal的get/ack/rollback协议和常规的jms协议有所不同,允许get/ack异步处理,比如可以连续调用get多次,后续异步按顺序提交ack/rollback,项目中称之为流式api.
流式api设计的好处:

  • get/ack异步化,减少因ack带来的网络延迟和操作成本 (99%的状态都是处于正常状态,异常的rollback属于个别情况,没必要为个别的case牺牲整个性能)
  • get获取数据后,业务消费存在瓶颈或者需要多进程/多线程消费时,可以不停的轮询get数据,不停的往后发送任务,提高并行化. (作者在实际业务中的一个case:业务数据消费需要跨中美网络,所以一次操作基本在200ms以上,为了减少延迟,所以需要实施并行化)
    流式API设计:
    在这里插入图片描述
  • 每次get操作都会在meta中产生一个mark,mark标记会递增,保证运行过程中mark的唯一性
  • 每次的get操作,都会在上一次的mark操作记录的cursor继续往后取,如果mark不存在,则在last ack cursor继续往后取
  • 进行ack时,需要按照mark的顺序进行数序ack,不能跳跃ack. ack会删除当前的mark标记,并将对应的mark位置更新为last ack cusor
  • 一旦出现异常情况,客户端可发起rollback情况,重新置位:删除所有的mark, 清理get请求位置,下次请求会从last ack cursor继续往后取

2.2.7、数据对象格式

数据对象格式:https://github.com/alibaba/canal/blob/master/protocol/src/main/java/com/alibaba/otter/canal/protocol/EntryProtocol.proto

每次调用get, 访问master的增量日志,返回数据格式如下:

Entry
	Header
		logfileName [binlog文件名]
		logfileOffset [binlog position]
		executeTime [binlog里记录变更发生的时间戳]
		schemaName [数据库实例]
		tableName [表名]
		eventType [insert/update/delete类型]
	entryType 	[事务头BEGIN/事务尾END/数据ROWDATA]
	storeValue 	[byte数据,可展开,对应的类型为RowChange]
RowChange
isDdl		[是否是ddl变更操作,比如create table/drop table]
sql		[具体的ddl sql]
rowDatas	[具体insert/update/delete的变更数据,可为多条,1个binlog event事件可对应多条变更,比如批处理]
beforeColumns [Column类型的数组]
afterColumns [Column类型的数组]


Column
index		[column序号]
sqlType		[jdbc type]
name		[column name]
isKey		[是否为主键]
updated		[是否发生过变更]
isNull		[值是否为null]
value		[具体的内容,注意为文本]

说明:
1)可以提供数据库变更前和变更后的字段内容,针对binlog中没有的name,isKey等信息进行补全
2)可以提供ddl的变更语句

2.2.8、HA机制设计

canal的ha分为两部分,canal server和canal client分别有对应的ha实现

1)canal server: 为了减少对mysql dump的请求,不同server上的instance要求同一时间只能有一个处于running,其他的处于standby状态.
2)canal client: 为了保证有序性,一份instance同一时间只能由一个canal client进行get/ack/rollback操作,否则客户端接收无法保证有序。

在这里插入图片描述
1、canal server要启动某个canal instance时都先向zookeeper进行一次尝试启动判断 (实现:创建EPHEMERAL节点,谁创建成功就允许谁启动)
2、创建zookeeper节点成功后,对应的canal server就启动对应的canal instance,没有创建成功的canal instance就会处于standby状态
3、一旦zookeeper发现canal server A创建的节点消失后,立即通知其他的canal server再次进行步骤1的操作,重新选出一个canal server启动instance.
4、canal client每次进行connect时,会首先向zookeeper询问当前是谁启动了canal instance,然后和其建立链接,一旦链接不可用,会重新尝试connect.

Canal Client的方式和canal server方式类似,也是利用zookeeper的抢占EPHEMERAL节点的方式进行控制。

三、Canal Server源码解析

3.1、EventParser

3.1.1、EventParser的架构设计图:

在这里插入图片描述
其关键步骤如下:
1、从 Log Position 管理器中获取上一次解析的日志位点。
2、向 Mysql Master 节点发送 BINLOG_DUMP 请求。
3、Mysql Master 节点从 Slave 端传入的日志位点开始向从节点推送 binlog 日志。
4、Slave 接收 binlog 日志,调用 BinlogParser 解析 binlog日志。
5、将解析后的结构化数据传入到 EventSink 组件。
6、定时记录解析 binlog 的日志,以便重启后继续进行增量订阅。
7、上图中还罗列一个HA 特性,即需要同步的 Master 如果宕机,可以从它的其他从节点继续同步 binlog 日志,避免单点故障。

EventParser关键类的关系:
在这里插入图片描述

3.1.2、源码之EventParser初始化

CanalInstanceWithManager.java的CanalInstanceWithManager方法,是instance的启动入口。

public CanalInstanceWithManager(Canal canal, String filter){
        this.parameters = canal.getCanalParameter();
        this.canalId = canal.getId();
        this.destination = canal.getName();
        this.filter = filter;

        logger.info("init CanalInstance for {}-{} with parameters:{}", canalId, destination, parameters);
        // 初始化报警机制
        initAlarmHandler();
        // 初始化metaManager
        initMetaManager();
        // 初始化eventStore
        initEventStore();
        // 初始化eventSink
        initEventSink();
        // 初始化eventParser;
        initEventParser();
        ...

下面看initEventParser方法:

protected void initEventParser() {
    logger.info("init eventParser begin...");
    SourcingType type = parameters.getSourcingType();

	/*
	 * Step1:获取数据库连接信息:
	 * 其中List<List<DataSourcing>>集合List<DataSourcing>中第一个元素是主库地址,第二个元素是从库地址
	 * 对于MySQL分库分表场景:加入192.168.1.166:3306为主,对应从为192.168.1.167:3306;
	 * 另一个主为192.168.1.168:3306,对应的从为192.168.1.169:3306,
	 * 那么,对应到List<List<DataSourcing>>为:
	 * List<List<DataSourcing>>的第一个元素List<DataSourcing>为192.168.1.166,和192.168.1.168组成的List
	 * 第二个元素List<DataSourcing>为192.168.1.167 和 192.168.1.169组成的List
	 */
    List<List<DataSourcing>> groupDbAddresses = parameters.getGroupDbAddresses();
    if (!CollectionUtils.isEmpty(groupDbAddresses)) {
    	// 此size对应上面例子,即为所有主的数量
        int size = groupDbAddresses.get(0).size();// 取第一个分组的数量,主备分组的数量必须一致
        List<CanalEventParser> eventParsers = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            List<InetSocketAddress> dbAddress = new ArrayList<>();
            SourcingType lastType = null;
            for (List<DataSourcing> groupDbAddress : groupDbAddresses) {
                if (lastType != null && !lastType.equals(groupDbAddress.get(i).getType())) {
                    throw new CanalException(String.format("master/slave Sourcing type is unmatch. %s vs %s",
                        lastType,
                        groupDbAddress.get(i).getType()));
                }

                lastType = groupDbAddress.get(i).getType();
                dbAddress.add(groupDbAddress.get(i).getDbAddress());
            }

            // 对于每个主节点,初始化一个eventParser实例
            eventParsers.add(doInitEventParser(lastType, dbAddress));
        }

        if (eventParsers.size() > 1) { // 如果存在分组,构造分组的parser
            GroupEventParser groupEventParser = new GroupEventParser();
            groupEventParser.setEventParsers(eventParsers);
            this.eventParser = groupEventParser;
        } else {
            this.eventParser = eventParsers.get(0);
        }
    } else {
        // 创建一个空数据库地址的parser,可能使用了tddl指定地址,启动的时候才会从tddl获取地址
        this.eventParser = doInitEventParser(type, new ArrayList<>());
    }

    logger.info("init eventParser end! \n\t load CanalEventParser:{}", eventParser.getClass().getName());
}
Step1:获取数据库连接信息:

其中List<List>集合List中第一个元素是主库地址,第二个元素是从库地址
对于MySQL分库分表场景:加入192.168.1.166:3306为主,对应从为192.168.1.167:3306;
另一个主为192.168.1.168:3306,对应的从为192.168.1.169:3306,
那么,对应到List<List>为:
List<List>的第一个元素List为192.168.1.166,和192.168.1.168组成的List
第二个元素List为192.168.1.167 和 192.168.1.169组成的List

Step2:根据配置的 MySQL ,为每个主节点,初始化一个EventParser的实例:

eventParsers.add(doInitEventParser(lastType, dbAddress));
下面看doInitEventParser方法的实现:

private CanalEventParser doInitEventParser(SourcingType type, List<InetSocketAddress> dbAddresses) {
        CanalEventParser eventParser;
        if (type.isMysql()) {
            MysqlEventParser mysqlEventParser = null;
            ...
        } else if (type.isLocalBinlog()) {
            LocalBinlogEventParser localBinlogEventParser = new LocalBinlogEventParser();
            ...
        } else if (type.isOracle()) {
            throw new CanalException("unsupport SourcingType for " + type);
        } else {
            throw new CanalException("unsupport SourcingType for " + type);
        }

        // add transaction support at 2012-12-06
        if (eventParser instanceof AbstractEventParser) {
            AbstractEventParser abstractEventParser = (AbstractEventParser) eventParser;
            ...
        }
        if (eventParser instanceof MysqlEventParser) {
            MysqlEventParser mysqlEventParser = (MysqlEventParser) eventParser;

            // 初始化haController,绑定与eventParser的关系,haController会控制eventParser
            CanalHAController haController = initHaController();
            mysqlEventParser.setHaController(haController);
        }
        return eventParser;
    }

可以看出目前Canal不支持oracle,仅支持MySQL和本地binlog文件(直接从binlog日志文件解析)。

Step3:MySQL的binlog解析

这里忽略掉跟阿里云相关的RDS,TSDB的支持,仅重点关注开源MySQL相关的binlog解析逻辑:

if (type.isMysql()) {
            MysqlEventParser mysqlEventParser = null;
            if (StringUtils.isNotEmpty(parameters.getRdsAccesskey())
                && StringUtils.isNotEmpty(parameters.getRdsSecretkey())
                && StringUtils.isNotEmpty(parameters.getRdsInstanceId())) {

                mysqlEventParser = new RdsBinlogEventParserProxy();
                ((RdsBinlogEventParserProxy) mysqlEventParser).setAccesskey(parameters.getRdsAccesskey());
                ((RdsBinlogEventParserProxy) mysqlEventParser).setSecretkey(parameters.getRdsSecretkey());
                ((RdsBinlogEventParserProxy) mysqlEventParser).setInstanceId(parameters.getRdsInstanceId());
            } else {
                mysqlEventParser = new MysqlEventParser();
            }
            mysqlEventParser.setDestination(destination);
            // 编码参数
            mysqlEventParser.setConnectionCharset(parameters.getConnectionCharset());
            mysqlEventParser.setConnectionCharsetNumber(parameters.getConnectionCharsetNumber());
            // 网络相关参数
            mysqlEventParser.setDefaultConnectionTimeoutInSeconds(parameters.getDefaultConnectionTimeoutInSeconds());
            mysqlEventParser.setSendBufferSize(parameters.getSendBufferSize());
            mysqlEventParser.setReceiveBufferSize(parameters.getReceiveBufferSize());
            // 心跳检查参数
            mysqlEventParser.setDetectingEnable(parameters.getDetectingEnable());
            mysqlEventParser.setDetectingSQL(parameters.getDetectingSQL());
            mysqlEventParser.setDetectingIntervalInSeconds(parameters.getDetectingIntervalInSeconds());
            // 数据库信息参数
            mysqlEventParser.setSlaveId(parameters.getSlaveId());
            if (!CollectionUtils.isEmpty(dbAddresses)) {
                mysqlEventParser.setMasterInfo(new AuthenticationInfo(dbAddresses.get(0),
                    parameters.getDbUsername(),
                    parameters.getDbPassword(),
                    parameters.getDefaultDatabaseName()));

                if (dbAddresses.size() > 1) {
                    mysqlEventParser.setStandbyInfo(new AuthenticationInfo(dbAddresses.get(1),
                        parameters.getDbUsername(),
                        parameters.getDbPassword(),
                        parameters.getDefaultDatabaseName()));
                }
            }
}

MySQL 的 binlog 事件解析器实现类为 MysqlEventParser,下面看各参数的含义:
1)destination
Canal Instance 实例的名称。
2)connectionCharset
字符集,解析 binlog 时会将指定的字节数据使用该编码级进行转换,默认为UTF-8。
3)connectionCharsetNumber
字符集的数字表现形式,UTF8对应的值为 33,该值在与 MySQL 的交互协议包中需要被用到,这里 Canal 处理的不是特别好,最好该属性设置为只读,由 connectionCharset 联动进行设置。
4)defaultConnectionTimeoutInSeconds
MySQL 默认连接超时时间,因为 Canal 会伪装为 MySQL 服务器的 Slave 节点,需要向 MySQL Master 发送请求,故需要先创建链接,这里就是创建连接的默认超时时间,默认为 30s。
5)sendBufferSize
用于网络通道发送端缓存区,目前在 Canal 中网络通道的实现类为 BioSocketChannelPool、NettySocketChannelPool,从代码的角度来看,目前这个参数并不会生效,即使用操作系统的默认值。
6)receiveBufferSize
用于网络通道接收缓存区大小,目前同 sendBufferSize 参数,并不会生效。
7)detectingEnable
是否开启心跳检测,默认为开启。
8)detectingSQL
心跳检测语句,例如 select 1,show master status 等。
9)detectingIntervalInSeconds
心跳间隔检测,默认为 3s。
10)slaveId
从服务器的 id,在同一个 MySQL 复制组内不能重复。

Step4:如果设置了 CanalPrameter 的 Listpositions 属性,则将其解析为 EntryPosition 实体

在这里插入图片描述
代码为:

EntryPosition masterPosition = JsonUtils.unmarshalFromString(parameters.getPositions().get(0),
                    EntryPosition.class);
// binlog位置参数
mysqlEventParser.setMasterPosition(masterPosition);

EntryPosition主要的核心参数如下:
1)long timestamp
时间戳,用时间戳来表示位置
2)String journalName
binlog 日志的文件名,例如 mysql-bin.000001。
3)Long position
使用偏移量来表示具体位点。
4)long serverId
设置 master 的 id。
5)String gtid
全局事务ID。

Step5:继续设置参数

1)fallbackIntervalInSeconds
如果 MySQL 主节点宕机,Canal 支持切换到其从节点继续同步 binlog 日志,但为了数据的完整性,可以设置一个回退时间,即会造成数据重复下发,但尽量不丢失,该值默认为 60s。
2)profilingEnabled
是否开启性能采集,主要采集的是一批日志经过 EventSink 组件处理到完成 存入EventStore 的时间消耗。
3)filterTableError
是否忽略表过滤异常,默认为 false,表过滤会在后续文章中详细介绍。
4)parallel
解析、canal 接入 prometheus 采集监控数据是否支持并发,默认为 false。
5)isGTIDMode
是否开启 gtid 模式。

Step6:继续填充解析器相关参数

1)transactionSize
Canal 提供了一种机制,尝试将一个数据库事务中所有的变更日志一起进行处理,这个为处理缓存事务日志的缓存区长度,默认为 1024。
2)logPositionManager
初始化日志位点管理器,Canal 提供了基于内存、zookeeper、内存与zookeepr混合管理器等日志位点管理器
3)AviaterRegexFilter
提供了基于 aviater 的正则表达式,对 table 名称进行过滤。
4)blackFilter
canal 提供了黑名单配置,提供黑名单正则表达式对 table 名称进行过滤。

Step7:如果解析器是 MySQL 解析器,提供了 HA 机制

即如果 MySQL Master 宕机,Canal 还能主动切换到 MYSQL Slave 节点,继续同步 binlog 日志。
相关代码为:

if (eventParser instanceof MysqlEventParser) {
   MysqlEventParser mysqlEventParser = (MysqlEventParser) eventParser;

    // 初始化haController,绑定与eventParser的关系,haController会控制eventParser
    CanalHAController haController = initHaController();
    mysqlEventParser.setHaController(haController);
}

3.1.3、源码之EventParser 工作流程

入口:MysqlEventParser.java的start方法:

public void start() throws CanalParseException {
    if (runningInfo == null) { // 第一次链接主库
        runningInfo = masterInfo;
    }

    super.start();
}

主要调用的是其父类AbstractEventParser的 start 方法。下面详细解读:

Step1:初始化环形缓冲区
// 配置transaction buffer
// 初始化缓冲队列
transactionBuffer.setBufferSize(transactionSize);// 设置buffer大小
transactionBuffer.start();

环形缓冲区的作用是,Canal 在解析 binlog 日志后,会尽量尝试将一个数据库事务所产生的全部变更日志(一个事务所有变更数据)当成一个整体提交给 EventSink 组件,从而 Canal 的消费方能一次将一个事务的数据全部同步,数据的完整性得到了保证。

注意:Canal 目前无法百分之百保证一个事务的数据就一定是一次消费,如果一个事务产生的变更日志超过了环形缓存区的容量,则会被强制提交消费,一个事务的数据会被分开消费,默认环形缓存区的长度为 1024.

Step2:构建一个 binlog 解析器
// 构造bin log parser
binlogParser = buildParser();// 初始化一下BinLogParser
binlogParser.start();

该buildParser方法在 AbstractEventParser 中为一个抽象方法,具体的实现在其子类AbstractMysqlEventParser.java中:

protected BinlogParser buildParser() {
    LogEventConvert convert = new LogEventConvert();
    if (eventFilter != null && eventFilter instanceof AviaterRegexFilter) {
        convert.setNameFilter((AviaterRegexFilter) eventFilter);
    }

    if (eventBlackFilter != null && eventBlackFilter instanceof AviaterRegexFilter) {
        convert.setNameBlackFilter((AviaterRegexFilter) eventBlackFilter);
    }

    convert.setFieldFilterMap(getFieldFilterMap());
    convert.setFieldBlackFilterMap(getFieldBlackFilterMap());

    convert.setCharset(connectionCharset);
    convert.setFilterQueryDcl(filterQueryDcl);
    convert.setFilterQueryDml(filterQueryDml);
    convert.setFilterQueryDdl(filterQueryDdl);
    convert.setFilterRows(filterRows);
    convert.setFilterTableError(filterTableError);
    convert.setUseDruidDdlFilter(useDruidDdlFilter);
    return convert;
}
Step3:启动一个独立的线程来负责 binlog 的解析
// 启动工作线程
parseThread = new Thread(new Runnable() {

    public void run() {
        MDC.put("destination", String.valueOf(destination));
        ErosaConnection erosaConnection = null;
        boolean isMariaDB = false;
        while (running) {
            // 省略相关代码   
        }
    }
});

parseThread.setUncaughtExceptionHandler(handler);
parseThread.setName(String.format("destination = %s , address = %s , EventParser",
    destination,
    runningInfo == null ? null : runningInfo.getAddress()));
parseThread.start();

其线程包含了 Canal Instance 的 destination、address 等信息,方便利用 jstack 去诊断 binlog 解析相关问题。接下来就是解读该线程的 run 方法,从而探究 binlog 的解析流程。

1:首先创建一条到需要解析 binlog 日志的服务器的连接
// 开始执行replication
// 1. 构造Erosa连接
erosaConnection = buildErosaConnection();

MysqlEventParser.java#buildEorsaConnection方法:

protected ErosaConnection buildErosaConnection() {
   return buildMysqlConnection(this.runningInfo);
}

private MysqlConnection buildMysqlConnection(AuthenticationInfo runningInfo) {
    MysqlConnection connection = new MysqlConnection(runningInfo.getAddress(),
        runningInfo.getUsername(),
        runningInfo.getPassword(),
        connectionCharsetNumber,
        runningInfo.getDefaultDatabaseName());
    connection.getConnector().setReceiveBufferSize(receiveBufferSize);
    connection.getConnector().setSendBufferSize(sendBufferSize);
    connection.getConnector().setSoTimeout(defaultConnectionTimeoutInSeconds * 1000);
    connection.setCharset(connectionCharset);
    connection.setReceivedBinlogBytes(receivedBinlogBytes);
    // 随机生成slaveId
    if (this.slaveId <= 0) {
        this.slaveId = generateUniqueServerId();
    }
    connection.setSlaveId(this.slaveId);
    return connection;
}

例如需要同步 192.168.1.166:3306 这个数据库实例的 binlog 日志,那 Canal 首先会使用拥有该库复制权限的账号去创建一条TCP连接。这里需要知晓 MySQL 通讯协议,通过TCP与MySQL建立连接,并按照 MySQL 通讯协议发送命令,例如 select、dump 等请求。
这步骤的重要关键点:
1)首先创建一条TCP连接,连接到 MySQL 服务器,Canal 提供了 BIO 与 Netty 两种实现方式
2)TCP 三次握手后成功建立TCP连接后,需要与 MySQL 进行握手,完成协议约定,客户端登录校验等,例如握手实现代码见:MysqlConnector negotiate。
3)一言以蔽之,MySqlConnection 的职责就是实现一个 MySQL 客户端。其效果等同于实现我们常用的 SQL 连接客户端,关于这方面的编程其实不难,如果有志成为一名数据库中间件方面的技术人员,只需按照 MySQL 官方文档中有关通讯协议即可。

2:给sink发送心跳包
// 2. 启动一个心跳线程
startHeartBeat(erosaConnection);
protected void startHeartBeat(ErosaConnection connection) {
    lastEntryTime = 0L; // 初始化
    if (timer == null) {// lazy初始化一下
        String name = String.format("destination = %s , address = %s , HeartBeatTimeTask",
            destination,
            runningInfo == null ? null : runningInfo.getAddress().toString());
        synchronized (AbstractEventParser.class) {
            // synchronized (MysqlEventParser.class) {
            // why use MysqlEventParser.class, u know, MysqlEventParser is
            // the child class 4 AbstractEventParser,
            // do this is ...
            if (timer == null) {
                timer = new Timer(name, true);
            }
        }
    }

    if (heartBeatTimerTask == null) {// fixed issue #56,避免重复创建heartbeat线程
        heartBeatTimerTask = buildHeartBeatTimeTask(connection);
        Integer interval = detectingIntervalInSeconds;
        timer.schedule(heartBeatTimerTask, interval * 1000L, interval * 1000L);
        logger.info("start heart beat.... ");
    }
}
protected TimerTask buildHeartBeatTimeTask(ErosaConnection connection) {
    return new TimerTask() {

        public void run() {
            try {
                if (exception == null || lastEntryTime > 0) {
                    // 如果未出现异常,或者有第一条正常数据
                    long now = System.currentTimeMillis();
                    long inteval = (now - lastEntryTime) / 1000;
                    if (inteval >= detectingIntervalInSeconds) {
                        Header.Builder headerBuilder = Header.newBuilder();
                        headerBuilder.setExecuteTime(now);
                        Entry.Builder entryBuilder = Entry.newBuilder();
                        entryBuilder.setHeader(headerBuilder.build());
                        entryBuilder.setEntryType(EntryType.HEARTBEAT);
                        Entry entry = entryBuilder.build();
                        // 提交到sink中,目前不会提交到store中,会在sink中进行忽略
                        consumeTheEventAndProfilingIfNecessary(Arrays.asList(entry));
                    }
                }

            } catch (Throwable e) {
                logger.warn("heartBeat run failed ", e);
            }
        }

    };
}

关键点:
1)利用 Timer 实现定时调度,心跳包发送间隔通过 detectingIntervalInSeconds 指定。
2)心跳包主要是构建一个 CanalEntry.Entry,其类型为EntryType.HEARTBEAT。
3)心跳包并不是发送给远端 MySQL 服务器,而是将 Entry 下发到 EventSink 组件。

3:执行发送 dump 命令正式从 MySQL 服务器接收 binlog 日志之前的准备工作
// 3. 执行dump前的准备工作
preDump(erosaConnection);

具体准备工作如下:

1)首先再创建一条专属数据库连接,主要用于查找 MySQL 的一些配置信息,统称元数据。
2)向 MySQL 服务器发送 show variables like 'binlog_format‘ 语句查询服务端配置的 binlog 格式,MySQL 支持 STATEMENT、ROW、MIXED 三种模式。
3)向 MySQL 服务端发送 show variables like ‘binlog_row_image’ 语句查询服务器端配置的 binlog_row_image

扩展阅读:binlog_format 我相信大家都不陌生,对 binlog_row_image 见过的估计比较少,那 binlog_row_image 有何作用呢?
binlog_row_image 主要是在 binlog_format 为 ROW 模式下,控制记录 binlog 事件的方式,binlog 的作用是记录数据的变化,例如 update 请求,需要记录一行记录变化之前的数据以及变化后的数据,在 binlog event 分别用 before 、after 记录变化前后的数据,但有一个问题,是只发生变化的字段的前后值呢,还是记录一行中所有字段修改前后的值呢?故引入了 binlog_row_image,该值支持如下选项:
1)full:在 before 与 after 中记录所有字段的值,针对每一个字段,使用 update 来表示该字段是否发生变化,该选项为默认值。
2)minimal:在 before 与 after 中只记录发生变化的字段,并且包含能够唯一识一行数据的值,例如主键。
3)noblob:在 before 与 after 中记录所有的列值,但 BLOB 与 TEXT 类型的字段列除外(如未更改)。

4:向 MySQL 服务端发送 show variables like ‘server_id’ 语句,查询服务端配置的 serverId
erosaConnection.connect();// 链接

long queryServerId = erosaConnection.queryServerId();
if (queryServerId != 0) {
    serverId = queryServerId;
}
5:通过日志位点管理器获取需要同步的位点
// 4. 获取最后的位置信息
long start = System.currentTimeMillis();
logger.warn("---> begin to find start position, it will be long time for reset or first position");
EntryPosition position = findStartPosition(erosaConnection);
final EntryPosition startPosition = position;
if (startPosition == null) {
    throw new PositionNotFoundException("can't find start position for " + destination);
}
protected EntryPosition findStartPosition(ErosaConnection connection) throws IOException {
    if (isGTIDMode()) {
        // GTID模式下,CanalLogPositionManager里取最后的gtid,没有则取instanc配置中的
        LogPosition logPosition = getLogPositionManager().getLatestIndexBy(destination);
        if (logPosition != null) {
            // 如果以前是非GTID模式,后来调整为了GTID模式,那么为了保持兼容,需要判断gtid是否为空
            if (StringUtils.isNotEmpty(logPosition.getPostion().getGtid())) {
                return logPosition.getPostion();
            }
        } else {
            if (masterPosition != null && StringUtils.isNotEmpty(masterPosition.getGtid())) {
                return masterPosition;
            }
        }
    }

    EntryPosition startPosition = findStartPositionInternal(connection);
    if (needTransactionPosition.get()) {
        logger.warn("prepare to find last position : {}", startPosition.toString());
        Long preTransactionStartPosition = findTransactionBeginPosition(connection, startPosition);
        if (!preTransactionStartPosition.equals(startPosition.getPosition())) {
            logger.warn("find new start Transaction Position , old : {} , new : {}",
                startPosition.getPosition(),
                preTransactionStartPosition);
            startPosition.setPosition(preTransactionStartPosition);
        }
        needTransactionPosition.compareAndSet(true, false);
    }
    return startPosition;
}
6:通过向 MySQL 发送 dump 请求,从服务器接收 binlog 日志,并进行处理

为了提高性能,Canal 支持该过程进行并行化处理,通过 parallel 属性设置是否支持并发,从而引入 disruptor 高性能并发框架

// 5. 开始dump数据
if (parallel) {
    // build stage processor
    multiStageCoprocessor = buildMultiStageCoprocessor();
    if (isGTIDMode() && StringUtils.isNotEmpty(startPosition.getGtid())) {
        // 判断所属instance是否启用GTID模式,是的话调用ErosaConnection中GTID对应方法dump数据
        GTIDSet gtidSet = parseGtidSet(startPosition.getGtid(),isMariaDB);
        ((MysqlMultiStageCoprocessor) multiStageCoprocessor).setGtidSet(gtidSet);
        multiStageCoprocessor.start();
        erosaConnection.dump(gtidSet, multiStageCoprocessor);
    } else {
        multiStageCoprocessor.start();
        if (StringUtils.isEmpty(startPosition.getJournalName())
            && startPosition.getTimestamp() != null) {
            erosaConnection.dump(startPosition.getTimestamp(), multiStageCoprocessor);
        } else {
            erosaConnection.dump(startPosition.getJournalName(),
                startPosition.getPosition(),
                multiStageCoprocessor);
        }
    }
} else {
    if (isGTIDMode() && StringUtils.isNotEmpty(startPosition.getGtid())) {
        // 判断所属instance是否启用GTID模式,是的话调用ErosaConnection中GTID对应方法dump数据
        erosaConnection.dump(parseGtidSet(startPosition.getGtid(), isMariaDB), sinkHandler);
    } else {
        if (StringUtils.isEmpty(startPosition.getJournalName())
            && startPosition.getTimestamp() != null) {
            erosaConnection.dump(startPosition.getTimestamp(), sinkHandler);
        } else {
            erosaConnection.dump(startPosition.getJournalName(),
                startPosition.getPosition(),
                sinkHandler);
        }
    }
}
7:通过接收到 MySQL 服务端返回的日志并解析为 Canal.Entry 对象,并传输到 EventSink 组件。
final SinkFunction sinkHandler = new SinkFunction<EVENT>() {

    private LogPosition lastPosition;

    public boolean sink(EVENT event) {
        try {
            CanalEntry.Entry entry = parseAndProfilingIfNecessary(event, false);

            if (!running) {
                return false;
            }

            if (entry != null) {
                exception = null; // 有正常数据流过,清空exception
                transactionBuffer.add(entry);
                // 记录一下对应的positions
                this.lastPosition = buildLastPosition(entry);
                // 记录一下最后一次有数据的时间
                lastEntryTime = System.currentTimeMillis();
            }
            return running;
        } catch (TableIdNotFoundException e) {
            throw e;
        } catch (Throwable e) {
            if (e.getCause() instanceof TableIdNotFoundException) {
                throw (TableIdNotFoundException) e.getCause();
            }
            // 记录一下,出错的位点信息
            processSinkError(e,
                this.lastPosition,
                startPosition.getJournalName(),
                startPosition.getPosition());
            throw new CanalParseException(e); // 继续抛出异常,让上层统一感知
        }
    }

};

上述过程反复执行,持续完成 binlog 日志的解析,实现数据的同步。

3.1.4、总结

EventParser 的主要职责就是与 MySQL 服务器“打交道”,将自己伪装成 MySQL 服务器的一个从节点,从服务器端接收 binlog 日志,并将二进制流解码成 Canal.Entry,看似简单,但实现起来还是比较困难的,下面这些方面是后续值得我们研究探讨的点:

环形缓存区的使用与技巧。
实现 MySQL 通讯协议,向 MySQL 发送相关SQL语句并解析返回结果,具体由 MysqlConnection 对象实现。
日志解析位点管理机制。
基于GTID、日志位点偏移量两种方式定位 binlog 日志方式。
dump 命令的发送、高性能设计( disruptor 框架的引入)

3.2、EventSink

Parser模块用来订阅binlog事件,然后通过sink投递给store。

EventSink模块做的事:
1)数据过滤:支持通配符的过滤模式,表名,字段内容等
2)数据路由/分发:解决1:n (1个parser对应多个store的模式)
3)数据归并:解决n:1 (多个parser对应1个store)
4)数据加工:在进入store之前进行额外的处理,比如join

sink的核心接口为CanalEventSink.java, 他的核心方法为sink。
CanalEventSink接口有两个核心实现类:EntryEventSink和GroupEventSink(用于多库场景)

在这里插入图片描述
在CanalInstanceWithManager#initEventSink创建这两个实现类:

protected void initEventSink() {
    logger.info("init eventSink begin...");

    int groupSize = getGroupSize();
    if (groupSize <= 1) {
        eventSink = new EntryEventSink();
    } else {
        eventSink = new GroupEventSink(groupSize);
    }

    if (eventSink instanceof EntryEventSink) {
        ((EntryEventSink) eventSink).setFilterTransactionEntry(false);
        ((EntryEventSink) eventSink).setEventStore(getEventStore());
    }

    logger.info("init eventSink end! \n\t load CanalEventSink:{}", eventSink.getClass().getName());
}

当Parser解析完毕后,会把数据(CanalEntry.Entry)放到一个环形队列TransactionBuffer中,所用方法为:
EventTransactionBuffer.java的add方法:

public void add(CanalEntry.Entry entry) throws InterruptedException {
    switch (entry.getEntryType()) {
        case TRANSACTIONBEGIN:
            flush();// 刷新上一次的数据
            put(entry);
            break;
        case TRANSACTIONEND:
            put(entry);
            flush();
            break;
        case ROWDATA:
            put(entry);
            // 针对非DML的数据,直接输出,不进行buffer控制
            EventType eventType = entry.getHeader().getEventType();
            if (eventType != null && !isDml(eventType)) {
                flush();
            }
            break;
        case HEARTBEAT:
            // master过来的heartbeat,说明binlog已经读完了,是idle状态
            put(entry);
            flush();
            break;
        default:
            break;
    }
}

根据不同的EntryType做不同的处理,如事务开始要先flush,事务结束,最后也要flush。

put代码:

private void put(CanalEntry.Entry data) throws InterruptedException {
    // 首先检查是否有空位
    if (checkFreeSlotAt(putSequence.get() + 1)) {
        long current = putSequence.get();
        long next = current + 1;

        // 先写数据,再更新对应的cursor,并发度高的情况,putSequence会被get请求可见,拿出了ringbuffer中的老的Entry值
        entries[getIndex(next)] = data;
        putSequence.set(next);
    } else {
        flush();// buffer区满了,刷新一下
        put(data);// 继续加一下新数据
    }
}

flush代码:

private void flush() throws InterruptedException {
    long start = this.flushSequence.get() + 1;
    long end = this.putSequence.get();

    if (start <= end) {
        List<CanalEntry.Entry> transaction = new ArrayList<>();
        for (long next = start; next <= end; next++) {
            transaction.add(this.entries[getIndex(next)]);
        }

        flushCallback.flush(transaction);
        flushSequence.set(end);// flush成功后,更新flush位置
    }
}

可以看到,put的时候会更新一个AtomicLong类型的putSequence位置,而flush也有个AtomicLong类型的flushSequence位置,在flush方法中,会把从flush上次flush完成的位置开始,到putSequence的位置结束的数据,全部刷新到下一个阶段。

在parser阶段,调用AbstractEventParser.java的AbstractEventParser方法:

public AbstractEventParser(){
    // 初始化一下
    transactionBuffer = new EventTransactionBuffer(transaction -> {
        boolean successed = consumeTheEventAndProfilingIfNecessary(transaction);
        if (!running) {
            return;
        }

        if (!successed) {
            throw new CanalParseException("consume failed!");
        }

        LogPosition position = buildLastTransactionPosition(transaction);
        if (position != null) { // 可能position为空
            logPositionManager.persistLogPosition(AbstractEventParser.this.destination, position);
        }
    });
}

关注其consumeTheEventAndProfilingIfNecessary方法:

protected boolean consumeTheEventAndProfilingIfNecessary(List<CanalEntry.Entry> entrys) throws CanalSinkException,
                                                                                           InterruptedException {
    long startTs = -1;
    boolean enabled = getProfilingEnabled();
    if (enabled) {
        startTs = System.currentTimeMillis();
    }

    boolean result = eventSink.sink(entrys, (runningInfo == null) ? null : runningInfo.getAddress(), destination);

    if (enabled) {
        this.processingInterval = System.currentTimeMillis() - startTs;
    }

    if (consumedEventCount.incrementAndGet() < 0) {
        consumedEventCount.set(0);
    }

    return result;
}

调用eventSink.sink,成功后,持久化parser的position位点信息:

logPositionManager.persistLogPosition(AbstractEventParser.this.destination, position);

具体代码在ZookeeperLogPositionManager.java#persistLogPosition:

public void persistLogPosition(String destination, LogPosition logPosition) throws CanalParseException {
    String path = ZookeeperPathUtils.getParsePath(destination);
    byte[] data = JsonUtils.marshalToByte(logPosition);
    try {
        zkClientx.writeData(path, data);
    } catch (ZkNoNodeException e) {
        zkClientx.createPersistent(path, data, true);
    }
}

回到eventSink.sink看其逻辑,代码在EntryEventSink.java#sink方法:

    public boolean sink(List<CanalEntry.Entry> entrys, InetSocketAddress remoteAddress, String destination)
                                                                                                           throws CanalSinkException,
                                                                                                           InterruptedException {
        return sinkData(entrys, remoteAddress);
    }

    private boolean sinkData(List<CanalEntry.Entry> entrys, InetSocketAddress remoteAddress)
                                                                                            throws InterruptedException {
        boolean hasRowData = false;
        boolean hasHeartBeat = false;
        List<Event> events = new ArrayList<>();
        for (CanalEntry.Entry entry : entrys) {
            if (!doFilter(entry)) {
                continue;
            }

            if (filterTransactionEntry
                && (entry.getEntryType() == EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND)) {
                long currentTimestamp = entry.getHeader().getExecuteTime();
                // 基于一定的策略控制,放过空的事务头和尾,便于及时更新数据库位点,表明工作正常
                if (lastTransactionCount.incrementAndGet() <= emptyTransctionThresold
                    && Math.abs(currentTimestamp - lastTransactionTimestamp) <= emptyTransactionInterval) {
                    continue;
                } else {
                    // fixed issue https://github.com/alibaba/canal/issues/2616
                    // 主要原因在于空事务只发送了begin,没有同步发送commit信息,这里修改为只对commit事件做计数更新,确保begin/commit成对出现
                    if (entry.getEntryType() == EntryType.TRANSACTIONEND) {
                        lastTransactionCount.set(0L);
                        lastTransactionTimestamp = currentTimestamp;
                    }
                }
            }

            hasRowData |= (entry.getEntryType() == EntryType.ROWDATA);
            hasHeartBeat |= (entry.getEntryType() == EntryType.HEARTBEAT);
            Event event = new Event(new LogIdentity(remoteAddress, -1L), entry, raw);
            events.add(event);
        }

        if (hasRowData || hasHeartBeat) {
            // 存在row记录 或者 存在heartbeat记录,直接跳给后续处理
            return doSink(events);
        } else {
            // 需要过滤的数据
            if (filterEmtryTransactionEntry && !CollectionUtils.isEmpty(events)) {
                long currentTimestamp = events.get(0).getExecuteTime();
                // 基于一定的策略控制,放过空的事务头和尾,便于及时更新数据库位点,表明工作正常
                if (Math.abs(currentTimestamp - lastEmptyTransactionTimestamp) > emptyTransactionInterval
                    || lastEmptyTransactionCount.incrementAndGet() > emptyTransctionThresold) {
                    lastEmptyTransactionCount.set(0L);
                    lastEmptyTransactionTimestamp = currentTimestamp;
                    return doSink(events);
                }
            }

            // 直接返回true,忽略空的事务头和尾
            return true;
        }
    }

先看doFilter的过滤逻辑:

protected boolean doFilter(CanalEntry.Entry entry) {
    if (filter != null && entry.getEntryType() == EntryType.ROWDATA) {
        String name = getSchemaNameAndTableName(entry);
        boolean need = filter.filter(name);
        if (!need) {
            logger.debug("filter name[{}] entry : {}:{}",
                name,
                entry.getHeader().getLogfileName(),
                entry.getHeader().getLogfileOffset());
        }

        return need;
    } else {
        return true;
    }
}

只有ROWDATA类型的数据才会过滤。过滤的实现是在canal的filter模块,接口为CanalEventFilter.java,filter模块主要用于过滤binlog的表和字段,在使用canal时,可以在服务端或客户端配置,filter基于Aviater来做匹配,有下面几个实现类:
1)AviaterELFilter:基于aviater el表达式的匹配过滤
2)AviaterRegexFilter:基于aviater进行tableName正则匹配的过滤算法
3)AviaterSimpleFilter:基于aviater进行tableName简单过滤计算,不支持正则匹配

看完了过滤,接下来看sink的核心方法doSink

protected boolean doSink(List<Event> events) {
    for (CanalEventDownStreamHandler<List<Event>> handler : getHandlers()) {
        events = handler.before(events);
    }
    long blockingStart = 0L;
    int fullTimes = 0;
    do {
        if (eventStore.tryPut(events)) {
            if (fullTimes > 0) {
                eventsSinkBlockingTime.addAndGet(System.nanoTime() - blockingStart);
            }
            for (CanalEventDownStreamHandler<List<Event>> handler : getHandlers()) {
                events = handler.after(events);
            }
            return true;
        } else {
            if (fullTimes == 0) {
                blockingStart = System.nanoTime();
            }
            applyWait(++fullTimes);
            if (fullTimes % 100 == 0) {
                long nextStart = System.nanoTime();
                eventsSinkBlockingTime.addAndGet(nextStart - blockingStart);
                blockingStart = nextStart;
            }
        }

        for (CanalEventDownStreamHandler<List<Event>> handler : getHandlers()) {
            events = handler.retry(events);
        }

    } while (running && !Thread.interrupted());
    return false;
}

通过反复向store中写eventStore.tryPut(events),直到成功。

3.3、EventStore源码分析

暂略

3.4、MetaManager源码分析

Canal中的位点管理主要有两部分:

1)管理从MySQL dump增量日志binlog的位点,位于parser模块
在这里插入图片描述
2)管理Canal Client的消费位点,meta模块
在这里插入图片描述

3.4.1、管理parser消费binlog位点

canal server发送dump请求前需要确定mysql的同步位点,主要包括canal server启动,mysql主备切换,canal server主备切换,dump异常后重启等情况。

核心类:FailbackLogPositionManager.java
FailbackLogPositionManager:基于failover查找的机制完成meta的操作
应用场景:比如针对内存buffer,出现HA切换,先尝试从内存buffer区中找到lastest position,如果不存在才尝试找一下meta里消费的信息。

包括两个成员变量:

private final CanalLogPositionManager primary;
private final CanalLogPositionManager secondary;

1)其中primary是在内存中记录位点变化的管理器MemoryLogPositionManager,对应canal server中dump下来的binlog event最新位点。

2)secondary是位点管理器MetaLogPositionManager,它有一个成员变量metaManager是PeriodMixedMetaManager。
MetaLogPositionManager:是canal client消费信息管理器,他定时刷新canal client消费位点信息到zk上的位点管理器。

在parser模块解析binlog,并持久化存储成功后,会定时记录binlog的offest。

消费binlog找位点步骤:
Step1:先到primary中寻找canal server中dump下来的binlog event最新位点(内存中)
Step2:找不到就到secondary中寻找canal client成功消费位点

3.4.2、管理Canal Client消费Canal Server的位点

在canal client不断从canal server读取数据的过程中, canal client需要告知 canal server自己消费成功的位点,这样当发生canal client崩溃或者canal server崩溃重启后,都会考虑是否按照原来消费成功的位点之后继续消费或dump。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值