本文主要分析的部分是instance启动时,parser的一个启动和工作过程。主要关注的是AbstractEventParser的start()方法中的parseThread。

一、序列图

二、源码分析

parseThread中包含的内容比较清晰,代码不是很长,我们逐步分析下。

2.1 构造数据库连接

erosaConnection = buildErosaConnection();

这里构造的,应该是一个mysql的链接,包括的内容都是从配置文件中过来的一些信息,包括mysql的地址,账号密码等。

2.2 启动心跳线程

startHeartBeat(erosaConnection);

这里的心跳,感觉是个假的心跳,并没有用到connection相关的内容。启动一个定时任务,默认3s发送一个心跳的binlog给sink阶段,表名parser还在工作。在sink阶段,会把心跳的binlog直接过滤,不会走到store过程。

2.3 dump之前准备工作

这一步的代码也不复杂。

preDump(erosaConnection);

我们看看preDump都能够做什么?在MysqlEventParser中,我们可以看到,主要做了几件事:

  • 针对binlog格式进行过滤,也就是我们在配置文件中指定binlog的格式,不过目前我们默认的都是ROW模式。
  • 针对binlog image进行过滤,目前默认是FULL,也就是binlog记录的是变更前后的数据,如果配置为minimal,那么只记录变更后的值,可以减少binlog的文件大小。
  • 构造表结构源数据的缓存TableMetaCache

2.4 获取最后的位置信息

这一步是比较核心的,也是保证binlog不丢失的核心代码。

EntryPosition position = findStartPosition(erosaConnection);
final EntryPosition startPosition = position;
if (startPosition == null) {
throw new CanalParseException("can't find start position for " + destination);
} if (!processTableMeta(startPosition)) {
throw new CanalParseException("can't find init table meta for " + destination
+ " with position : " + startPosition);
}

具体的findStartPosition是怎么实现的,请查阅下一篇文章

如果没有找到最后的位置信息,那么直接抛出异常,否则还要进行一次判断,也就是processTableMeta,我们看下这个方法做了什么。

protected boolean processTableMeta(EntryPosition position) {
if (isGTIDMode()) {
if (binlogParser instanceof LogEventConvert) {
// 记录gtid
((LogEventConvert) binlogParser).setGtidSet(MysqlGTIDSet.parse(position.getGtid()));
}
} if (tableMetaTSDB != null) {
if (position.getTimestamp() == null || position.getTimestamp() <= 0) {
throw new CanalParseException("use gtid and TableMeta TSDB should be config timestamp > 0");
} return tableMetaTSDB.rollback(position);
} return true;
}

如果开启了GTID模式,那么直接设置GTID集合。如果tableMetaTSDB不为空,那么直接根据位置信息回滚到对应的表结构。这个tableMetaTSDB记录的是一个表结构的时序,使用的是Druid的一个功能,把所有DDL记录在数据库中,一般来说,每24小时生成一份快照插入到数据库中,这样能解决DDL产生的表结构不一致的问题,也就是增加了一个表结构的回溯功能。

这边的rollback主要做的事情为:

  • 根据位置信息position从数据库去查询对应的信息,包括binlog文件名、位点等。然后记录到内存中,使用的Druid的SchemaRepository.console方法。

2.5 开始dump数据

在dump之前,代码中构造了一个sink类,也就是SinkFunction。里面定义了一个sink方法,主要的内容是对哪些数据进行过滤。

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); // 继续抛出异常,让上层统一感知
}

首先判断parser是否在运行,如果不运行,那么就直接抛弃。运行时,判断entry是否为空,不为空的情况下,直接将entry加入到transactionBuffer中。这里我们说下这个transactionBuffer,其实类似于Disruptor中的一个环形队列(默认长度为1024),维护了几个指针,包括put、get、ack三个指针,里面存储了需要进行传递到下一阶段的数据。

加到环形队列之后,记录一下当前的位置信息和时间。如果这个过程出错了,需要记录下出错的位置信息,这里的processSinkError其实就是打印了一下错误日志,然后抛出了一个CanalException,让上一层感知。

说了这么多,还没到真正开始dump的地方。下面开始吧。

if (isGTIDMode()) {
erosaConnection.dump(MysqlGTIDSet.parse(startPosition.getGtid()), sinkHandler);
} else {
if (StringUtils.isEmpty(startPosition.getJournalName()) && startPosition.getTimestamp() != null) {
erosaConnection.dump(startPosition.getTimestamp(), sinkHandler);
} else {
erosaConnection.dump(startPosition.getJournalName(),
startPosition.getPosition(),
sinkHandler);
}
}

在新版本中,增加了GTID的模式,所以这里的dump需要判断怎么dump,发送什么命令给mysql来获取什么样的binlog。

2.5.1 GTID模式

如果开启了GTID模式(在instance.properties开启),那么需要发送COM_BINLOG_DUMP_GTID命令,然后开始接受binlog信息,进行binlog处理。

