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

本文主要解析下canal server的启动过程,希望能有所收获。

一、序列图

1.1 启动

966953-20180524152430556-1029658713.png

1.2 停止

966953-20180524152441602-1839315166.png

二、源码分析

整个server启动的过程比较复杂,看图难以理解,需要辅以文字说明。

首先程序的入口在CanalLauncher的main方法中。

2.1 加载配置文件

String conf = System.getProperty("canal.conf", "classpath:canal.properties");
Properties properties = new Properties();
if (conf.startsWith(CLASSPATH_URL_PREFIX)) {
    conf = StringUtils.substringAfter(conf, CLASSPATH_URL_PREFIX);
    properties.load(CanalLauncher.class.getClassLoader().getResourceAsStream(conf));
} else {
    properties.load(new FileInputStream(conf));
}

从canal.properties文件中load所有的配置信息,加载到上下文中。不再赘述。

2.2 构造CanalController

根据配置文件来构造CanalController,这块的代码比较多,主要分为七个步骤,具体如下。

2.2.1 初始化全局参数配置

调用initGlobalConfig方法,过程如下:

  • 判断运行模式,是从spring加载还是manager加载,目前开源版本建议使用spring
  • 获取是否懒加载
  • 如果是manager模式启动,获取manager的ip地址;如果是spring模式启动,获取spring xml的文件地址,加载到全部配置中
  • 构造一个实例构造器CanalInstanceGenerator,我们用到的就是在spring的beanFactory中加上destination的bean,这个destination就是canal instance的名称

这块逻辑在CanalController的initGlobalConfig方法中。

2.2.2 初始化实例配置

这块的逻辑是从instance.properties里面初始化实例。

private void initInstanceConfig(Properties properties) {
    String destinationStr = getProperty(properties, CanalConstants.CANAL_DESTINATIONS);
    String[] destinations = StringUtils.split(destinationStr, CanalConstants.CANAL_DESTINATION_SPLIT);

    for (String destination : destinations) {
        InstanceConfig config = parseInstanceConfig(properties, destination);
        InstanceConfig oldConfig = instanceConfigs.put(destination, config);

        if (oldConfig != null) {
            logger.warn("destination:{} old config:{} has replace by new config:{}", new Object[] { destination,
                    oldConfig, config });
        }
    }
}

从这段代码中可以看出,我们在一个canal.properties文件中,可以配置多个destination,也就是可以配置多个instance,不同的instance以逗号隔开。这里主要看的是parseInstanceConfig()方法,里面的逻辑如下:

  • 获取启动模式,是manager还是spring,我们这边默认都是spring。
  • 获取懒加载字段
  • 获取spring xml配置文件地址

2.2.3 初始SocketChannel

从配置文件中获取canal.socketChannel字段,放到全局变量中。

2.2.4 准备canal server

从配置文件中分别获取canal.id、ip、port(对外提供socket服务的端口),获取一个内存级的server单例,同时也获取一个对外提供Netty服务的单例。

cid = Long.valueOf(getProperty(properties, CanalConstants.CANAL_ID));
ip = getProperty(properties, CanalConstants.CANAL_IP);
port = Integer.valueOf(getProperty(properties, CanalConstants.CANAL_PORT));
embededCanalServer = CanalServerWithEmbedded.instance();
embededCanalServer.setCanalInstanceGenerator(instanceGenerator);// 设置自定义的instanceGenerator
canalServer = CanalServerWithNetty.instance();
canalServer.setIp(ip);
canalServer.setPort(port);

2.2.5 初始化系统目录

从配置文件中获取zk地址(canal.zkServers),启动一个zk客户端,然后初始化两个系统目录,分别是:

  • /otter/canal/destinations
  • /otter/canal/cluster

2.2.6 初始化系统监控

根据destination构造运行时监控,其实就是根据instance名来构造ServerRunningMonitor。其实就是实现了ServerRunningListener中的一些方法。

public interface ServerRunningListener {

    /**
     * 启动时回调做点事情
     */
    public void processStart();

    /**
     * 关闭时回调做点事情
     */
    public void processStop();

    /**
     * 触发现在轮到自己做为active,需要载入上一个active的上下文数据
     */
    public void processActiveEnter();

    /**
     * 触发一下当前active模式失败
     */
    public void processActiveExit();

}

然后初始化一下ServerRunningMonitor。

runningMonitor.init();

这个init方法跟踪的结果,其实就是执行了ServerRunningListener中的processStart方法。

public void processStart() {
    try {
        if (zkclientx != null) {
            final String path = ZookeeperPathUtils.getDestinationClusterNode(destination, ip + ":" + port);
            initCid(path);
            zkclientx.subscribeStateChanges(new IZkStateListener() {

                public void handleStateChanged(KeeperState state) throws Exception {

                }

                public void handleNewSession() throws Exception {
                    initCid(path);
                }

                @Override
                public void handleSessionEstablishmentError(Throwable error) throws Exception {
                    logger.error("failed to connect to zookeeper", error);
                }
            });
        }
    } finally {
        MDC.remove(CanalConstants.MDC_DESTINATION);
    }
}

