binlog的寻找过程可能的场景如下:

  • instance第一次启动
  • 发生数据库主备切换
  • canal server HA情况下的切换

所以这个过程是能够保证binlog不丢失的关键点。

本文从源码的角度来分析下启动过程中的binlog寻找过程。

一、流程图

下图是根据源码画出的流程图,需要结合源码分析来一起看。

二、源码分析

入口在AbstractEventParser的start()方法中,这个start方法其实是instance的整个启动过程。具体启动过程中都做了哪些事情,请见另一篇文章的分析。这块不再赘述。我们主要看的地方是

  1. // 4. 获取最后的位置信息
  2. EntryPosition position = findStartPosition(erosaConnection);

这一行就是获取binlog的解析位置,也是本文着重要分析的地方。因为我们目前所配置的都是MysqlEventParser,所以我们分析的也是这个类中的相关代码。

  1. protected EntryPosition findStartPosition(ErosaConnection connection) throws IOException {
  2. if (isGTIDMode()) {
  3. // GTID模式下,CanalLogPositionManager里取最后的gtid,没有则取instanc配置中的
  4. LogPosition logPosition = getLogPositionManager().getLatestIndexBy(destination);
  5. if (logPosition != null) {
  6. return logPosition.getPostion();
  7. }
  8. if (StringUtils.isNotEmpty(masterPosition.getGtid())) {
  9. return masterPosition;
  10. }
  11. }
  12. EntryPosition startPosition = findStartPositionInternal(connection);
  13. if (needTransactionPosition.get()) {
  14. logger.warn("prepare to find last position : {}", startPosition.toString());
  15. Long preTransactionStartPosition = findTransactionBeginPosition(connection, startPosition);
  16. if (!preTransactionStartPosition.equals(startPosition.getPosition())) {
  17. logger.warn("find new start Transaction Position , old : {} , new : {}",
  18. startPosition.getPosition(),
  19. preTransactionStartPosition);
  20. startPosition.setPosition(preTransactionStartPosition);
  21. }
  22. needTransactionPosition.compareAndSet(true, false);
  23. }
  24. return startPosition;
  25. }

2.1 GTID模式

我们目前的数据库架构一般都是M-S,所以binlog的位点很可能不一致,这就需要开启数据库GTID模式(通过在instance.properties中配置canal.instance.gtidon=true即可开启),这是一个全局的事务ID,能够防止主从位点不一致的情况下,找不到位点的问题。目前这块是从CanalLogPositionManager中取最后的GTID。default-instance.xml中,使用的CanalLogPositionManager是FailbackLogPositionManager,一个两级的位点管理器,XML配置如下:

  1. <!-- 解析位点记录 -->
  2. <property name="logPositionManager">
  3. <bean class="com.alibaba.otter.canal.parse.index.FailbackLogPositionManager">
  4. <constructor-arg>
  5. <bean class="com.alibaba.otter.canal.parse.index.MemoryLogPositionManager" />
  6. </constructor-arg>
  7. <constructor-arg>
  8. <bean class="com.alibaba.otter.canal.parse.index.MetaLogPositionManager">
  9. <constructor-arg ref="metaManager"/>
  10. </bean>
  11. </constructor-arg>
  12. </bean>
  13. </property>

一级是放到本地缓存中,第二级直接打了个info日志,有点弱,其实考虑的情况是性能,另一个考虑可能是因为DB的主从切换,并不会导致instance挂掉,内存中还是存储了之前DB的一些解析位点信息。其实都没有放到zk中,不利于做HA,所以这块目前还不是很完善,真正要使用GTID的话,需要对CanalLogPositionManager进行修改。目前已经提供了其他的一些实现,包括定时刷新到zk中等等。

如果CanalLogPositionManager中没有存储的话,也可以在instance.properties里面指定位点和GTID信息,也能从binlog中获取。

2.2 非GTID模式

如果canal没有开启GTID模式,那么我们就需要走一个binlog的寻找过程。

  1. EntryPosition startPosition = findStartPositionInternal(connection);

这个方法是个冗长的方法,里面的判断逻辑就是上面的流程图,我们来梳理一下。

首先还是从CanalLogPositionManager中获取,也就是基本上从内存中获取LogPosition。

  1. LogPosition logPosition = logPositionManager.getLatestIndexBy(destination);

2.2.1 内存中不存在LogPosition

2.2.1.1

首先判断配置文件中的主库信息是否与当前的数据库连接connection的地址一致,如果一致,如果一致,那么直接取properties文件中的master的位点信息。