public void dump(GTIDSet gtidSet, SinkFunction func) throws IOException {
updateSettings();
sendBinlogDumpGTID(gtidSet); DirectLogFetcher fetcher = new DirectLogFetcher(connector.getReceiveBufferSize());
fetcher.start(connector.getChannel());
LogDecoder decoder = new LogDecoder(LogEvent.UNKNOWN_EVENT, LogEvent.ENUM_END_EVENT);
LogContext context = new LogContext();
while (fetcher.fetch()) {
LogEvent event = null;
event = decoder.decode(fetcher, context); if (event == null) {
throw new CanalParseException("parse failed");
} if (!func.sink(event)) {
break;
}
}
}

调用LogDecoder.decode方法,对二进制进行解析,解析为我们需要的LogEvent,如果解析失败,抛出异常。否则进行sink,如果sink返回的false,那么直接跳过,否则加入到transactionBuffer中。

2.5.2 非GTID模式

这块有个逻辑判断,如果找到的最后的位置信息中包含了时间戳,如果没有binlog文件名,那么在MysqlConnection中直接报错,也就是必须既要有时间戳,又要有binlog文件名,才能进行dump操作。

这里的dump分了两步,第一步就是发送COM_REGISTER_SLAVE命令,伪装自己是一个slave,然后发送COM_BINLOG_DUMP命令接收binlog。

public void dump(String binlogfilename, Long binlogPosition, SinkFunction func) throws IOException {
updateSettings();
sendRegisterSlave();
sendBinlogDump(binlogfilename, binlogPosition);
DirectLogFetcher fetcher = new DirectLogFetcher(connector.getReceiveBufferSize());
fetcher.start(connector.getChannel());
LogDecoder decoder = new LogDecoder(LogEvent.UNKNOWN_EVENT, LogEvent.ENUM_END_EVENT);
LogContext context = new LogContext();
while (fetcher.fetch()) {
LogEvent event = null;
event = decoder.decode(fetcher, context); if (event == null) {
throw new CanalParseException("parse failed");
} if (!func.sink(event)) {
break;
} if (event.getSemival() == 1) {
sendSemiAck(context.getLogPosition().getFileName(), binlogPosition);
}
}
}

这里有个mysql半同步的标识,semival。如果semival==1,说明需要进行ack,发送SEMI_SYNC_ACK给master(我们这边more都不开启)。

2.5.3 异常处理

如果整个过程中发生了异常,有以下几种处理方式:

  • 没有找到表,说明起始的position在一个事务中,需要重新找到事务的开始点
  • 其他异常,processDumpError,如果是IO异常,而且message中包含errno = 1236错误,表示从master读取binlog发生致命错误,处理方法如下:http://blog.sina.com.cn/s/blog_a1e9c7910102wv2v.html。
  • 如果当前parser不在运行,抛出异常;如果在运行,抛出异常之后,发送一个告警信息。
  • 异常处理完成后,在finally中,首先将当前线程置为interrupt,然后关闭mysql连接。如果关闭连接过程中,抛出异常,需要进行处理。
  • 整个异常处理后,首先暂停sink过程,然后重置缓冲队列TransctionBuffer,重置binlogParser。最后,如果parser还在运行,那么sleep一段时间后重试。
} catch (TableIdNotFoundException e) {
exception = e;
// 特殊处理TableIdNotFound异常,出现这样的异常,一种可能就是起始的position是一个事务当中,导致tablemap
// Event时间没解析过
needTransactionPosition.compareAndSet(false, true);
logger.error(String.format("dump address %s has an error, retrying. caused by ",
runningInfo.getAddress().toString()), e);
} catch (Throwable e) {
processDumpError(e);
exception = e;
if (!running) {
if (!(e instanceof java.nio.channels.ClosedByInterruptException || e.getCause() instanceof java.nio.channels.ClosedByInterruptException)) {
throw new CanalParseException(String.format("dump address %s has an error, retrying. ",
runningInfo.getAddress().toString()), e);
}
} else {
logger.error(String.format("dump address %s has an error, retrying. caused by ",
runningInfo.getAddress().toString()), e);
sendAlarm(destination, ExceptionUtils.getFullStackTrace(e));
}
} finally {
// 重新置为中断状态
Thread.interrupted();
// 关闭一下链接
afterDump(erosaConnection);
try {
if (erosaConnection != null) {
erosaConnection.disconnect();
}
} catch (IOException e1) {
if (!running) {
throw new CanalParseException(String.format("disconnect address %s has an error, retrying. ",
runningInfo.getAddress().toString()),
e1);
} else {
logger.error("disconnect address {} has an error, retrying., caused by ",
runningInfo.getAddress().toString(),
e1);
}
}
}
// 出异常了,退出sink消费,释放一下状态
eventSink.interrupt();
transactionBuffer.reset();// 重置一下缓冲队列,重新记录数据
binlogParser.reset();// 重新置位 if (running) {
// sleep一段时间再进行重试
try {
Thread.sleep(10000 + RandomUtils.nextInt(10000));
} catch (InterruptedException e) {
}
}

