如果用户新加入了一个peerid,那怎么处理,这部分逻辑的入口在ReplicationSourceManager的addSource方法中:
addSource接受peerid作为参数,它会新创建一个ReplicationSourceInterface(实际就是ReplicationSource)和ReplicationEndpoint,并分别初始化两者,ReplicationSource负责hlog的读取逻辑,ReplicationEndpoint负责ship逻辑。其中,ReplicationSource是一个线程,该线程初始化需要将ReplicationEndpoint这个对象传入。
我们知道每个regionserver某个时刻,只有一个wal log处于正在被写入的状态,会有一个线程定期roll hlog,也就是把当前的正在写入的hlog关闭,形成hdfs上的一个hlog文件,并开启一个新的输入流,每个regionserver最多只有32个hlog,如果超过了32,会从最老的hlog开始删(并不会立即删除,而是转移到archive目录oldWALs下,由master上的core定期检查是否可删)
hlog在roll之前埋了一个hook,tellListenersAboutPreLogRoll,告诉listener马上就要有个新的hlog要被roll出形成了,Replication线程会获取这个listener的消息,把最新形成的hlog添加到ReplicationSourceManager维护的一个Set集合中(latestPaths),这一切都是在preLogRoll中完成的。
那么这个集合有什么用,我们通过addSource新加了一个peer cluster,latestPaths中的hlog文件作为初始化文件针对这个新的peer cluster启动一个ReplicationSource,这也就是说每个新加入的从集群只能从主集群同步最早加入时刻之后的数据,而之前的数据,即便hlog还存在,也不能够通过replication同步了。
回到addSource,该方法初始化ReplicationSource和ReplicationEndpoint之后,将latestPaths的文件逐一添加到ReplicationSource的内部map结构中以作为待处理的文件列表(实际是个map结构,map的key是hlog的前缀名,value是hlog路径名组成的queue,将hlog文件按前缀名做分组应该是考虑到multi-wal的功能而设计的,这个组数我们记为num,这个列表我们还会在后面的postLogRoll中遇到)。
addSource中初始化了ReplicationSource之后,就启动了这个线程。每个peerid只会实例化一个ReplicationSource,而每个ReplicationSource线程会实例化num个工作线程(workthread),实例化工作线程时,将ReplicationSource内部map结构中的value,也就是那个queue传递给这个工作线程作为他的待处理队列,这样每个工作线程处理来自于同一个分组的hlog(queue中是有排序的,按时间戳大小排序),同时保证了对hlog的读取是顺序进行的。
workthread是个生命周期非常长的线程,它会一直运行,每个循环里从queue中取出一个hlog赋值给currentPath,也就是这个workthread正在处理的hlog文件,然后调reader打开这个文件进行处理,这里reader的维护由ReplicationWALReaderManager负责,这个类中除了包括一个reader之外还有个成员变量position,这个变量记录了当前处理到的文件位点。然后reader向前读,每读取出一个hlogEntry,将position也向前移动,读出的hlogEntry放入List中,这个读取的循环有三个条件来判停,如下:
1、一直到读到文件末尾(hlog文件有magic word标记文件头和文件尾);
2、List的大小超过了我们配置的大小,默认是25000个
3、List中所有entry的大小超过我们的配置,默认是64M;
读取出来的entries经shipEdits传递到目标端集群,entries作为参数构造replicationContext,由replicationEndpoint传输到目标端,replicationEndpoint的传输方法replicate是个阻塞的方法,阻塞至返回给源端此次操作的结果(true or false)。replicate返回传输成功之后,调logPositionAndCleanOldLogs,将当前的处理信息在zk上持久化,包括处理的hlog名称以及处理的位点。
集群间数据复制的replicationEndpoint实现在HBaseInterClusterReplicationEndpoint这个类,具体replicate的执行是交由线程池去执行,线程池中的线程数默认是10个,所有的entries会均分为10组,每组构造一个Replicator对象(实现了callable接口的线程)将数据序列化后发送到目标端,发送的结果从future中返回。
上面的分析是从新加入一个peer cluster为入口分析的,对于已经长时间运行起来的replicate任务,如何发现此时有新的hlog形成,又如何记录这些hlog。
hlog更新前会调用preLogRoll,在preLogRoll中,首先调recordLog将这个newLog记录下来,这个recordLog都做了哪些事,首先在zk上的每个peerid下面,以这个新的hlog文件名为名创建一个zookeeper节点,可见主从复制的replicate状态信息都是持久化在zookeeper节点上,包括了当前正在有哪些hlog等待replicate和正在replicate的hlog处理到了哪个position。
hlog更新后会调用postLogRoll,postLogRoll将新产生的hlog文件逐一添加到已经运行起来的ReplicationSource中(也就是添加到我们前面提起过的那个列表queue,queue中将hlog按前缀名分组,每组指派一个worker读取改组的hlog文件),也就是更新每个目的端待处理文件的列表,源码如下:
void postLogRoll(Path newLog) throws IOException {
for (ReplicationSourceInterface source : this.sources) {
source.enqueueLog(newLog);
}
}
状态信息存储在zookeeper上,方便源端出现regionserver挂掉时,能够从zk上准确恢复replicate任务的状态,但是在replicate任务调度时并不是每次都是从zk上获取,而是在内存中存有一份roadmap,也就是ReplicationSourceManager的walsById这个成员变量。recordLog在更新zk上的信息时会同步更新walsById。
由于preLogRoll所依赖的hook只会监听在ReplicationSourceManger所在的那个regionserver的hlog roll变化,也就是说,每个regionserver只同步自己产生的hlog,除非本身regionserver进程挂掉,此时未完成的hlog同步会由其他regionserver接管;
HLog Roll更新由一个线程LogRoller负责,更新的周期是3600000ms(可配置,hbase.regionserver.logrool.period),具体hlog的roll流程需要研究hlog的写入模型,但是前面说到的postLogRoll和preLogRoll是通过内部hook埋设在roll的前后。
用户可以指定表级别的同步任务,所有的同步任务都发布到zk上面,每个目标集群有个peerId,ReplicationSourceManger实现了ReplicationListener接口,ReplicationSourceManager初始化之后将自己注册到replicationTracker上面,replicationTracker一方面会监听peerId节点的变化,以及时同步用户新加入的目标集群,另一方面会监听其它regionserver节点的变化,如果有regionserver节点挂掉了,会启动failover流程;
(一)如果有regionserver节点挂掉了,会回调listener(ReplicationSourceManager)的regionserver removed,ReplicationSourceManager发起一个NodeFailoverWorker任务处理失败节点上的hlog;
(二)如果用户新加了目标,会写在peerId节点下,此时Tracker会回调listener的peerListChanged方法,以更新ReplicationSourceManager所持有的replicationPeers列表;