RocketMQ 整合 DLedger(多副本)即主从切换实现平滑升级的设计技巧

3、DLedgerCommitLog 详解


温馨提示:由于 Commitlog 的绝大部分方法都已经在《RocketMQ技术内幕》一书中详细介绍了,并且

【一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义】

浏览器打开:qq.cn.hn/FTf 免费领取

DLedgerCommitLog 的实现原理与 Commitlog 文件的实现原理类同,本文会一笔带过关于存储部分的实现细节。

3.1 核心类图

在这里插入图片描述

DLedgerCommitlog 继承自 Commitlog。让我们一一来看一下它的核心属性。

  • DLedgerServer dLedgerServer

基于 raft 协议实现的集群内的一个节点,用 DLedgerServer 实例表示。

  • DLedgerConfig dLedgerConfig

DLedger 的配置信息。

  • DLedgerMmapFileStore dLedgerFileStore

DLedger 基于文件映射的存储实现。

  • MmapFileList dLedgerFileList

DLedger 所管理的存储文件集合,对比 RocketMQ 中的 MappedFileQueue。

  • int id

节点ID,0 表示主节点,非0表示从节点

  • MessageSerializer messageSerializer

消息序列器。

  • long beginTimeInDledgerLock = 0

用于记录 消息追加的时耗(日志追加所持有锁时间)。

  • long dividedCommitlogOffset = -1

记录的旧 commitlog 文件中的最大偏移量,如果访问的偏移量大于它,则访问 dledger 管理的文件。

  • boolean isInrecoveringOldCommitlog = false

是否正在恢复旧的 commitlog 文件。

接下来我们将详细介绍 DLedgerCommitlog 各个核心方法及其实现要点。

3.2 构造方法

public DLedgerCommitLog(final DefaultMessageStore defaultMessageStore) {

super(defaultMessageStore); // @1

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().getMapedFileSizeCommitLog());

dLedgerConfig.setDeleteWhen(defaultMessageStore.getMessageStoreConfig().getDeleteWhen());

dLedgerConfig.setFileReservedHours(defaultMessageStore.getMessageStoreConfig().getFileReservedTime() + 1);

id = Integer.valueOf(dLedgerConfig.getSelfId().substring(1)) + 1; // @2

dLedgerServer = new DLedgerServer(dLedgerConfig); // @3

dLedgerFileStore = (DLedgerMmapFileStore) dLedgerServer.getdLedgerStore();

DLedgerMmapFileStore.AppendHook appendHook = (entry, buffer, bodyOffset) -> {

assert bodyOffset == DLedgerEntry.BODY_OFFSET;

buffer.position(buffer.position() + bodyOffset + MessageDecoder.PHY_POS_POSITION);

buffer.putLong(entry.getPos() + bodyOffset);

};

dLedgerFileStore.addAppendHook(appendHook); // @4

dLedgerFileList = dLedgerFileStore.getDataFileList();

this.messageSerializer = new MessageSerializer(defaultMessageStore.getMessageStoreConfig().getMaxMessageSize()); // @5

}

代码@1:调用父类 即 CommitLog 的构造函数,加载 ${ROCKETMQ_HOME}/store/ comitlog 下的 commitlog 文件,以便兼容升级 DLedger 的消息。我们稍微看一下 CommitLog 的构造函数:

在这里插入图片描述

代码@2:构建 DLedgerConfig 相关配置属性,其主要属性如下:

  • enableDiskForceClean

是否强制删除文件,取自 broker 配置属性 cleanFileForciblyEnable,默认为 true 。

  • storeType

DLedger 存储类型,固定为 基于文件的存储模式。

  • dLegerSelfId

leader 节点的 id 名称,示例配置:n0,其配置要求第二个字符后必须是数字。

  • dLegerGroup

DLeger group 的名称,建议与 broker 配置属性 brokerName 保持一致。

  • dLegerPeers

DLeger Group 中所有的节点信息,其配置示例 n0-127.0.0.1:40911;n1-127.0.0.1:40912;n2-127.0.0.1:40913。多个节点使用分号隔开。

  • storeBaseDir

设置 DLedger 的日志文件的根目录,取自 borker 配件文件中的 storePathRootDir ,即 RocketMQ 的数据存储根路径。

  • mappedFileSizeForEntryData

