文章目录
broker启动图示
实际上broker启动非常复杂
我们简化来看主要分两部分:加载和start加载阶段完成各个组件的初始化,其中最重要的是DefaultMessageStore组件
start 阶段完成通信监听,注册nameserver调度等等
- DefaultMessageStore管理了MQ的消息存储,线程,锁等资源,协调文件读写,线程工作
文件恢复
初始化阶段完成DefaultMessageStore加载
CommitLog消息文件加载,consumeQueueTable消费队列加载,indexService文件加载
加载完成后会进行文件恢复
- 异常恢复: 消息写入CommitLog后,异步由reputMessageService线程完成索引文件和消息队列文件的构建,该过程可能宕机
- 正常恢复: 当加载完成后,磁盘上的文件会被加载到内存中,但此时尚不知道文件写到哪里,需要根据文件头获取当前写入位置指针等文件元数据信息
DefaultMessageStore.recover源码分析
先恢复ConsumeQueue
在进行commitlog恢复
最后根据commitlog和consumequeue的指针比较清除(commitlog中不存在, consumequeue中存在)consumequeue信息
- lastExitOK当.abort文件存在为false,不存在为true 【该文件启动时创建,关闭时销毁,异常关闭存在】
private void recover(final boolean lastExitOK) {
恢复消息队列,返回最大物理偏移量
表示消息最后写完的指针在文件中的偏移量
long maxPhyOffsetOfConsumeQueue = this.recoverConsumeQueue();
if (lastExitOK) {
正常恢复
this.commitLog.recoverNormally(maxPhyOffsetOfConsumeQueue);
} else {
异常恢复
this.commitLog.recoverAbnormally(maxPhyOffsetOfConsumeQueue);
}
this.recoverTopicQueueTable();
}
recoverNormally正常恢复
- 1 正常恢复意味刷盘完毕,则从倒数第三个进行commitlog文件进行恢复
- 2 找到最新的commitlog,获取commitlog文件头的元信息
- 3 根据元信息构建commitlog的指针信息
- 4 比较commitlog指针和consumequeue指针,释放consumequeue超出消息的指针对应的bytebuf
public void recoverNormally(long maxPhyOffsetOfConsumeQueue) {
是否校验消息CRC
boolean checkCRCOnRecover = this.defaultMessageStore.getMessageStoreConfig().isCheckCRCOnRecover();
获取commitlog文件
final List<MappedFile> mappedFiles = this.mappedFileQueue.getMappedFiles();
if (!mappedFiles.isEmpty()) {
从倒数第三个commitlog进行恢复 确保足够的安全
int index = mappedFiles.size() - 3;
不足三个则从第一个开始恢复
if (index < 0)
index = 0;
MappedFile mappedFile = mappedFiles.get(index);
ByteBuffer byteBuffer = mappedFile.sliceByteBuffer();
long processOffset = mappedFile.getFileFromOffset();
long mappedFileOffset = 0;
while (true) {
获取commitlog文件头构建DispatchRequest
DispatchRequest dispatchRequest = this.checkMessageAndReturnSize(byteBuffer, checkCRCOnRecover);
int size = dispatchRequest.getMsgSize();
当前commitlog文件写了一部分
if (dispatchRequest.isSuccess() && size > 0) {
mappedFileOffset += size;
}
当前文件以写满,跳下一个文件
else if (dispatchRequest.isSuccess() && size == 0) {
index++;
if (index >= mappedFiles.size()) {
log.info("recover last 3 physics file over, last mapped file " + mappedFile.getFileName());
break;
} else {
mappedFile = mappedFiles.get(index);
byteBuffer = mappedFile.sliceByteBuffer();
processOffset = mappedFile.getFileFromOffset();
mappedFileOffset = 0;
log.info("recover next physics file, " + mappedFile.getFileName());
}
}
else if (!dispatchRequest.isSuccess()) {
log.info("recover physics file end, " + mappedFile.getFileName());
break;
}
}
processOffset += mappedFileOffset;
文件commitlog的相关指针信息 提交到哪里 刷盘到哪里 截断脏位在案例
this.mappedFileQueue.setFlushedWhere(processOffset);
this.mappedFileQueue.setCommittedWhere(processOffset);
this.mappedFileQueue.truncateDirtyFiles(processOffset);
如果发现consumequeue的指针比commitlog的最大指针还要大
则需要清空consumequeue的脏条目信息,因为根据该consumequeue消费commitlog的消息肯定查不到
if (maxPhyOffsetOfConsumeQueue >= processOffset) {
log.warn("maxPhyOffsetOfConsumeQueue({}) >= processOffset({}), truncate dirty logic files", maxPhyOffsetOfConsumeQueue, processOffset);
this.defaultMessageStore.truncateDirtyLogicFiles(processOffset);
}
}
...... 删除其他代码
}
recoverAbnormally异常恢复
整体机制同正常恢复,但起始处理commitlog不是从倒数第三个文件开始
根据checkpoint倒序遍历文件,获取第一个完整无异常的commitlog
- 异常恢复无法确认从倒数第三个文件开始计算,因为不能确定最后的刷盘点在哪里
- 所有需要checkpoint机制从最后一个文件开始倒序遍历检查出正常文件
- checkpoint在刷盘时会写入一个时间戳,commitlog在刷盘时会写入时间戳
- 当前commitlog文件时间戳小于checkpoint时间戳,则该commitlog文件正常刷盘,作为备选文件
public void recoverAbnormally(long maxPhyOffsetOfConsumeQueue) {
boolean checkCRCOnRecover = this.defaultMessageStore.getMessageStoreConfig().isCheckCRCOnRecover();
final List<MappedFile> mappedFiles = this.mappedFileQueue.getMappedFiles();
int index = mappedFiles.size() - 1;
MappedFile mappedFile = null;
for (; index >= 0; index--) {
mappedFile = mappedFiles.get(index);
/**
* 根据checkpoint文件 寻找第一个匹配的文件
*
* 通过savePoint检查点与每一个文件存储的
*/
if (this.isMappedFileMatchedRecover(mappedFile)) {
log.info("recover from this mapped file " + mappedFile.getFileName());
break;
}
}
------------------------------------------------------------------------------------
if (index < 0) {
index = 0;
mappedFile = mappedFiles.get(index);
}
ByteBuffer byteBuffer = mappedFile.sliceByteBuffer();
long processOffset = mappedFile.getFileFromOffset();
long mappedFileOffset = 0;
while (true) {
获取commitlog文件头构建DispatchRequest
DispatchRequest dispatchRequest = this.checkMessageAndReturnSize(byteBuffer, checkCRCOnRecover);
int size = dispatchRequest.getMsgSize();
if (dispatchRequest.isSuccess()) {
当前commitlog文件写了一部分
if (size > 0) {
mappedFileOffset += size;
if (this.defaultMessageStore.getMessageStoreConfig().isDuplicationEnable()) {
if (dispatchRequest.getCommitLogOffset() < this.defaultMessageStore.getConfirmOffset()) {
this.defaultMessageStore.doDispatch(dispatchRequest);
}
} else {
重新构建index consumequeue
this.defaultMessageStore.doDispatch(dispatchRequest);
}
}
当前文件以写满,跳下一个文件
else if (size == 0) {
index++;
if (index >= mappedFiles.size()) {
break;
} else {
mappedFile = mappedFiles.get(index);
byteBuffer = mappedFile.sliceByteBuffer();
processOffset = mappedFile.getFileFromOffset();
mappedFileOffset = 0;
}
}
} else {
break;
}
}
commitlog恢复到这里
processOffset += mappedFileOffset;
this.mappedFileQueue.setFlushedWhere(processOffset);
this.mappedFileQueue.setCommittedWhere(processOffset);
this.mappedFileQueue.truncateDirtyFiles(processOffset);
清除 ConsumeQueue 冗余数据
if (maxPhyOffsetOfConsumeQueue >= processOffset) {
log.warn("maxPhyOffsetOfConsumeQueue({}) >= processOffset({}), truncate dirty logic files", maxPhyOffsetOfConsumeQueue, processOffset);
this.defaultMessageStore.truncateDirtyLogicFiles(processOffset);
}
}
总结
- 图解broker启动
- 讲述了消息队列的正常和异常恢复
扩展点一commitlog和consumequeue文件概述
一个commitlog文件 | 一个consumequeue文件 |
---|---|
默认1G | 默认600万个字节,约5.72M |
每个消息长度不定 | 每个条目固定20字节,共30万条目 |
所有topic共享一个(逻辑)commitlog文件存储 | 每个topic有各自consumequeue文件 |
发送消息时消息存储commitlog,后异步构建consumequeue | 消费消息根据消费位点读取下一个consumequeue条目【固定20字节】根据consumequeue的offset读取commitlog消息内容 |
consumequeue条目如下: 是commitlog的索引文件