在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 currentInstanceNames = new HashSet();
// 判断目录内文件的变化
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 newFileInfo = new ArrayList();
for (File instanceConfig : instanceConfigs) {
newFileInfo.add(new FileInfo(instanceConfig.getName(), instanceConfig.lastModified()));
}
lastFile.setInstanceFiles(newFileInfo);
}
}
}
}
// 判断目录是否删除
Set deleteInstanceNames = new HashSet();
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() {
public CanalInstance apply(String destination) {
return canalInstanceGenerator.generate(destination);
}
});
// lastRollbackPostions = new MapMaker().makeMap();
}
}
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释放资源
server和instance如下:一个server中可以包含多个instance,每个instance(有一个spring的beanfactory相对应)都具有evenparse、eventsik、eventstore、metamanager四个组件
根据第一部分的介绍我们知道,一个instance对应于conf目录下的一个子文件夹,其对应的beanfactory通过加载canal.properties和该instance目录下的instance.properties来实例化相关组件和instance对象
<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">
<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"/>
<property name="locationNames">
<list>
<value>classpath:canal.propertiesvalue>
<value>classpath:${canal.instance.destination:}/instance.propertiesvalue>
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="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>
<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>
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是实现了基于循环队列的数据库事件的存储机制,其中ack
注意:
1. metamanger只记录每次客户端请求时,数据库binlog的开始位置、结束位置、ack位置(即custor位置和真正的ack行为没有直接关系),实际的binlog数据变更的详情并没有记载和客户端紧密相连。
2. eventsotre是记录的binlog的详细数据,其中ack表示被客户端已经处理成功的日志记录。get表示客户端正在处理的日志记录。put表示新加入的日志的位置。
用于处理得到的entry日志,并保存到eventstore中。
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可以实现多个客户端同时获取数据。
<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方法的调用
该组件主要负责将LogEvent日志转化为可传输的Entry对象。其中在解析的过程中会根据eventType进行处理,对于所有ROWS_EVENT事件都会应用过滤条件eventFilter和eventBlackFilter。
Parese主要是通过启动一个parseThread来实现数据库binlog的dump功能,其通过binlog协议与mysql之间建立连接,然后不断的fetcher数据,之后调用SinkFunction中的BinLogParse来将数据库的EVENT数据转化为可以在网络间传输的CanalEntry.Entry数据,并且会调用EventTransactionBuffer来实现CanalEntry.Entry数据的存储,EventTransactionBuffer会将存储的数据按照事务的维度进行切分,将切分好的数据调用EntryEventSink将数据存储到EventStore中,最后通过从EventStore取出数据传输到Client端,并且对于客户端数据的处理情况会提供一个CanalMetaManager来记录客户端获取数据的进度和实现ack、回滚等功能。
如我在介绍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一定是有问题的
id="eventSink" class="com.alibaba.otter.canal.sink.entry.EntryEventSink">
<property name="eventStore" ref="eventStore" />
<property name="filterTransactionEntry" value="true" />
在此需要说明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
* <pre>
* /otter
* canal
* destinations
* dest1
* client1
* filter
* batch_mark
* 1
* 2
* 3
* pre>
我们知道EntryEventSink是按照数据的先进先出的顺序进行存储的,而GroupEventSink的设计是为了满足当数据库分库分表时,又想使得数据的变更记录也是按照数据库执行的先后顺序存储到EventStore中(其实现主要是通过加锁和优先级队列PriorityBlockingQueue来保证变更数据存储到eventstore中的顺序)
暂时只提供了MemoryEventStoreWithBuffer,其支持一次取多大内存和多少条数据两个方式
在MysqlEventParser中会启动一个线程定时线程进行心跳检测,当每次进行心跳检测发现master连接异常时,会调用HAController的onFailed方法,会进行错误次数的累加,当达到detectingRetryTimes时,就会调用MysqlEventParser的master和standby切换。另外只要一次成功,那么以前的失败次数清零。
根据上述图,其实我们可以理解为常见的分布式系统中如何控制多个jvm在不同的机器中启动,我们如何保证只有一台机器能正常运行,其他机器都处于阻塞状态。这个可以看我前面的关系elasticjob的文档中已经说明的,主要就是通过zookeeper的瞬时有序节点来实现分布式锁的功能。具体的类LeaderLatch
另外对于集群类型的server的client也实现了ClusterCanalClientTest的实现,其本质上就是通过zookeeper去取正在运行的server地址,从而保证server自动切换好之后,client也能继续进行消费。