阿里中间件canal学习笔记

Canal学习记录

canal启动过程

在canal中一个server中可以包含多个instance,每个instance对应着不同数据库中的不同表格的数据变更。举例说明就是:你可以启动一个server(对应一个netty服务或者jvm服务),在改server中可以有两个instance,一个对应highso库中的crmchance表的数据变更,另外一个对应着order表的数据变更
1. server启动过程:首先通过canalcontroller读取cananl.properties的属性,通过读取的属性配置,netty启动时的ip端口等。
2. 通过扫描目录conf目录下字母目录数量(除去spring目录)确定instance的个数,然后对每个instance启动一个spring的beanfactory,并加入本地缓存。另外对比目录下的文件的内容发现是否有内容的变更,如果有变更,先stop改benfactory中的组件。然后从缓存中移除改beanfactory,最后重新启动beanfactory(读取新的配置文件canal.properties和instance.properties),再加入到缓存中。

#判断配置文件是否有变更
# notifyStart(instanceDir, destination, instanceConfigs);处理新增的实例,直接新实例化一个beanfactory然后加入缓存
# notifyStop(deleteInstanceName);处理删除实例,先调用beanfactory中的组件的stop操作进行优雅的关闭和释放资源,然后从缓存中删除该实例
# notifyReload(destination);处理变更配置的情形,先调用notifystop再调用notifystart,并更新beanfactory的缓存
#CanalController的SpringInstanceConfigMonitor monitor = new SpringInstanceConfigMonitor();

private void scan() {
        File rootdir = new File(rootConf);
        if (!rootdir.exists()) {
            return;
        }

        File[] instanceDirs = rootdir.listFiles(new FileFilter() {

            public boolean accept(File pathname) {
                String filename = pathname.getName();
                return pathname.isDirectory() && !"spring".equalsIgnoreCase(filename);
            }
        });

        // 扫描目录的新增
        Set<String> currentInstanceNames = new HashSet<String>();

        // 判断目录内文件的变化
        for (File instanceDir : instanceDirs) {
            String destination = instanceDir.getName();
            currentInstanceNames.add(destination);
            File[] instanceConfigs = instanceDir.listFiles(new FilenameFilter() {

                public boolean accept(File dir, String name) {
                    // return !StringUtils.endsWithIgnoreCase(name, ".dat");
                    // 限制一下,只针对instance.properties文件,避免因为.svn或者其他生成的临时文件导致出现reload
                    return StringUtils.equalsIgnoreCase(name, "instance.properties");
                }

            });

            if (!actions.containsKey(destination) && instanceConfigs.length > 0) {
                // 存在合法的instance.properties,并且第一次添加时,进行启动操作
                notifyStart(instanceDir, destination, instanceConfigs);
            } else if (actions.containsKey(destination)) {
                // 历史已经启动过
                if (instanceConfigs.length == 0) { // 如果不存在合法的instance.properties
                    notifyStop(destination);
                } else {
                    InstanceConfigFiles lastFile = lastFiles.get(destination);
                    // 历史启动过 所以配置文件信息必然存在
                    if (!isFirst && CollectionUtils.isEmpty(lastFile.getInstanceFiles())) {
                        logger.error("[{}] is started, but not found instance file info.", destination);
                    }

                    boolean hasChanged = judgeFileChanged(instanceConfigs, lastFile.getInstanceFiles());
                    // 通知变化
                    if (hasChanged) {
                        notifyReload(destination);
                    }

                    if (hasChanged || CollectionUtils.isEmpty(lastFile.getInstanceFiles())) {
                        // 更新内容
                        List<FileInfo> newFileInfo = new ArrayList<FileInfo>();
                        for (File instanceConfig : instanceConfigs) {
                            newFileInfo.add(new FileInfo(instanceConfig.getName(), instanceConfig.lastModified()));
                        }

                        lastFile.setInstanceFiles(newFileInfo);
                    }
                }
            }

        }

        // 判断目录是否删除
        Set<String> deleteInstanceNames = new HashSet<String>();
        for (String destination : actions.keySet()) {
            if (!currentInstanceNames.contains(destination)) {
                deleteInstanceNames.add(destination);
            }
        }
        for (String deleteInstanceName : deleteInstanceNames) {
            notifyStop(deleteInstanceName);
        }
    }
