一、序列图

1.1 启动

1.2 停止

二、源码分析

2.1 启动

这部分代码其实在ServerRunningMonitor的start()方法中。针对不同的destination,启动不同的CanalInstance。主要的方法在于initRunning()。

  1. private void initRunning() {
  2. if (!isStart()) {
  3. return;
  4. }
  5. String path = ZookeeperPathUtils.getDestinationServerRunning(destination);
  6. // 序列化
  7. byte[] bytes = JsonUtils.marshalToByte(serverData);
  8. try {
  9. mutex.set(false);
  10. zkClient.create(path, bytes, CreateMode.EPHEMERAL);
  11. activeData = serverData;
  12. processActiveEnter();// 触发一下事件
  13. mutex.set(true);
  14. } catch (ZkNodeExistsException e) {
  15. bytes = zkClient.readData(path, true);
  16. if (bytes == null) {// 如果不存在节点,立即尝试一次
  17. initRunning();
  18. } else {
  19. activeData = JsonUtils.unmarshalFromByte(bytes, ServerRunningData.class);
  20. }
  21. } catch (ZkNoNodeException e) {
  22. zkClient.createPersistent(ZookeeperPathUtils.getDestinationPath(destination), true); // 尝试创建父节点
  23. initRunning();
  24. }
  25. }

首先在zk中新增一个临时节点,表示的是正在运行destination的ip和端口,然后触发一下processActiveEnter()。我们主要看下这个方法,在controller启动时定义的。

  1. public void processActiveEnter() {
  2. try {
  3. MDC.put(CanalConstants.MDC_DESTINATION, String.valueOf(destination));
  4. embededCanalServer.start(destination);
  5. } finally {
  6. MDC.remove(CanalConstants.MDC_DESTINATION);
  7. }
  8. }
  9. public void start(final String destination) {
  10. final CanalInstance canalInstance = canalInstances.get(destination);
  11. if (!canalInstance.isStart()) {
  12. try {
  13. MDC.put("destination", destination);
  14. canalInstance.start();
  15. logger.info("start CanalInstances[{}] successfully", destination);
  16. } finally {
  17. MDC.remove("destination");
  18. }
  19. }
  20. }

主要在embededCanalServer.start中,我们看下这个canalInstance.start(),跟踪到AbstractCanalInstance。

2.1.1 启动metaManager

在默认的instance配置文件中,我们选择的metaManager是PeriodMixedMetaManager,定时(默认1s)刷新数据到zk中,所以我们主要关注这个类的start方法。这个类继承了MemoryMetaManager,首先启动一个MemoryMetaManager,然后再启动一个ZooKeeperMetaManager。

2.1.1.1 获取所有destination和client

  1. destinations = MigrateMap.makeComputingMap(new Function<String, List<ClientIdentity>>() {
  2. public List<ClientIdentity> apply(String destination) {
  3. return zooKeeperMetaManager.listAllSubscribeInfo(destination);
  4. }
  5. });

从/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。

  1. cursors = MigrateMap.makeComputingMap(new Function<ClientIdentity, Position>() {
  2. public Position apply(ClientIdentity clientIdentity) {
  3. Position position = zooKeeperMetaManager.getCursor(clientIdentity);
  4. if (position == null) {
  5. return nullCursor; // 返回一个空对象标识,避免出现异常
  6. } else {
  7. return position;
  8. }
  9. }
  10. });

有可能返回一个空。

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的范围。

  1. batches = MigrateMap.makeComputingMap(new Function<ClientIdentity, MemoryClientIdentityBatch>() {
  2. public MemoryClientIdentityBatch apply(ClientIdentity clientIdentity) {
  3. // 读取一下zookeeper信息,初始化一次
  4. MemoryClientIdentityBatch batches = MemoryClientIdentityBatch.create(clientIdentity);
  5. Map<Long, PositionRange> positionRanges = zooKeeperMetaManager.listAllBatchs(clientIdentity);
  6. for (Map.Entry<Long, PositionRange> entry : positionRanges.entrySet()) {
  7. batches.addPositionRange(entry.getValue(), entry.getKey()); // 添加记录到指定batchId
  8. }
  9. return batches;
  10. }
  11. });

2.1.1.4 启动定时刷zk任务

  1. // 启动定时工作任务
  2. executor.scheduleAtFixedRate(new Runnable() {
  3. public void run() {
  4. List<ClientIdentity> tasks = new ArrayList<ClientIdentity>(updateCursorTasks);
  5. for (ClientIdentity clientIdentity : tasks) {
  6. try {
  7. // 定时将内存中的最新值刷到zookeeper中,多次变更只刷一次
  8. zooKeeperMetaManager.updateCursor(clientIdentity, getCursor(clientIdentity));
  9. updateCursorTasks.remove(clientIdentity);
  10. } catch (Throwable e) {
  11. // ignore
  12. logger.error("period update" + clientIdentity.toString() + " curosr failed!", e);
  13. }
  14. }
  15. }
  16. }, period, period, TimeUnit.MILLISECONDS);