2.2.1.2

如果主库不一致,那么判断从库standby的connection地址,如果是从库,那么直接取从库的位点信息。

我们可以在xml配置中看到properties的一些信息。

  1. <!-- 解析起始位点 -->
  2. <property name="masterPosition">
  3. <bean class="com.alibaba.otter.canal.protocol.position.EntryPosition">
  4. <property name="journalName" value="${canal.instance.master.journal.name}" />
  5. <property name="position" value="${canal.instance.master.position}" />
  6. <property name="timestamp" value="${canal.instance.master.timestamp}" />
  7. <property name="gtid" value="${canal.instance.master.gtid}" />
  8. </bean>
  9. </property>
  10. <property name="standbyPosition">
  11. <bean class="com.alibaba.otter.canal.protocol.position.EntryPosition">
  12. <property name="journalName" value="${canal.instance.standby.journal.name}" />
  13. <property name="position" value="${canal.instance.standby.position}" />
  14. <property name="timestamp" value="${canal.instance.standby.timestamp}" />
  15. <property name="gtid" value="${canal.instance.standby.gtid}" />
  16. </bean>
  17. </property>

2.2.1.3

如果内存中没有,配置文件中也没有,那么系统默认从当前时间开始消费。

  1. entryPosition = findEndPositionWithMasterIdAndTimestamp(mysqlConnection); // 默认从当前最后一个位置进行消费
  2. protected EntryPosition findEndPositionWithMasterIdAndTimestamp(MysqlConnection connection) {
  3. MysqlConnection mysqlConnection = (MysqlConnection) connection;
  4. final EntryPosition endPosition = findEndPosition(mysqlConnection);//获取当前最新的位点信息
  5. if (tableMetaTSDB != null) {
  6. long startTimestamp = System.currentTimeMillis();
  7. return findAsPerTimestampInSpecificLogFile(mysqlConnection,
  8. startTimestamp,
  9. endPosition,
  10. endPosition.getJournalName(),
  11. true);
  12. } else {
  13. return endPosition;
  14. }
  15. }

这里的findEndPosition()方法,其实就是执行了一个Mysql命令:

  1. show master status

返回的内容中,包含binlog文件信息和位点position,甚至包括GTID信息。

找到了最新的binlog位点信息后,根据当前时间戳和binlog的时间戳等信息,去服务器上面寻找binlog。其实逻辑基本上都在findAsPerTimestampInSpecificLogFile()中,这个方法是根据时间戳去寻找,离时间戳最近(小于时间戳)的一个事务起始位置。由于这块的代码比较长,所以我们只做分析,不做代码粘贴,具体的代码在MysqlEventParser这个类中。整个寻找的过程如下:

先看一下这个seek的过程,见代码注释:

  1. /**
  2. * 加速主备切换时的查找速度,做一些特殊优化,比如只解析事务头或者尾
  3. */
  4. public void seek(String binlogfilename, Long binlogPosition, SinkFunction func) throws IOException {
  5. updateSettings();//在mysql中执行一些dump之前的命令
  6. sendBinlogDump(binlogfilename, binlogPosition);//指定位点和binlog文件,发送dump命令,COM_BINLOG_DUMP
  7. DirectLogFetcher fetcher = new DirectLogFetcher(connector.getReceiveBufferSize());
  8. fetcher.start(connector.getChannel());//开始获取
  9. LogDecoder decoder = new LogDecoder();
  10. decoder.handle(LogEvent.ROTATE_EVENT);
  11. decoder.handle(LogEvent.FORMAT_DESCRIPTION_EVENT);
  12. decoder.handle(LogEvent.QUERY_EVENT);
  13. decoder.handle(LogEvent.XID_EVENT);
  14. LogContext context = new LogContext();
  15. while (fetcher.fetch()) {//遍历获取
  16. LogEvent event = null;
  17. event = decoder.decode(fetcher, context);//解析为event
  18. if (event == null) {
  19. throw new CanalParseException("parse failed");
  20. }
  21. if (!func.sink(event)) {//调用SinkFunction.sink()过滤
  22. break;
  23. }
  24. }
  25. }

