ROCKETMQ源码分析(broker高可用03-存储兼容)

215 阅读6分钟

前情提要

我们上一节说到了broker的高可用的主从切换的选举部分,我们今天主要探讨的是rocketmq的日志兼容问题

初探:broker的启动--messageStore

image.png

我们可以看到出现了我们熟悉的Dleger也就是高可用的标志,上面这行代码也就是说如果开启了高可用的话默认初始化一个DLedgerCommitLog否则就初始化原始的commitLog,我们到这里就可以想到了,这个DLedgerCommitLog和原始的CommitLog相比肯定是多了往子节点同步的部分,接下来我们来看看他的结构

rocketmq整合DLedger

DLedger的存储结构

image.png

我们可以看到DLedgerCommitLog实际上是继承了CommitLog的,那么DLedgerCommitLog的存储结构又是怎么样的呢,如何兼容CommitLog呢,其实我们根据上面的知识可以想到其实我们的主从高可用只是比普通模式的Log需要多记录一些term,channel等这些元数据信息:

image.png

看到这里我们能想到,我们只要把commitLog的原本信息放到body里不就可以兼容commitLog了,而且改动也不大,对于历史数据也能很好的兼容,rocketmq确实是这么做的。接下来我们就来详细看看~

Dledger的构造函数

public DLedgerCommitLog(final DefaultMessageStore defaultMessageStore) {
    super(defaultMessageStore); //调用父类的构造函数  也就是说开启了主从架构也会兼容历史的消息
    dLedgerConfig = new DLedgerConfig();
    dLedgerConfig.setEnableDiskForceClean(defaultMessageStore.getMessageStoreConfig().isCleanFileForciblyEnable());
    dLedgerConfig.setStoreType(DLedgerConfig.FILE);
    dLedgerConfig.setSelfId(defaultMessageStore.getMessageStoreConfig().getdLegerSelfId());
    dLedgerConfig.setGroup(defaultMessageStore.getMessageStoreConfig().getdLegerGroup());
    dLedgerConfig.setPeers(defaultMessageStore.getMessageStoreConfig().getdLegerPeers());
    dLedgerConfig.setStoreBaseDir(defaultMessageStore.getMessageStoreConfig().getStorePathRootDir());
    dLedgerConfig.setMappedFileSizeForEntryData(defaultMessageStore.getMessageStoreConfig().getMappedFileSizeCommitLog());
    dLedgerConfig.setDeleteWhen(defaultMessageStore.getMessageStoreConfig().getDeleteWhen());
    dLedgerConfig.setFileReservedHours(defaultMessageStore.getMessageStoreConfig().getFileReservedTime() + 1);
    id = Integer.valueOf(dLedgerConfig.getSelfId().substring(1)) + 1;
    dLedgerServer = new DLedgerServer(dLedgerConfig);  //初始化DledgerServer 主要是进行主从复制以及选举使用
    dLedgerFileStore = (DLedgerMmapFileStore) dLedgerServer.getdLedgerStore();
    DLedgerMmapFileStore.AppendHook appendHook = (entry, buffer, bodyOffset) -> {
        //我们上面说过其实当我们开启了主从同步之后我们追加消息的时候其实只有body是存储的原始的commitLog结构其他对于客户端都是无用的信息
        //所以这里设置的追加消息的钩子函数就是为了返回body的offset
        assert bodyOffset == DLedgerEntry.BODY_OFFSET;
        buffer.position(buffer.position() + bodyOffset + MessageDecoder.PHY_POS_POSITION);
        buffer.putLong(entry.getPos() + bodyOffset);
    };
    dLedgerFileStore.addAppendHook(appendHook);
    dLedgerFileList = dLedgerFileStore.getDataFileList();
    this.messageSerializer = new MessageSerializer(defaultMessageStore.getMessageStoreConfig().getMaxMessageSize());

}