定时刷新position到zk后,从任务中删除。刷新的频率为1s。

2.1.2 启动alarmHandler

这块比较简单。

  1. if (!alarmHandler.isStart()) {
  2. alarmHandler.start();
  3. }

其实默认是LogAlarmHandler,用于发送告警信息的。

2.1.3 启动eventStore

启动EventStore,默认是MemoryEventStoreWithBuffer。start方法也比较简单。

  1. public void start() throws CanalStoreException {
  2. super.start();
  3. if (Integer.bitCount(bufferSize) != 1) {
  4. throw new IllegalArgumentException("bufferSize must be a power of 2");
  5. }
  6. indexMask = bufferSize - 1;
  7. entries = new Event[bufferSize];
  8. }

2.1.4 启动eventSink

这块默认是EntryEventSink。这块也不复杂。

  1. public void start() {
  2. super.start();
  3. Assert.notNull(eventStore);
  4. for (CanalEventDownStreamHandler handler : getHandlers()) {
  5. if (!handler.isStart()) {
  6. handler.start();
  7. }
  8. }
  9. }

正常的启动,将running状态置为true。

2.1.5 启动eventParser

  1. if (!eventParser.isStart()) {
  2. beforeStartEventParser(eventParser);
  3. eventParser.start();
  4. afterStartEventParser(eventParser);
  5. }

我们分别看下。

2.1.5.1 beforeStartEventParser

  1. protected void beforeStartEventParser(CanalEventParser eventParser) {
  2. boolean isGroup = (eventParser instanceof GroupEventParser);
  3. if (isGroup) {
  4. // 处理group的模式
  5. List<CanalEventParser> eventParsers = ((GroupEventParser) eventParser).getEventParsers();
  6. for (CanalEventParser singleEventParser : eventParsers) {// 需要遍历启动
  7. startEventParserInternal(singleEventParser, true);
  8. }
  9. } else {
  10. startEventParserInternal(eventParser, false);
  11. }
  12. }

判断是不是集群的parser(用于分库),如果是GroupParser,需要一个个启动CanalEventParser。我们主要看下startEventParserInternal方法。我们只关注MysqlEventParser,因为他支持HA。

  1. if (eventParser instanceof MysqlEventParser) {
  2. MysqlEventParser mysqlEventParser = (MysqlEventParser) eventParser;
  3. CanalHAController haController = mysqlEventParser.getHaController();
  4. if (haController instanceof HeartBeatHAController) {
  5. ((HeartBeatHAController) haController).setCanalHASwitchable(mysqlEventParser);
  6. }
  7. if (!haController.isStart()) {
  8. haController.start();
  9. }
  10. }

启动一个HeartBeatHAController。主要作用是用于当parser失败次数超过阈值时,执行mysql的主备切换。

2.1.5.2 eventParser.start()

这里也区分是GroupParser还是单个的MysqlParser,其实最终都是启动Parser,不过前者是启动多个而已。我们看下单个的start方法。具体实现在AbstractMysqlEventParser中

  1. public void start() throws CanalParseException {
  2. if (enableTsdb) {
  3. if (tableMetaTSDB == null) {
  4. // 初始化
  5. tableMetaTSDB = TableMetaTSDBBuilder.build(destination, tsdbSpringXml);
  6. }
  7. }
  8. super.start();
  9. }

首先如果启用了Tsdb功能(也就是DDL后表结构的回溯),那么需要从xml中初始化表结构源数据,然后调用AbstractEventParser的start方法。

  • 首先初始化缓冲队列transactionBuffer,默认队列长度为1024
  • 初始化BinlogParser,将其running状态置为true
  • 启动工作线程parseThread,开始订阅binlog,这个线程中做的事在下一篇文章中有。

2.1.5.3 afterStartEventParser

  1. protected void afterStartEventParser(CanalEventParser eventParser) {
  2. // 读取一下历史订阅的filter信息
  3. List<ClientIdentity> clientIdentitys = metaManager.listAllSubscribeInfo(destination);
  4. for (ClientIdentity clientIdentity : clientIdentitys) {
  5. subscribeChange(clientIdentity);
  6. }
  7. }