#beanfactory本地缓存部分,对应instance的初始化过程
instanceGenerator = new CanalInstanceGenerator() {

            public CanalInstance generate(String destination) {
                ..................
                            // 设置当前正在加载的通道,加载spring查找文件时会用到该变量
                            System.setProperty(CanalConstants.CANAL_DESTINATION_PROPERTY, destination);
                            instanceGenerator.setBeanFactory(getBeanFactory(config.getSpringXml()));
                            return instanceGenerator.generate(destination);
                ....................
            }

        };

private BeanFactory getBeanFactory(String springXml) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext(springXml);
        return applicationContext;
}

public class SpringCanalInstanceGenerator implements CanalInstanceGenerator, BeanFactoryAware {

    private String      defaultName = "instance";
    private BeanFactory beanFactory;

    public CanalInstance generate(String destination) {
        String beanName = destination;
        if (!beanFactory.containsBean(beanName)) {
            beanName = defaultName;
        }

        return (CanalInstance) beanFactory.getBean(beanName);
    }

    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }

}
#canal中大量使用了guava的本地缓存技术和事件技术,当在本地缓存找不到的时候,会调用缓存对象的load方法。
#例如:当调用stop事件从canalInstances的map中移除了改实例的时候,在第一次调用get时候,会调用canalInstanceGenerator.generate(destination);即上面介绍的beanfacotry初始化的过程。
public void start() {
        if (!isStart()) {
            super.start();

            canalInstances = MigrateMap.makeComputingMap(new Function<String, CanalInstance>() {

                public CanalInstance apply(String destination) {
                    return canalInstanceGenerator.generate(destination);
                }
            });

            // lastRollbackPostions = new MapMaker().makeMap();
        }
    }

canal-server 交互流程

cananl-server和canal-client之间交互是通过pull的模式进行,其中canal-client使用的阻塞方式进行数据读取(30秒超时时间),【pull与push方式比较其好处是在push在数据量大时会出现一些资源占用过大的问题)。其实际的连接过程如下:
1. 首先canal-client向canal-server发起连接请求
2. canal-server接收到请求,发送handshake(握手消息)消息
3. canal-client接受到handshake消息,发送认证消息用户名和密码等(默认为空)
4. canal-server进行认证,发送认证成功消息,移除handshake和clientauthen的handle
5. canal-client接受到认证成功消息,连接成功,返回交互的socket.回滚上次没有ack的请求. canal-client 发起subscribe请求
6. canal-server接收到subcribe请求,如果instance没有初始化完成,进行初始化,并设置binlog的开始位置。发送ack消息
7. canal-client接收到ack消息,完成ack过程,开始发起get请求去请求新增binlog日志内容
8. canal-server开始获取内存中最新的binlog日志,发送最新binlog日志给canal-client
9. canal-client获取到binlog日志进行处理,处理成功发送clientack消息,处理失败发送clientrollback
10. canal-server获取到ack消息,更新相关游标位置。如果收到clientrollback根据batchid回滚到之前的位置。
11. 如果客户端处理完成,发送unscribe消息,服务器端清除相关的缓存信息,如果该instance没有其他的客户端进行连接,则关闭该instance释放资源
image

instance组件

server和instance如下:一个server中可以包含多个instance,每个instance(有一个spring的beanfactory相对应)都具有evenparse、eventsik、eventstore、metamanager四个组件
image