主要流程节点:

  1. 调用父类的构造函数 也就是说开启了主从架构也会兼容历史的消息

  2. 构建配置文件类

  3. 根据DledgerConfig构建DledgerServer 主要负责主从日志同步以及选举

  4. 设置追加消息的钩子函数

     我们上面说过其实当我们开启了主从同步之后我们追加消息的时候其实只有body是存储的原始的commitLog结构其他对于客户端都是无用的信息,所以这里设置的追加消息的钩子函数就是为了返回body的offset
     
    

DLedgerCommitLog#Load()

@Override
public boolean load() {
    return super.load();
}

这里其实就是去加载commitLog中的信息为了进行历史消息的兼容

DLedgerCommitLog#recover() 数据恢复

在broker启动的时候需要进行CommitLog以及ConsumeQueue等文件的恢复

step1:主要是加载commitLog以及index文件的wrotePosition,flushedPosition,committedPosition重要的指针

dLedgerFileStore.load();

step2:

if (dLedgerFileList.getMappedFiles().size() > 0) {
    //如果存在dLedgerFile 只需要恢复dLedgerFile即可
    //存在dLedgerFile 恢复dLedgerFile
    dLedgerFileStore.recover();
    //设置dividedCommitlogOffset为dLedger文件的最小offset  作为和老的commitLog的分割  小于这个offset需要访问老的commitLog
    dividedCommitlogOffset = dLedgerFileList.getFirstMappedFile().getFileFromOffset();
    MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile();
    if (mappedFile != null) {
        //如果存在旧的commitLog则禁止删除Dledger防止出现日志断层影响查询
        disableDeleteDledger();
    }
    //最大物理offset
    long maxPhyOffset = dLedgerFileList.getMaxWrotePosition();
    // Clear ConsumeQueue redundant data
    if (maxPhyOffsetOfConsumeQueue >= maxPhyOffset) {
        log.warn("[TruncateCQ]maxPhyOffsetOfConsumeQueue({}) >= processOffset({}), truncate dirty logic files", maxPhyOffsetOfConsumeQueue, maxPhyOffset);
        this.defaultMessageStore.truncateDirtyLogicFiles(maxPhyOffset);
    }
    return;
}

step3: 从这里开始针对的是第一次开启主从同步的并且是初次启动的流程,调用commitLog的recoverNormall() 进行commitLog文件的恢复

super.recoverNormally(maxPhyOffsetOfConsumeQueue); 

step4:如果不存在旧的commitLog直接结束文件日志的恢复流程

MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile();
if (mappedFile == null) {  //不存在旧的commitLog直接返回
    return;
}

step5:如果存在旧的commitLog文件,需要将文件剩余的部分填充数据,即不再接受新的数据写入,使新的数据全部写入Deldger的数据文件,关键的实现点如下。

①尝试找到最后一个commitLog文件,如果没找到就停止

②从最后一个文件的最后写入点尝试查找写入的魔数,如果存在魔数并且等CommitLog.BLANK_MAGIC_CODE则无需写入魔数

③初始化dividedCommitlogOffset,等于最后一个文件的起始偏移量加上文件的大小,即该指针指向最后一个文件的结束位置

④ 将最后一个文件全部写满,其方法为设置消息体的大小以及魔数

⑤设置最后一个文件的WrotePosition,CommittedPosition,FlushedPosition 表示文件已经被写满
isInrecoveringOldCommitlog = true;
//No need the abnormal recover
super.recoverNormally(maxPhyOffsetOfConsumeQueue);  //调用父类commitLog进行旧的commitLog的数据的恢复
isInrecoveringOldCommitlog = false;
MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile();
if (mappedFile == null) {  //不存在旧的commitLog直接返回
    return;
}
ByteBuffer byteBuffer = mappedFile.sliceByteBuffer();
byteBuffer.position(mappedFile.getWrotePosition());
boolean needWriteMagicCode = true;
// 1 TOTAL SIZE
byteBuffer.getInt(); //size
int magicCode = byteBuffer.getInt();
if (magicCode == CommitLog.BLANK_MAGIC_CODE) {
    needWriteMagicCode = false;
} 
dLedgerConfig.setEnableDiskForceClean(false);
dividedCommitlogOffset = mappedFile.getFileFromOffset() + mappedFile.getFileSize();

