阿里中间件canal学习笔记
来源:互联网 发布:政治敏感 知乎 编辑:程序博客网 时间:2024/05/22 17:02
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释放资源
instance组件
server和instance如下:一个server中可以包含多个instance,每个instance(有一个spring的beanfactory相对应)都具有evenparse、eventsik、eventstore、metamanager四个组件
根据第一部分的介绍我们知道,一个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线程
- buildMysqlConnection 新建MysqlConnector对象,并设置连接mysql数据库需要的用户名、密码等信息。
- startHeartBeat 开启一个心跳shcedule线程,每隔3秒钟执行以下select 1 来保持长连接的有效性,不会被mysql自动的关闭。其会在建立正式binlog连接后才会正常的运行。
- preDump 在建立binlog连接之前的准备工作(扩展用)
- erosaConnection.connect(); 正式建立连接,主要根据header协议等,建立nio需要的SocketChannel等。
- findStartPosition 寻找应该从哪个位置开始进行binlog日志的同步,寻找优先级为:1)logPositionManager(如果为内存模式则该值为空,可以通过配置为FileMixedLogPositionManager,从文件读取) 2)读取instance.property配置文件中的位置信息,同时兼容基于位置和基于时间的binlog记录 3)通过执行show master status得到的位置信息,即当前binlog日志开始。4) 如果是通过在配置文件中设置时间的方式设置binlog起点,需要设置为离该时间最近的事务开始位置
- erosaConnection.reconnect(); 重新链接,因为在找position过程中可能有状态,需要断开后重建
- erosaConnection.dump 设置一些链接信息以及获取编码信息等,并向mysql发送binlog起始位置信息,根据dump协议开始fetcher日志,然后调用decoder将二进制数据解码为EVENT,最后调用SinkFunction去sink数据。该方法会一直调用去fetcher数据。除非fetcher调用失败
- 如果出现异常会调用相关对象的方法,释放资源。
SinkFunction
- SinkFunction调用sink方法去处理从mysql获取的EVENT数据,先会调用binlogParser.parse将EVENT对象转化为可以在网络中传输的CanalEntry.Entry对象。
- 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主页上的流程图描述整个过程:
其他
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的选型
- MemoryMetaManager 人如其名,所有的batch数据和cursor数据都保存在内存中,重启后所有信息丢失,不能断点续传
- FileMixedMetaManager 此处的MIXED表示该类型兼容MemoryMetaManager,实际上是其子类,其如MemoryMetaManager会将所有数据保存到内存中,并且会有一个ScheduledThreadPool来隔1秒钟把cursor刷新到文件meta.dat中。当重启时,会去读取meta.dat作为binlog读取的开始位置,从而实现了断点续传的功能。
- ZooKeeperMetaManager 同MemoryMetaManager一层,其将所有数据都保存到zookeeper中,因此其支持断点续传的功能。路径结构
* <pre> * /otter * canal * destinations * dest1 * client1 * filter * batch_mark * 1 * 2 * 3 * </pre>
- MixedMetaManager 有一点类似于FileMixedMetaManager模型,所有的数据都会保存到内存中,但是会通过FixedThreadPool去异步的把内存的所有数据(不仅仅是cursor)同步到zookeeper上。
- PeriodMixedMetaManager同FileMixedMetaManager几乎一样,也是只持久化cursor,只不过是持久化到zookeeper。
GroupEventSink
我们知道EntryEventSink是按照数据的先进先出的顺序进行存储的,而GroupEventSink的设计是为了满足当数据库分库分表时,又想使得数据的变更记录也是按照数据库执行的先后顺序存储到EventStore中(其实现主要是通过加锁和优先级队列PriorityBlockingQueue来保证变更数据存储到eventstore中的顺序)
EventStore
暂时只提供了MemoryEventStoreWithBuffer,其支持一次取多大内存和多少条数据两个方式
CanalEventParser
- 支持LocalBinlogEventParser、MysqlEventParser、GroupEventParser三种类型。LocalBinlogEventParser支持直接从本地的mysql数据的binlog文件中读取日志记录。
- MysqlEventParser除了上面已经介绍的会开启一个线程去dump binlog日志外,还支持HA切换的功能,其通过切换在配置文件中的masterInfo和standbyInfo信息,实现了在发生异常时切换到standbyInfo指定的数据库上。
- GroupEventParser 其主要用于处理分库分表时需要从多个数据库中获取binlog日志,然后调用同一个EventStore把数据加入到
HeartBeatHAController
在MysqlEventParser中会启动一个线程定时线程进行心跳检测,当每次进行心跳检测发现master连接异常时,会调用HAController的onFailed方法,会进行错误次数的累加,当达到detectingRetryTimes时,就会调用MysqlEventParser的master和standby切换。另外只要一次成功,那么以前的失败次数清零。
CanalInstance
- CanalInstanceWithSpring 通过spring去xml文件中初始化metaManager、eventStore、eventSink、eventParser,已经配置之间的引用关系,以及相应的start方法,启动整个instance服务。
- CanalInstanceWithManager 提供一种通过传入参数来启动上述组件和instance服务等。
canal官方开发文档中所说的HA
根据上述图,其实我们可以理解为常见的分布式系统中如何控制多个jvm在不同的机器中启动,我们如何保证只有一台机器能正常运行,其他机器都处于阻塞状态。这个可以看我前面的关系elasticjob的文档中已经说明的,主要就是通过zookeeper的瞬时有序节点来实现分布式锁的功能。具体的类LeaderLatch
另外对于集群类型的server的client也实现了ClusterCanalClientTest的实现,其本质上就是通过zookeeper去取正在运行的server地址,从而保证server自动切换好之后,client也能继续进行消费。
- 阿里中间件canal学习笔记
- 阿里中间件学习笔记
- Canal笔记
- [笔记]2016阿里中间件性能挑战赛(一)
- [笔记]2016阿里中间件性能挑战赛(二)
- [笔记]2016阿里中间件性能挑战赛(三)
- java中间件学习笔记
- 阿里canal使用步骤实例
- canal学习链接地址
- 【django 学习笔记】16-中间件
- (学习笔记) Laravel 中间件
- Kafka消息中间件学习笔记
- 阿里Canal部署安装小白教程
- 阿里Dubbo学习笔记
- 阿里大于---------学习笔记
- Java消息中间件学习笔记一 -- 什么是消息中间件?
- canal
- 阿里消息中间件
- (--)和(++)在值的前面和后面的区别
- webpack笔记--
- 【小白的CFD之旅】02 江小白
- android.database.sqlite.SQLiteConstraintException: NOT NULL constraint failed: GROUP_FILE.FILE_NAME
- 【小白的CFD之旅】03 老蓝
- 阿里中间件canal学习笔记
- RAID技术详解
- 评价一个学习算法(斯坦福machine learning week 6)
- C语言程序-递归和非递归分别实现求n的阶乘
- 【小白的CFD之旅】04 任务
- ios web交互收录:高度获取
- JVM系列(二)内存分区
- mysql联合查询和关联查询
- 包含.cpp .h的文件怎么运行(VisualStudio2013)