首先获取了/otter/canal/destinations/{destination}/cluster/ip:port的内容,其实就是server的地址,最后一个ip:port是个zk的临时节点。然后订阅一下节点事件,当节点有事件推送过来后,做一些动作。

2.2.7 初始化配置文件监控

如果canal.auto.scan配置为true(默认为true),首先定义一个InstanceAction,包含了启动、停止、重启instance的动作。

定义一个SpringInstanceConfigMonitor,配置定时扫描的事件为canal.auto.scan.interval,默认5s,扫描canal.conf.dir目录下的文件,与上面定义的InstanceAction结合起来。

2.3 启动CanalController

上面的构造方法其实就是定义一些必要的内容,真正的启动在这个方法中。

2.3.1 创建工作节点

创建临时节点/otter/canal/cluster/ip:port,同时启动监听器.

2.3.2 启动embeded服务

embededCanalServer.start();

这个start里面,一个是将当前server的running状态置为true,同时根据destination构建CanalInstance。

2.3.3 HA启动

遍历Map<String, InstanceConfig>中的InstanceConfig,如果CanalInsance还没启动,如果不是懒加载的话,直接HA启动ServerRunningMonitor。

ServerRunningMonitor runningMonitor = ServerRunningMonitors.getRunningMonitor(destination);
if (!config.getLazy() && !runningMonitor.isStart()) {
    runningMonitor.start();
}

public synchronized void start() {
    super.start();
    try {
        processStart();
        if (zkClient != null) {
            // 如果需要尽可能释放instance资源,不需要监听running节点,不然即使stop了这台机器,另一台机器立马会start
            String path = ZookeeperPathUtils.getDestinationServerRunning(destination);
            zkClient.subscribeDataChanges(path, dataListener);

            initRunning();
        } else {
            processActiveEnter();// 没有zk,直接启动
        }
    } catch (Exception e) {
        logger.error("start failed", e);
        // 没有正常启动,重置一下状态,避免干扰下一次start
        stop();
    }

}

这里面启动的内容我们来看看。

  • 首先调用super.start()把当前的running状态置为true。
  • 然后启动zk节点的监听(这边的processStart是否多余了?)。
  • 监听路径/otter/canal/destinations/{destination}/running节点的变化
zkClient.subscribeDataChanges(path, dataListener);
  • 这里的dataListener是ServerRunningMonitor构造函数中定义的,就是定义一些zk节点监听的动作。
    • 如果有数据变化,如果running节点中的内容ServerRunningData发生了变化,字段active变为了false,而且address就是本机,说明本机出现了主动释放,需要释放运行时状态。此时需要调用到processActiveExit方法,其实就是停止了本机的server中destination对应的instance。
    • 如果节点发生了删除动作,如果上一次active的状态就是本机,则即时触发一下active抢占,调用initRunning()方法,当然,如果启动失败,也不是立即切换,而是会等待5s,再尝试启动。这个启动方法中,主要调用的是processActiveEnter()方法,来启动了embededCanalServer.start(destination)。其实就是启动canalInstance,这块后续再分析。
  • 其实除了监听器,在本身的ServerRunningMonitor的start方法中,也有initRunning方法。这块启动canalInstance的方法,我们下一篇文章分析。

2.3.4 instance文件扫描启动

在扫描之前,把destination和InstanceAction绑定到缓存中。

instanceConfigMonitors.get(config.getMode()).register(destination, defaultAction);

首先启动一个全局扫描,然后再对应的destination配置文件的扫描。

if (autoScan) {
    instanceConfigMonitors.get(globalInstanceConfig.getMode()).start();
    for (InstanceConfigMonitor monitor : instanceConfigMonitors.values()) {
        if (!monitor.isStart()) {
            monitor.start();
        }
    }
}

这个start方法启动了一个定时器,默认5s扫描一次。扫描的内容就是配置文件路径下的内容,针对文件的新增、删除、修改,对应InstanceAction中的start,stop和reload方法。也就是说,我们在canal运行的过程中,通过动态修改配置文件,来实现动态调整运行时参数,主要可以用来进行重复消费,位点的迁移等等。

2.3.5 网络接口启动

CanalServerWithNetty的启动,首先需要启动CanalServerWithEmbedded,主要的业务逻辑在SessionHandler中。这块其实是暴露外部服务,给canal client进行调用。

2.4 增加关闭hook

Runtime.getRuntime().addShutdownHook(new Thread() {

    public void run() {
        try {
            logger.info("## stop the canal server");
            controller.stop();
        } catch (Throwable e) {
            logger.warn("##something goes wrong when stopping canal Server:", e);
        } finally {
            logger.info("## canal server is down.");
        }
    }

});

在server停止时,调用controller.stop()方法。