根据第一部分的介绍我们知道,一个instance对应于conf目录下的一个子文件夹,其对应的beanfactory通过加载canal.properties和该instance目录下的instance.properties来实例化相关组件和instance对象

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:aop="http://www.springframework.org/schema/aop" xmlns:lang="http://www.springframework.org/schema/lang"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd
           http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang-2.0.xsd
           http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd
           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd"
    default-autowire="byName">

    <!-- properties -->
    <bean class="com.alibaba.otter.canal.instance.spring.support.PropertyPlaceholderConfigurer" lazy-init="false">
        <property name="ignoreResourceNotFound" value="true" />
        <property name="systemPropertiesModeName" value="SYSTEM_PROPERTIES_MODE_OVERRIDE"/><!-- 允许system覆盖 -->
        <property name="locationNames">
            <list>
                <value>classpath:canal.properties</value>
                <value>classpath:${canal.instance.destination:}/instance.properties</value>
            </list>
        </property>
    </bean>

    <bean id="socketAddressEditor" class="com.alibaba.otter.canal.instance.spring.support.SocketAddressEditor" />
    <bean class="org.springframework.beans.factory.config.CustomEditorConfigurer"> 
        <property name="propertyEditorRegistrars">
            <list>
                <ref bean="socketAddressEditor" />
            </list>
        </property>
    </bean>

    <bean id="instance" class="com.alibaba.otter.canal.instance.spring.CanalInstanceWithSpring">
        <property name="destination" value="${canal.instance.destination}" />
        <property name="eventParser">
            <ref local="eventParser" />
        </property>
        <property name="eventSink">
            <ref local="eventSink" />
        </property>
        <property name="eventStore">
            <ref local="eventStore" />
        </property>
        <property name="metaManager">
            <ref local="metaManager" />
        </property>
        <property name="alarmHandler">
            <ref local="alarmHandler" />
        </property>
    </bean>

    <!-- 报警处理类 -->
    <bean id="alarmHandler" class="com.alibaba.otter.canal.common.alarm.LogAlarmHandler" />

    <bean id="metaManager" class="com.alibaba.otter.canal.meta.FileMixedMetaManager">
        <property name="dataDir" value="${canal.file.data.dir:../conf}" />
        <property name="period" value="${canal.file.flush.period:1000}" />
    </bean>

    <bean id="eventStore" class="com.alibaba.otter.canal.store.memory.MemoryEventStoreWithBuffer">
        <property name="bufferSize" value="${canal.instance.memory.buffer.size:16384}" />
        <property name="bufferMemUnit" value="${canal.instance.memory.buffer.memunit:1024}" />
        <property name="batchMode" value="${canal.instance.memory.batch.mode:MEMSIZE}" />
        <property name="ddlIsolation" value="${canal.instance.get.ddl.isolation:false}" />
    </bean>

    <bean id="eventSink" class="com.alibaba.otter.canal.sink.entry.EntryEventSink">
        <property name="eventStore" ref="eventStore" />
    </bean>

    <bean id="eventParser" class="com.alibaba.otter.canal.parse.inbound.mysql.MysqlEventParser">
        <property name="destination" value="${canal.instance.destination}" />
        <property name="slaveId" value="${canal.instance.mysql.slaveId:1234}" />
        <!-- 心跳配置 -->
        <property name="detectingEnable" value="${canal.instance.detecting.enable:false}" />
        <property name="detectingSQL" value="${canal.instance.detecting.sql}" />
        <property name="detectingIntervalInSeconds" value="${canal.instance.detecting.interval.time:5}" />
        <property name="haController">
            <bean class="com.alibaba.otter.canal.parse.ha.HeartBeatHAController">
                <property name="detectingRetryTimes" value="${canal.instance.detecting.retry.threshold:3}" />
                <property name="switchEnable" value="${canal.instance.detecting.heartbeatHaEnable:false}" />
            </bean>
        </property>

        <property name="alarmHandler" ref="alarmHandler" />

        <!-- 解析过滤处理 -->
        <property name="eventFilter">
            <bean class="com.alibaba.otter.canal.filter.aviater.AviaterRegexFilter" >
                <constructor-arg index="0" value="${canal.instance.filter.regex:.*\..*}" />
            </bean>
        </property>

        <property name="eventBlackFilter">
            <bean class="com.alibaba.otter.canal.filter.aviater.AviaterRegexFilter" >
                <constructor-arg index="0" value="${canal.instance.filter.black.regex:}" />
                <constructor-arg index="1" value="false" />
            </bean>
        </property>
        <!-- 最大事务解析大小,超过该大小后事务将被切分为多个事务投递 -->
        <property name="transactionSize" value="${canal.instance.transaction.size:1024}" />

        <!-- 网络链接参数 -->
        <property name="receiveBufferSize" value="${canal.instance.network.receiveBufferSize:16384}" />
        <property name="sendBufferSize" value="${canal.instance.network.sendBufferSize:16384}" />
        <property name="defaultConnectionTimeoutInSeconds" value="${canal.instance.network.soTimeout:30}" />

        <!-- 解析编码 -->
        <!-- property name="connectionCharsetNumber" value="${canal.instance.connectionCharsetNumber:33}" /-->
        <property name="connectionCharset" value="${canal.instance.connectionCharset:UTF-8}" />

        <!-- 解析位点记录 -->
        <property name="logPositionManager">
            <bean class="com.alibaba.otter.canal.parse.index.FailbackLogPositionManager">
                <property name="primary">
                    <bean class="com.alibaba.otter.canal.parse.index.MemoryLogPositionManager" />
                </property>
                <property name="failback">
                    <bean class="com.alibaba.otter.canal.parse.index.MetaLogPositionManager">
                        <property name="metaManager" ref="metaManager" />
                    </bean>
                </property>
            </bean>
        </property>

        <!-- failover切换时回退的时间 -->
        <property name="fallbackIntervalInSeconds" value="${canal.instance.fallbackIntervalInSeconds:60}" />

        <!-- 解析数据库信息 -->
        <property name="masterInfo">
            <bean class="com.alibaba.otter.canal.parse.support.AuthenticationInfo">
                <property name="address" value="${canal.instance.master.address}" />
                <property name="username" value="${canal.instance.dbUsername:retl}" />
                <property name="password" value="${canal.instance.dbPassword:retl}" />
                <property name="defaultDatabaseName" value="${canal.instance.defaultDatabaseName:retl}" />
            </bean>
        </property>
        <property name="standbyInfo">
            <bean class="com.alibaba.otter.canal.parse.support.AuthenticationInfo">
                <property name="address" value="${canal.instance.standby.address}" />
                <property name="username" value="${canal.instance.dbUsername:retl}" />
                <property name="password" value="${canal.instance.dbPassword:retl}" />
                <property name="defaultDatabaseName" value="${canal.instance.defaultDatabaseName:retl}" />
            </bean>
        </property>

        <!-- 解析起始位点 -->
        <property name="masterPosition">
            <bean class="com.alibaba.otter.canal.protocol.position.EntryPosition">
                <property name="journalName" value="${canal.instance.master.journal.name}" />
                <property name="position" value="${canal.instance.master.position}" />
                <property name="timestamp" value="${canal.instance.master.timestamp}" />
            </bean>
        </property>
        <property name="standbyPosition">
            <bean class="com.alibaba.otter.canal.protocol.position.EntryPosition">
                <property name="journalName" value="${canal.instance.standby.journal.name}" />
                <property name="position" value="${canal.instance.standby.position}" />
                <property name="timestamp" value="${canal.instance.standby.timestamp}" />
            </bean>
        </property>
        <property name="filterQueryDml" value="${canal.instance.filter.query.dml:false}" />
        <property name="filterQueryDcl" value="${canal.instance.filter.query.dcl:false}" />
        <property name="filterQueryDdl" value="${canal.instance.filter.query.ddl:false}" />
        <property name="filterRows" value="${canal.instance.filter.rows:false}" />
        <property name="filterTableError" value="${canal.instance.filter.table.error:false}" />
        <property name="supportBinlogFormats" value="${canal.instance.binlog.format}" />
        <property name="supportBinlogImages" value="${canal.instance.binlog.image}" />
    </bean>
