版本
- 基于
rocketmq-all-4.3.1
版本
异常分析
-
由于ConsumeQueue和Index文件都是根据CommitLog文件异步构建的,所以ConsumeQueue、Index与CommitLog文件的数据就是最终一致,而不是强一致的。这样在Broker重启时就可能出现不一致性的情况
- CommitLog文件同步刷盘,当准备转发给ConsumeQueue文件时突然断电或者出现故障,导致ConsumeQueue存储失败
- 在刷盘时,由于突然断电,只写入一部分数据到磁盘CommitLog文件中
- 当数据写入CommitLog文件后才会将刷盘点记录到检查点中,有可能刷盘完成,但是写入检查点文件并没有完成
-
RocketMQ 有两种文件恢复机制。判断异常的方式是在 broker启动的时候创建一个 abort 空文件,在正常结束的时候删掉这个文件。在下一次启动 broker 时,如果发现了 abort 文件,则认为是异常宕机,否则就是正常关机。
- 正常关机恢复:先从倒数第三个文件开始进行恢复,然后按照消息的存储格式进行查找,如果改文件中所有的消息都符合消息存储格式,则继续查找下一个文件,直到找到最后一条消息所在的位置
- 异常宕机恢复:异常停止刷盘时,从最后一个文件开始查找,在查找时读取改文件第一条消息的存储时间,如果这个存储时间小于检查点文件中的刷盘时间,就可以从这个文件开始恢复,如果这个文件中第一条消息的存储时间大于检查点,说明不能从这个文件开始恢复,需要寻找上一个文件。因为检查点文件中的刷盘点代表的是100%可靠的消息。
-
关机恢复机制设计的目的就是保证数据0丢失,RocketMQ通过abort和checkpoint来保证数据0丢失
- abort文件:abort文件时一个空文件,在Broker启动时会被创建,当正常关闭的时候会被删除。如果Broker是异常关闭,则不会删除此文件
- checkpoint文件:是一个检查点文件,此文件保存了Broker最后一次正常存储数据的时间,当重启Broker时,恢复程序可以从此文件获取应该从哪个时刻开始恢复数据
-
当索引文件刷盘成功,消息队列消费文件未刷盘成功且宕机时,会造成消息消费队列文件丢失的问题。但只要 Commitlog 文件没有丢失,就可以利用 RocketMQ 的文件恢复机制,恢复丢失的消息消费队列文件。在 RocketMQ 的文件恢复机制中,有针对异常宕机进行文件恢复的机制。当 broker 异常启动,在文件恢复过程中,RocketMQ 会将最后一个有效文件的所有消息转发到消息消费队列和索引文件,确保不丢失消息,但同时也会带来重复消费的问题,RocketMQ 保证消息不丢失但不保证消息不会重复消费,故消息消费业务方需要实现消息消费的幂等设计。
StoreCheckpoint
-
StoreCheckpoint(检查点)主要用于记录
CommitLog
、ConsumeQueue
、Index
文件的刷盘时间点,当上一次broker是异常结束时,会根据StoreCheckpoint的数据进行恢复。checkpoint(检查点)文件固定长度为4KB
-
当索引文件刷盘成功,消息队列消费文件未刷盘成功且宕机时,会造成消息消费队列文件丢失的问题。但只要 Commitlog 文件没有丢失,就可以利用 RocketMQ 的文件恢复机制,恢复丢失的消息消费队列文件。在 RocketMQ 的文件恢复机制中,有针对异常宕机进行文件恢复的机制。当 broker 异常启动,在文件恢复过程中,RocketMQ 会将最后一个有效文件的所有消息转发到消息消费队列和索引文件,确保不丢失消息,但同时也会带来重复消费的问题,RocketMQ 保证消息不丢失但不保证消息不会重复消费,故消息消费业务方需要实现消息消费的幂等设计。
-
StoreCheckpoint文件源码
public class StoreCheckpoint { private static final InternalLogger log = InternalLoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); private final RandomAccessFile randomAccessFile; private final FileChannel fileChannel; private final MappedByteBuffer mappedByteBuffer; //CommitLog最新一条记录的存储时间 private volatile long physicMsgTimestamp = 0; //ConsumeQueue最新一条记录的存储时间 private volatile long logicsMsgTimestamp = 0; //Index File最新一条记录的存储时间 private volatile long indexMsgTimestamp = 0; public StoreCheckpoint(final String scpPath) throws IOException { File file = new File(scpPath); MappedFile.ensureDirOK(file.getParent()); boolean fileExists = file.exists(); this.randomAccessFile = new RandomAccessFile(file, "rw"); //一旦建立映射(map),fileChannel其实就可以关闭了,关闭fileChannel对映射不会有影响 //TODO 所以这个地方的fileChannel是不是直接关闭就好? this.fileChannel = this.randomAccessFile.getChannel(); this.mappedByteBuffer = fileChannel.map(MapMode.READ_WRITE, 0, MappedFile.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); log.info("store checkpoint file physicMsgTimestamp " + this.physicMsgTimestamp + ", " + UtilAll.timeMillisToHumanString(this.physicMsgTimestamp)); log.info("store checkpoint file logicsMsgTimestamp " + this.logicsMsgTimestamp + ", " + UtilAll.timeMillisToHumanString(this.logicsMsgTimestamp)); log.info("store checkpoint file indexMsgTimestamp " + this.indexMsgTimestamp + ", " + UtilAll.timeMillisToHumanString(this