RocketMQ消息刷盘
流程图
1.源码分析
RocketMQ 存储与读写是基于 JDK NIO 的内存映射机制( MappedByteBuffer )的,消息存储时首先将消息追加到内存,再根据配 的刷盘 略在不同时间进行刷写磁盘 ,如果是同步刷盘,消息追加到内存后,将同步调用 MappedByteBuffer的force()方法;;如果是异步刷盘,在消息追加到内存后立刻返回给消息发送端 RocketMQ 使用 一个单独的线程按照某一个设定的频率执行刷盘操作.通过在 broker 配置文件中配置 flushDiskType 来设定刷盘方式,可选值为 ASYNC FLUSH (异步刷盘) C_FLUSH 同步刷盘) 默认为异步刷盘
刷盘策略
CommitLog在初始化的时候,会根据配置,启动两种不同的刷盘服务。
1. Broker 同步刷盘
if (FlushDiskType.SYNC_FLUSH == this.defaultMessageStore.getMessageStoreConfig().getFlushDiskType()) {
//构建GroupCommitRequest 同步任务,并提交到GroupCommitService
final GroupCommitService service = (GroupCommitService) this.flushCommitLogService;
if (messageExt.isWaitStoreMsgOK()) {
GroupCommitRequest request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes());
service.putRequest(request);
CompletableFuture<PutMessageStatus> flushOkFuture = request.future();
PutMessageStatus flushStatus = null;
try {
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 {
service.wakeup();
}
}
//先放到内部的一个请求队列中,并利用waitPoint通知新请求到来
//客户端提交同步刷盘任务到 GroupCommi tServic 线程,如果废线程处于等待状态则将其唤醒
public synchronized void putRequest(final GroupCommitRequest request) {
synchronized (this.requestsWrite) {
this.requestsWrite.add(request);
}
if (hasNotified.compareAndSet(false, true)) {
waitPoint.countDown(); // notify
}
}
//由于避免同步刷盘消费任务与其他消息生产者提交任务直接的锁竞争, GroupCommitservice 提供读容器与写容器,这两个容器每执行完一次任务后,交互,继续消费任务
private void swapRequests() {
List<GroupCommitRequest> tmp = this.requestsWrite;
this.requestsWrite = this.requestsRead;
this.requestsRead = tmp;
}
- 构建GroupCommitRequest 同步任务,并提交到GroupCommitService.
同步等待刷盘结果,刷盘失败也会标志消息存储失败,返回 FLUSH_DISK_TIMEO. 进行同步刷盘的服务为 GroupCommitService,当GroupCommitRequest请求被提交给GroupCommitService后,GroupCommitService并不是立即处理,而是先放到内部的一个请求队列中,并利用waitPoint通知新请求到来 - 等待同步刷盘任务完成,如果超时则返回刷盘错误, 刷盘成功后正常返 回给调用方GroupCommitRequest
public boolean waitForFlush(long timeout) {
try {
this.countDownLatch.await(timeout, TimeUnit.MILLSECONDS);
return this.flushOK;
}catch(InterruoptedException e){
log.error(e)
return false;
}
- 消费发送线程将消息追加到内存映射文件后,将同步任务 GroupCommitRequest 提交到GroupCommitService 线程,然后调用阻塞等待刷盘结果,超时时间默认 5s
public void wakeupCustomer(final boolean flushOK) {
long endTimestamp = System.currentTimeMillis();
PutMessageStatus result = (flushOK && ((endTimestamp - this.startTimestamp) <= this.timeoutMillis)) ?
PutMessageStatus.PUT_OK : PutMessageStatus.FLUSH_SLAVE_TIMEOUT;
this.flushOKFuture.complete(result);
}
4.GroupCommitService 线程处理 GroupCommitRequest 对象后将调用 wakeupCustomer法将消费发送线程唤醒,并将刷盘告知 GroupCommitRequest
public void run() {
while (!this.isStopped()) {
try {
this.waitForRunning(10);
this.doCommit();
} catch (Exception e) {
CommitLog.log.warn(this.getServiceName() + " service has exception. ", e);
}
}
}
- GroupCommitService 每处理一 批同步刷盘请求( requestsRead 容器中请求)后“休息” 1Oms 然后继续处理 一批,其任务的核心实现为 do Commit 方法
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);
}
执行刷盘操作,即调用 MappedByteBuffer#force 方法
1))遍历同步刷盘任务列表,根据加入顺序逐一执行刷盘逻辑
- 调用 mappedFileQueeu的flush 方法执行刷盘操作,最终会调用 MappedByteBuffer# force ()方法, 如果已刷盘指针大于等于提交的刷盘点,表示刷盘成功 每执行一次 刷盘操作后,立即调用 GroupCommitReques#wakeupCustomer唤醒消息发送线程并通知刷盘结果
3))处理完所有同步刷盘任务后,更新刷盘检测点 StoreCheckpoint 中的 ph ysicMsgTimestamp ,但并没有执行检测点的刷盘操作,刷盘检测点的刷盘操作将在 写消息队列文件时触发
同步刷盘的任务虽然也是在异步线程中执行,但是消息存储的主流程中会同步等待刷盘结果,所以本质上还是同步操作。
2.Broker 异步刷盘
异步刷盘根据是否开启 transientStorePoolEnable 机制 ,刷盘实现会有细微差别. 如果transientStorePoolEnable为true, RocketMQ 会单独申请一个与目标物理文 commitlog) 同样大小的堆外内存, 该堆外内存将使用 内存锁定,确保不会被置换到虚拟内存中去,消息首先追加到堆外内存,然后提交到与物理文件的内存映射内存中,再 flush 磁盘 ,如果transientStorePoolEnable为 flalse ,消息直接追加到与物理文件直接映射的内存中,然后刷写到磁盘
异步刷盘的服务为FlushRealTimeService,不过当内存缓存池TransientStorePool 可用时,消息会先提交到TransientStorePool 中的WriteBuffer内部,再提交到MappedFile的FileChannle中,此时异步刷盘服务就是 CommitRealTimeService,它继承自 FlushRealTimeService。
// Asynchronous flush
else {
if (!this.defaultMessageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
flushCommitLogService.wakeup();
} else {
commitLogService.wakeup();
}
}
flushCommitLogService.wakeup();
- FlushRealTimeService 在启动后,会在死循环中周期性的进行刷盘操作
while (!this.isStopped()) {
// 休眠策略,为 true 时,调用 Thread.sleep()休眠,为false时,调用wait()休眠,默认 false
boolean flushCommitLogTimed = CommitLog.this.defaultMessageStore.getMessageStoreConfig().isFlushCommitLogTimed();
// 获取刷盘周期,默认为 500 ms
int interval = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushIntervalCommitLog();
// 每次刷盘至少要刷多少页内容,每页大小为 4 k,默认每次要刷 4 页
int flushPhysicQueueLeastPages = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushCommitLogLeastPages();
// 两次刷写之间的最大时间间隔,默认 10 s
int flushPhysicQueueThoroughInterval =
CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushCommitLogThoroughInterval();
boolean printFlushProgress = false;
// Print flush progress
long currentTimeMillis = System.currentTimeMillis();
// 判断当前时间距离上次刷盘时间是否已经超出设置的两次刷盘最大间隔
if (currentTimeMillis >= (this.lastFlushTimestamp + flushPhysicQueueThoroughInterval)) {
this.lastFlushTimestamp = currentTimeMillis;
// 如果已经超时,则将flushPhysicQueueLeastPages设置为0,表明将所有内存缓存全部刷到文件中
flushPhysicQueueLeastPages = 0;
printFlushProgress = (printTimes++ % 10) == 0;
}
try {
// 根据不同休眠策略,进行休眠等待
if (flushCommitLogTimed) {
Thread.sleep(interval);
} else {
this.waitForRunning(interval);
}
if (printFlushProgress) {
this.printFlushProgress();
}
long begin = System.currentTimeMillis();
// 休眠结束,开始执行刷盘操作
CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages);
long storeTimestamp = CommitLog.this.mappedFileQueue.getStoreTimestamp();
if (storeTimestamp > 0) {
CommitLog.this.defaultMessageStore.getStoreCheckpoint().setPhysicMsgTimestamp(storeTimestamp);
}
long past = System.currentTimeMillis() - begin;
if (past > 500) {
log.info("Flush data to disk costs {} ms", past);
}
} catch (Throwable e) {
CommitLog.log.warn(this.getServiceName() + " service has exception. ", e);
this.printFlushProgress();
}
}
- 获取四个配置参数
- 如果距上次提交间隔超过 flushPhysicQueueThoroughinterval ,则本次刷盘任务将忽略flushPhysicQueueLeastPages 也就是如果待刷 写数据小于指定页数也执行刷写磁盘操作
- :执行一次刷盘任务前先等待指定时间间隔, 然后再执行刷盘任务
- 调用 flush 方法将内存中数据刷写到 盘,并且更新存储检测点文件的comm1tlog 文件的更新时间戳,文件检测点文件( checkpoint 文件)的刷盘动作在刷盘消息消费队列中执行, 其入口为 DefaultMessageStore#FlushConsumeQueueService
通过上面这段逻辑可知,异步刷盘就在异步线程中,周期性的将内存缓冲区的内容刷到文件中,在消息主流程中,只会唤醒异步刷盘线程,而不会同步等待刷盘结果,所以称为异步刷盘。
3.MappedFile的刷盘
无论是上面哪种刷盘策略,最终都调用了下面这个方法进行刷盘:
CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages);
public boolean flush(final int flushLeastPages) {
boolean result = true;
//从mappedFileQueue保存的所有MappedFile中,找出所要刷盘的MappedFile
MappedFile mappedFile = this.findMappedFileByOffset(this.flushedWhere, this.flushedWhere == 0);
if (mappedFile != null) {
long tmpTimeStamp = mappedFile.getStoreTimestamp();
//刷盘的终极目的地就在MappedFile的flush
int offset = mappedFile.flush(flushLeastPages);
long where = mappedFile.getFileFromOffset() + offset;
result = where == this.flushedWhere;
this.flushedWhere = where;
if (0 == flushLeastPages) {
this.storeTimestamp = tmpTimeStamp;
}
}
return result;
}
- 从mappedFileQueue保存的所有MappedFile中,找出所要刷盘的MappedFile
- 如果找到了对应的MappedFile,则对该MappedFile中的内容执行刷盘操作,并更新flushedWhere
public int flush(final int flushLeastPages) {
//判断是否满足刷盘条件
if (this.isAbleToFlush(flushLeastPages)) {
if (this.hold()) {
int value = getReadPosition();
//如果满足刷盘条件,则将内存中的内容刷到文件中
try {
// 如果writeBuffer不为空,则表明消息是先提交到writeBuffer中,已经从writeBuffer提交到fileChannel,直接fileChannel.force()
if (writeBuffer != null || this.fileChannel.position() != 0) {
this.fileChannel.force(false);
} else {
// // 反之,消息是直接存储在文件内存映射缓冲区mappedByteBuffer中,直接调用它的force()即可
this.mappedByteBuffer.force();
}
} catch (Throwable e) {
log.error("Error occurred when force data to disk.", e);
}
this.flushedPosition.set(value);
this.release();
} else {
log.warn("in flush, hold failed, flush offset = " + this.flushedPosition.get());
this.flushedPosition.set(getReadPosition());
}
}
return this.getFlushedPosition();
}
- 判断是否满足刷盘条件,isAbleToFlush()其实就是判断当前剩余未刷盘内容长度,是否超过最小刷盘长度:flushLeastPages,避免不必要的刷盘操作。
- 如果满足刷盘条件,则将内存中的内容刷到文件中
总结
-
同步刷盘的简单描述就是,消息生产者在消息服务端将消息内容追加到内存映射文件中(内存)后,需 同步将内存的内容立刻刷写到磁盘. 通过调用内存映射文件( MappedB yteBuffer的force 方法)可将内存中的数据写入磁盘
-
同步刷盘的任务虽然也是在异步线程中执行,但是消息存储的主流程中会同步等待刷盘结果,所以本质上还是同步操作。
-
异步刷盘就在异步线程中,周期性的将内存缓冲区的内容刷到文件中,在消息主流程中,只会唤醒异步刷盘线程,而不会同步等待刷盘结果,所以称为异步刷盘