RocketMQ刷盘机制
概览
RocketMQ的存储读写是基于JDK NIO的内存映射机制的,消息存储时首先将消息追加到内存中。在根据不同的刷盘策略在不同的时间进行刷盘
。如果是同步刷盘,消息追加到内存后,将同步调用MappedByteBuffer的force()方法,同步等待刷盘结果,进行刷盘结果返回。如果是异步刷盘,
在消息追加到内存后立刻,不等待刷盘结果立刻返回存储成功结果给消息发送端。RocketMQ使用一个单独的线程按照一个设定的频率执行刷盘操作。
通过在broker配置文件中配置flushDiskType来设定刷盘方式,ASYNC_FLUSH(异步刷盘)、SYNC_FLUSH(同步刷盘)。默认为异步刷盘。
本次以Commitlog文件刷盘机制为例来讲解刷盘机制。Consumequeue、IndexFile刷盘原理和Commitlog一直。索引文件的刷盘机制并不是采取定时刷盘机制,
而是每更新一次索引文件就会将上一次的改动刷写到磁盘。
刷盘服务是将commitlog、consumequeue两者中的MappedFile文件中的MappedByteBuffer或者FileChannel中的内存中的数据,刷写到磁盘。
还有将IndexFile中的MappedByteBuffer(this.mappedByteBuffer = this.mappedFile.getMappedByteBuffer())中内存的数据刷写到磁盘。
刷盘服务的入口
刷盘服务的入口是CommitLog类对象,FlushCommitLogService是刷盘服务对象,如果是同步刷盘它被赋值为GroupCommitService,
如果是异步刷盘它被赋值为FlushRealTimeService;还有一个FlushCommitLogService的commitLogService对象,这个是将 TransientStorePoll 中的直接内存ByteBuffer,
写到FileChannel映射的磁盘文件中的服务。
// 异步、同步刷盘服务初始化
if (FlushDiskType.SYNC_FLUSH == defaultMessageStore.getMessageStoreConfig().getFlushDiskType()) {
// 同步刷盘服务为 GroupCommitService
this.flushCommitLogService = new GroupCommitService();
} else {
// 异步刷盘服务为 FlushRealTimeService
this.flushCommitLogService = new FlushRealTimeService();
}
// 定时将 transientStorePoll 中的直接内存 ByteBuffer,提交到内存映射 MappedByteBuffer 中
this.commitLogService = new CommitRealTimeService();
刷盘方法调用入口
putMessage()方法,将消息写入内存的方式不同,调用的刷盘方式也不同。如果是asyncPutMessage()异步将消息写入内存,submitFlushRequest()方法是刷盘入口。
如果是putMessage()同步将消息写入内存,handleDiskFlush()方法是刷盘入口。handleDiskFlush()和submitFlushRequest()都包含有同步刷盘和异步刷盘的方法。
// 异步的方式存放消息
public CompletableFuture<PutMessageResult> asyncPutMessage(final MessageExtBrokerInner msg) {
// 异步存储消息,提交刷盘请求
CompletableFuture<PutMessageStatus> flushResultFuture = submitFlushRequest(result, putMessageResult, msg);
CompletableFuture<PutMessageStatus> replicaResultFuture = submitReplicaRequest(result, putMessageResult, msg);
// 根据刷盘结果副本结果,返回存放消息的结果
return flushResultFuture.thenCombine(replicaResultFuture, (flushStatus, replicaStatus) -> {
if (flushStatus != PutMessageStatus.PUT_OK) {
putMessageResult.setPutMessageStatus(PutMessageStatus.FLUSH_DISK_TIMEOUT);
}
if (replicaStatus != PutMessageStatus.PUT_OK) {
putMessageResult.setPutMessageStatus(replicaStatus);
}
return putMessageResult;
});
}
// 同步方式存放消息
public PutMessageResult putMessage(final MessageExtBrokerInner msg) {
// handle 硬盘刷新
handleDiskFlush(result, putMessageResult, msg);
// handle 高可用
handleHA(result, putMessageResult, msg);
// 返回存储消息的结果
return putMessageResult;
}
同步刷盘
一条消息调用一次刷盘服务,等待刷盘结果返回,然后再将结果返回;才能处理下一条刷盘消息。以handleDiskFlush()方法来介绍同步刷盘和异步刷盘,
这里是区分刷盘方式的分水岭。
/**
* 一条消息进行刷盘
* @param result 扩展到内存ByteBuffer的结果
* @param putMessageResult 放入ByteBuffer这个过程的结果
* @param messageExt 存放的消息
*/
public void handleDiskFlush(AppendMessageResult result, PutMessageResult putMessageResult, MessageExt messageExt) {
// Synchronization flush 同步
if (FlushDiskType.SYNC_FLUSH == this.defaultMessageStore.getMessageStoreConfig().getFlushDiskType()) {
final GroupCommitService service = (GroupCommitService) this.flushCommitLogService;
// 是否等待服务器将这一条消息存储完毕再返回(等待刷盘完成),还是直接处理其他写队列requestsWrite里面的请求
if (messageExt.isWaitStoreMsgOK()) {
//刷盘请求
GroupCommitRequest request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes());
//放入写请求队列
service.putRequest(request);
// 同步等待获取刷盘结果
CompletableFuture<PutMessageStatus> flushOkFuture = request.future();
PutMessageStatus flushStatus = null;
try {
// 5秒超市等待刷盘结果
flushStatus = flushOkFuture.get(this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout(),
TimeUnit.MILLISECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
//flushOK=false;
}
// 刷盘失败,更新存放消息结果超时
if (flushStatus != PutMessageStatus.PUT_OK) {
log.error("do groupcommit, wait for flush failed, topic: " + messageExt.getTopic() + " tags: " + messageExt.getTags()
+ " client address: " + messageExt.getBornHostString());
putMessageResult.setPutMessageStatus(PutMessageStatus.FLUSH_DISK_TIMEOUT);
}
} else {
// 唤醒处理刷盘请求写磁盘线程,处理刷盘请求线程和提交刷盘请求之前的协调,通过CountDownLatch(1)操作,通过控制hasNotified状态来实现写队列和读队列的交换
service.wakeup();
}
}
// 异步
// Asynchronous flush
else {
if (!this.defaultMessageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
flushCommitLogService.wakeup();
} else {
commitLogService.wakeup();
}
}
}
同步刷盘会创造一个刷盘请求,然后将请求放入处理写刷盘请求的requestsWrite队列,请求里面封装了CompletableFuture对象用来记录刷盘结果,
利用CompletableFuturee的get方法同步等待获取结果。flushStatus = flushOkFuture.get(this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout(),TimeUnit.MILLISECONDS);
flushStatus为刷盘结果,默认等待5秒超时。
GroupCommitService为一个线程,用来定时处理requestsWrite队列里面的写刷盘请求,进行刷盘;它的requestsWrite和requestsRead队列进行了读写分离,
写GroupCommitRequest请求到requestsWrite队列,读GroupCommitRequest请求从requestsRead读取,读取请求今夕写盘操作。这两个队列,形成了化零为整,
将一个个请求,划分为一批,处理一批的GroupCommitRequest请求,然后requestsWrite和requestsRead队列进行交换,requestsRead作为写队列,
requestsWrite作为读队列,实现读写分离。从中使用CountDownLatch2来实现处理刷盘请求线程和提交刷盘请求之前的协调,通过控制hasNotified状态来实现写队列和读队列的交换。
// 同步刷盘服务
class GroupCommitService extends FlushCommitLogService {
// 两个队列,读写请求分离
// 刷盘服务写入请求队列
private volatile List<GroupCommitRequest> requestsWrite = new ArrayList<GroupCommitRequest>();
// 刷盘服务读取请求队列
private volatile List<GroupCommitRequest> requestsRead = new ArrayList<GroupCommitRequest>();
// 将请求同步写入requestsWrite
public synchronized void putRequest(final GroupCommitRequest request) {
synchronized (this.requestsWrite) {
this.requestsWrite.add(request);
}
// 唤醒刷盘线程处理请求
this.wakeup();
}
// 写队列和读队列交换
private void swapRequests() {
List<GroupCommitRequest> tmp = this.requestsWrite;
this.requestsWrite = this.requestsRead;
this.requestsRead = tmp;
}
private void doCommit() {
// 上锁读请求队列
synchronized (this.requestsRead) {
if (!this.requestsRead.isEmpty()) {
// 每一个请求进行刷盘
for (GroupCommitRequest req : this.requestsRead) {
// There may be a message in the next file, so a maximum of
// two times the flush
// 一个落盘请求,处理两次,第一次为false,进行刷盘,一次刷盘的数据是多个offset,并不是只有当前这个offset的值,这个offset的值进行了刷盘,
这个请求的第二次刷盘,这个offset已经已经落盘了,
// flushWhere这个值在flush方法已经更新变大,所以flushOK=true,跳出for循环,通知flushOKFuture已经完成。
boolean flushOK = false;
for (int i = 0; i < 2 && !flushOK; i++) {
// 是否已经刷过,false未刷,true已刷
flushOK = CommitLog.this.mappedFileQueue.getFlushedWhere() >= req.getNextOffset();
// false 刷盘
if (!flushOK) {
//0代码立刻刷盘,不管缓存中消息有多少
CommitLog.this.mappedFileQueue.flush(0);
}
}
// flushOK:true,返回ok,已经刷过盘了,不用再刷盘;false:刷盘中,返回超时
// 唤醒等待刷盘结果的线程
req.wakeupCustomer(flushOK ? PutMessageStatus.PUT_OK : PutMessageStatus.FLUSH_DISK_TIMEOUT);
}
// 更新checkpoint的刷盘commitlog的最后刷盘时间,但是只写写到了checkpoint的内存ByteBuffer,并没有刷盘
long storeTimestamp = CommitLog.this.mappedFileQueue.getStoreTimestamp();
if (storeTimestamp > 0) {
CommitLog.this.defaultMessageStore.getStoreCheckpoint().setPhysicMsgTimestamp(storeTimestamp);
}
// 清空队列
this.requestsRead.clear();
} else {
// Because of individual messages is set to not sync flush, it
// will come to this process
// 因为个别的消息不是同步刷盘的,所以它回到这里进行处理
CommitLog.this.mappedFileQueue.flush(0);
}
}
}
public void run() {
CommitLog.log.info(this.getServiceName() + " service started");
// 线程是否停止