概覽
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");
// 線程是否停止
while (!this.isStopped()) {
try {
// 設置hasNotified為false,未被通知,然后交換寫對隊列和讀隊列,重置waitPoint為(1),休息200ms,出事化為10ms,finally設置hasNotified為未被通知,交換寫對隊列和讀隊列
this.waitForRunning(10);
// 進行刷盤服務處理,一次處理一批請求,單個請求返回給等待刷盤服務結果的線程
this.doCommit();
} catch (Exception e) {
CommitLog.log.warn(this.getServiceName() + " service has exception. ", e);
}
}
// 處理非正常停機,sleep10ms,交換寫請求隊列和讀請求隊列,等待數據處理
// Under normal circumstances shutdown, wait for the arrival of the
// request, and then flush
try {
Thread.sleep(10);
} catch (InterruptedException e) {
CommitLog.log.warn("GroupCommitService Exception, ", e);
}
synchronized (this) {
this.swapRequests();
}
// 進行請求處理
this.doCommit();
CommitLog.log.info(this.getServiceName() + " service end");
}
@Override
protected void onWaitEnd() {
// 寫隊列和讀隊列交換
this.swapRequests();
}
@Override
public String getServiceName() {
return GroupCommitService.class.getSimpleName();
}
// 5 分鐘
@Override
public long getJointime() {
return 1000 * 60 * 5;
}
}
處理刷盤請求線程和提交刷盤請求之前的協調
# org.apache.rocketmq.common.ServiceThread
// 喚醒處理刷盤請求寫磁盤線程,處理刷盤請求線程和提交刷盤請求之前的協調,通過控制hasNotified狀態來實現寫隊列和讀隊列的交換
public void wakeup() {
// hasNotified默認值是false,未被喚醒,這個操作之后喚醒了,處理刷盤請求
if (hasNotified.compareAndSet(false, true)) {
// waitPoint默認是1,然后其他線程處理
waitPoint.countDown(); // notify
}
}
/**
* 設置hasNotified為false,未被通知,然后交換寫對隊列和讀隊列,重置waitPoint為(1),休息200ms,finally設置hasNotified為未被通知,交換寫對隊列和讀隊列
* @param interval 200ms
*/
protected void waitForRunning(long interval) {
// compareAndSet(except,update);如果真實值value==except,設置value值為update,返回true;如果真實值value !=except,真實值不變,返回false;
// 如果hasNotified真實值為true,那么設置真實值為false,返回true;hasNotified真實值為false,那就返回false,真實值不變
// 如果已經通知了,那就狀態變為未通知,如果是同步刷盤任務,交換寫請求隊列和讀請求隊列
if (hasNotified.compareAndSet(true, false)) {
// 同步刷盤:寫隊列和讀隊列交換
this.onWaitEnd();
return;
}
// 重置countDownLatch對象,等待接受刷盤請求的線程寫入請求到requestsRead,寫完后,waitPoint.countDown,喚醒處理刷盤請求的線程,開始刷盤
//entry to wait
waitPoint.reset();
try {
// 等待interval毫秒
waitPoint.await(interval, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
log.error("Interrupted", e);
} finally {
// 設置是否通知為false
hasNotified.set(false);
this.onWaitEnd();
}
}
// 等待這個方法的步驟完成。比如:同步刷盤:寫隊列和讀隊列交換
protected void onWaitEnd() {
}
異步刷盤
異步刷盤根據是否開啟TransientStorePool暫存池,來區分是否有commit操作。開啟TransientStorePool會將writerBuffer中的數據commit到FileChannel中(fileChannel.write(writerBuffer)),然后再將FileChannel中的數據通過flush操作(fileChannel.force())到磁盤中;
如果為開啟TransientStorePool,就不會有commit操作,直接flush(MappedByteBuffer.force())到磁盤中。
// 異步刷盤
// Asynchronous flush
if (!this.defaultMessageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
//執行flush操作
flushCommitLogService.wakeup();
} else {
//執行commit操作,然后喚醒執行flush操作
commitLogService.wakeup();
}
CommitRealTimeService
定時將 transientStorePool 中的直接內存 ByteBuffer,提交到FileChannel中,然后喚醒刷盤操作。
// 定時將 transientStorePoll 中的直接內存 ByteBuffer,提交到FileChannel中
class CommitRealTimeService extends FlushCommitLogService {
private long lastCommitTimestamp = 0;
@Override
public String getServiceName() {
return CommitRealTimeService.class.getSimpleName();
}
@Override
public void run() {
CommitLog.log.info(this.getServiceName() + " service started");
// 刷盤線程是否停止
while (!this.isStopped()) {
// writerBuffer寫數據到FileChannel時間間隔200ms
int interval = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getCommitIntervalCommitLog();
// writerBuffer寫數據到FileChannel頁數大小4
int commitDataLeastPages = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getCommitCommitLogLeastPages();
// writerBuffer寫數據到FileChannel跨度時間間隔200ms
int commitDataThoroughInterval =
CommitLog.this.defaultMessageStore.getMessageStoreConfig().getCommitCommitLogThoroughInterval();
// 開始時間
long begin = System.currentTimeMillis();
// 觸發commit機制有兩種方式:1.commit時間超過了兩次commit時間間隔,然后只要有數據就進行提交 2.commit數據頁數大于默認設置的4頁
// 本次commit時間>上次commit時間+兩次commit時間間隔,則進行commit,不用關心commit頁數的大小,設置commitDataLeastPages=0
if (begin >= (this.lastCommitTimestamp + commitDataThoroughInterval)) {
this.lastCommitTimestamp = begin;
commitDataLeastPages = 0;
}
try {
// result=false,表示提交了數據,多與上次提交的位置;表示此次有數據提交;result=true,表示沒有新的數據被提交
boolean result = CommitLog.this.mappedFileQueue.commit(commitDataLeastPages);
long end = System.currentTimeMillis();
// result = false means some data committed.表示此次有數據提交,然后進行刷盤
if (!result) {
this.lastCommitTimestamp = end; // result = false means some data committed.
//now wake up flush thread.
// 喚起刷盤線程,進行刷盤
flushCommitLogService.wakeup();
}
if (end - begin > 500) {
log.info("Commit data to file costs {} ms", end - begin);
}
// 暫停200ms,再運行
this.waitForRunning(interval);
} catch (Throwable e) {
CommitLog.log.error(this.getServiceName() + " service has exception. ", e);
}
}
boolean result = false;
// 正常關機,循環10次,進行10次的有數據就提交的操作
for (int i = 0; i < RETRY_TIMES_OVER && !result; i++) {
result = CommitLog.this.mappedFileQueue.commit(0);
CommitLog.log.info(this.getServiceName() + " service shutdown, retry " + (i + 1) + " times " + (result ? "OK" : "Not OK"));
}
CommitLog.log.info(this.getServiceName() + " service end");
}
}
FlushRealTimeService
異步刷盤服務
class FlushRealTimeService extends FlushCommitLogService {
private long lastFlushTimestamp = 0;
// 刷盤次數
private long printTimes = 0;
public void run() {
CommitLog.log.info(this.getServiceName() + " service started");
while (!this.isStopped()) {
// 默認值為false,表示await方法等待,如果為true,表示使用Thread.sleep方法等待
boolean flushCommitLogTimed = CommitLog.this.defaultMessageStore.getMessageStoreConfig().isFlushCommitLogTimed();
// 刷盤任務時間間隔,多久刷一次盤500ms
int interval = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushIntervalCommitLog();
// 一次刷寫任務至少包含頁數,如果待刷寫數據不足,小于該參數配置的值,將忽略本次刷寫任務,默認4頁
int flushPhysicQueueLeastPages = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushCommitLogLeastPages();
// 兩次真實刷寫任務最大跨度,默認10s
int flushPhysicQueueThoroughInterval =
CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushCommitLogThoroughInterval();
// 打印記錄日志標志
boolean printFlushProgress = false;
// Print flush progress
long currentTimeMillis = System.currentTimeMillis();
// 觸發刷盤機制有兩種方式:1.刷盤時間超過了兩次刷盤時間間隔,然后只要有數據就進行提交 2.commit數據頁數大于默認設置的4頁
// 本次刷盤時間>上次刷盤時間+兩次刷盤時間間隔,則進行刷盤,不用關心刷盤頁數的大小,設置commitDataLeastPages=0
if (currentTimeMillis >= (this.lastFlushTimestamp + flushPhysicQueueThoroughInterval)) {
this.lastFlushTimestamp = currentTimeMillis;
flushPhysicQueueLeastPages = 0;
// 每間隔10次記錄一次刷盤日志
printFlushProgress = (printTimes++ % 10) == 0;
}
try {
// 刷盤之前,進行線程sleep
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();
// 更新checkpoint最后刷盤時間
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();
}
}
// while循環結束,正常關機,保證所有的數據刷寫到磁盤
// Normal shutdown, to ensure that all the flush before exit
boolean result = false;
for (int i = 0; i < RETRY_TIMES_OVER && !result; i++) {
result = CommitLog.this.mappedFileQueue.flush(0);
CommitLog.log.info(this.getServiceName() + " service shutdown, retry " + (i + 1) + " times " + (result ? "OK" : "Not OK"));
}
// 打印日志
this.printFlushProgress();
CommitLog.log.info(this.getServiceName() + " service end");
}
@Override
public String getServiceName() {
return FlushRealTimeService.class.getSimpleName();
}
private void printFlushProgress() {
// CommitLog.log.info("how much disk fall behind memory, "
// + CommitLog.this.mappedFileQueue.howMuchFallBehind());
}
@Override
public long getJointime() {
return 1000 * 60 * 5;
}
}
刷盤是否開啟TransientStorePool的區別
這里講一下刷盤是否開啟TransientStorePool的區別。
不開啟TransientStorePool:
MappedByteBuffer是直接內存,它暫時存儲了message消息,MappedFile.mapp()方法做好MappedByteBuffer對象直接內存和落盤文件的映射關系,然后flush()方法執行MappedByteBuffer.force():強制將ByteBuffer中的任何內容的改變寫入到磁盤文件。
開啟TransientStorePool:
MappedFile的writerBuffer為直接開辟的內存,然后MappedFile的初始化操作,做好FileChannel和磁盤文件的映射,commit()方法實質是執行fileChannel.write(writerBuffer),將writerBuffer的數據寫入到FileChannel映射的磁盤文件,flush操作執行FileChannel.force():將映射文件中的數據強制刷新到磁盤。
TransientStorePool的作用
TransientStorePool 相當于在內存層面做了讀寫分離,寫走內存磁盤,讀走pagecache,同時最大程度消除了page cache的鎖競爭,降低了毛刺。它還使用了鎖機制,避免直接內存被交換到swap分區。
參考:https://github.com/apache/rocketmq/issues/2466
FileChannel.force VS MappedByteBuffer.force區別
This method is only guaranteed to force changes that were made to this channel's file via the methods defined in this class. It may or may not force changes that were made by modifying the content of a{@link MappedByteBuffer <i>mapped byte buffer</i>} obtained by invoking the {@link #map map} method. Invoking the {@link MappedByteBuffer#force force} method of the mapped byte buffer will force changes made to the buffer's content to be written.
FileChannel和MappedByteBuffer都是NIO模塊的類,ByteBuffer直接內存映射到磁盤文件通過FileChannel。
FileChannel.force()只會將FileChannel類中方法使FileChannel發生改變的內容強制刷新到存儲設備文件中。
MappedByteBuffer.force()會將Map類中方法使ByteBuffer發生改變的內容強制刷新到存儲設備文件中。
發送消息的方式可以分為:同步、異步、oneway
消息寫入ByteBuffer的處理方式分為:同步、異步
刷盤的處理方式分為:同步、異步
三個處理方式互不干擾,發送消息的為同步,寫入ByteBuffer可以為異步的方式,刷盤可以為同步的方式。最后,消息發送端會以同步的方式等待寫入ByteBuffer、刷盤成功的結果。