rocketMQ在实现消息稳定,不丢失等高可用特性时,采用了2种技术方案,一种是经典Master/Slave设计风格,但是有个最致命的缺点在出现主节点故障时,无法切换某个slave作为主节点,导致该节点集群不可使用,必须将master节点恢复后才能使用。第二种是通过Dledger框架,不仅实现了数据消息的备份,也可以实现故障转移,保证节点集群可以继续使用。
本文主要熟悉Master/Slave是如何实现,并且rocketMQ也是有自身实现代码,并没有引入第三方框架。
作为Broker服务,他有2种角色,分别是Master,Slave。当然生产消息主要与Master交互,但是消息消费2者都能使用。为了消息的稳定,安全,持久化,将消息备份在异地服务器是重要手段,所以Slave节点主要是保持与Master中的消息持续获取,并且保持在本地磁盘中。当然Slave与Master同步数据,不仅仅只有消息,还有很多其他的数据。
SlaveSynchronize Slave同步服务
在BrokerController中,在start启动方法时,针对broker角色进行处理
if (!messageStoreConfig.isEnableDLegerCommitLog()) {
startProcessorByHa(messageStoreConfig.getBrokerRole());
handleSlaveSynchronize(messageStoreConfig.getBrokerRole());
}
当然采用Master/Slaver模式才会执行代码,在handleSlaveSynchronize方法中
private void handleSlaveSynchronize(BrokerRole role) {
if (role == BrokerRole.SLAVE) {
if (null != slaveSyncFuture) {
slaveSyncFuture.cancel(false);
}
this.slaveSynchronize.setMasterAddr(null);
slaveSyncFuture = this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
BrokerController.this.slaveSynchronize.syncAll();
}
catch (Throwable e) {
log.error("ScheduledTask SlaveSynchronize syncAll error.", e);
}
}
}, 1000 * 3, 1000 * 10, TimeUnit.MILLISECONDS);
} else {
//handle the slave synchronise
if (null != slaveSyncFuture) {
slaveSyncFuture.cancel(false);
}
this.slaveSynchronize.setMasterAddr(null);
}
}
在角色为Slave时,会开启定时任务,执行slaveSynchronize对象的syncAll同步方法
public void syncAll() {
this.syncTopicConfig();
this.syncConsumerOffset();
this.syncDelayOffset();
this.syncSubscriptionGroupConfig();
}
在这块同步代码块中,同步了很多内容,包括同步topic主题配置数据,消费进度数据,消息延迟队列数据同步,和订阅组配置同步。举例其中topic配置同步内容
private void syncTopicConfig() {
String masterAddrBak = this.masterAddr;
if (masterAddrBak != null && !masterAddrBak.equals(brokerController.getBrokerAddr())) {
try {
TopicConfigSerializeWrapper topicWrapper =
this.brokerController.getBrokerOuterAPI().getAllTopicConfig(masterAddrBak);
if (!this.brokerController.getTopicConfigManager().getDataVersion()
.equals(topicWrapper.getDataVersion())) {
this.brokerController.getTopicConfigManager().getDataVersion()
.assignNewOne(topicWrapper.getDataVersion());
this.brokerController.getTopicConfigManager().getTopicConfigTable().clear();
this.brokerController.getTopicConfigManager().getTopicConfigTable()
.putAll(topicWrapper.getTopicConfigTable());
this.brokerController.getTopicConfigManager().persist();
log.info("Update slave topic config from master, {}", masterAddrBak);
}
} catch (Exception e) {
log.error("SyncTopicConfig Exception, {}", masterAddrBak, e);
}
}
}
从master服务器上获取到了topicWrapper包装好的topic配置内容,然后更新自身brokerController中的相关的topic数据,最终进行持久化。其他的同步数据,也是一样,需要持久化到本地文件中。在这里,目前只是同步了一些外围的配置数据,消费进度数据等,但是并没有说明消息是如何同步的。
消息同步
消息同步,设计到存储相关,所以将这部分核心代码放在了store模块下,其中HAService是核心服务。
首先了解一下HAService服务内部类HAClient,他是最为Slave客户端,发起请求同步Master消息数据。
HAClient
private final AtomicReference masterAddress = new AtomicReference<>(); // master地址
private final ByteBuffer reportOffset = ByteBuffer.allocate(8);
private SocketChannel socketChannel;
private Selector selector;
private long lastWriteTimestamp = System.currentTimeMillis();
private long currentReportedOffset = 0;
private int dispatchPosition = 0; // 该位置,是不停的增长的,当byteBufferRead写满,会重置为0
private ByteBuffer byteBufferRead = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE);
private ByteBuffer byteBufferBackup = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE);
这部分是client的基本数据,其中记录了masterAddress地址,currentReportedOffset 当前上报的偏移量,dispatchPosition临时记录存储的位置。由于HAClient是实现了ServiceThread类,所以他也是一个线程类
public void run() {
log.info(this.getServiceName() + " service started");
while (!this.isStopped()) {
try {
if (this.connectMaster()) {
if (this.isTimeToReportOffset()) {
boolean result = this.reportSlaveMaxOffset(this.currentReportedOffset);
if (!result) {
this.closeMaster();
}
}
this.selector.select(1000);
boolean ok = this.processReadEvent();
if (!ok) {
this.closeMaster();
}
// 上报一下slave的最大偏移量进度
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");
}
作为客户端,他需要主动发起连接master,所以在尝试得到获取数据时,确认master是否连接成功,在这里,rocketMQ自己实现了基于jdk的NIO实现通信。
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) {
this.socketChannel.register(this.selector, SelectionKey.OP_READ);
}
}
}
this.currentReportedOffset = HAService.this.defaultMessageStore.getMaxPhyOffset();
this.lastWriteTimestamp = System.currentTimeMillis();
}
return this.socketChannel != null;
}
在连接master时,确认masterAddress地址是否存在,然后同步该地址得到channel,并且注册到selector中,进行读写监听。当然在连接初始化时,得到currentReportedOffset值,即为当前slave的存储消息的最大偏移量。在连接master成功后,都会定时进行上报master当前偏移量
private boolean reportSlaveMaxOffset(final long maxOffset) {
this.reportOffset.position(0);
this.reportOffset.limit(8);
this.reportOffset.putLong(maxOffset);
this.reportOffset.position(0);
this.reportOffset.limit(8);
// 上报一个slave最大的偏移量
for (int i = 0; i < 3 && this.reportOffset.hasRemaining(); i++) {
try {
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();
}
上报内容也很简单,将reportOffset 字节缓存进行重置,并且添加8个子节点maxOffset值,然后写入到socketChannel中。这样master就能直到当前slave的上报进度了。当然定时上报还有另外好处,可以用这样的方式代替实时心跳检测,保证channel长时间连接。
在master下发数据时候,slave就能监听到读事件,然后channel就能从通道中读取字节流。
private boolean processReadEvent() {
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;
}
processReadEvent 就是用以处理读事件的,其中byteBufferRead是用来保存读取channel通道中的字节流。当读取到内容时,readSize是大于0的。读取完内容后,就开始执行分发读请求 dispatchReadRequest方法。
private boolean dispatchReadRequest() {
final int msgHeaderSize = 8 + 4; // phyoffset + size
int readSocketPos = this.byteBufferRead.position();
while (true) {
int diff = this.byteBufferRead.position() - this.dispatchPosition;
if (diff >= msgHeaderSize) {
// 得到物理偏移量
long masterPhyOffset = this.byteBufferRead.getLong(this.dispatchPosition);
// 得到body的大小
int bodySize = this.byteBufferRead.getInt(this.dispatchPosition + 8);
long slavePhyOffset = HAService.this.defaultMessageStore.getMaxPhyOffset();
if (slavePhyOffset != 0) {
if (slavePhyOffset != masterPhyOffset) {
log.error("master pushed offset not equal the max phy offset in slave, SLAVE: "
+ slavePhyOffset + " MASTER: " + masterPhyOffset);
return false;
}
}
if (diff >= (msgHeaderSize + bodySize)) { // 至少是一个完整的包
byte[] bodyData = new byte[bodySize];
this.byteBufferRead.position(this.dispatchPosition + msgHeaderSize);
this.byteBufferRead.get(bodyData);
// 写入到slave中去
HAService.this.defaultMessageStore.appendToCommitLog(masterPhyOffset, bodyData);
this.byteBufferRead.position(readSocketPos);
this.dispatchPosition += msgHeaderSize + bodySize; // dispatchPosition 记录的是n个完整的包长度
if (!reportSlaveMaxOffsetPlus()) {
// 上报一下slave的最大偏移量
return false;
}
continue;
}
}
if (!this.byteBufferRead.hasRemaining()) {
this.reallocateByteBuffer();
}
break;
}
return true;
}
定义了一个msgHeaderSize 消息头长度,readSocketPos是当前byteBufferRead的位置,首先明确,byteBufferRead如果从channel中读取数据时候,他的pos位置也是增长的。所以readSocketPos不是读取消息的位置,当前消息的终点位置。那么怎么得到读取消息的起始位置呢,在HAClient中,有个属性dispatchPosition 就是用来保存上一次读取的位置。在master发送消息数据的时候,基本协议的结构是 消息头(包括物理偏移量,消息body的长度)+ 数据内容。在准备读取内容时,确认上一次读取的位置dispatchPosition与当前byteBufferRead的位置的长度差大于等于一个消息头的大小。先从上一个起始点开始读取8个字节,即为masterPhyOffset最大偏移量,然后读取4个字节bodySize,消息内容长度。当前会进行判断slave服务中当前最大的物理偏移量slavePhyOffset 是否与master推送过来的masterPhyOffset是否一致。这样的目的是保证slave与master数据同步一致性,保证是不会丢失。在diff >= (msgHeaderSize + bodySize)比较中,确定是否存在一个完整的数据包。然后声明长度为bodySize的bodyData字节数组,并且重置了byteBufferRead的pos位置,然后将数据复制给bodyData,然后将数据添加到commitLog中。数据添加完成后,又将byteBufferRead的pos位置重置为readSocketPos,并且dispatchPosition 又增加了一个完整数据包的长度,最后及时上报master服务当前slave的最大物理偏移量。
byteBufferRead最为存储通信数据的载体,长度为4M。在目前数据获取时,byteBufferRead是一直在增加,即pos在增加,即有读取到新得数据,byteBufferRead的pos一直变大,直到limit=pos时无法添加内容。为保证数据持续获取,肯定需要将byteBufferRead重置,然后再读取。所以才会有reallocateByteBuffer方法
private void reallocateByteBuffer() {
int remain = READ_MAX_BUFFER_SIZE - this.dispatchPosition;
if (remain > 0) {
this.byteBufferRead.position(this.dispatchPosition);
this.byteBufferBackup.position(0);
this.byteBufferBackup.limit(READ_MAX_BUFFER_SIZE);
this.byteBufferBackup.put(this.byteBufferRead); // 将read没有读取的部分放入到backup中
}
this.swapByteBuffer();
this.byteBufferRead.position(remain);
this.byteBufferRead.limit(READ_MAX_BUFFER_SIZE);
this.dispatchPosition = 0;
}
当byteBufferRead数据存满,但是任然会出现 最后一个数据包是不完整的。肯定需要将最后一段数据进行保存起来,下次继续使用。在这里,他采用了byteBufferBackup一个备份的字节缓存。remain是指一个数据包的部分数据长度。当remain大于0,说明包不完整,就会将byteBufferRead剩余部分复制给重置后的byteBufferBackup,此时byteBufferBackup是存在数据,并且当前pos为remain。swapByteBuffer方法就是将byteBufferRead和byteBufferBackup执行对象互相交换,即现在的byteBufferRead就是原来的byteBufferBackup对象。然后又重置了byteBufferRead的limit值,并且dispatchPosition的位置也变成0了。下次byteBufferRead再从socketChannel中读取的位置就是从remain开始了。
这是作为slave端,如何主动发起连接,并且上报最大偏移量和消息数据获取并存储的逻辑。
Master服务端
最为服务端,首先他可以由多个slave同时同步消息数据,所以需要有个管理客户端的服务。
AcceptSocketService
该类是HAService内部类,他是监听管理连接slave的connection服务
在配置通信服务端时
public void beginAccept() throws Exception {
this.serverSocketChannel = ServerSocketChannel.open();
this.selector = RemotingUtil.openSelector();
this.serverSocketChannel.socket().setReuseAddress(true);
this.serverSocketChannel.socket().bind(this.socketAddressListen);
this.serverSocketChannel.configureBlocking(false);
this.serverSocketChannel.register(this.selector, SelectionKey.OP_ACCEPT);
}
绑定端口,配置非阻塞,并且注册了接收事件,即接受客户端连接请求。
在开启线程时
public void run() {
log.info(this.getServiceName() + " service started");
while (!this.isStopped()) {
try {
this.selector.select(1000);
Set selected = this.selector.selectedKeys();
if (selected != null) {
for (SelectionKey k : selected) {
if ((k.readyOps() & SelectionKey.OP_ACCEPT) != 0) {
// 有请求连接
SocketChannel sc = ((ServerSocketChannel) k.channel()).accept();
if (sc != null) {
HAService.log.info("HAService receive new connection, "
+ sc.socket().getRemoteSocketAddress());
try {
// 创建一个客户端的连接,
HAConnection conn = new HAConnection(HAService.this, sc);
conn.start();
HAService.this.addConnection(conn);
} catch (Exception e) {
log.error("new HAConnection exception", e);
sc.close();
}
}
} else {
log.warn("Unexpected ops in select " + k.readyOps());
}
}
selected.clear();
}
} catch (Exception e) {
log.error(this.getServiceName() + " service has exception.", e);
}
}
log.info(this.getServiceName() + " service end");
}
就是实时监听连接请求,selected 即为得到事件的所以连接通道,然后判断确认存在OP_ACCEPT事件,将客户端SocketChannel封装成HAConnection并且添加到HAService中connectionList集合中。当然AcceptSocketService只是管理连接客户端数,服务器真正读写逻辑在HAConnection完成的。
HAConnection
public HAConnection(final HAService haService, final SocketChannel socketChannel) throws IOException {
this.haService = haService;
this.socketChannel = socketChannel;
this.clientAddr = this.socketChannel.socket().getRemoteSocketAddress().toString();
this.socketChannel.configureBlocking(false);
this.socketChannel.socket().setSoLinger(false, -1);
this.socketChannel.socket().setTcpNoDelay(true);
this.socketChannel.socket().setReceiveBufferSize(1024 * 64);
this.socketChannel.socket().setSendBufferSize(1024 * 64);
this.writeSocketService = new WriteSocketService(this.socketChannel);
this.readSocketService = new ReadSocketService(this.socketChannel);
this.haService.getConnectionCount().incrementAndGet();
}
构成器中创建了readSocketService读服务,和writeSocketService写服务。当然读服务主要任务是读取 slave上报的最大偏移量,而写服务是写入消息数据到通道中。
ReadSocketService
读服务只要监听读事件即可,并且通过线程,实时监听数据,然后执行处理读请求
private boolean processReadEvent() {
int readSizeZeroTimes = 0;
if (!this.byteBufferRead.hasRemaining()) {
this.byteBufferRead.flip();
this.processPosition = 0;
}
while (this.byteBufferRead.hasRemaining()) {
try {
int readSize = this.socketChannel.read(this.byteBufferRead);
if (readSize > 0) {
readSizeZeroTimes = 0;
this.lastReadTimestamp = HAConnection.this.haService.getDefaultMessageStore().getSystemClock().now();
if ((this.byteBufferRead.position() - this.processPosition) >= 8) {
// slave上报的最大偏移量占8个子节,正常情况下byteBufferRead的position是8的倍数,但是不能确定出现粘包情况的出现
// 所以pos重新计算,规避该情况的出现,保证获取的位置是正确的
int pos = this.byteBufferRead.position() - (this.byteBufferRead.position() % 8);
long readOffset = this.byteBufferRead.getLong(pos - 8);
this.processPosition = pos;
HAConnection.this.slaveAckOffset = readOffset;
if (HAConnection.this.slaveRequestOffset < 0) {
HAConnection.this.slaveRequestOffset = readOffset;
log.info("slave[" + HAConnection.this.clientAddr + "] request offset " + readOffset);
}
HAConnection.this.haService.notifyTransferSome(HAConnection.this.slaveAckOffset);
}
} else if (readSize == 0) {
if (++readSizeZeroTimes >= 3) {
break;
}
} else {
log.error("read socket[" + HAConnection.this.clientAddr + "] < 0");
return false;
}
} catch (IOException e) {
log.error("processReadEvent exception", e);
return false;
}
}
return true;
}
}
byteBufferRead 存储最大长度为1M,但是当byteBufferRead满后,就直接重置,并没有处理未完整包,为什么呢?因为slave上报最大偏移量时,占用8个字节,所以byteBufferRead的长度肯定能存储完整的包。processPosition是上一次读取数据的位置。从socketChannel读取内容时都会复制到byteBufferRead中,然后与上一次去读位置比较,因为一个包只要8个字节,所以差值大于等于8即可。但是在获取slave上报的偏移量时,他只是获取了最近的一个包,即如果this.byteBufferRead.position() - this.processPosition是8的好几倍,他也是读取最某位的数据。不关心,中间包的数据情况。
int pos = this.byteBufferRead.position() - (this.byteBufferRead.position() % 8); 该pos获取的方式,就是为了保证pos是8的倍数,剔除掉某位可能出现不完整包的情况。然后readOffset的偏移量就是pos-8 的位置读取8个字节的数据。并且重置了processPosition的位置。slaveAckOffset 就是认为slave存储的数据也是持久化成功的确认位置,然后执行haService.notifyTransferSome服务(待会聊,为什么需要这个通知服务)。其中slaveRequestOffset 是HAConnection的属性,当小于0时需要将此次读取到readOffset赋值给slaveRequestOffset。
这是一个读取服务,既可以直到当前slave的消息持久的位置,也能保证心跳连接方式。
WriteSocketService
该服务肯定是推送消息数据给slave,并且注册了写事件。在slave实现读消息时,已经知道一个包的组成结构,消息头(起始偏移量+消息长度)+ 消息内容。在实现线程接口方法时,分开讲解,一部分 初始情况怎么做,第二部数据是怎么写的,如果没有写完怎么做
第一部分 初始操作
if (-1 == HAConnection.this.slaveRequestOffset) {
Thread.sleep(10);
continue;
}
if (-1 == this.nextTransferFromWhere) {
// 初始化
if (0 == HAConnection.this.slaveRequestOffset) {
// slave 没有任何请求偏移量,那么默认从一个文件开始
long masterOffset = HAConnection.this.haService.getDefaultMessageStore().getCommitLog().getMaxOffset();
masterOffset =
masterOffset - (masterOffset % HAConnection.this.haService.getDefaultMessageStore().getMessageStoreConfig()
.getMappedFileSizeCommitLog());
if (masterOffset < 0) {
masterOffset = 0;
}
this.nextTransferFromWhere = masterOffset;
} else {
this.nextTransferFromWhere = HAConnection.this.slaveRequestOffset;
}
log.info("master transfer data from " + this.nextTransferFromWhere + " to slave[" + HAConnection.this.clientAddr
+ "], and slave request " + HAConnection.this.slaveRequestOffset);
}
slaveRequestOffset 等于-1 说明slave还没有发送当前最大偏移量,作为master,是无法确定给slave推送消息数据的起始位置,所以不能继续执行。
当nextTransferFromWhere为-1时,说明master还没有开始推送,需要确认推送给slave的起始位置。如果slaveRequestOffset等于0,说明slave当前状况完全是新服务,没有本地存储的消息,那么master推送给slave的起始位置是存储消息的最后一个文件开始位置。masterOffset是当前master最大偏移量,最终取模相减之后,即为最后文件的起始位置。
如果说slaveRequestOffset大于0,那么master推送给slave起始位置,即为slave当前最大的位置slaveRequestOffset。
这是初始工作,最重要的任务就是需要确认master推送数据时的起始位置。
第二部分 推送数据
SelectMappedBufferResult selectResult =
HAConnection.this.haService.getDefaultMessageStore().getCommitLogData(this.nextTransferFromWhere);
if (selectResult != null) {
int size = selectResult.getSize();
if (size > HAConnection.this.haService.getDefaultMessageStore().getMessageStoreConfig().getHaTransferBatchSize()) {
size = HAConnection.this.haService.getDefaultMessageStore().getMessageStoreConfig().getHaTransferBatchSize();
}
long thisOffset = this.nextTransferFromWhere;
this.nextTransferFromWhere += size;
selectResult.getByteBuffer().limit(size);
this.selectMappedBufferResult = selectResult;
// Build Header
this.byteBufferHeader.position(0);
this.byteBufferHeader.limit(headerSize);
this.byteBufferHeader.putLong(thisOffset);
this.byteBufferHeader.putInt(size);
this.byteBufferHeader.flip();
this.lastWriteOver = this.transferData();
} else {
HAConnection.this.haService.getWaitNotifyObject().allWaitForRunning(100);
}
通过nextTransferFromWhere位置截取SelectMappedBufferResult数据,由于受到推送最大长度限制,默认最大size为32k数据。需要对byteBuffer进行limit 限制最大为size长度,然后将selectResult对象指向给当前属性的selectMappedBufferResult。开始先定义头数据byteBufferHeader ,放入了nextTransferFromWhere值8个字节,和内容长度size 4个字节。最后执行传输数据方法transferData
private boolean transferData() throws Exception {
int writeSizeZeroTimes = 0;
// Write Header
while (this.byteBufferHeader.hasRemaining()) {
int writeSize = this.socketChannel.write(this.byteBufferHeader);
if (writeSize > 0) {
writeSizeZeroTimes = 0;
this.lastWriteTimestamp = HAConnection.this.haService.getDefaultMessageStore().getSystemClock().now();
} else if (writeSize == 0) {
if (++writeSizeZeroTimes >= 3) {
break;
}
} else {
throw new Exception("ha master write header error < 0");
}
}
if (null == this.selectMappedBufferResult) {
return !this.byteBufferHeader.hasRemaining();
}
writeSizeZeroTimes = 0;
// Write Body
if (!this.byteBufferHeader.hasRemaining()) {
while (this.selectMappedBufferResult.getByteBuffer().hasRemaining()) {
int writeSize = this.socketChannel.write(this.selectMappedBufferResult.getByteBuffer());
if (writeSize > 0) {
writeSizeZeroTimes = 0;
this.lastWriteTimestamp = HAConnection.this.haService.getDefaultMessageStore().getSystemClock().now();
} else if (writeSize == 0) {
if (++writeSizeZeroTimes >= 3) {
break;
}
} else {
throw new Exception("ha master write body error < 0");
}
}
}
boolean result = !this.byteBufferHeader.hasRemaining() && !this.selectMappedBufferResult.getByteBuffer().hasRemaining();
if (!this.selectMappedBufferResult.getByteBuffer().hasRemaining()) {
this.selectMappedBufferResult.release();
this.selectMappedBufferResult = null;
}
return result;
}
写数据分2部分,一部分是头数据,一部分是写消息数据。首先保证byteBufferHeader写完,才能写入消息内容。在写完消息数据后,有个注意点,就是将selectMappedBufferResult进行release释放。因为从commitLog中获取selectMappedBufferResult时,标记为使用状态。如果用完,需要主动释放。假设此次没有写完时,在下个循环会继续执行。
有个问题需要思考一下,在master推送数据时,推送的数据都是完整的消息内容吗?
答案肯定是否定,首先.规定了消息最大推送长度为32k,假设slave是全新的服务器或者长时间没有同步,导致slave与master服务的最大偏移量差会很大。那么每次推送的数据中肯定都不是非常完整的消息内容了。所以只要偏差小于32k,基本上就能推送完整的消息了。
GroupTransferService
在master读取slave最大偏移量时,会执行HAService中的notifyTransferSome方法
public void notifyTransferSome(final long offset) {
for (long value = this.push2SlaveMaxOffset.get(); offset > value; ) {
// 通知一下slave已经更新到了最大的偏移量,有些同步等待的可以确认slave成功了
boolean ok = this.push2SlaveMaxOffset.compareAndSet(value, offset);
if (ok) {
this.groupTransferService.notifyTransferSome();
break;
} else {
value = this.push2SlaveMaxOffset.get();
}
}
}
因为一个master同时保持多个slave连接,并且每个slave上报的偏移量可能都不太一致,所以该放入通过原子长整型对象push2SlaveMaxOffset,来保证原子性。当offset确实大于了value值,并且push2SlaveMaxOffset更新成功,那么才能执行groupTransferService.notifyTransferSome();方法。如果更新失败,那么从新判断更新。
那么GroupTransferService最用是什么呢?在之前消息存储讲到,当broker是同步状态时候,一条消息存储成功,是需要消息在本地broker持久化成功,第二如果存在Slave,保证slave也需要将消息持久化成功。
在CommitLog类handleHA方法
public void handleHA(AppendMessageResult result, PutMessageResult putMessageResult, MessageExt messageExt) {
if (BrokerRole.SYNC_MASTER == this.defaultMessageStore.getMessageStoreConfig().getBrokerRole()) {
HAService service = this.defaultMessageStore.getHaService();
if (messageExt.isWaitStoreMsgOK()) {
// Determine whether to wait
if (service.isSlaveOK(result.getWroteOffset() + result.getWroteBytes())) {
GroupCommitRequest request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes());
service.putRequest(request);
service.getWaitNotifyObject().wakeupAll();
boolean flushOK =
request.waitForFlush(this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout());
if (!flushOK) {
log.error("do sync transfer other node, wait return, but failed, topic: " + messageExt.getTopic() + " tags: "
+ messageExt.getTags() + " client address: " + messageExt.getBornHostNameString());
putMessageResult.setPutMessageStatus(PutMessageStatus.FLUSH_SLAVE_TIMEOUT);
}
}
// Slave problem
else {
// Tell the producer, slave not available
putMessageResult.setPutMessageStatus(PutMessageStatus.SLAVE_NOT_AVAILABLE);
}
}
}
}
通过DefaulteMessageStore中获取到HAService服务,并且slave都正常,然后创建GroupCommitRequest 对象放入到HAservice中,即为GroupTransferService中,然后进行等待,等待核心就是通过CountDownLatch实现。
GroupTransferService 服务也是实现ServiceThread,
private final WaitNotifyObject notifyTransferObject = new WaitNotifyObject();
private volatile List requestsWrite = new ArrayList<>();
private volatile List requestsRead = new ArrayList<>();
其中requestsWrite 用于放置提交请求的,requestsRead是用来去读请求,通过swapRequests方法进行读写转换。swapRequests方法是通过等待结束后会调用onWaitEnd方法,然后执行读写转换。
首先是放入提交请求方法。
public synchronized void putRequest(final CommitLog.GroupCommitRequest request) {
synchronized (this.requestsWrite) {
this.requestsWrite.add(request);
}
if (hasNotified.compareAndSet(false, true)) {
waitPoint.countDown(); // notify
}
}
首先对requestsWrite进行锁定,然后放入提交请求。如果当前线程没有通知,那么将hasNotified标记为已经通知,然后唤醒线程,因为线程await是通过waitPoint实现的。
private void doWaitTransfer() {
synchronized (this.requestsRead) {
if (!this.requestsRead.isEmpty()) {
for (CommitLog.GroupCommitRequest req : this.requestsRead) {
boolean transferOK = HAService.this.push2SlaveMaxOffset.get() >= req.getNextOffset();
for (int i = 0; !transferOK && i < 5; i++) {
this.notifyTransferObject.waitForRunning(1000);
transferOK = HAService.this.push2SlaveMaxOffset.get() >= req.getNextOffset();
}
if (!transferOK) {
log.warn("transfer messsage to slave timeout, " + req.getNextOffset());
}
req.wakeupCustomer(transferOK);
}
this.requestsRead.clear();
}
}
}
doWaitTransfer方法是核心,首先锁住requestsRead,然后就行遍历。当push2SlaveMaxOffset值大于等于了req中物理偏移量值(该值是消息的起始物理偏移量+消息长度),那么需要唤醒写入线程,并且标记成功。如何transferOK =false,则会进行等待,等待时长最多 5s。如果失败,然后会写入唤醒线程,但是标记为失败。写入线程唤醒后,通过状态进行反馈,如果flushOK为false ,putMessageResult放置状态FLUSH_SLAVE_TIMEOUT。
那么有没有可能,存在request丢失的情况,不会出现。极端情况,当put请求时当前对象为write,但此时GroupTransferService线程正好等待结束,进行读写转换了,此时读的对象变成了写,写的对象变成了读,但是放置线程对象执行了读的遍历。因为在放置时,还是读取遍历时,都进行对对象锁定。保证其他线程不能修改了。但是会出现一种情况,就是读取线程遍历完后,会清空读请求,然后释放锁,此时put线程就获取到该对象,然后放置了request对象。那么该对象何时会被读取到呢?需要进行2次swap后才能被读取到,但是不会被清空。
作为broker 节点 slave角色,需要同步很多数据,但是其中有2个数据不同步,一个时消费队列ConsumeQueue数据,一个是索引数据IndexFile。那么回顾一下这2个数据是如何实现的,在BrokerController中,有一个线程服务ReputMessageService,会不停的从commitLog中获取消息,然后进行分发,从而会添加消费队列数据和索引数据。