携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第16天,点击查看活动详情
概述
Broker的关机恢复是指恢复CommitLog
,Consume Queue
,Index File
等数据文件。Broker关机分为正常关机和异常关机。
- 正常关机:正常调用命令关机
- 异常关机:异常被迫进程终止关机
关机恢复机制设计的目的就是追求0数据丢失。与之相关的文件有两个:abort
和checkpoint
。
abort
是一个空文件,当Broker启动的时候会新建,当正常关闭的时候会主动删除。若是异常关机,则不会删除此文件。
checkpoint
是一个检查点文件,保存了Broker最后一次正常存储数据的时间,当重启Broker的时候,恢复程序就知道应该从哪个时刻开始恢复数据。
检车点的代码如下org/apache/rocketmq/store/StoreCheckpoint.java
。
public class StoreCheckpoint {
private final RandomAccessFile randomAccessFile;
private final FileChannel fileChannel;
private final MappedByteBuffer mappedByteBuffer;
private volatile long physicMsgTimestamp = 0;
private volatile long logicsMsgTimestamp = 0;
private volatile long indexMsgTimestamp = 0;
private volatile long masterFlushedOffset = 0;
}
physicMsgTimestamp
表示最后一条存储CommitLog的消息的存储时间logicsMsgTimestamp
表示最后一条存储Consume Queue的消息的存储时间indexMsgTimestamp
表示最后一条已存储Index File的消息的存储时间
physicMsgTimestamp
和logicsMsgTimestamp
都是在数据成功存储后被记录,而indexMsgTimestamp
则是Index File
成功刷盘时才会记录。
在如下代码中,当Index File
刷盘之后,最后存储消息的时间getEndTimestamp()
会被赋值给indexMsgTimestamp
。
public void flush(final IndexFile f) {
if (null == f) {
return;
}
long indexMsgTimestamp = 0;
//成功刷盘
if (f.isWriteFull()) {
indexMsgTimestamp = f.getEndTimestamp();
}
f.flush();
if (indexMsgTimestamp > 0) {
this.defaultMessageStore.getStoreCheckpoint().setIndexMsgTimestamp(indexMsgTimestamp);
this.defaultMessageStore.getStoreCheckpoint().flush();
}
}
2 Broker关机恢复流程
Broker
启动时会启动存储服务org/apache/rocketmq/store/DefaultMessageStore
,在其初始化的时候会执行load
方法加载全部数据
@Override
public boolean load() {
boolean result = true;
try {
//检查abort文件 #1
boolean lastExitOK = !this.isTempFileExist();
LOGGER.info("last shutdown {}, root dir: {}", lastExitOK ? "normally" : "abnormally", messageStoreConfig.getStorePathRootDir());
// 加载 Commit Log #2
result = result && this.commitLog.load();
// 加载 Consume Queue #3
result = result && this.consumeQueueStore.load();
if (result) {
// 初始化Checkpoint文件为StoreCheckpoint对象 #4
this.storeCheckpoint =
new StoreCheckpoint(StorePathConfigHelper.getStoreCheckpoint(this.messageStoreConfig.getStorePathRootDir()));
this.masterFlushedOffset = this.storeCheckpoint.getMasterFlushedOffset();
// 加载Index File索引 #5
this.indexService.load(lastExitOK);
// 恢复全部数据 #6
this.recover(lastExitOK);
LOGGER.info("load over, and the max phy offset = {}", this.getMaxPhyOffset());
}
long maxOffset = this.getMaxPhyOffset();
this.setBrokerInitMaxOffset(maxOffset);
LOGGER.info("load over, and the max phy offset = {}", maxOffset);
} catch (Exception e) {
LOGGER.error("load exception", e);
result = false;
}
if (!result) {
this.allocateMappedFileService.shutdown();
}
return result;
}
具体分为以下几步
2.1 异常退出检查 #1
如果abort
文件存在,则表示上次是异常退出的
private boolean isTempFileExist() {
String fileName = StorePathConfigHelper.getAbortFile(this.messageStoreConfig.getStorePathRootDir());
File file = new File(fileName);
return file.exists();
}
2.2 加载CommitLog文件 #2
在加载CommitLog
文件的时候,会依次加载CommitLog
目录下的所有文件,值得注意的是:当有文件大小与设置不一样的时候,则不会恢复文件。
public boolean doLoad(List<File> files) {
// ascending order
files.sort(Comparator.comparing(File::getName));
for (File file : files) {
//判断文件大小与配置值
if (file.length() != this.mappedFileSize) {
log.warn(file + "\t" + file.length()
+ " length not matched message store config value, please check it manually");
return false;
}
try {
MappedFile mappedFile = new DefaultMappedFile(file.getPath(), mappedFileSize);
//设置读写指针
mappedFile.setWrotePosition(this.mappedFileSize);
//设置已刷盘指针
mappedFile.setFlushedPosition(this.mappedFileSize);
//设置已提交指针
mappedFile.setCommittedPosition(this.mappedFileSize);
this.mappedFiles.add(mappedFile);
log.info("load " + file.getPath() + " OK");
} catch (IOException e) {
log.error("load file " + file + " error", e);
return false;
}
}
return true;
}
2.3 加载Consume Queue数据 #3
加载所有的Topic
、queueId
作为ConsumeQueue
对象,再调用load
方法初始化每一个ConsumeQueue
。
private boolean loadConsumeQueues(String storePath, CQType cqType) {
File dirLogic = new File(storePath);
File[] fileTopicList = dirLogic.listFiles();
if (fileTopicList != null) {
//轮询Topic
for (File fileTopic : fileTopicList) {
String topic = fileTopic.getName();
File[] fileQueueIdList = fileTopic.listFiles();
if (fileQueueIdList != null) {
//轮询queueId
for (File fileQueueId : fileQueueIdList) {
int queueId;
try {
queueId = Integer.parseInt(fileQueueId.getName());
} catch (NumberFormatException e) {
continue;
}
//检查cqType
queueTypeShouldBe(topic, cqType);
//构建ConsumeQueue
ConsumeQueueInterface logic = createConsumeQueueByType(cqType, topic, queueId, storePath);
this.putConsumeQueue(topic, queueId, logic);
if (!this.load(logic)) {
return false;
}
}
}
}
}
2.4 初始化Checkpoint文件为StoreCheckpoint对象 #4
在这里主要初始化了physicMsgTimestamp
,logicsMsgTimestamp
,indexMsgTimestamp
等数据,新版本加入了masterFlushedOffset
。
public StoreCheckpoint(final String scpPath) throws IOException {
File file = new File(scpPath);
UtilAll.ensureDirOK(file.getParent());
boolean fileExists = file.exists();
this.randomAccessFile = new RandomAccessFile(file, "rw");
this.fileChannel = this.randomAccessFile.getChannel();
this.mappedByteBuffer = fileChannel.map(MapMode.READ_WRITE, 0, DefaultMappedFile.OS_PAGE_SIZE);
if (fileExists) {
log.info("store checkpoint file exists, " + scpPath);
this.physicMsgTimestamp = this.mappedByteBuffer.getLong(0);
this.logicsMsgTimestamp = this.mappedByteBuffer.getLong(8);
this.indexMsgTimestamp = this.mappedByteBuffer.getLong(16);
this.masterFlushedOffset = this.mappedByteBuffer.getLong(24);
//...
} else {
log.info("store checkpoint file not exists, " + scpPath);
}
}
2.5 加载Index File索引 #5
加载./index
目录下的所有索引文件,若是异常退出并且索引文件最后操作的时间戳大于Checkpoint
中保存的时间戳,则证明文件有错误,直接销毁文件
public boolean load(final boolean lastExitOK) {
File dir = new File(this.storePath);
File[] files = dir.listFiles();
if (files != null) {
// ascending order
Arrays.sort(files);
for (File file : files) {
try {
IndexFile f = new IndexFile(file.getPath(), this.hashSlotNum, this.indexNum, 0, 0);
f.load();
//判断是否异常退出
if (!lastExitOK) {
//索引文件最后操作的时间戳大于Checkpoint中保存的时间戳,则证明文件有错误,直接销毁文件
if (f.getEndTimestamp() > this.defaultMessageStore.getStoreCheckpoint()
.getIndexMsgTimestamp()) {
f.destroy(0);
continue;
}
}
LOGGER.info("load index file OK, " + f.getFileName());
this.indexFileList.add(f);
} catch (IOException e) {
LOGGER.error("load file {} error", file, e);
return false;
} catch (NumberFormatException e) {
LOGGER.error("load file {} error", file, e);
}
}
}
return true;
}
2.6 恢复全部数据 #6
private void recover(final boolean lastExitOK) {
long recoverCqStart = System.currentTimeMillis();
//恢复ConsumeQueue
long maxPhyOffsetOfConsumeQueue = this.recoverConsumeQueue();
long recoverCqEnd = System.currentTimeMillis();
if (lastExitOK) {
//正常时恢复CommitLog
this.commitLog.recoverNormally(maxPhyOffsetOfConsumeQueue);
} else {
//异常时恢复CommitLog
this.commitLog.recoverAbnormally(maxPhyOffsetOfConsumeQueue);
}
long recoverClogEnd = System.currentTimeMillis();
//恢复内存中的TopicQueueTable,纠正ConsumeQueue的最小位点
this.recoverTopicQueueTable();
long recoverOffsetEnd = System.currentTimeMillis();
LOGGER.info("Recover end total:{} recoverCq:{} recoverClog:{} recoverOffset:{}",
recoverOffsetEnd - recoverCqStart, recoverCqEnd - recoverCqStart, recoverClogEnd - recoverCqEnd, recoverOffsetEnd - recoverClogEnd);
}
2.6.1 恢复Consume Queue
recoverConsumeQueue()
方法循环所有Topic
对应的Consume Queue
依次调用recover()
方法来恢复数据。返回的是当前消息队列最大物理位点。
public long recover() {
long maxPhysicOffset = -1;
for (ConcurrentMap<Integer, ConsumeQueueInterface> maps : this.consumeQueueTable.values()) {
for (ConsumeQueueInterface logic : maps.values()) {
this.recover(logic);
if (logic.getMaxPhysicOffset() > maxPhysicOffset) {
maxPhysicOffset = logic.getMaxPhysicOffset();
}
}
}
return maxPhysicOffset;
}
2.6.2 恢复CommitLog
恢复Commitlog
时分为异常恢复和正常恢复
- 正常恢复在
org.apache.rocketmq.store.CommitLog#recoverNormally
中,正常恢复认为数据都是正常的,并且只恢复最新的三个文件。 - 异常恢复在
org.apache.rocketmq.store.CommitLog#recoverAbnormally
中,首先判断文件是否损坏,会从最后一个文件开始倒序恢复全部文件。不过这个方法有可能在不久的将来被舍弃。
CommitLog
恢复完毕之后会将文件中的消息分发,创建Consume Queue
和Index File
。
到此为止,Broker
重启之后,所有文件恢复完成。