</beans>

canal组件

CanalMetaManager

CanalMetaManager主要用于记录客户端获取的未ack的PostionRange日志信息(开始位置、结束位置、ack位置以及对应的batchId),实现重试功能,保证数据传输的可靠性。提供如下功能:
- 订阅行为处理:记录destination和ClientIdentity的对应关系
- 未ack日志记录行为处理:通过MemoryClientIdentityBatch来实现获取指定batchId、最新或者第一个的未ack日志的PositionRange。
- 添加、获取未ack的日志记录:通过从eventstore中获取指定数量的event的PostionRange后(并不保存数据信息),添加到metamanager中,并通过唯一batchId进行绑定,支持通过batchid获取未ack日志记录的功能。
- 删除已经ack日志记录的行为:通过batchId删除已经ack过的日志记录。注意:ack和rollback必须按照分发处理的顺序处理,即只能ack当前最小的batchId。不然容易出现丢数据的问题
- 获取、清空所有未处理ack日志:获取和清空MemoryClientIdentityBatch中的记录
- 更新最近被ack的日志文件位置:从positionRange中获取到应该ack的Position位置,进行更新到cursor游标中

常用对象说明:
- ClientIdentity:保存instance名字和clientId(客户端设置默认1001)
EntryPosition:保存binlog的日志文件名、位置、时间点等
- LogIdentity:保存canal server的slaveId和IP地址等信息
- PositionRange: 保存日志的开始位置、结束位置和ack位置
- MetaqPosition:保存消息中间件消费的日志位置信息
- MemoryClientIdentityBatch:batches 保存batchId和PositionRange 、atomicMaxBatchId记录最大batchid、clientIdentity 记录客户端对象

