数据存储这一块,RocketMQ使用的是文件编程模型。为了提高文件的写入性能通常会引入内存映射机制,数据先写入页缓存然后再择机将页缓存数据刷盘到磁盘,写入涉及性能与数据可靠性是必须要考虑的。针对刷盘策略一般会有同步刷盘与异步刷盘,RocketMQ也是如此,默认使用异步刷盘。
先来简单看下RocketMQ刷盘操作的代码块:
try {
//We only append data to fileChannel or mappedByteBuffer, never both.
if (writeBuffer != null || this.fileChannel.position() != 0) {
this.fileChannel.force(false);
} else {
// 注释4.8.1:同步落盘
this.mappedByteBuffer.force();
}
} catch (Throwable e) {
log.error("Error occurred when force data to disk.", e);
}
可以看到刷盘其实就是调用了MappedByteBuffer的force方法。
同步刷盘
同步刷盘指的 Broker 端收到消息发送者的消息后,先写入内存,然后将内容持久化到磁盘后才向客户端返回消息发送成功。
刷盘要分两条线进行分析:
- 第一条线是broker在启动的时候会启动一个刷盘线程,调用路径为:BrokerController#start()->DefaultMessageStore#start()->CommitLog#start()->GroupCommitService#start()->MappedFileQueue#flush();
- 第二条线是broker在接收到消息后加载或更新MappedFile然后存入MappedFileQueue,调用路径为:SendMessageProcessor#processRequest()->DefaultMessageStore#putMessage()->CommitLog#putMessage()->CommitLog#handleDiskFlush()->GroupCommitRequest#waitForFlush().
第一条线的刷盘线程会在一个while循环里每间隔10ms执行一次刷盘操作,刷盘成功后会唤醒第二条线里中等待响应的线程,在第二条线里组装好MappedFileQueue(CopyOnWriteArrayList类型)之后便会调用countDownLatch的await方法等待刷盘线程的执行。
//broker接收到消息组装好MappedFileQueue后等待刷盘线程执行
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;
if (messageExt.isWaitStoreMsgOK()) {
GroupCommitRequest request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes());
service.putRequest(request);
//等待刷盘线程执行
boolean flushOK = request.waitForFlush(this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout());
if (!flushOK) {
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 {
service.wakeup();
}
}
// Asynchronous flush
// 注释4.8.2:异步刷盘
else {
if (!this.defaultMessageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
flushCommitLogService.wakeup();
} else {
commitLogService.wakeup();
}
}
}
for (GroupCommitRequest req : this.requestsRead) {
// There may be a message in the next file, so a maximum of
// two times the flush
boolean flushOK = false;
for (int i = 0; i < 2 && !flushOK; i++) {
//当前已刷盘指针大于该条消息对应的物理偏移量说明已刷完
flushOK = CommitLog.this.mappedFileQueue.getFlushedWhere() >= req.getNextOffset();
if (!flushOK) {
//刷盘操作
CommitLog.this.mappedFileQueue.flush(0);
}
}
//唤醒等待刷盘的线程
req.wakeupCustomer(flushOK);
}
关于同步刷盘需要提一下就是每次刷盘并非只刷写一条消息,而是一组消息。
异步刷盘
同步刷盘的优点是能保证消息不丢失,即向客户断返回成功就代表这条消息已被持久化到磁盘,即消息非常可靠,但是以牺牲写入性能为前提条件的,但由于 RocketMQ 的消息是先写入 PageCache,故消息丢失的可能性较小,如果能容 忍一定几率的消息丢失,但能提高性能,可以考虑使用异步刷盘。
异步刷盘指的是 Broker 将消息存储到 PageCache 后就立即返回成功,然后开启一个异步线程定时执行 FileChannel 的 forece 方法将内存中的数据定时刷写到磁盘,默认间隔为 500ms。在 RocketMQ 的异步刷盘实现类为 FlushRealTimeService。看到这个默认间隔为 500ms,大家是不是会猜测 FlushRealTimeService 是使用了定时任务?
其实不然。这里引入了带超时时间的 CountDown await 方法,这样做的好处时如果没有新的消息写入,会休眠 500ms,但收到了新的消息后,可以被唤醒,做到消息及时被刷盘,而不是一定要等 500 ms。
刷盘线程的等待在CommitRealTimeService#run方法里,唤醒刷盘线程刷盘是在CommitLog#handleDiskFlush的异步分支里。
文件的恢复
这里只是简单提一下。
文件恢复分了正常退出后文件恢复与异常退出的文件恢复。
- 正常退出后的恢复:以ConsumerQueue为依据,获取里面最后一条消息消费的物理偏移量。如果这个偏移量大于CommitLog文件里的偏移量,则会删除ConsumerQueue里多余的数据;如果小于CommitLog文件里的偏移量,则将多出来的物理偏移量对应的消息进行重发,保证两个文件一致。
- 异常后的恢复:broker会记录commitlog、index、consumequeue 等文件的最后一次刷盘时间戳,之后还会记录一个checkpoint时间戳(checkpoint文件也会刷盘生成文件)。以checkpoint里的时间戳为基准对比commitlog里的刷盘时间戳进行相应操作。
RocketMQ 启动时候会创建一个名为 abort 的文件,然后在正常退出时会删除该文件,故判断 RocketMQ 进程是否是异常退出只需要查看 abort 文件是否存在,如果存在表示异常退出。
文件恢复入口:DefaultMessageStore#recover,详情可以参考:从 RocketMQ 学基于文件的编程模式(二)
另外,这里涉及到的缓存页与MappedByteBuffer,零拷贝有关,可以参考之前的一篇文章:Java里的零拷贝
相关的文章:RocketMQ源码-MappedFile介绍,其中涉及到了TransientStorePool暂存池,MappedFile预分配,写入与刷盘
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。