if (needWriteMagicCode) {
    byteBuffer.position(mappedFile.getWrotePosition());
    byteBuffer.putInt(mappedFile.getFileSize() - mappedFile.getWrotePosition());
    byteBuffer.putInt(BLANK_MAGIC_CODE);
    mappedFile.flush(0);
}
//设置最后一个文件的WrotePosition,CommittedPosition,FlushedPosition 表示文件已经被写满
mappedFile.setWrotePosition(mappedFile.getFileSize());
mappedFile.setCommittedPosition(mappedFile.getFileSize());
mappedFile.setFlushedPosition(mappedFile.getFileSize());
dLedgerFileList.getLastMappedFile(dividedCommitlogOffset);

问题:

为什么存在旧的commitLog就不允许删除Deledger文件了? 因为在这种情况下如果Dledger文件被删除的话就会出现物理偏移量的断层,如下图所示

image.png

正常情况下,maxCommitLogPhyOffset和minDledgerPhyOffset是连续的,非常方便访问commitlog以及dledger文件,但是当dledger文件被删除了的话,这两个值就不连续了,会造成中间文件的空洞,无法连续的访问

从数据的最佳探究数据的存储兼容

DledgerCommitLog#putMessage()

关键点一: 追加消息的时候不再写入之前的commitLog而是调用dlegerserver的handleAppend进行日志的写入&子节点日志的复制(后面会详细讲解),只有超过半数以上的节点复制成功才会返回成功,如果追加成功则会返回追加成功的起始偏移量即pos属性类似于commitLog中的物理偏移量

AppendEntryRequest request = new AppendEntryRequest();
request.setGroup(dLedgerConfig.getGroup());
request.setRemoteId(dLedgerServer.getMemberState().getSelfId());
request.setBody(encodeResult.data);
dledgerFuture = (AppendFuture<AppendEntryResponse>) dLedgerServer.handleAppend(request);

关于dLedgerServer如何进行日志的分发我们会放在下一章去讲解

关键点二: 根据dledger的起始偏移量计算真正的消息的存储offset。

long wroteOffset = dledgerFuture.getPos() + DLedgerEntry.BODY_OFFSET;

消息的查找:

消息的查找起始和原来还是没有什么区别的,还是使用二分查找法通过offset获取mappedFile文件,只是多了一个dividedCommitlogOffset的判断是否是老数据,如果是老数据直接走commitLog,新数据就走Dledger维护的文件列表

public SelectMappedBufferResult getMessage(final long offset, final int size) {
    if (offset < dividedCommitlogOffset) {
        return super.getMessage(offset, size); //如果是小于dividedCommitlogOffset 证明是旧数据 -> 从commitLog获取
    }
    //从 dledger获取
    int mappedFileSize = this.dLedgerServer.getdLedgerConfig().getMappedFileSizeForEntryData();
    MmapFile mappedFile = this.dLedgerFileList.findMappedFileByOffset(offset, offset == 0);
    if (mappedFile != null) {
        int pos = (int) (offset % mappedFileSize);
        return convertSbr(mappedFile.selectMappedBuffer(pos, size)); //获取文件并转换为 DLedgerSelectMappedBufferResult 类型
    }
    return null;
}

总结:

今天主要是给大家介绍了rockmq集群模式下如何记录日志的怎么和以前的模式进行兼容以及切换主从模式后数据如何恢复,下一次会给大家带来rocketmq如何进行主从节点间的日志同步并且如何保证半数以上从节点同步完成等