实现类说明:
- MemoryMetaManager:将所有客户端日志消息情况保存于内存中。
- FileMixedMetaManager:在支持MemoryMetaManager的基础上,每隔1s将client信息以及处理了日志文件位置cursor记录到文件中

EventStore

eventstore是实现了基于循环队列的数据库事件的存储机制,其中ack

数据添加过程

  • eventstore-put操作(批量):通过ack位置(落后于get位置)和put位置判断是否有空位(循环队列),有添加数据到队列中。
  • eventstore-get操作(批量batchSize):从eventstore的循环队列中获取batchSize个事件对象(其中的position其实位置,只会使用include字段来判断是否需要在get位置+1)。可能出现数据库事务事件被分割。因此返回对象中:PositionRange的start标识事件的开始位置,end标识事件的结束位置,其中最后一个的事务的开始位置、结束位置以及ddl操作可以作为事务的ack位置。因此最终数据的获取严格按照get位置来进行操作和传入poistion的位置没有关系。
  • metamanager-addBatch: 从eventstore中get获取到event列表和PositionRange,对于每个PositionRange会和一个batchId绑定在一起保存到metamanager中。metamanger实现了根据batchId进行回滚的操作,但是eventsotre没有,直接将get回滚到ack位置,因此会出现重复数据获取的问题。在metamanager-addBatch操作中主要是每一个PositionRange的获取都会被产生一个batchId保存在map中。
  • 实际发送的数据来源:batchId来源于metamanger,实际的entries来源于eventsotre
  • metamanger-ack: 移除batchId和PositionRange的关系,并更新custor为PositinRange的ack对象(不是start对象和end对象),其目的是客户端崩溃后,重复消息的获取起点是一个事务的开始或者结束位置,不会出现时一个事务的数据变更的中间部分,造成脏数据。
  • eventstore-ack(传入具体的位置,如positinRange的end位置):根据制定的position位置,将ack和get之间的postion位置前的数据都清空,ack位置为positon的位置。
  • metamanger-rollback: 其行为同metamanager的ack相似,都是先移除batchId对应的PositionRange,不同的是不进行custor的更新,另外会调用eventstore的rollback
  • eventstore-rollback:没有实现根据batchId回滚的功能,会将get的位置重置为ack位置,可能会造成后续获取重复数据。

注意:
1. metamanger只记录每次客户端请求时,数据库binlog的开始位置、结束位置、ack位置(即custor位置和真正的ack行为没有直接关系),实际的binlog数据变更的详情并没有记载和客户端紧密相连。
2. eventsotre是记录的binlog的详细数据,其中ack表示被客户端已经处理成功的日志记录。get表示客户端正在处理的日志记录。put表示新加入的日志的位置。

EntryEventSink