这块订阅的主要是filter的变化。

  1. public boolean subscribeChange(ClientIdentity identity) {
  2. if (StringUtils.isNotEmpty(identity.getFilter())) {
  3. logger.info("subscribe filter change to " + identity.getFilter());
  4. AviaterRegexFilter aviaterFilter = new AviaterRegexFilter(identity.getFilter());
  5. boolean isGroup = (eventParser instanceof GroupEventParser);
  6. if (isGroup) {
  7. // 处理group的模式
  8. List<CanalEventParser> eventParsers = ((GroupEventParser) eventParser).getEventParsers();
  9. for (CanalEventParser singleEventParser : eventParsers) {// 需要遍历启动
  10. ((AbstractEventParser) singleEventParser).setEventFilter(aviaterFilter);
  11. }
  12. } else {
  13. ((AbstractEventParser) eventParser).setEventFilter(aviaterFilter);
  14. }
  15. }
  16. // filter的处理规则
  17. // a. parser处理数据过滤处理
  18. // b. sink处理数据的路由&分发,一份parse数据经过sink后可以分发为多份,每份的数据可以根据自己的过滤规则不同而有不同的数据
  19. // 后续内存版的一对多分发,可以考虑
  20. return true;
  21. }

至此,CanalInstance启动成功。

2.2 停止

同样的,停止的触发也是在ServerRunningMonitor中,停止的代码如下:

  1. public void stop() {
  2. super.stop();
  3. logger.info("stop CannalInstance for {}-{} ", new Object[] { canalId, destination });
  4. if (eventParser.isStart()) {
  5. beforeStopEventParser(eventParser);
  6. eventParser.stop();
  7. afterStopEventParser(eventParser);
  8. }
  9. if (eventSink.isStart()) {
  10. eventSink.stop();
  11. }
  12. if (eventStore.isStart()) {
  13. eventStore.stop();
  14. }
  15. if (metaManager.isStart()) {
  16. metaManager.stop();
  17. }
  18. if (alarmHandler.isStart()) {
  19. alarmHandler.stop();
  20. }
  21. logger.info("stop successful....");
  22. }

2.2.1 停止EventParser

和启动一样,在前后也可以做一些事情。

  • 停止前,目前默认什么都不做;
  • 停止时,我们主要看MysqlEventParser
    • 首先断开mysql的连接
    • 清理缓存中表结构源数据tableMetaCache.clearTableMeta()
    • 调用AbstractMysqlEventParser的stop方法,首先从spring上下文中,删除tableMetaTSDB。然后调用AbstractEventParser中的stop方法。
  1. public void stop() {
  2. super.stop();
  3. stopHeartBeat(); // 先停止心跳
  4. parseThread.interrupt(); // 尝试中断
  5. eventSink.interrupt();
  6. try {
  7. parseThread.join();// 等待其结束
  8. } catch (InterruptedException e) {
  9. // ignore
  10. }
  11. if (binlogParser.isStart()) {
  12. binlogParser.stop();
  13. }
  14. if (transactionBuffer.isStart()) {
  15. transactionBuffer.stop();
  16. }
  17. }

首先关闭心跳的定时器,然后中断解析线程,等待当前运行的任务结束后,停止binlogParser,清空transactionBuffer。这里看下怎么清空transactionBuffer的。

  1. public void stop() throws CanalStoreException {
  2. putSequence.set(INIT_SQEUENCE);
  3. flushSequence.set(INIT_SQEUENCE);
  4. entries = null;
  5. super.stop();
  6. }

将put和flush的序列置为初始序列,也就是不再允许向队列中put数据。

停止parser后,停止位点管理和HAController。其实只是将running置为false。

2.2.2 停止EventSink

类似于启动,停止也不复杂。

  1. public void stop() {
  2. super.stop();
  3. for (CanalEventDownStreamHandler handler : getHandlers()) {
  4. if (handler.isStart()) {
  5. handler.stop();
  6. }
  7. }
  8. }

2.2.3 停止EventStore

主要部分在这边

  1. public void cleanAll() throws CanalStoreException {
  2. final ReentrantLock lock = this.lock;
  3. lock.lock();
  4. try {
  5. putSequence.set(INIT_SQEUENCE);
  6. getSequence.set(INIT_SQEUENCE);
  7. ackSequence.set(INIT_SQEUENCE);
  8. putMemSize.set(0);
  9. getMemSize.set(0);
  10. ackMemSize.set(0);
  11. entries = null;
  12. // for (int i = 0; i < entries.length; i++) {
  13. // entries[i] = null;
  14. // }
  15. } finally {
  16. lock.unlock();
  17. }
  18. }

其实也是将RingBuffer的指针置为初始值。

2.2.4 停止metaManager

我们看下PeriodMixedMetaManager。主要调用了两块的stop,一个是MemoryMetaManager,另一个是ZooKeeperMetaManager。清理内存中的数据,然后让zk的管理器running状态改为false。

