版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
阅读须知
文章中使用/* */注释的方法会做深入分析
正文
在RocketMQ源码解析之Broker消息存储这篇文章中我们分析了Broker消息存储流程,流程的最后一步,就是将消息刷盘:
CommitLog:
public void handleDiskFlush(AppendMessageResult result, PutMessageResult putMessageResult, MessageExt messageExt) {
// 同步刷盘
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);
// 阻塞等待刷盘结果,等待超时返回false代表刷盘超时
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();
}
}
// 异步刷盘
else {
// 判断是否启用了临时存储池来决定使用的刷盘服务
if (!this.defaultMessageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
// 唤醒刷盘服务
flushCommitLogService.wakeup();
} else {
// 唤醒刷盘服务
commitLogService.wakeup();
}
}
}
唤醒刷盘服务的操作比较简单,就是递减刷盘服务持有的闭锁,从而进行唤醒,同步、异步、临时存储池都会有不同的刷盘实现,在构造CommitLog时,会根据不同的消息刷盘策略构造不同的消息刷盘服务,对应同步刷盘的服务是GroupCommitService,它同样继承了ServiceThread,我们来看它的run方法:
CommitLog.GroupCommitService:
public void run() {
CommitLog.log.info(this.getServiceName() + " service started");
while (!this.isStopped()) {
try {
// 阻塞等待唤醒
this.waitForRunning(10);
/* 唤醒后做提交操作 */
this.doCommit();
} catch (Exception e) {
CommitLog.log.warn(this.getServiceName() + " service has exception. ", e);
}
}
// 在正常的shutdown情况,等待请求的到来,然后进行刷盘
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");
}
CommitLog.GroupCommitService:
private void doCommit() {
synchronized (this.requestsRead) {
if (!this.requestsRead.isEmpty()) {
for (GroupCommitRequest req : this.requestsRead) {
// 下一个文件中可能有消息,所以最多刷新两次
boolean flushOK = false;
for (int i = 0; i < 2 && !flushOK; i++) {
// 这里req.getNextOffset()的值为存放消息结果中消息的写操作的开始偏移量和所需字节数的和,也就是写消息之后的偏移量
// 根据MappedFileQueue的上次刷新位置与写消息之后的偏移量进行对比来判断消息是否刷盘完成
flushOK = CommitLog.this.mappedFileQueue.getFlushedWhere() >= req.getNextOffset();
if (!flushOK) {
/* 消息刷盘 */
CommitLog.this.mappedFileQueue.flush(0);
}
}
req.wakeupCustomer(flushOK);
}
long storeTimestamp = CommitLog.this.mappedFileQueue.getStoreTimestamp();
if (storeTimestamp > 0) {
CommitLog.this.defaultMessageStore.getStoreCheckpoint().setPhysicMsgTimestamp(storeTimestamp);
}
this.requestsRead.clear();
} else {
// 由于个别消息被设置为不同步刷新,因此它将进入此分支
CommitLog.this.mappedFileQueue.flush(0);
}
}
}
MappedFileQueue:
public boolean flush(final int flushLeastPages) {
boolean result = true;
// 根据上次刷新的位置,获取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;
}
MappedFile:
public int flush(final int flushLeastPages) {
/* 判断是否可以刷盘 */
if (this.isAbleToFlush(flushLeastPages)) {
// 持有资源的引用
if (this.hold()) {
int value = getReadPosition();
try {
// 只将数据追加到fileChannel或mappedByteBuffer当中的饿一个,而不是两者
if (writeBuffer != null || this.fileChannel.position() != 0) {
this.fileChannel.force(false);
} else {
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();
}
MappedFile:
private boolean isAbleToFlush(final int flushLeastPages) {
int flush = this.flushedPosition.get();
int write = getReadPosition();
if (this.isFull()) {
return true;
}
// 如果flushLeastPages大于0,则需要判断当前写入的偏移与上次刷新偏移量之间的间隔
// 如果超过flushLeastPages页数,则刷新,否则本次不刷新
if (flushLeastPages > 0) {
return ((write / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE)) >= flushLeastPages;
}
return write > flush;
}
异步刷盘:
CommitLog.FlushRealTimeService:
public void run() {
CommitLog.log.info(this.getServiceName() + " service started");
// 循环刷新
while (!this.isStopped()) {
boolean flushCommitLogTimed = CommitLog.this.defaultMessageStore.getMessageStoreConfig().isFlushCommitLogTimed();
int interval = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushIntervalCommitLog();
int flushPhysicQueueLeastPages = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushCommitLogLeastPages();
int flushPhysicQueueThoroughInterval =
CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushCommitLogThoroughInterval();
boolean printFlushProgress = false;
// 打印刷新进度
long currentTimeMillis = System.currentTimeMillis();
if (currentTimeMillis >= (this.lastFlushTimestamp + flushPhysicQueueThoroughInterval)) {
this.lastFlushTimestamp = currentTimeMillis;
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();
}
}
// 正常退出,确保退出前全部刷新
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");
}
这里涉及到mappedFileQueue的flush方法调用,上文已经分析过。下面我们来分析在没有启用临时存储池的情况下的异步刷盘:
CommitLog.CommitRealTimeService:
public void run() {
CommitLog.log.info(this.getServiceName() + " service started");
while (!this.isStopped()) {
int interval = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getCommitIntervalCommitLog();
int commitDataLeastPages = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getCommitCommitLogLeastPages();
int commitDataThoroughInterval =
CommitLog.this.defaultMessageStore.getMessageStoreConfig().getCommitCommitLogThoroughInterval();
long begin = System.currentTimeMillis();
if (begin >= (this.lastCommitTimestamp + commitDataThoroughInterval)) {
this.lastCommitTimestamp = begin;
commitDataLeastPages = 0;
}
try {
/* mappedFileQueue提交动作 */
boolean result = CommitLog.this.mappedFileQueue.commit(commitDataLeastPages);
long end = System.currentTimeMillis();
if (!result) {
// result = false 意味着一些数据提交了
this.lastCommitTimestamp = end;
// 唤醒刷新线程
flushCommitLogService.wakeup();
}
if (end - begin > 500) {
log.info("Commit data to file costs {} ms", end - begin);
}
this.waitForRunning(interval);
} catch (Throwable e) {
CommitLog.log.error(this.getServiceName() + " service has exception. ", e);
}
}
// 同样正常退出,确保退出前全部刷新
boolean result = false;
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");
}
MappedFileQueue:
public boolean commit(final int commitLeastPages) {
boolean result = true;
// 根据偏移量获取MappedFile
MappedFile mappedFile = this.findMappedFileByOffset(this.committedWhere, this.committedWhere == 0);
if (mappedFile != null) {
/* MappedFile提交动作 */
int offset = mappedFile.commit(commitLeastPages);
long where = mappedFile.getFileFromOffset() + offset;
result = where == this.committedWhere;
// 记录提交的位置
this.committedWhere = where;
}
return result;
}
MappedFile:
public int commit(final int commitLeastPages) {
if (writeBuffer == null) {
// 如果写缓冲区为空,则无需将数据提交到FileChannel
// 只需将writePosition视为committedPosition
return this.wrotePosition.get();
}
// 判断是否能够提交,与上文分析的isAbleToFlush方法逻辑很相似
if (this.isAbleToCommit(commitLeastPages)) {
if (this.hold()) {
/* 提交动作 */
commit0(commitLeastPages);
this.release();
} else {
log.warn("in commit, hold failed, commit offset = " + this.committedPosition.get());
}
}
// 所有脏数据都已提交给FileChannel
if (writeBuffer != null && this.transientStorePool != null && this.fileSize == this.committedPosition.get()) {
this.transientStorePool.returnBuffer(writeBuffer);
this.writeBuffer = null;
}
return this.committedPosition.get();
}
MappedFile:
protected void commit0(final int commitLeastPages) {
int writePos = this.wrotePosition.get();
int lastCommittedPosition = this.committedPosition.get();
if (writePos - this.committedPosition.get() > 0) {
try {
ByteBuffer byteBuffer = writeBuffer.slice();
byteBuffer.position(lastCommittedPosition);
byteBuffer.limit(writePos);
this.fileChannel.position(lastCommittedPosition);
this.fileChannel.write(byteBuffer);
this.committedPosition.set(writePos);
} catch (Throwable e) {
log.error("Error occurred when commit data to FileChannel.", e);
}
}
}
这里的提交动作主要就是java nio的一些操作,到这里,Broker刷盘刷盘流程就分析完成了。