用于处理得到的entry日志,并保存到eventstore中。

  • CanalEventFilter:在处理entry日志时,可以通过设置不同的eventFilter来过滤日志记录,获取需要的日志记录。AviaterRegexFilter
  • CanalEventDownStreamHandler:用于在处理entry之前和之后添加一些处理逻辑,从而过滤一些不需要的entry日志,如HeartBeatEntryEventHandler用于过滤心跳日志
  • sink:提供将entry日志保存到eventstore中的功能。其中比较重要的参数是filterTransactionEntry=true,表示只要ROWDATA的entry日志。默认为false

上述组件之间的关系

CanalEventParser 通过EventSink 将获取到的CanalEntry.Entry binlog日志记录sink到EventStore中,EventStore将sink过来的日志记录保存到内存中,其通过ack、get、put三个标志位来标识循环队列中日志记录的处理情况。在server端,会每个destination对应一个instance,在instance中包含eventsink、eventstore以及metaManager,metamanger和eventstore是通过从eventstore中获取CanalEntry.Entry然后存储到metaManger中。通过metaManager可以实现多个客户端同时获取数据。

CanalEventParser组件

EventTransactionBuffer

  • 主要是对传入的日志事件进行事务的切分,按照事务的维度[即日志的类型,事务开始、事务结束、数据]将日志事件存储到eventstore中,具体的事件切分代码见public void add(CanalEntry.Entry entry)方法,事务日志记录的刷新到eventstore时通过其TransactionFlushCallback对象实现。
  • 通过调用EventParse中设置的CanalLogPositionManager来记录最后一个事务的结尾位置(放在内存中)
<!-- 解析位点记录 -->
        <property name="logPositionManager">
            <bean class="com.alibaba.otter.canal.parse.index.FailbackLogPositionManager">
                <property name="primary">
                    <bean class="com.alibaba.otter.canal.parse.index.MemoryLogPositionManager" />
                </property>
                <property name="failback">
                    <bean class="com.alibaba.otter.canal.parse.index.MetaLogPositionManager">
                        <property name="metaManager" ref="metaManager" />
                    </bean>
                </property>
            </bean>
        </property>

具体的触发过程是在AbstractEventParser中会启动一个新的线程parseThread,在该线程会调用transactionBuffer.add方法从而实现EntryEventSink.sink方法的调用

BinlogParser

该组件主要负责将LogEvent日志转化为可传输的Entry对象。其中在解析的过程中会根据eventType进行处理,对于所有ROWS_EVENT事件都会应用过滤条件eventFilter和eventBlackFilter。

parseThread线程

  1. buildMysqlConnection 新建MysqlConnector对象,并设置连接mysql数据库需要的用户名、密码等信息。
  2. startHeartBeat 开启一个心跳shcedule线程,每隔3秒钟执行以下select 1 来保持长连接的有效性,不会被mysql自动的关闭。其会在建立正式binlog连接后才会正常的运行。
  3. preDump 在建立binlog连接之前的准备工作(扩展用)
  4. erosaConnection.connect(); 正式建立连接,主要根据header协议等,建立nio需要的SocketChannel等。
  5. findStartPosition 寻找应该从哪个位置开始进行binlog日志的同步,寻找优先级为:1)logPositionManager(如果为内存模式则该值为空,可以通过配置为FileMixedLogPositionManager,从文件读取) 2)读取instance.property配置文件中的位置信息,同时兼容基于位置和基于时间的binlog记录 3)通过执行show master status得到的位置信息,即当前binlog日志开始。4) 如果是通过在配置文件中设置时间的方式设置binlog起点,需要设置为离该时间最近的事务开始位置
  6. erosaConnection.reconnect(); 重新链接,因为在找position过程中可能有状态,需要断开后重建
  7. erosaConnection.dump 设置一些链接信息以及获取编码信息等,并向mysql发送binlog起始位置信息,根据dump协议开始fetcher日志,然后调用decoder将二进制数据解码为EVENT,最后调用SinkFunction去sink数据。该方法会一直调用去fetcher数据。除非fetcher调用失败
  8. 如果出现异常会调用相关对象的方法,释放资源。

