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也能继续进行消费。