【Canal源码分析】parser工作过程的更多相关文章

  1. Dubbo 源码分析 - 服务调用过程

    注: 本系列文章已捐赠给 Dubbo 社区,你也可以在 Dubbo 官方文档中阅读本系列文章. 1. 简介 在前面的文章中,我们分析了 Dubbo SPI.服务导出与引入.以及集群容错方面的代码.经过 ...

  2. MyBatis 源码分析 - 配置文件解析过程

    * 本文速览 由于本篇文章篇幅比较大,所以这里拿出一节对本文进行快速概括.本篇文章对 MyBatis 配置文件中常用配置的解析过程进行了较为详细的介绍和分析,包括但不限于settings,typeAl ...

  3. 源码分析HotSpot GC过程(三):TenuredGeneration的GC过程

    老年代TenuredGeneration所使用的垃圾回收算法是标记-压缩-清理算法.在回收阶段,将标记对象越过堆的空闲区移动到堆的另一端,所有被移动的对象的引用也会被更新指向新的位置.看起来像是把杂陈 ...

  4. SOFA 源码分析 —— 服务引用过程

    前言 在前面的 SOFA 源码分析 -- 服务发布过程 文章中,我们分析了 SOFA 的服务发布过程,一个完整的 RPC 除了发布服务,当然还需要引用服务. So,今天就一起来看看 SOFA 是如何引 ...

  5. 源码分析HotSpot GC过程(一)

    «上一篇:源码分析HotSpot GC过程(一)»下一篇:源码分析HotSpot GC过程(三):TenuredGeneration的GC过程 https://blogs.msdn.microsoft ...

  6. nodejs的Express框架源码分析、工作流程分析

    nodejs的Express框架源码分析.工作流程分析 1.Express的编写流程 2.Express关键api的使用及其作用分析 app.use(middleware); connect pack ...

  7. openVswitch(OVS)源码分析之工作流程(哈希桶结构体的解释)

    这篇blog是专门解决前篇openVswitch(OVS)源码分析之工作流程(哈希桶结构体的疑惑)中提到的哈希桶结构flex_array结构体成员变量含义的问题. 引用下前篇blog中分析讨论得到的f ...

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

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

  9. 【Canal源码分析】Canal Instance启动和停止

    一.序列图 1.1 启动 1.2 停止 二.源码分析 2.1 启动 这部分代码其实在ServerRunningMonitor的start()方法中.针对不同的destination,启动不同的Cana ...

随机推荐

  1. 【Web页面测试】测试点和测试用例

    1. 需求符合度测试 1. 各级菜单名称显示是否按照需求说明书规定的设计,并且没有遗漏和多余 2. 各级菜单所完成的功能是否按照需求说明书规定的设计,并且没有遗漏和多余 3. 各级菜单的操作顺序和操作 ...

  2. Day7 面向对象和类的介绍

    面向对象讲解: ''' 面向过程: 核心是过程二字,过程指的是问题的解决步骤,基于过程去设计程序,就好比在设计一条流水线,是一种机械式的思维方式. 优点:复杂的问题流程化,进而简单化 缺点:可扩展性差 ...

  3. vue-cli目录结构

  4. AI 学习之路

    前言:本文章纯属自己学习路线纪录,不喜勿喷. 最近AI很火,几乎是个程序员 都要去学习AI,作为一个菜鸡小前端,我也踏上了学习AI的方向. 在学习之中,最开始遇到了很多的困难,比如你不知道如何切入进来 ...

  5. J2EE--常见面试题总结 -- (二)

    1 Spring拦截器的基本功能是什么? 拦截器是基于Java的反射机制的,是在面向切面编程的就是在你的service或者一个方法,前调用一个方法,或者在方法后调用一个方法比如动态代理就是拦截器的简单 ...

  6. 微信小程序入门三实战

    微信小应用借鉴了很多web的理念,但是其与传统的webApp.微信公共号这些BS架构不同,他是CS架构,是客户端的程序 小程序开发实战--豆瓣电影 项目配置 -在app.jsop中进行简单配置 --n ...

  7. CSS学习笔记二:css 画立体图形

    继上一次学了如何去运用css画平面图形,这一次学如何去画正方体,从2D向着3D学习,虽然有点满,但总是一个过程,一点一点积累,然后记录起来. Transfrom3D 在这一次中运用到了一下几种属性: ...

  8. Selenium2Lib库之鼠标事件常用关键字实战

    1.2 鼠标事件常用关键字 1.2.1 Click Button关键字按F5 查看Click Button关键字的说明,如下图: Click Button关键字 是用于点击页面上的按钮.参数locat ...

  9. jmeter如何录制App及Web应用

    1.添加一个线程组(Test Plan上右键,添加_Threads_线程组) 2.添加一个HTTP代理服务器(Test Plan上右键,添加_非测试元件_HTTP代理服务器) 3.在HTTP代理服务器 ...

  10. ARCGIS 数据格式

    1. 开篇 刚开始接触 GIS 时,老师说过这样一句话"做我们这一行的,数据就是命,没有数据,什么都干不了".现在我们需要做一个 webgis 的小项目,体会到了这句阐述的精髓.数 ...