【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开发框架 ...
 
随机推荐
- getElementById 用法的一个技巧
			
假设实现把 TextBox1 的字符实时的拷贝到 TextBox2 中,代码如下: <Script language="Javascript"> fun ...
 - webservice入门简介
			
为了梦想,努力奋斗! 追求卓越,成功就会在不经意间追上你 webservice入门简介 1.什么是webservice? webservice是一种跨编程语言和跨操作系统平台的远程调用技术. 所谓的远 ...
 - C++string函数之strcat_s
			
跟上一篇的strcpy_s一样,是新推出的较为安全的strcat函数 strcat_s脱胎于strcat,用于两个字符串的链接,strcat(str1,str2)直接返回新的str1. 但在vs200 ...
 - ruby1.9.2 +windowxp
			
ruby1.9.2 install on the window xp 1:在公司上網是有windows代理的(ntlm),而rails又都是gem安裝,對于接觸rails不多的人來時真是一場災難,我是 ...
 - C语言中函数中传入一个数组,并且返回一个数组
			
一.C语言可以很容易将一个数组传递给一个自定义函数,格式如下: main() { adb(float a[],int n); } float adb(float a[],int n) { …… ret ...
 - php数据导出excel
			
/** * 导出数据为excel表格 *@param $data 一个二维数组,结构如同从数据库查出来的数组 *@param $title excel的第一行标题,一个数组,如果为空则没有标题 *@p ...
 - NGINX按天生成日志文件的简易配置
			
NGINX按天生成日志文件的简易配置 0x01 最近后端童鞋遇到一个小需求,拆分nginx生成的log文件,最好是按天生成,看着她还有很多bug待改的状态,我说这个简单啊,我来吧.曾经搞node后端的 ...
 - 利用Hive分析nginx日志
			
这里用到的nginx日志是网站的访问日志,比如日志格式: 180.173.250.74 - - [08/Jan/2015:12:38:08 +0800] "GET /avatar/xxx.p ...
 - idea 整合ssm 启动页404问题
 - zfs文件系统简单使用
			
关于ubuntu下zfs的使用参考:https://github.com/zfsonlinux/zfs/wiki/Ubuntu%2016.04%20Root%20on%20ZFS 安装zfs: 启动z ...