本文主要分析的部分是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. jQuery学习小结

    1.jQuery hide() 和 show() 通过 jQuery,您可以使用 hide() 和 show() 方法来隐藏和显示 HTML 元素: $("#hide").clic ...

  2. 深入理解springMVC思想

     转载:http://elf8848.iteye.com/blog/875830 深入理解Spring MVC 思想   目录  一.前言二.spring mvc 核心类与接口三.spring mvc ...

  3. 用xml还是json

    1.定义介绍 (1).XML定义 扩展标记语言 (Extensible Markup Language, XML) ,用于标记电子文件使其具有结构性的标记语言,可以用来标记数据.定义数据类型,是一种允 ...

  4. java并发包分析之———ConcurrentSkipListMap

    一.前言 concurrentHashMap与ConcurrentSkipListMap性能测试 在4线程1.6万数据的条件下,ConcurrentHashMap 存取速度是ConcurrentSki ...

  5. JavaScript怎么把对象里的数据整合进另外一个数组里

    https://blog.csdn.net/qq_26222859/article/details/70331833 var json1 = [ {"guoshui":[ 3000 ...

  6. Python进程与线程

    进程与线程:*进程: 进程是系统中程序执行和资源分配的基本单元, 每个进程都有自己的数据段(存储数据).代码段(存储代码).堆栈段(对象和变量). # 全局变量等资源在多个进程中不能          ...

  7. day10_cookie&session学习笔记

    一.会话概述 1.什么是会话?如同打电话. 会话可简单理解为:用户开一个浏览器,点击多个超链接,访问服务器多个web资源,然后关闭浏览器,整个过程称之为一个会话. 2.会话过程要解决的问题是什么?保持 ...

  8. 文本分类学习 (七)支持向量机SVM 的前奏 结构风险最小化和VC维度理论

    前言: 经历过文本的特征提取,使用LibSvm工具包进行了测试,Svm算法的效果还是很好的.于是开始逐一的去了解SVM的原理. SVM 是在建立在结构风险最小化和VC维理论的基础上.所以这篇只介绍关于 ...

  9. 前端打包工具——build release介绍

    前言 对于前端开发者来说,资源打包是日常过程中一个必不可少的过程:目前我们大多数时候使用grunt.gulp.webpack这三个工具来完成这个工作:但是有一个特点就是我们没创建一个项目都要对应的去编 ...

  10. PHP中的 $_SERVER 函数说明详解

    用php在开发软件的时候,我们经常用到 $_SERVER[]这个函数,今天就来讲下这个数组的值,方便以后使用: A: $_SERVER['ARGC'] #包含传递给程序的 命令行参数的个数(如果运行在 ...