2.2.5 停止alarmHandler

将running状态置为false。

【Canal源码分析】Canal Instance启动和停止的更多相关文章

  1. 【Canal源码分析】Canal Server的启动和停止过程

    本文主要解析下canal server的启动过程,希望能有所收获. 一.序列图 1.1 启动 1.2 停止 二.源码分析 整个server启动的过程比较复杂,看图难以理解,需要辅以文字说明. 首先程序 ...

  2. Tomcat源码分析之—具体启动流程分析

    从Tomcat启动调用栈可知,Bootstrap类的main方法为整个Tomcat的入口,在init初始化Bootstrap类的时候为设置Catalina的工作路径也就是Catalina_HOME信息 ...

  3. JVM源码分析之JVM启动流程

      原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 “365篇原创计划”第十四篇. 今天呢!灯塔君跟大家讲: JVM源码分析之JVM启动流程 前言: 执行Java类的main方法,程序就能运 ...

  4. 「从零单排canal 03」 canal源码分析大纲

    在前面两篇中,我们从基本概念理解了canal是一个什么项目,能应用于什么场景,然后通过一个demo体验,有了基本的体感和认识. 从这一篇开始,我们将从源码入手,深入学习canal的实现方式.了解can ...

  5. 【Canal源码分析】parser工作过程

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

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

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

  7. tomcat8 源码分析 | 组件及启动过程

    tomcat 8 源码分析 ,本文主要讲解tomcat拥有哪些组件,容器,又是如何启动的 推荐访问我的个人网站,排版更好看呦: https://chenmingyu.top/tomcat-source ...

  8. [Abp vNext 源码分析] - 1. 框架启动流程分析

    一.简要说明 本篇文章主要剖析与讲解 Abp vNext 在 Web API 项目下的启动流程,让大家了解整个 Abp vNext 框架是如何运作的.总的来说 ,Abp vNext 比起 ABP 框架 ...

  9. Jvm(jdk8)源码分析1-java命令启动流程详解

    JDK8加载源码分析 1.概述 现在大多数互联网公司都是使用java技术体系搭建自己的系统,所以对java开发工程师以及java系统架构师的需求非常的多,虽然普遍的要求都是需要熟悉各种java开发框架 ...

随机推荐

  1. JavaScript中将对象数组中的某个属性值,批量替换成另一个数值

    原文链接 https://segmentfault.com/q/1010000010352622 希望将下列数组中的sh替换成沪,sz替换成深 var stooges = [ {label:1,val ...

  2. Day5_模块与包(import)(form......import....)

    一个文件中定义了很多模块,然后可以再别的文件中调用这几个模块. #导入模块(import) #1,执行源文件 #2,产生以源文件为基础的全局名称空间.

  3. mysql Access denied for user \'root\'@\'localhost\'” 本人解决方案:

    直接上图   昨天还是好的今天就不行了,密码是没错的,就是本地的连接不上,Linux上的mysql可以连, 网上找各种解决方案,什么权限,什么加一句话,还有这个 如果连这个都进不去那就直接重装吧,其实 ...

  4. 界面渐变特效 -- CSS实现 -- 兼容IE8

    特别注意:里面的RGB颜色值必须要全写,不能使用缩写.左右:background: -webkit-gradient(linear, 0 0, 0 100%, from(#80c1e7), to(#2 ...

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

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

  6. LeeCode数组第15题三数之和

    题目:三数之和 内容: 给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?找出所有满足条件且不重复的三元组. 注意:答案中 ...

  7. dubbo+zookeeper+springboot构建服务

    本次和大家分享的是dubbo框架应用的初略配置和zookeeper注册中心的使用:说到注册中心现在我使用过的只有两种:zookeeper和Eureka,zk我结合dubbo来使用,而Eureka结合s ...

  8. IP路由及静态路由配置

    IP路由及静态路由配置 qianghaohao(CodingNutter) 链接来源:http://www.cnblogs.com/codingnutter/p/5654271.html 一.IP路由 ...

  9. C/C++静态代码安全检查工具

    静态代码安全检查工具是一种能够帮助程序员自动检测出源程序中是否存在安全缺陷的软件.它通过逐行分析程序的源代码,发现软件中潜在的安全漏洞.本文针对 C/C++语言程序设计中容易存在的多种安全问题,分别分 ...

  10. Java中的String类型

    1.基本类型和引用类型 在C语言里面,是有指针这么一个变量类型的,指针变量保存的就是所要指向内容的地址.在Java里面,没有了指针的这么个说法,而是换了一个词:引用类型变量. 先说Java里面的基本类 ...