文章目录
- rocketmq HA
- 主从同步复制实现原理
- 元数据同步
- commitlog同步
由于HA机制需要两台机器运行,我笔者只有一台电脑,不然就需要修改很多端口文件配置来进行启动,难度较大,所以这节就直接分析代码,就不debug进入到程序了。
rocketmq HA
rocketmq添加slave有如下好处:
数据备份:保证多台broker数据的冗余,特别是主从同步复制的情况下,master出现不可恢复的故障之后,保证数据不丢失
高可用性:即使master掉线,consumer会自动从连到对应的slave机器,不会出现消费停滞的情况
提高性能:分担master的压力,主要表现在,拉取消息的最大偏移量与本地存储最大物理偏移差值超过一定阈值的时候,会发生缺页
问 题,就会发生虚拟内存的swap交换,影响broker性能,所以这个时候就会建议到slave上拉取消息。
所以对一个中间件来HA是很有必要的
本节主要分析如下类容:
- 主从同步复制实现原理
- rocketmq读写分离机制
主从同步复制实现原理
首先我们先思考一下主从复制需要做什么?
- broker的一切信息slave都需要总master去拿,比如topic信息,消费偏移量,延迟消息,主题信息,因为这些东西都只在broker上也就是说,只有在写broker上有,也就是在master上,所以需要同步这些数据。
- 还有commitlog信息应为slave将自己的
偏移量
发送给broker,然后broker来将数据
发送给slave来顺序写就可以了,
commitlog同步主要是如下流程:
元数据同步
在rocketmq元数据同步主要有如下几个元数据:
- ConsumerOffsetManager 消费进度
- ScheduleMessageService 延时消息
- SubscriptionGroupManager 主题信息
- TopicConfigManager topic信息
我们直接来到org.apache.rocketmq.broker.slave.SlaveSynchronize#syncAll方法,调用栈如下:
从Brokercontroller启动开始,执行了handleSlaveSynchronize传入了当前broker的角色,如果是slave那么就会开启一个定时线程每10s执行syncall方法。
接下来我们分析一下其中一个元数据TopicConfigManager,
看下面这段代码,
主要是this.brokerController.getBrokerOuterAPI().getAllTopicConfig(masterAddrBak);从master里面拿到topic信息,然后设置到brokercontroller里面的topicconfigmanager里面也就是我们的元数据之一TopicConfigManager
我们跟进getAllTopicConfig方法:通过remoting模块像服务器发送了RequestCode.GET_ALL_TOPIC_CONFIG这个code的请求,可以看到同步元数据这里使用的还是remoting模块
接下来我们主要看一下TopicConfigManager里面有哪些重要信息:
- topicconfigtable 存储topic信息
- dataversion 版本信息(主从版本比对是否一致)
其余几个元数据的同步也大概是这个流程,就不展开了,读者可以翻看源码。
commitlog同步
接着来到我们最主要的commitlog同步,也就是主要的消息文件的同步:
消息文件同步服务,主要是在org.apache.rocketmq.store.ha.HAService这个类,
在broker启动的时候会执行如下几个方法:
- this.acceptSocketService.beginAccept nio服务类,打开接受一个连接请求
- this.acceptSocketService.start nio 开启一个死循环,每1s钟接受一个请求,接受到请求将SocketChannel请求给 HAConnection来执行
- this.groupTransferService.start 开启一个线程来查看同步请求是否完成,如果某个请求被同步完成了就通知producer
- this.haClient.start slave端需要启动的,上报offset给master,接受master发送的数据
介绍完启动的主要动作,接下来我们从消息同步发送,然后HA,这个流程来分析整个代码。
在org.apache.rocketmq.store.CommitLog#putMessage这个方法里面最后有一个handleHA(result, putMessageResult, msg);方法,
整个方法是在org.apache.rocketmq.broker.processor.SendMessageProcessor里面被调用的,
而SendMessageProcessor对应的code是SEND_MESSAGE,
也就是说,在producer发送消息到broker之后,在broker端最后会执行HandlHA这个方法来进行主从同步,
那么问题来了,这边并没有判断是否是同步异步啊?
带着这个问题,我们进入到handleHA方法:
在这个方法里面判断了如果broker的角色是SYNC_MASTER,是同步的,那就会构建一个GroupCommitRequest
请求,然后将这个请求放到HAservice里面,
看到这里我们就会联想到consumer的流程,也就构建一个请求放到一个集合里面,然后由consumerservice启动一个线程来轮询发送请求
所以这里我们猜想也是方法哦HAservice里面,然后里面一定有一个轮训来判断是否完成
,
放到HAservice里面,接着唤醒了HAconnection里面的写线程,这里我们先不管,
接着调用了request.future().get方法进行阻塞,看到答案了,也就是在这里如果是同步broker就会在这里进行阻塞,也就是说GroupCommitRequest里面有一个future
对象,在这里阻塞,那一定在同步完成之后,将这个future来complete
,我们接着往下看:
回到我们上面那个启动方法里面有一个this.groupTransferService.start()方法是轮训的任务我们点进去看看:
org.apache.rocketmq.store.ha.HAService.GroupTransferService#run方法,里面是一个死循环调用了this.doWaitTransfer();方法,我们跟进:如下图
还记得刚刚我们将GroupCommitRequest放到一个集合里面,就是这边的this.requestsRead,然后这边是遍历这个集合然后判断HAService.this.push2SlaveMaxOffset.get()当前master的broker对应的几个slave上面最大的偏移量是否大于request中的偏移量
,如果大于,说明同步已经完成,否则拿到waitUntilWhen最大等待时间,每1s利用偏移量判断一次是否完成,最多5次,也就是5s
,也就是ha同步最大超时时间
- 这里也就解释了slave只要有一个同步完成了,就算同步完成了,因为取得最大值
如果某个请求完成了同步:
那么就会执行下面的req.wakeupCustomer方法:如下:
将请求里面的future来complete掉,就会唤醒我们的broker的putmessage主线程,也就是说我们同步完成完全是靠的push2SlaveMaxOffset这个slave最大偏移量:
那么问题来了:我们这个push2SlaveMaxOffset最大偏移量什么时候会更新呢?
我们再次回到一开始的启动方法:
这次我们看this.haClient.start();方法:
⚠️注意:client里面的方法是在slave里面执行的,也就是说,我们下面分析的client的东西,都是slave这个角色来执行的!
如下图:
在run方法里面有一个死循环,
this.isTimeToReportOffset()这个方法是判断是否到执行时间,每5s执行一次
,感兴趣的同学可以点进方法看一下,我们接着往下:
this.reportSlaveMaxOffset(this.currentReportedOffset);将自己的当前的currentReportedOffset通过nio发送给master,
接下来,master是不是要接受到slave的offset了,我们又要回到启动方法
这次我们看到的是this.acceptSocketService.start()方法:
⚠️这里又回到了master角色:
方法启动了AcceptSocketService这个线程类:
看名字就知道是接受socket消息的,这边可以看到进行了一些校验之后构建了一个HAConnection,注意这边是将 socketchannel传入了进去,因为使用的原生nio并没有用netty,所以在HAConnectino里面一定会有个解析过程,解决沾包拆包问题,然后start:
我们来到org.apache.rocketmq.store.ha.HAConnection类的start方法:
方法启动了两个线程:
readSocketService是负责读去socket内容,WriteSocketService是负责将master上的消息通过nio发送给slave。
我们先看readSocketService线程:
直接看到他的run方法里面执行了this.processReadEvent():
里面是根据socketchannel来拿到slave的offset:
因为是nio不是netty这里主要流程是处理了拆包的过程:从socketchannel拿数据,如果不到8个字节,就继续拿,最多拿3次,拿到8个字节之后,然后getlong拿到8个字节
也就是刚刚slave发送的偏移量readOffset,然后将这个偏移量放到HAConnection.this.slaveRequestOffset
里面,交给WriteSocketService线程去处理,
来到org.apache.rocketmq.store.ha.HAConnection.WriteSocketService的run方法:
HAConnection.this.slaveRequestOffset是刚刚设置的slave偏移量,
this.nextTransferFromWhere默认等于-1
HAConnection.this.slaveRequestOffset不等于0,
this.nextTransferFromWhere = HAConnection.this.slaveRequestOffset:将nextTransferFromWhere设置成刚刚拉取的slave的offset
如果上次写完了,那这次就执行这次的写数据:
将byteBufferHeader设置一下请求头,然后开始transferData()将请求头用this.socketChannel.write写入到socketChannel里面,
接下来更具nextTransferFromWhere来从messagestore里面更具最大发送长度来拿到selectResult数据,这里的haTransferBatchSize是32KB,然后将数据传给this.selectMappedBufferResult这个全局变量,调用this.transferData()方法来写入到socketChannel里面:
这个时候我们再回到haclient这个类:
⚠️现在又回到了slave
如下图:
在刚刚master将数据写回来之后this.selector.select(1000)会拿到连接,接着执行this.processReadEvent()这个方法来处理请求:
在这个方法里面同样也需要处理拆包问题,也是最多拉取3次,然后执行了dispatchReadRequest方法:
来到最核心的方法,是slave将数据持久化到commitlog里面:
this.byteBufferRead.position()是整个读取的长度,如果长度比12个字节长,那就根据请求头里面后4个字节读出消息数据的长度,bodySize,然后将position移到12,然后读取bodySize长度到bodyData里面,接着,调用messagestore将数据持久化:HAService.this.defaultMessageStore.appendToCommitLog(masterPhyOffset, bodyData);
那么有小伙伴会问了:那master怎么知道你这儿的最大偏移量呢?
这个地方slave会在下一次拉取master数据的时候将最大偏移量发送给master,所以发送给master的offset既有告诉master slave的情况,又有告诉master slave需要拉取的情况。
至此,rocketmq的主从同步ha机制就已经完成!