设置 DLedger 的单个日志文件的大小,取自 broker 配置文件中的 - mapedFileSizeCommitLog,即与 commitlog 文件的单个文件大小一致。

  • deleteWhen

DLedger 日志文件的删除时间,取自 broker 配置文件中的 deleteWhen,默认为凌晨 4点。

  • fileReservedHours

DLedger 日志文件保留时长,取自 broker 配置文件中的 fileReservedHours,默认为 72h。

代码@3:根据 DLedger 配置信息创建 DLedgerServer,即创建 DLedger 集群节点,集群内各个节点启动后,就会触发选主。

代码@4:构建 appendHook 追加钩子函数,这是兼容 Commitlog 文件很关键的一步,后面会详细介绍其作用。

代码@5:构建消息序列化。

根据上述的流程图,构建好 DefaultMessageStore 实现后,就是调用其 load 方法,在启用 DLedger 机制后,会依次调用 DLedgerCommitlog 的 load、recover 方法。

3.3 load

public boolean load() {

boolean result = super.load();

if (!result) {

return false;

}

return true;

}

DLedgerCommitLog 的 laod 方法实现比较简单,就是调用 其父类 Commitlog 的 load 方法,即这里也是为了启用 DLedger 时能够兼容以前的消息。

3.4 recover

在 Broker 启动时会加载 commitlog、consumequeue等文件,需要恢复其相关是数据结构,特别是与写入、刷盘、提交等指针,其具体调用 recover 方法。

DLedgerCommitLog#recover

public void recoverNormally(long maxPhyOffsetOfConsumeQueue) { // @1

recover(maxPhyOffsetOfConsumeQueue);

}

首先会先恢复 consumequeue,得出 consumequeue 中记录的最大有效物理偏移量,然后根据该物理偏移量进行恢复。

接下来看一下该方法的处理流程与关键点。

DLedgerCommitLog#recover

dLedgerFileStore.load();

Step1:加载 DLedger 相关的存储文件,并一一构建对应的 MmapFile,其初始化三个重要的指针 wrotePosition、flushedPosition、committedPosition 三个指针为文件的大小。

DLedgerCommitLog#recover

if (dLedgerFileList.getMappedFiles().size() > 0) {

dLedgerFileStore.recover(); // @1

dividedCommitlogOffset = dLedgerFileList.getFirstMappedFile().getFileFromOffset(); // @2

MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile();

if (mappedFile != null) { // @3

disableDeleteDledger();

}

long maxPhyOffset = dLedgerFileList.getMaxWrotePosition();

// Clear ConsumeQueue redundant data

if (maxPhyOffsetOfConsumeQueue >= maxPhyOffset) { // @4

log.warn("[TruncateCQ]maxPhyOffsetOfConsumeQueue({}) >= processOffset({}), truncate dirty logic files", maxPhyOffsetOfConsumeQueue, maxPhyOffset);

this.defaultMessageStore.truncateDirtyLogicFiles(maxPhyOffset);

}

return;

}

Step2:如果已存在 DLedger 的数据文件,则只需要恢复 DLedger 相关数据文建,因为在加载旧的 commitlog 文件时已经将其重要的数据指针设置为最大值。其关键实现点如下:

  • 首先调用 DLedger 文件存储实现类 DLedgerFileStore 的 recover 方法,恢复管辖的 MMapFile 对象(一个文件对应一个MMapFile实例)的相关指针,其实现方法与 RocketMQ 的 DefaultMessageStore 的恢复过程类似。

  • 设置 dividedCommitlogOffset 的值为 DLedger 中所有物理文件的最小偏移量。操作消息的物理偏移量小于该值,则从 commitlog 文件中查找;物理偏移量大于等于该值的话则从 DLedger 相关的文件中查找消息。

  • 如果存在旧的 commitlog 文件,则禁止删除 DLedger 文件,其具体做法就是禁止强制删除文件,并将文件的有效存储时间设置为 10 年。

  • 如果 consumequeue 中存储的最大物理偏移量大于 DLedger 中最大的物理偏移量,则删除多余的 consumequeue 文件。

温馨提示:为什么当存在 commitlog 文件的情况下,不能删除 DLedger 相关的日志文件呢?

