RockerMQ源码分析--Broker的关机之后咋恢复的嘞?

148 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第16天,点击查看活动详情

概述

Broker的关机恢复是指恢复CommitLogConsume QueueIndex File等数据文件。Broker关机分为正常关机异常关机

  • 正常关机:正常调用命令关机
  • 异常关机:异常被迫进程终止关机

关机恢复机制设计的目的就是追求0数据丢失。与之相关的文件有两个:abortcheckpoint

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的消息的存储时间

physicMsgTimestamplogicsMsgTimestamp都是在数据成功存储后被记录,而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

加载所有的TopicqueueId作为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

在这里主要初始化了physicMsgTimestamplogicsMsgTimestampindexMsgTimestamp等数据,新版本加入了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 QueueIndex File

到此为止,Broker重启之后,所有文件恢复完成。