【Canal源码分析】Canal Instance启动和停止
一、序列图
1.1 启动
1.2 停止
二、源码分析
2.1 启动
这部分代码其实在ServerRunningMonitor的start()方法中。针对不同的destination,启动不同的CanalInstance。主要的方法在于initRunning()。
private void initRunning() {
if (!isStart()) {
return;
}
String path = ZookeeperPathUtils.getDestinationServerRunning(destination);
// 序列化
byte[] bytes = JsonUtils.marshalToByte(serverData);
try {
mutex.set(false);
zkClient.create(path, bytes, CreateMode.EPHEMERAL);
activeData = serverData;
processActiveEnter();// 触发一下事件
mutex.set(true);
} catch (ZkNodeExistsException e) {
bytes = zkClient.readData(path, true);
if (bytes == null) {// 如果不存在节点,立即尝试一次
initRunning();
} else {
activeData = JsonUtils.unmarshalFromByte(bytes, ServerRunningData.class);
}
} catch (ZkNoNodeException e) {
zkClient.createPersistent(ZookeeperPathUtils.getDestinationPath(destination), true); // 尝试创建父节点
initRunning();
}
}
首先在zk中新增一个临时节点,表示的是正在运行destination的ip和端口,然后触发一下processActiveEnter()。我们主要看下这个方法,在controller启动时定义的。
public void processActiveEnter() {
try {
MDC.put(CanalConstants.MDC_DESTINATION, String.valueOf(destination));
embededCanalServer.start(destination);
} finally {
MDC.remove(CanalConstants.MDC_DESTINATION);
}
}
public void start(final String destination) {
final CanalInstance canalInstance = canalInstances.get(destination);
if (!canalInstance.isStart()) {
try {
MDC.put("destination", destination);
canalInstance.start();
logger.info("start CanalInstances[{}] successfully", destination);
} finally {
MDC.remove("destination");
}
}
}
主要在embededCanalServer.start中,我们看下这个canalInstance.start(),跟踪到AbstractCanalInstance。
2.1.1 启动metaManager
在默认的instance配置文件中,我们选择的metaManager是PeriodMixedMetaManager,定时(默认1s)刷新数据到zk中,所以我们主要关注这个类的start方法。这个类继承了MemoryMetaManager,首先启动一个MemoryMetaManager,然后再启动一个ZooKeeperMetaManager。
2.1.1.1 获取所有destination和client
destinations = MigrateMap.makeComputingMap(new Function<String, List<ClientIdentity>>() {
public List<ClientIdentity> apply(String destination) {
return zooKeeperMetaManager.listAllSubscribeInfo(destination);
}
});
从/otter/canal/destinations/{destination}获取所有的client信息,返回的内容是List,包括了destination、clientId、filter等等。
2.1.1.2 获取client指针cursor
根据ClientIdentity去zk获取指针,从zk的/otter/canal/destinations/{destination}/{clientId}/cursor下面去获取,返回的内容是个LogPosition。
cursors = MigrateMap.makeComputingMap(new Function<ClientIdentity, Position>() {
public Position apply(ClientIdentity clientIdentity) {
Position position = zooKeeperMetaManager.getCursor(clientIdentity);
if (position == null) {
return nullCursor; // 返回一个空对象标识,避免出现异常
} else {
return position;
}
}
});
有可能返回一个空。
2.1.1.3 获取批次batch
创建一个基于内存的MemoryClientIdentityBatch,包含位点的start、end、ack信息。然后从zk节点/otter/canal/destinations/{destination}/{clientId}/mark获取,取出来的数据进行排序,然后从/otter/canal/destinations/{destination}/{clientId}/mark/{batchId}中取出PositionRange这个类,描述的是一个position的范围。
batches = MigrateMap.makeComputingMap(new Function<ClientIdentity, MemoryClientIdentityBatch>() {
public MemoryClientIdentityBatch apply(ClientIdentity clientIdentity) {
// 读取一下zookeeper信息,初始化一次
MemoryClientIdentityBatch batches = MemoryClientIdentityBatch.create(clientIdentity);
Map<Long, PositionRange> positionRanges = zooKeeperMetaManager.listAllBatchs(clientIdentity);
for (Map.Entry<Long, PositionRange> entry : positionRanges.entrySet()) {
batches.addPositionRange(entry.getValue(), entry.getKey()); // 添加记录到指定batchId
}
return batches;
}
});
2.1.1.4 启动定时刷zk任务
// 启动定时工作任务
executor.scheduleAtFixedRate(new Runnable() {
public void run() {
List<ClientIdentity> tasks = new ArrayList<ClientIdentity>(updateCursorTasks);
for (ClientIdentity clientIdentity : tasks) {
try {
// 定时将内存中的最新值刷到zookeeper中,多次变更只刷一次
zooKeeperMetaManager.updateCursor(clientIdentity, getCursor(clientIdentity));
updateCursorTasks.remove(clientIdentity);
} catch (Throwable e) {
// ignore
logger.error("period update" + clientIdentity.toString() + " curosr failed!", e);
}
}
}
}, period, period, TimeUnit.MILLISECONDS);
定时刷新position到zk后,从任务中删除。刷新的频率为1s。
2.1.2 启动alarmHandler
这块比较简单。
if (!alarmHandler.isStart()) {
alarmHandler.start();
}
其实默认是LogAlarmHandler,用于发送告警信息的。
2.1.3 启动eventStore
启动EventStore,默认是MemoryEventStoreWithBuffer。start方法也比较简单。
public void start() throws CanalStoreException {
super.start();
if (Integer.bitCount(bufferSize) != 1) {
throw new IllegalArgumentException("bufferSize must be a power of 2");
}
indexMask = bufferSize - 1;
entries = new Event[bufferSize];
}
2.1.4 启动eventSink
这块默认是EntryEventSink。这块也不复杂。
public void start() {
super.start();
Assert.notNull(eventStore);
for (CanalEventDownStreamHandler handler : getHandlers()) {
if (!handler.isStart()) {
handler.start();
}
}
}
正常的启动,将running状态置为true。
2.1.5 启动eventParser
if (!eventParser.isStart()) {
beforeStartEventParser(eventParser);
eventParser.start();
afterStartEventParser(eventParser);
}
我们分别看下。
2.1.5.1 beforeStartEventParser
protected void beforeStartEventParser(CanalEventParser eventParser) {
boolean isGroup = (eventParser instanceof GroupEventParser);
if (isGroup) {
// 处理group的模式
List<CanalEventParser> eventParsers = ((GroupEventParser) eventParser).getEventParsers();
for (CanalEventParser singleEventParser : eventParsers) {// 需要遍历启动
startEventParserInternal(singleEventParser, true);
}
} else {
startEventParserInternal(eventParser, false);
}
}
判断是不是集群的parser(用于分库),如果是GroupParser,需要一个个启动CanalEventParser。我们主要看下startEventParserInternal方法。我们只关注MysqlEventParser,因为他支持HA。
if (eventParser instanceof MysqlEventParser) {
MysqlEventParser mysqlEventParser = (MysqlEventParser) eventParser;
CanalHAController haController = mysqlEventParser.getHaController();
if (haController instanceof HeartBeatHAController) {
((HeartBeatHAController) haController).setCanalHASwitchable(mysqlEventParser);
}
if (!haController.isStart()) {
haController.start();
}
}
启动一个HeartBeatHAController。主要作用是用于当parser失败次数超过阈值时,执行mysql的主备切换。
2.1.5.2 eventParser.start()
这里也区分是GroupParser还是单个的MysqlParser,其实最终都是启动Parser,不过前者是启动多个而已。我们看下单个的start方法。具体实现在AbstractMysqlEventParser中
public void start() throws CanalParseException {
if (enableTsdb) {
if (tableMetaTSDB == null) {
// 初始化
tableMetaTSDB = TableMetaTSDBBuilder.build(destination, tsdbSpringXml);
}
}
super.start();
}
首先如果启用了Tsdb功能(也就是DDL后表结构的回溯),那么需要从xml中初始化表结构源数据,然后调用AbstractEventParser的start方法。
- 首先初始化缓冲队列transactionBuffer,默认队列长度为1024
- 初始化BinlogParser,将其running状态置为true
- 启动工作线程parseThread,开始订阅binlog,这个线程中做的事在下一篇文章中有。
2.1.5.3 afterStartEventParser
protected void afterStartEventParser(CanalEventParser eventParser) {
// 读取一下历史订阅的filter信息
List<ClientIdentity> clientIdentitys = metaManager.listAllSubscribeInfo(destination);
for (ClientIdentity clientIdentity : clientIdentitys) {
subscribeChange(clientIdentity);
}
}
这块订阅的主要是filter的变化。
public boolean subscribeChange(ClientIdentity identity) {
if (StringUtils.isNotEmpty(identity.getFilter())) {
logger.info("subscribe filter change to " + identity.getFilter());
AviaterRegexFilter aviaterFilter = new AviaterRegexFilter(identity.getFilter());
boolean isGroup = (eventParser instanceof GroupEventParser);
if (isGroup) {
// 处理group的模式
List<CanalEventParser> eventParsers = ((GroupEventParser) eventParser).getEventParsers();
for (CanalEventParser singleEventParser : eventParsers) {// 需要遍历启动
((AbstractEventParser) singleEventParser).setEventFilter(aviaterFilter);
}
} else {
((AbstractEventParser) eventParser).setEventFilter(aviaterFilter);
}
}
// filter的处理规则
// a. parser处理数据过滤处理
// b. sink处理数据的路由&分发,一份parse数据经过sink后可以分发为多份,每份的数据可以根据自己的过滤规则不同而有不同的数据
// 后续内存版的一对多分发,可以考虑
return true;
}
至此,CanalInstance启动成功。
2.2 停止
同样的,停止的触发也是在ServerRunningMonitor中,停止的代码如下:
public void stop() {
super.stop();
logger.info("stop CannalInstance for {}-{} ", new Object[] { canalId, destination });
if (eventParser.isStart()) {
beforeStopEventParser(eventParser);
eventParser.stop();
afterStopEventParser(eventParser);
}
if (eventSink.isStart()) {
eventSink.stop();
}
if (eventStore.isStart()) {
eventStore.stop();
}
if (metaManager.isStart()) {
metaManager.stop();
}
if (alarmHandler.isStart()) {
alarmHandler.stop();
}
logger.info("stop successful....");
}
2.2.1 停止EventParser
和启动一样,在前后也可以做一些事情。
- 停止前,目前默认什么都不做;
- 停止时,我们主要看MysqlEventParser
- 首先断开mysql的连接
- 清理缓存中表结构源数据tableMetaCache.clearTableMeta()
- 调用AbstractMysqlEventParser的stop方法,首先从spring上下文中,删除tableMetaTSDB。然后调用AbstractEventParser中的stop方法。
public void stop() {
super.stop();
stopHeartBeat(); // 先停止心跳
parseThread.interrupt(); // 尝试中断
eventSink.interrupt();
try {
parseThread.join();// 等待其结束
} catch (InterruptedException e) {
// ignore
}
if (binlogParser.isStart()) {
binlogParser.stop();
}
if (transactionBuffer.isStart()) {
transactionBuffer.stop();
}
}
首先关闭心跳的定时器,然后中断解析线程,等待当前运行的任务结束后,停止binlogParser,清空transactionBuffer。这里看下怎么清空transactionBuffer的。
public void stop() throws CanalStoreException {
putSequence.set(INIT_SQEUENCE);
flushSequence.set(INIT_SQEUENCE);
entries = null;
super.stop();
}
将put和flush的序列置为初始序列,也就是不再允许向队列中put数据。
停止parser后,停止位点管理和HAController。其实只是将running置为false。
2.2.2 停止EventSink
类似于启动,停止也不复杂。
public void stop() {
super.stop();
for (CanalEventDownStreamHandler handler : getHandlers()) {
if (handler.isStart()) {
handler.stop();
}
}
}
2.2.3 停止EventStore
主要部分在这边
public void cleanAll() throws CanalStoreException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
putSequence.set(INIT_SQEUENCE);
getSequence.set(INIT_SQEUENCE);
ackSequence.set(INIT_SQEUENCE);
putMemSize.set(0);
getMemSize.set(0);
ackMemSize.set(0);
entries = null;
// for (int i = 0; i < entries.length; i++) {
// entries[i] = null;
// }
} finally {
lock.unlock();
}
}
其实也是将RingBuffer的指针置为初始值。
2.2.4 停止metaManager
我们看下PeriodMixedMetaManager。主要调用了两块的stop,一个是MemoryMetaManager,另一个是ZooKeeperMetaManager。清理内存中的数据,然后让zk的管理器running状态改为false。
2.2.5 停止alarmHandler
将running状态置为false。
【Canal源码分析】Canal Instance启动和停止的更多相关文章
- 【Canal源码分析】Canal Server的启动和停止过程
本文主要解析下canal server的启动过程,希望能有所收获. 一.序列图 1.1 启动 1.2 停止 二.源码分析 整个server启动的过程比较复杂,看图难以理解,需要辅以文字说明. 首先程序 ...
- Tomcat源码分析之—具体启动流程分析
从Tomcat启动调用栈可知,Bootstrap类的main方法为整个Tomcat的入口,在init初始化Bootstrap类的时候为设置Catalina的工作路径也就是Catalina_HOME信息 ...
- JVM源码分析之JVM启动流程
原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 “365篇原创计划”第十四篇. 今天呢!灯塔君跟大家讲: JVM源码分析之JVM启动流程 前言: 执行Java类的main方法,程序就能运 ...
- 「从零单排canal 03」 canal源码分析大纲
在前面两篇中,我们从基本概念理解了canal是一个什么项目,能应用于什么场景,然后通过一个demo体验,有了基本的体感和认识. 从这一篇开始,我们将从源码入手,深入学习canal的实现方式.了解can ...
- 【Canal源码分析】parser工作过程
本文主要分析的部分是instance启动时,parser的一个启动和工作过程.主要关注的是AbstractEventParser的start()方法中的parseThread. 一.序列图 二.源码分 ...
- 【Canal源码分析】Sink及Store工作过程
一.序列图 二.源码分析 2.1 Sink Sink阶段所做的事情,就是根据一定的规则,对binlog数据进行一定的过滤.我们之前跟踪过parser过程的代码,发现在parser完成后,会把数据放到一 ...
- tomcat8 源码分析 | 组件及启动过程
tomcat 8 源码分析 ,本文主要讲解tomcat拥有哪些组件,容器,又是如何启动的 推荐访问我的个人网站,排版更好看呦: https://chenmingyu.top/tomcat-source ...
- [Abp vNext 源码分析] - 1. 框架启动流程分析
一.简要说明 本篇文章主要剖析与讲解 Abp vNext 在 Web API 项目下的启动流程,让大家了解整个 Abp vNext 框架是如何运作的.总的来说 ,Abp vNext 比起 ABP 框架 ...
- Jvm(jdk8)源码分析1-java命令启动流程详解
JDK8加载源码分析 1.概述 现在大多数互联网公司都是使用java技术体系搭建自己的系统,所以对java开发工程师以及java系统架构师的需求非常的多,虽然普遍的要求都是需要熟悉各种java开发框架 ...
随机推荐
- 02_Linux学习_命令
帮助命令: xxx --help man xxx 列出当前目录下的目录和文件: ls ls -l ls --help ...
- JAVA堆栈的区别
1. 栈(stack)与堆(heap)都是Java用来在Ram中存放数据的地方.与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆. 2. 栈的优势是,存取速度 ...
- Pydev Console中文提示乱码的问题
1. 像这样的规则内容请这样处理"\u305d\u3093\u306a\u306b"style unicode string : print str.decode("un ...
- jvm垃圾回收(三)
一.分代思想(年轻代.老年代.永久代): 1.一个新人(new对象)会优先在伊甸园(Eden区)出生,当伊甸园(Eden区)人口达到最大容量时,JVM会派MinorGC去看看哪些人还有价值 2.伊甸园 ...
- CSS后代选择器“空格”和“>”的使用辨析
要点: 1. "空格":包含子孙 2. ">":含子不含孙 举个栗子: html代码如下 <body> <div class=" ...
- MySQL快速生成本地测试数据
利用数据的存储过程生成测试数据: 我们可以通过数据库的的 INSERT 语句直接在存储过程中向普通数据表中添加数据,但是 当我们添加到百万数据后,往普通表插入测试数据的性能就会明显降低.所以在这里建议 ...
- Spring Cloud Config - RSA简介以及使用RSA加密配置文件
简介 RSA非对称加密有着非常强大的安全性,HTTPS的SSL加密就是使用这种方法进行HTTPS请求加密传输的.因为RSA算法会涉及Private Key和Public Key分别用来加密和解密,所以 ...
- 佛祖镇楼,BUG避易
def FZZL(): print(" _ooOoo_ ") print(" o8888888o ") print(" 88 . 88 ") ...
- java原子操作的实现原理--转载
原文地址:http://www.infoq.com/cn/articles/atomic-operation 1. 引言 原子(atom)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic ...
- 开发自己的 chart - 每天5分钟玩转 Docker 容器技术(167)
Kubernetes 给我们提供了大量官方 chart,不过要部署微服务应用,还是需要开发自己的 chart,下面就来实践这个主题. 创建 chart 执行 helm create mychart 的 ...