下面我们看下数据过滤这块:

  • 起始位置为4,也就是跳过一个魔法值,具体可以看binlog的结构说明
  • 之后就是一个过滤的过程
    • 首先把事件event解析一个entry,这个entry使用的是消息模型EntryProtocol.proto
    • 首先判断当前事件是否为事务开始或者结束的位置,如果是,判断事件的时间,如果在我们需要的时间之后,直接过滤这条entry
    • 如果当前entry的binlog文件名和最新的binlog文件名相同,并且最新的位点小于entry的位点,那么直接过滤
    • 如果当前entry的类型表示的是事务开始或者事务结束,那么直接取当前entry的位点信息,利用当前entry构建位点信息,也就是找到了我们需要的事务起点。

2.2.1.4

如果binlog文件名为空,首先判断时间戳是否存在,如果存在,那么直接按照时间戳去取,否则默认从当前最后一个位置进行消费。

  1. // 如果没有指定binlogName,尝试按照timestamp进行查找
  2. if (entryPosition.getTimestamp() != null && entryPosition.getTimestamp() > 0L) {
  3. logger.warn("prepare to find start position {}:{}:{}",
  4. new Object[] { "", "", entryPosition.getTimestamp() });
  5. return findByStartTimeStamp(mysqlConnection, entryPosition.getTimestamp());
  6. } else {
  7. logger.warn("prepare to find start position just show master status");
  8. return findEndPositionWithMasterIdAndTimestamp(mysqlConnection); // 默认从当前最后一个位置进行消费
  9. }

这块我们看下findByStartTimestamp()这个方法,也就是只根据时间来查找binlog。这块的逻辑是这样的:

  • 首先获取最新和最老的binlog文件
  • 从最新的binlog中,根据时间去找,调用的方法也是findAsPerTimestampInSpecificLogFile()
  • 如果已经从最新的到最老的binlog文件中找遍了,没找到,说明根本没有对应时间的binlog
  • 否则不断的遍历binlog文件,因为binlog文件名的后缀都是连续的,所以可以很快的寻找

2.2.1.5

binlog文件名不为空,首先判断是否有位点信息,如果有的话,直接根据当前内存中存储的位点和文件信息去Mysql获取。

否则,根据当前内存中管理的时间戳去获取,根据时间戳和binlog文件名去获取位点。当然,如果时间戳也不存在,直接从binlog文件名的文件开头去获取binlog。

2.2.2 内存中存在历史成功记录

2.2.2.1 内存中的位点信息对应的数据库ip和当前连接的ip一致

如果dump错误的次数超过了一定的阈值,默认是2次,也就是连续几次定位失败,有几种情况:

  • binlog位点被删除
  • vip模式的mysql,发生了主备切换

这种需要进行一次判断,判断内容:

  1. boolean case2 = (standbyInfo == null || standbyInfo.getAddress() == null)
  2. && logPosition.getPostion().getServerId() != null
  3. && !logPosition.getPostion().getServerId().equals(findServerId(mysqlConnection));

判断几个,第一个配置文件中的standby为空,第二个内存中的logPosition存在数据库ip,第三个内存中的logPosition的数据库ip和当前数据库连接connection的数据库ip不一致。

满足这三个条件,说明发生了vip的主备切换,此时需要把logPosition中的时间戳向前推一个回退时间,默认60s,然后根据新的时间戳去找binlog文件和位点信息。

  1. if (case2) {
  2. long timestamp = logPosition.getPostion().getTimestamp();
  3. long newStartTimestamp = timestamp - fallbackIntervalInSeconds * 1000;
  4. logger.warn("prepare to find start position by last position {}:{}:{}", new Object[] { "", "",
  5. logPosition.getPostion().getTimestamp() });
  6. EntryPosition findPosition = findByStartTimeStamp(mysqlConnection, newStartTimestamp);
  7. // 重新置为一下
  8. dumpErrorCount = 0;
  9. return findPosition;
  10. }

2.2.2.2 不一致的情况

说明发生了主从切换,这种情况下,直接把logPosition中的时间回退60s,然后根据回退后的时间去binlog中寻找,然后返回。