SinkFunction

  1. SinkFunction调用sink方法去处理从mysql获取的EVENT数据,先会调用binlogParser.parse将EVENT对象转化为可以在网络中传输的CanalEntry.Entry对象。
  2. transactionBuffer.add(entry) 该调用会将解析后的日志,通过EntryEventSink的sink方法存储到eventstore中。

CanalEventParser组件功能介绍

Parese主要是通过启动一个parseThread来实现数据库binlog的dump功能,其通过binlog协议与mysql之间建立连接,然后不断的fetcher数据,之后调用SinkFunction中的BinLogParse来将数据库的EVENT数据转化为可以在网络间传输的CanalEntry.Entry数据,并且会调用EventTransactionBuffer来实现CanalEntry.Entry数据的存储,EventTransactionBuffer会将存储的数据按照事务的维度进行切分,将切分好的数据调用EntryEventSink将数据存储到EventStore中,最后通过从EventStore取出数据传输到Client端,并且对于客户端数据的处理情况会提供一个CanalMetaManager来记录客户端获取数据的进度和实现ack、回滚等功能。

借助canal主页上的流程图描述整个过程:
image

其他

Canal如何实现断点续传功能

如我在介绍CanalMetaManager的时候,其有一种实现方式是FileMixedMetaManager,该组件会将客户端的获取过的Entry对象存储在MetaManager的内存中,其中FileMixedMetaManager会隔一分钟向文件中写入当前处理到的binlog位置(并且一定是transaction的开始或者结束位置,因此在mysql的dump协议中不能处理事务中间位置的续传功能)。

{"clientDatas":[{"clientIdentity":{"clientId":1001,"destination":"third_tb","filter":""},"cursor":{"identity":{"slaveId":-1,"sourceAddress":{"address":"master","port":3306}},"postion":{"included":false,"journalName":"mysql-bin.000044","position":2207,"serverId":2,"timestamp":1510657504000}}}],"destination":"third_tb"}

在我们重新启动canal实例的时候其会通过logPositionManager中配置的metaMangaer即FileMixedMetaManager去读取meta.dat文件中的位置作为binlog的开始位置

<!-- 解析位点记录 -->
        <property name="logPositionManager">
            <bean class="com.alibaba.otter.canal.parse.index.FailbackLogPositionManager">
                <property name="primary">
                    <bean class="com.alibaba.otter.canal.parse.index.MemoryLogPositionManager" />
                </property>
                <property name="failback">
                    <bean class="com.alibaba.otter.canal.parse.index.MetaLogPositionManager">
                        <property name="metaManager" ref="metaManager" />
                    </bean>
                </property>
            </bean>
        </property>

针对上面的问题需要比较重要的说明一点,meta.dat中记录的日志位置必须是事务的开始或者结束位置。否则会在进行数据库dump连接时抛出如下异常:ERROR ## parse this event has an error , last position : [mysql-bin.000044,1209]

因此对于使用FileMetaManger作为metaManager时,一定不要如下设置EventSink,因为这会导致过滤到binlog日志中事务的开始事件和结束事件,因此存储的meta.dat一定是有问题的

<bean id="eventSink" class="com.alibaba.otter.canal.sink.entry.EntryEventSink">
    <property name="eventStore" ref="eventStore" />
    <property name="filterTransactionEntry" value="true" />
</bean>

Canal支持直接存储parse过的binlog日志位置

在此需要说明parse的binlog位置,不是meta.dat的日志位置,因为该存的是client已经消费ack的日志位置,相当于parse的位置>meta的位置。
如下可以实现该需求

<!-- 解析位点记录 -->
        <property name="logPositionManager">
            <bean class="com.alibaba.otter.canal.parse.index.FailbackLogPositionManager">
                <property name="primary">
                    <bean class="com.alibaba.otter.canal.parse.index.MemoryLogPositionManager" />
                </property>
                <property name="failback">
                    <bean class="com.alibaba.otter.canal.parse.index.MetaLogPositionManager">
                        <property name="metaManager" ref="metaManager" />
                    </bean>
                </property>
            </bean>
        </property>

改为

