目录
一、MappedFile
MappedFile是文件映射到内存的结构。根据transientStorePoolEnable配置的不同其操作的内存位置也不同。如果transientStorePoolEnable=true,则其直接操作堆外内存,该对外内存会被刷新到pageCache当中,并最终落盘。如果transientStorePoolEnable=false,则MappedFile直接操作pageCache当中,并由操作系统刷盘线程最终落盘。
MappedFile有两处优化:预热和mlock锁。预热是为了让其常驻内存,mlock锁是将其锁定在内存,避免其换出到swap或者磁盘上去。因为换出到这两个地方都会导致操作文件速度下降很多倍。这是一个高性能中间件不能接受的结果。
二、Rocketmq三种刷盘方式
public DefaultFlushManager() {
if (FlushDiskType.SYNC_FLUSH == CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushDiskType()) {
this.flushCommitLogService = new CommitLog.GroupCommitService();
} else {
this.flushCommitLogService = new CommitLog.FlushRealTimeService();
}
this.commitLogService = new CommitLog.CommitRealTimeService();
}
DefaultFlushManager是CommitLog的一个属性,也就是说针对每一个commitlog文件,都有一个刷盘线程
DefaultFlushManager随着CommitLog初始化而初始化,CommitLog启动而启动
2.1、DefaultFlushManager初始化
public DefaultFlushManager() {
if (FlushDiskType.SYNC_FLUSH == CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushDiskType()) {
this.flushCommitLogService = new CommitLog.GroupCommitService();
} else {
this.flushCommitLogService = new CommitLog.FlushRealTimeService();
}
this.commitLogService = new CommitLog.CommitRealTimeService();
}
DefaultFlushManager初始化的时候如果刷盘策略为同步则初始化GroupCommitService,如果是异步在初始化FlushRealTimeService。不管同步还是异步都会初始化CommitRealTimeService。
2.2、DefaultFlushManager启动
public void start() {
this.flushCommitLogService.start();
if (defaultMessageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
this.commitLogService.start();
}
}
如果transientStorePoolEnable为true broker角色不为slave,则会启动commitLogService。当然flushCommitLogService必须提前启动。
组合 | 策略 |
FlushRealTimeService + CommitRealTimeService | 刷盘策略叫异步+缓存刷盘策略 |
FlushRealTimeService | 异步刷盘策略 |
GroupCommitService | 同步刷盘策略 |
接下来介绍三个线程任务执行流程。
2.1、CommitRealTimeService
CommitRealTimeService是一个线程任务,启动后会执行以下步骤
1、计算当前时间是否大于(上一次提交时间+broker设置commitDataThoroughInterval)。如果大于则
commitDataLeastPages=0.否则commitDataLeastPages=4,做到批量刷盘。
2、调用mappedFileQueue.commit 提交数据,
2.1、找到带提交的commitLog文件
2.2、如果找到了会到调用mappedFile.commit做提交动作,该动作就是将writeBuffer写入fileChannel的过程。
3、如果有数据提交到fileChannle则调用CommitLog.this.flushManager.wakeUpFlush();唤醒刷盘线程执行刷盘。
4、暂停commitIntervalCommitLog时间,默认是200毫秒,再做一次循环。
该任务就是针对transientStorePoolEnable=true的情况,将对外内存数据写入pageCache,并唤醒刷新任务进行刷盘。
2.2、GroupCommitService
GroupCommitService针对的是同步刷盘策略,每10毫秒会执行该任务。
GroupCommitService任务抓取requestsRead请求,并获取当前已刷盘位置和请求需要刷盘位置大小关系
如果已刷盘大小小于请求刷盘位置,则说明pagecache有数据页没有刷新到磁盘,调用以下代码进行刷盘
CommitLog.this.mappedFileQueue.flush(0);
最终调用
int offset = mappedFile.flush(flushLeastPages);
返回刷盘位移,用该offset位移和commitLog文件位移相加,就是当前刷新位置的绝对位移。
如果最终刷盘位移大于当前请求刷盘位移,会跳出刷盘操作。
GroupCommitService虽然叫同步刷盘策略,其实做了同步转异步的动作,所以最终要根据req的CompletableFuture类型字段返回结果。
req.wakeupCustomer(flushOK ? PutMessageStatus.PUT_OK : PutMessageStatus.FLUSH_DISK_TIMEOUT);
再自上而下看下同步刷盘流程
1、broker获取远程client的写请求
SendMessageProcessor#sendMessage
2、sendMessage方法调用MessageStore#putMessage将数据写入。该方法直接将写入commitLog过程同步转异步
public PutMessageResult putMessage(MessageExtBrokerInner msg) {
return waitForPutResult(asyncPutMessage(msg));
}
asyncPutMessage(msg)最终调用异步请求获取写入commitLog结果。
this.commitLog.asyncPutMessage(msg)
该方法的最后一行就是刷盘结果
return handleDiskFlushAndHA(putMessageResult, msg, needAckNums, needHandleHA);
所以如果刷盘成功GroupCommitService通过调用wakeupCustomer方法给CompleteFuture一个PUT_OK结果,最终SendMessageProcessor#handlePutMessageResult方法会处理PUT_OK,给客户端一个成功的结果。
同步刷盘同步转异步是通过将刷盘请求包装成GroupCommitRequest并将其写入GroupCommitService的requestsWrite队列。
GroupCommitService线程任务通过每次将requestsWrite队列里面的请求转换到requestsRead并执行刷盘任务实现。之所以需要两个队列,也是减少读写锁竞争,提高并发性。
2.3、FlushRealTimeService
FlushRealTimeService负责异步刷盘
该线程任务启动之后执行以下步骤:
1、获取配置flushCommitLogTimed ,该flushCommitLogTimed=true,直接使用线程睡眠方式等待interval(默认500毫秒)时间;如果flushCommitLogTimed=false,CountDownLatch2对象的await方法睡眠interval时间。两种方式主要是新老版本兼容。
2、设置printFlushProgress,如果该值为true才会输出日志,避免每次都输出日志导致日志过大。这种优化在rocketmq里面随处可见。
3、获取配置的flushCommitLogLeastPages,默认是4,该值是为了批量刷盘使用的页个数,也就是16K。如果不够刷盘页数本次刷盘是不执行的。不能每次请求都要刷盘,过于频繁的刷盘效率太低,另外,如果当前时间>(最后一次刷盘时间+flushPhysicQueueThoroughInterval),会设置flushCommitLogLeastPages=0,导致立即刷盘,不计算当前要刷盘页面是否大于某个值。flushCommitLogThoroughInterval默认为10秒钟,也就是说如果10秒钟过去还未刷屏,会强刷一次盘。
4、mappedFileQueue.flush执行刷盘。
①获取待刷新的mappedFile,这个mappedFile跟根据transientStorePoolEnable配置的不同有两种。每次刷新完成都会把最新刷新位移同步到flushedWhere当中。
②、调用mappedFile的flush方法
2.1、获取待写数据位置
2.2、如果使用了对外内存调用this.fileChannel.force(false);。因为在提交的时候writeBuffer数据已经放入到了fileChannel当中,所以直接刷fileChannel即可
2.3、如果不是对外内存,直接刷新pageCache,即this.mappedByteBuffer.force()
2.4、更新刷新位置flushedPosition
自顶向下考虑下异步刷盘时机。
FlushRealTimeService在CommitLog启动的时候启动,但是启动之后并没有做立即刷盘动作,而是在第一次梳理刷盘请求的时候执行了commitLogService.wakeup();,使用CountDownLatch2对象启动了任务循环体。
三、小结
rocketmq有三个线程负责刷盘,根据不同的配置场景rocketmq的刷盘策略分为三种:1、异步刷盘+缓存刷盘策略 ;2、异步刷盘策略,3同步刷盘策略。本文通过介绍三个负责刷盘线程的执行步骤,交代三种策略的具体内容。