public void stop() throws Throwable {
    canalServer.stop();

    if (autoScan) {
        for (InstanceConfigMonitor monitor : instanceConfigMonitors.values()) {
            if (monitor.isStart()) {
                monitor.stop();
            }
        }
    }

    for (ServerRunningMonitor runningMonitor : ServerRunningMonitors.getRunningMonitors().values()) {
        if (runningMonitor.isStart()) {
            runningMonitor.stop();
        }
    }

    // 释放canal的工作节点
    releaseCid(ZookeeperPathUtils.getCanalClusterNode(ip + ":" + port));
    logger.info("## stop the canal server[{}:{}]", ip, port);
        
    if (zkclientx != null) {
        zkclientx.close();
    }
}

主要是停止controller,server相关的monitor,instance相关的monitor,然后释放zk节点,关闭zk连接。

转载于:https://www.cnblogs.com/f-zhao/p/9083099.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在上一篇文章中,我们介绍了使用 Canal 分析 Binlog 的基本流程和使用方法。本文将继续深入探讨 Canal源码实现。 Canal 的架构设计 Canal 的整体架构可以分为三个部分:Client、Server、Store。其中,Client 是客户端,用于连接 MySQL 数据库并获取 Binlog;Server服务端,用于接收 Client 发送的 Binlog,并进行解析处理;Store 是存储层,用于保存解析后的 Binlog 信息。 在 Client 端,Canal 使用了阿里开源的 Cobar 连接池,保证了高并发的连接请求。对于每一个连接,Canal 都会为其分配一个线程,用于读取 Binlog 并发送到 Server 端。 在 Server 端,Canal 采用 NIO 的方式进行网络通信。当收到 Binlog 数据后,Canal 会将其解析为一条条的事件,并将事件发送给所有监听该实例的客户端。同时,Canal 还支持对 Binlog 进行过滤和转换,以满足不同的业务需求。 在 Store 层,Canal 提供了多种存储方式,包括内存、文件、Kafka、MQ 等。用户可以根据自己的需求进行选择。 Canal 的核心实现 在分析 Canal 的核心实现之前,我们需要先了解一下 Binlog 的结构。Binlog 是 MySQL 用于记录数据库变更的一种日志格式,其主要由 event header 和 event data 两部分构成。其中,event header 包含了事件类型、时间戳、server id 等信息,event data 则包含了具体的事件内容。 Canal 的核心实现主要包括两个部分:BinlogParser 和 CanalEventParser。 BinlogParser 用于解析 Binlog,并将其转化为事件对象。在解析 Binlog 时,BinlogParser 首先会读取 event header,然后根据 event header 中的事件类型选择相应的 CanalEventParser 进行处理。CanalEventParser 则负责将 event data 解析为具体的事件对象。 CanalEventParser 实现了一系列的事件解析器,包括 QueryEventParser、TableMapEventParser、WriteRowsEventParser 等。每个事件解析器都负责将 event data 转化为相应的事件对象,并填充事件对象中的各个字段。例如,WriteRowsEventParser 将 event data 解析为 WriteRowsEvent,并设置其对应的表名、列名、新插入的行等信息。 Canal 的事件模型 Canal 的事件模型主要由 CanalEvent 和 CanalEventSink 两部分构成。CanalEvent 表示一个数据库事件,包括事件类型、事件数据、表名、库名等信息。CanalEventSink 则表示一个事件处理器,用于接收并处理 CanalEvent。 在 Canal 中,每个客户端都会创建一个对应的 CanalEventSink,并将其注册到 Server 端。当 Server 端接收到 Binlog 数据并解析为 CanalEvent 后,会将 CanalEvent 发送给所有注册的 CanalEventSink。CanalEventSink 则根据事件类型进行相应的处理,例如将 InsertEvent 存储到数据库中。 Canal 的优点和缺点 Canal 的主要优点在于其高效、可扩展和灵活的架构设计。Canal 使用了阿里开源的 Cobar 连接池和 NIO 网络通信,保证了高并发的连接请求和网络数据传输。同时,Canal 的存储层也支持多种存储方式,可以根据用户需求进行选择。此外,Canal 还支持对 Binlog 进行过滤和转换,以满足不同的业务需求。 Canal 的缺点在于其对 MySQL 版本有一定的限制。由于 Binlog 格式在不同的 MySQL 版本之间存在差异,因此 Canal 只支持特定版本的 MySQL,且需要用户手动配置相应的 Binlog 格式。此外,Canal 无法保证数据的完整性,如果在解析 Binlog 过程中出现异常,可能会导致部分数据丢失。 总结 Canal 是一款高效、可扩展和灵活的 Binlog 解析工具,可以帮助用户实现对 MySQL 数据库变更的监控和同步。Canal 的核心实现包括 BinlogParser 和 CanalEventParser,其中 BinlogParser 用于解析 Binlog,CanalEventParser 则负责将 event data 转化为具体的事件对象。Canal 的事件模型主要由 CanalEvent 和 CanalEventSink 两部分构成,可以根据用户需求进行灵活配置。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值