<!-- 解析位点记录 -->
        <property name="logPositionManager">
            <bean class="com.alibaba.otter.canal.parse.index.FailbackLogPositionManager">
                <property name="primary">
                    <bean class="com.alibaba.otter.canal.parse.index.FileMixedLogPositionManager" />
                </property>
                <property name="failback">
                    <bean class="com.alibaba.otter.canal.parse.index.MetaLogPositionManager">
                        <property name="metaManager" ref="metaManager" />
                    </bean>
                </property>
            </bean>
        </property>

最终存储的文件是parse.dat

高级篇

CanalMetaManager的选型

  1. MemoryMetaManager 人如其名,所有的batch数据和cursor数据都保存在内存中,重启后所有信息丢失,不能断点续传
  2. FileMixedMetaManager 此处的MIXED表示该类型兼容MemoryMetaManager,实际上是其子类,其如MemoryMetaManager会将所有数据保存到内存中,并且会有一个ScheduledThreadPool来隔1秒钟把cursor刷新到文件meta.dat中。当重启时,会去读取meta.dat作为binlog读取的开始位置,从而实现了断点续传的功能。
  3. ZooKeeperMetaManager 同MemoryMetaManager一层,其将所有数据都保存到zookeeper中,因此其支持断点续传的功能。路径结构
* <pre>
 * /otter
 *    canal
 *      destinations
 *        dest1 
 *          client1
 *            filter
 *            batch_mark
 *              1
 *              2
 *              3
 * </pre>
  1. MixedMetaManager 有一点类似于FileMixedMetaManager模型,所有的数据都会保存到内存中,但是会通过FixedThreadPool去异步的把内存的所有数据(不仅仅是cursor)同步到zookeeper上。
  2. PeriodMixedMetaManager同FileMixedMetaManager几乎一样,也是只持久化cursor,只不过是持久化到zookeeper。

GroupEventSink

我们知道EntryEventSink是按照数据的先进先出的顺序进行存储的,而GroupEventSink的设计是为了满足当数据库分库分表时,又想使得数据的变更记录也是按照数据库执行的先后顺序存储到EventStore中(其实现主要是通过加锁和优先级队列PriorityBlockingQueue来保证变更数据存储到eventstore中的顺序)

EventStore

暂时只提供了MemoryEventStoreWithBuffer,其支持一次取多大内存和多少条数据两个方式

CanalEventParser

  1. 支持LocalBinlogEventParser、MysqlEventParser、GroupEventParser三种类型。LocalBinlogEventParser支持直接从本地的mysql数据的binlog文件中读取日志记录。
  2. MysqlEventParser除了上面已经介绍的会开启一个线程去dump binlog日志外,还支持HA切换的功能,其通过切换在配置文件中的masterInfo和standbyInfo信息,实现了在发生异常时切换到standbyInfo指定的数据库上。
  3. GroupEventParser 其主要用于处理分库分表时需要从多个数据库中获取binlog日志,然后调用同一个EventStore把数据加入到

HeartBeatHAController

在MysqlEventParser中会启动一个线程定时线程进行心跳检测,当每次进行心跳检测发现master连接异常时,会调用HAController的onFailed方法,会进行错误次数的累加,当达到detectingRetryTimes时,就会调用MysqlEventParser的master和standby切换。另外只要一次成功,那么以前的失败次数清零。

CanalInstance

  1. CanalInstanceWithSpring 通过spring去xml文件中初始化metaManager、eventStore、eventSink、eventParser,已经配置之间的引用关系,以及相应的start方法,启动整个instance服务。
  2. CanalInstanceWithManager 提供一种通过传入参数来启动上述组件和instance服务等。

canal官方开发文档中所说的HA

image

根据上述图,其实我们可以理解为常见的分布式系统中如何控制多个jvm在不同的机器中启动,我们如何保证只有一台机器能正常运行,其他机器都处于阻塞状态。这个可以看我前面的关系elasticjob的文档中已经说明的,主要就是通过zookeeper的瞬时有序节点来实现分布式锁的功能。具体的类LeaderLatch
另外对于集群类型的server的client也实现了ClusterCanalClientTest的实现,其本质上就是通过zookeeper去取正在运行的server地址,从而保证server自动切换好之后,client也能继续进行消费。

  • 0
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值