因为在此种情况下,如果 DLedger 中的物理文件有删除,则物理偏移量会断层。

在这里插入图片描述

正常情况下, maxCommitlogPhyOffset 与 dividedCommitlogOffset 是连续的,这样非常方便是访问 commitlog 还是 访问 DLedger ,但如果DLedger 部分文件删除后,这两个值就变的不连续,就会造成中间的文件空洞,无法被连续访问。

DLedgerCommitLog#recover

isInrecoveringOldCommitlog = true;

super.recoverNormally(maxPhyOffsetOfConsumeQueue);

isInrecoveringOldCommitlog = false;

Step3:如果启用了 DLedger 并且是初次启动(还未生成 DLedger 相关的日志文件),则需要恢复 旧的 commitlog 文件。

DLedgerCommitLog#recover

MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile();

if (mappedFile == null) { // @1

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) { // @2

needWriteMagicCode = false;

} else {

log.info(“Recover old commitlog found a illegal magic code={}”, magicCode);

}

dLedgerConfig.setEnableDiskForceClean(false);

dividedCommitlogOffset = mappedFile.getFileFromOffset() + mappedFile.getFileSize(); // @3

log.info(“Recover old commitlog needWriteMagicCode={} pos={} file={} dividedCommitlogOffset={}”, needWriteMagicCode, mappedFile.getFileFromOffset() + mappedFile.getWrotePosition(), mappedFile.getFileName(), dividedCommitlogOffset);

if (needWriteMagicCode) { // @4

byteBuffer.position(mappedFile.getWrotePosition());

byteBuffer.putInt(mappedFile.getFileSize() - mappedFile.getWrotePosition());

byteBuffer.putInt(BLANK_MAGIC_CODE);

mappedFile.flush(0);

}

mappedFile.setWrotePosition(mappedFile.getFileSize()); // @5

mappedFile.setCommittedPosition(mappedFile.getFileSize());

mappedFile.setFlushedPosition(mappedFile.getFileSize());

dLedgerFileList.getLastMappedFile(dividedCommitlogOffset);

log.info(“Will set the initial commitlog offset={} for dledger”, dividedCommitlogOffset);

}

Step4:如果存在旧的 commitlog 文件,需要将最后的文件剩余部分全部填充,即不再接受新的数据写入,新的数据全部写入到 DLedger 的数据文件中。其关键实现点如下:

  • 尝试查找最后一个 commitlog 文件,如果未找到,则结束。

  • 从最后一个文件的最后写入点(原 commitlog 文件的 待写入位点)尝试去查找写入的魔数,如果存在魔数并等于 CommitLog.BLANK_MAGIC_CODE,则无需再写入魔数,在升级 DLedger 第一次启动时,魔数为空,故需要写入魔数。

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

  • 将最后一个 commitlog 未写满的数据全部写入,其方法为 设置消息体的 size 与 魔数即可。

  • 设置最后一个文件的 wrotePosition、flushedPosition、committedPosition 为文件的大小,同样有意味者最后一个文件已经写满,下一条消息将写入 DLedger 中。

在启用 DLedger 机制时 Broker 的启动流程就介绍到这里了,相信大家已经了解 DLedger 在整合 RocketMQ 上做的努力,接下来我们从消息追加、消息读取两个方面再来探讨 DLedger 是如何无缝整合 RocketMQ 的,实现平滑升级的。

4、从消息追加看 DLedger 整合 RocketMQ 如何实现无缝兼容


温馨提示:本节同样也不会详细介绍整个消息追加(存储流程),只是要点出与 DLedger(多副本、主从切换)相关的核心关键点。如果想详细了解消息追加的流程,可以阅读笔者所著的《RocketMQ技术内幕》一书。

DLedgerCommitLog#putMessage

AppendEntryRequest request = new AppendEntryRequest();

request.setGroup(dLedgerConfig.getGroup());

request.setRemoteId(dLedgerServer.getMemberState().getSelfId());

request.setBody(encodeResult.data);

dledgerFuture = (AppendFuture) dLedgerServer.handleAppend(request);

if (dledgerFuture.getPos() == -1) {

return new PutMessageResult(PutMessageStatus.OS_PAGECACHE_BUSY, new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR));

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值