【源码分析】Canal之Binlog的寻找过程的更多相关文章

  1. Spring AOP 源码分析 - 拦截器链的执行过程

    1.简介 本篇文章是 AOP 源码分析系列文章的最后一篇文章,在前面的两篇文章中,我分别介绍了 Spring AOP 是如何为目标 bean 筛选合适的通知器,以及如何创建代理对象的过程.现在我们的得 ...

  2. Spring IOC 容器源码分析 - 创建单例 bean 的过程

    1. 简介 在上一篇文章中,我比较详细的分析了获取 bean 的方法,也就是getBean(String)的实现逻辑.对于已实例化好的单例 bean,getBean(String) 方法并不会再一次去 ...

  3. [源码分析] 消息队列 Kombu 之 启动过程

    [源码分析] 消息队列 Kombu 之 启动过程 0x00 摘要 本系列我们介绍消息队列 Kombu.Kombu 的定位是一个兼容 AMQP 协议的消息队列抽象.通过本文,大家可以了解 Kombu 是 ...

  4. 【Canal源码分析】Sink及Store工作过程

    一.序列图 二.源码分析 2.1 Sink Sink阶段所做的事情,就是根据一定的规则,对binlog数据进行一定的过滤.我们之前跟踪过parser过程的代码,发现在parser完成后,会把数据放到一 ...

  5. 【Netty源码分析】客户端connect服务端过程

    上一篇博客[Netty源码分析]Netty服务端bind端口过程 我们介绍了服务端绑定端口的过程,这一篇博客我们介绍一下客户端连接服务端的过程. ChannelFuture future = boos ...

  6. Tomcat源码分析 (八)----- HTTP请求处理过程(一)

    终于进行到Connector的分析阶段了,这也是Tomcat里面最复杂的一块功能了.Connector中文名为连接器,既然是连接器,它肯定会连接某些东西,连接些什么呢? Connector用于接受请求 ...

  7. 精尽Spring Boot源码分析 - SpringApplication 启动类的启动过程

    该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...

  8. 【SpringBoot源码分析】-Bean的加载过程

    -- 以下内容均基于2.1.8.RELEASE版本 在<SpringBoot启动过程的分析>系列文章中简要的对SpringBoot整体的启动流程作了梳理,但并未针对诸多细节进行分析.前面的 ...

  9. Spark源码分析(一)-Standalone启动过程

    原创文章,转载请注明: 转载自http://www.cnblogs.com/tovin/p/3858065.html 为了更深入的了解spark,现开始对spark源码进行分析,本系列文章以spark ...

随机推荐

  1. JavaScript中对象数组 根据某个属性值 然后push到新的数组

    原文链接 https://segmentfault.com/q/1010000010075035 将下列对象数组中,工资大于1w的员工,增加到对象数组 WanSalary中 var BaiduUser ...

  2. EF Core使用SQL调用返回其他类型的查询

    假设你想要 SQL 本身编写,而不使用 LINQ. 需要运行 SQL 查询中返回实体对象之外的内容. 在 EF Core 中,执行该操作的另一种方法是编写 ADO.NET 代码,并从 EF 获取数据库 ...

  3. 发布开源库到JCenter所遇到的一些问题记录

    这周末自己瞎折磨了下,如何发布开源库到 JCenter,然后这过程中碰到了一些问题,在此记录分享一下 本篇是基于上一篇:教你一步步发布一个开源库到 JCenter 介绍的流程.步骤中所遇到的问题,所以 ...

  4. 浅谈 RxAndroid + Retrofit + Databinding

    http://jcodecraeer.com/a/anzhuokaifa/androidkaifa/2016/0131/3930.html 最近 RxAndroid .MVP.MVVM 一直是 And ...

  5. AQS分析(AbstractQueuedSynchronizer)(三)

    1.AQS是什么 AQS同步器是Java并发编程的基础,从资源共享的角度分成独占和共享两种模式,像ReentrantLock.ThreadPoolExecutor.CountDownLatch等都是基 ...

  6. JeeSite数据分页与翻页

    本文章介绍的是JeeSite开源项目二次开发时的一些笔记,对于没有使用过JeeSite的可以不用往下看了,因为下面的代码是跟JeeSite二次开发相关的代码,不做JeeSite的二次开发,以下代码对您 ...

  7. flex 生成多边形时内、外环计算

    //顺时钟 var pA:Array = [{x:"2969925.6674000006",y:"476254.4874999998"},{x:"29 ...

  8. 浅谈编程语言中的新宠Python,你叫它如何不火?

    论述 凡是对编程有所关注的朋友都已经知道,Python公布于1991年,即使出现的时间不是很遥远,但是在众多爱好者的贡献下已经发展到全民Python的地步. Python最近火起来的笼统原因:面向企业 ...

  9. python_code list_3

    >>> seq=['foo','x41','?','***']>>> def func(x): return x.isalnum() >>> li ...

  10. PAT1086:Tree Traversals Again

    1086. Tree Traversals Again (25) 时间限制 200 ms 内存限制 65536 kB 代码长度限制 16000 B 判题程序 Standard 作者 CHEN, Yue ...