接着上一篇 RocketMQv4.9.1源码分析-HA主从 Master读写处理 解答了Master相关的问题,这篇文章围绕Slave相关的一些问题继续看代码。
对于slave,我们有如下一些疑惑:
slave如何获取master的路由信息slave如何向master报告offset的slave如何处理master同步的数据
在整体的类图布局中,slave相关的代码都在HAClient类中
HAClient
HAClient部分是Slave处理的核心,其中包括三部分:
- slave与master建立连接
- slave向master汇报同步进度
- slave接收master的同步数据并处理
HAClient 启动
之前提到了,HAClient启动的触发时机是HAService启动的方法中,在store/src/main/java/org/apache/rocketmq/store/ha/HAService.start()中。
// HAService 启动
public void start() throws Exception {
this.acceptSocketService.beginAccept();
this.acceptSocketService.start();
this.groupTransferService.start();
this.haClient.start();
}
HAClient的启动代码在store/src/main/java/org/apache/rocketmq/store/ha/HAService$HAClient.run()路径下。
其中3个核心的步骤在代码注释中有进行标记
@Override
public void run() {
log.info(this.getServiceName() + " service started");
while (!this.isStopped()) {
try {
// 步骤一:连接到master
if (this.connectMaster()) {
// 步骤二:如果距离上次报告的时间间隔到了最大等待时间,立刻执行一次报告
if (this.isTimeToReportOffset()) {
// 报告slave offset
boolean result = this.reportSlaveMaxOffset(this.currentReportedOffset);
if (!result) {
this.closeMaster();
}
}
// 检查是否有读事件
this.selector.select(1000);
//步骤三:处理master返回的消息
boolean ok = this.processReadEvent();
if (!ok) {
this.closeMaster();
}
// 处理完读事件后,如果slave offset更新,需要再次发送新的slave offset
if (!reportSlaveMaxOffsetPlus()) {
continue;
}
long interval = HAService.this.getDefaultMessageStore().getSystemClock().now() - this.lastWriteTimestamp;
if (interval > HAService.this.getDefaultMessageStore().getMessageStoreConfig().getHaHousekeepingInterval()) {
log.warn("HAClient, housekeeping, found this connection[" + this.masterAddress + "] expired, " + interval);
this.closeMaster();
log.warn("HAClient, master not response some time, so close connection");
}
} else {
this.waitForRunning(1000 * 5);
}
} catch (Exception e) {
log.warn(this.getServiceName() + " service has exception. ", e);
this.waitForRunning(1000 * 5);
}
}
log.info(this.getServiceName() + " service end");
}
Slave连接Master
connectMaster()方法的目的是与master进行连接。
// master 地址(master的地址在配置文件中配置)
private final AtomicReference<String> masterAddress = new AtomicReference<>();
private boolean connectMaster() throws ClosedChannelException {
if (null == socketChannel) {
String addr = this.masterAddress.get();
if (addr != null) {
SocketAddress socketAddress = RemotingUtil.string2SocketAddress(addr);
if (socketAddress != null) {
this.socketChannel = RemotingUtil.connect(socketAddress);
if (this.socketChannel != null) {
// 读事件,用于监听master的返回消息
this.socketChannel.register(this.selector, SelectionKey.OP_READ);
}
}
}
// 设置为当前commitlog的偏移量
this.currentReportedOffset = HAService.this.defaultMessageStore.getMaxPhyOffset();
this.lastWriteTimestamp = System.currentTimeMillis();
}
return this.socketChannel != null;
}
有一个点关注下,就是currentReportedOffset字段,这个字段的目的是表示当前slave已同步的进度,后续在向master进行汇报时也是使用该字段的值。这里进行初始化的时候直接设置为commitlog文件的最大偏移量,如果没有commitlog文件则为0。
Slave汇报offset
// 步骤二:如果距离上次报告的时间间隔到了最大等待时间,立刻执行一次报告
if (this.isTimeToReportOffset()) {
// 报告slave offset
boolean result = this.reportSlaveMaxOffset(this.currentReportedOffset);
if (!result) {
this.closeMaster();
}
}
isTimeToReportOffset()的作用是判断上一次进行汇报的时间与当前时间差是否大于最大等待间隔(默认5s),这意味着即使5s没有收到master的任何消息,slave也会发送一个汇报请求给master,作用相对于一个心跳包。
看下reportSlaveMaxOffset()方法:
private boolean reportSlaveMaxOffset(final long maxOffset) {
// 写位置设为0
this.reportOffset.position(0);
// 可写长度为8字节
this.reportOffset.limit(8);
// 数据内容为slave当前的offset
this.reportOffset.putLong(maxOffset);
// 写模式切换到模式
// 将读位置设为0
this.reportOffset.position(0);
// 可读长度为8字节
this.reportOffset.limit(8);
for (int i = 0; i < 3 && this.reportOffset.hasRemaining(); i++) {
try {
// 将数据写入channel中
this.socketChannel.write(this.reportOffset);
} catch (IOException e) {
log.error(this.getServiceName() + "reportSlaveMaxOffset this.socketChannel.write exception", e);
return false;
}
}
lastWriteTimestamp = HAService.this.defaultMessageStore.getSystemClock().now();
return !this.reportOffset.hasRemaining();
}
可以知道slave的汇报请求的数据包内容很简单,就是一个8字节大小的offset数据。
这里代码中有一个细节,rocketmq的作用再进行写模式切换到读模式时没有使用flip()方法,而是手动设置position和limit,这是由于NIO是一个非阻塞IO,write方法不一定会一次将ByteBuffer的数据全部写入。
Slave处理同步数据
步骤三中,调用processReadEvent()对master返回的数据进行处理,看代码前,我们已经知道master返回的数据是未同步的消息,那么slave要做的事情是什么?当然就是把这个未同步的数据保存到本地的commitlog文件中。
private boolean processReadEvent() {
// 连续读取到数据大小为0的次数
int readSizeZeroTimes = 0;
// 一直读取缓冲区的数据,直到没有剩余
while (this.byteBufferRead.hasRemaining()) {
try {
int readSize = this.socketChannel.read(this.byteBufferRead);
if (readSize > 0) {
readSizeZeroTimes = 0;
boolean result = this.dispatchReadRequest();
if (!result) {
log.error("HAClient, dispatchReadRequest error");
return false;
}
} else if (readSize == 0) {
// 如果连续三次读到为空,则跳出方法,这里的作用?
if (++readSizeZeroTimes >= 3) {
break;
}
} else {
log.info("HAClient, processReadEvent read socket < 0");
return false;
}
} catch (IOException e) {
log.info("HAClient, processReadEvent read socket exception", e);
return false;
}
}
return true;
}
代码里调用了dispatchReadRequest()方法进行请求的处理,看下这个方法:
private boolean dispatchReadRequest() {
final int msgHeaderSize = 8 + 4; // phyoffset + size
while (true) {
int diff = this.byteBufferRead.position() - this.dispatchPosition;
if (diff >= msgHeaderSize) {
// master commitlog 偏移量
long masterPhyOffset = this.byteBufferRead.getLong(this.dispatchPosition);
// 消息大小
int bodySize = this.byteBufferRead.getInt(this.dispatchPosition + 8);
// 本地 commitlog 偏移量
long slavePhyOffset = HAService.this.defaultMessageStore.getMaxPhyOffset();
if (slavePhyOffset != 0) {
// 如果slave的offset和master的offset不一样,说明数据同步过程发生了问题,不再继续同步。
if (slavePhyOffset != masterPhyOffset) {
log.error("master pushed offset not equal the max phy offset in slave, SLAVE: " + slavePhyOffset + " MASTER: " + masterPhyOffset);
return false;
}
}
// 剩余空间够用,将消息追加到commitlog中
if (diff >= (msgHeaderSize + bodySize)) {
// 消息数组
byte[] bodyData = byteBufferRead.array();
// 消息的
int dataStart = this.dispatchPosition + msgHeaderSize;
// 添加数据到本地commitlog中
HAService.this.defaultMessageStore.appendToCommitLog(masterPhyOffset, bodyData, dataStart, bodySize);
this.dispatchPosition += msgHeaderSize + bodySize;
if (!reportSlaveMaxOffsetPlus()) {
return false;
}
continue;
}
}
if (!this.byteBufferRead.hasRemaining()) {
this.reallocateByteBuffer();
}
break;
}
return true;
}
整体的逻辑可以分为俩部分,第一部分是解析请求包,获取消息数据,第二部分是把消息数据写入到commitlog文件中。
这两部分代码已经写的很清楚了,比较容易理解,就不再赘述。