rocketmq在存储消息的时候,最终是通过mmap映射成磁盘文件进行存储的,本文就消息的存储流程作一个整理。源码版本是4.9.2

主要的存储组件有如下4个:

CommitLog:存储的业务层,接收“保存消息”的请求

MappedFile:存储的最底层对象,一个MappedFile对象就对应了一个实际的文件

MappedFileQueue:管理MappedFile的容器

AllocateMappedFileService:异步创建mappedFile的服务

对于rocketmq来说,存储消息的主要文件被称为CommitLog,因此就从该类入手。处理存储请求的入口方法是asyncPutMessage,主要流程如下:

public CompletableFuture<PutMessageResult> asyncPutMessage(final MessageExtBrokerInner msg) {
...
//可能会有多个线程并发请求,虽然支持集群,但是对于每个单独的broker都是本地存储,所以内存锁就足够了
putMessageLock.lock();
try {
//获取最新的文件
MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile();
...
//如果文件为空,或者已经存满,则创建一个新的commitlog文件
if (null == mappedFile || mappedFile.isFull()) {
mappedFile = this.mappedFileQueue.getLastMappedFile(0); // Mark: NewFile may be cause noise
}
...
//调用底层的mappedFile进行出处,但是注意此时还没有刷盘
result = mappedFile.appendMessage(msg, this.appendMessageCallback, putMessageContext);
...
} finally {
putMessageLock.unlock();
}
PutMessageResult putMessageResult = new PutMessageResult(PutMessageStatus.PUT_OK, result);
...
}

因此对于Commitlog.asyncPutMessage来说,主要的工作就是2步:

1.获取或者创建一个MappedFile

2.调用appendMessage进行存储

接下去我们先看MappedFile的创建,查看mappedFileQueue.getLastMappedFile方法,最终会调用到doCreateMappedFile方法,调用流如下:

getLastMappedFile-->tryCreateMappedFile-->doCreateMappedFile

protected MappedFile doCreateMappedFile(String nextFilePath, String nextNextFilePath) {
MappedFile mappedFile = null;
//如果异步服务对象不为空,那么就采用异步创建文件的方式
if (this.allocateMappedFileService != null) {
mappedFile = this.allocateMappedFileService.putRequestAndReturnMappedFile(nextFilePath,
nextNextFilePath, this.mappedFileSize);
} else {
//否则就同步创建
try {
mappedFile = new MappedFile(nextFilePath, this.mappedFileSize);
} catch (IOException e) {
log.error("create mappedFile exception", e);
}
}
...
return mappedFile;
}

因此对于MappedFileQueue来说,主要工作就2步:

1.如果有异步服务,那么就异步创建mappedFile

2.否则就同步创建

接下去主要看异步创建的流程,查看allocateMappedFileService.putRequestAndReturnMappedFile

public MappedFile putRequestAndReturnMappedFile(String nextFilePath, String nextNextFilePath, int fileSize) {
...
//创建mappedFile的请求,
AllocateRequest nextReq = new AllocateRequest(nextFilePath, fileSize);
//将其放入ConcurrentHashMap中,主要用于并发判断,保证不会创建重复的mappedFile
boolean nextPutOK = this.requestTable.putIfAbsent(nextFilePath, nextReq) == null;
//如果map添加成功,就可以将request放入队列中,实际创建mappedFile的线程也是从该queue中获取request
if (nextPutOK) {
boolean offerOK = this.requestQueue.offer(nextReq);
} AllocateRequest result = this.requestTable.get(nextFilePath);
try {
if (result != null) {
//因为是异步创建,所以这里需要await,等待mappedFile被异步创建成功
boolean waitOK = result.getCountDownLatch().await(waitTimeOut, TimeUnit.MILLISECONDS);
//返回创建好的mappedFile
return result.getMappedFile();
}
} catch (InterruptedException e) {
log.warn(this.getServiceName() + " service has exception. ", e);
}
return null;
}

因此对于AllocateMappedFileService.putRequestAndReturnMappedFile,主要工作也是2步:

1.将“创建mappedFile”的请求放入队列中

2.等待异步线程实际创建完mappedFile

接下去看异步线程是如何具体创建mappedFile的。既然AllocateMappedFileService本身就是负责创建mappedFile的,并且其本身也实现了Runnable接口,我们查看其run方法,其中会调用mmapOperation,这就是最终执行创建mappedFile的方法

private boolean mmapOperation() {
boolean isSuccess = false;
AllocateRequest req = null;
try {
//从队列中拿request
req = this.requestQueue.take();
...
if (req.getMappedFile() == null) {
MappedFile mappedFile;
//判断是否采用堆外内存
if (messageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
try {
//如果开启了堆外内存,rocketmq允许外部注入自定义的MappedFile实现
mappedFile = ServiceLoader.load(MappedFile.class).iterator().next();
mappedFile.init(req.getFilePath(), req.getFileSize(), messageStore.getTransientStorePool());
} catch (RuntimeException e) {
//如果没有自定义实现,那么就采用默认的实现
log.warn("Use default implementation.");
mappedFile = new MappedFile(req.getFilePath(), req.getFileSize(), messageStore.getTransientStorePool());
}
} else {
//如果未采用堆外内存,那么就直接采用默认实现
mappedFile = new MappedFile(req.getFilePath(), req.getFileSize());
}
...
//这里会预热文件,这里涉及到了系统的底层调用
mappedFile.warmMappedFile(this.messageStore.getMessageStoreConfig().getFlushDiskType(),
this.messageStore.getMessageStoreConfig().getFlushLeastPagesWhenWarmMapedFile());
req.setMappedFile(mappedFile);
}
...
} finally {
if (req != null && isSuccess)
//无论是否创建成功,都要唤醒putRequestAndReturnMappedFile方法中的等待线程
req.getCountDownLatch().countDown();
}
return true;
}

因此对于mmapOperation创建mappedFile,主要工作为4步:

1.从队列中获取putRequestAndReturnMappedFile方法存放的request

2.根据是否启用对外内存,分支创建mappedFile

3.预热mappedFile

4.唤醒putRequestAndReturnMappedFile方法中的等待线程

接下去查看mappedFile内部的具体实现,我们可以发现在构造函数中,也会调用内部的init方法,这就是主要实现mmap的方法

private void init(final String fileName, final int fileSize) throws IOException {
...
//创建文件对象
this.file = new File(fileName);
try {
//获取fileChannel
this.fileChannel = new RandomAccessFile(this.file, "rw").getChannel();
//进行mmap操作,将磁盘空间映射到内存
this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize);
...
} finally {
...
}
}

因此对于init执行mmap,主要工作分为2步:

1.获取文件的fileChannel

2.执行mmap映射

而如果采用了堆外内存,那么除了上述的mmap操作,还会额外分配对外内存

this.writeBuffer = transientStorePool.borrowBuffer();

到这里,CommitLog.asyncPutMessage方法中的获取或创建mappedFile就完成了。

接下去需要查看消息具体是符合被写入文件中的。查看mappedFile的appendMessage方法,最终会调用到appendMessagesInner方法:

public AppendMessageResult appendMessagesInner(final MessageExt messageExt, final AppendMessageCallback cb,
PutMessageContext putMessageContext) {
//如果是启用了对外内存,那么会优先写入对外内存,否则直接写入mmap内存
ByteBuffer byteBuffer = writeBuffer != null ? writeBuffer.slice() : this.mappedByteBuffer.slice();
byteBuffer.position(currentPos);
AppendMessageResult result;
...
//调用外部的callback执行实际的写入操作
result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos,
(MessageExtBrokerInner) messageExt, putMessageContext);
...
return result;
}

因此对于appendMessage方法,主要工作分为2步:

1.判断是否启用对外内存,从而选择对应的buffer对象

2.调用传入的callback方法进行实际写入

接下去查看外部传入的callback方法,是由CommitLog.asyncPutMessage传入

result = mappedFile.appendMessage(msg, this.appendMessageCallback, putMessageContext);

而this.appendMessageCallback则是在CommitLog的构造函数中初始化的

this.appendMessageCallback = new DefaultAppendMessageCallback(defaultMessageStore.getMessageStoreConfig().getMaxMessageSize());

查看DefaultAppendMessageCallback.doAppend方法,因为本文不关心消息的具体结构,所以省略了大部分构造buffer的代码:

public AppendMessageResult doAppend(final long fileFromOffset, final ByteBuffer byteBuffer, final int maxBlank,
final MessageExtBrokerInner msgInner, PutMessageContext putMessageContext) {
...
//获取消息编码后的buffer
ByteBuffer preEncodeBuffer = msgInner.getEncodedBuff();
...
//写入buffer中,如果启用了对外内存,那么就会写入外部传入的writerBuffer,否则直接写入mappedByteBuffer中
byteBuffer.put(preEncodeBuffer);
...
return result;
}

因此对于doAppend方法,主要工作分为2步:

1.将消息编码

2.将编码后的消息写入buffer中,可以是writerBuffer或者mappedByteBuffer

此时虽然字节流已经写入了buffer中,但是对于堆外内存,此时数据还仅存在于内存中,而对于mappedByteBuffer,虽然会有系统线程定时刷数据落盘,但是这并非我们可以控,因此也只能假设还未落盘。为了保证数据能落盘,rocketmq还有一个异步刷盘的线程,接下去再来看下异步刷盘是如何处理的。

查看CommitLog的构造函数,其中有3个service,分别负责同步刷盘、异步刷盘和堆外内存写入fileChannel

public CommitLog(final DefaultMessageStore defaultMessageStore) {
...
//同步刷盘
if (FlushDiskType.SYNC_FLUSH == defaultMessageStore.getMessageStoreConfig().getFlushDiskType()) {
this.flushCommitLogService = new GroupCommitService();
} else {
//异步刷盘
this.flushCommitLogService = new FlushRealTimeService();
}
//将对外内存的数据写入fileChannel
this.commitLogService = new CommitRealTimeService();
...
}

先看CommitRealTimeService.run方法,其中最关键的代码如下:

boolean result = CommitLog.this.mappedFileQueue.commit(commitDataLeastPages);

查看mappedFileQueue.commit方法,关键如下:

int offset = mappedFile.commit(commitLeastPages);

查看mappedFile.commit方法:

public int commit(final int commitLeastPages) {
//如果为空,说明不是堆外内存,就不需要任何操作,只需等待刷盘即可
if (writeBuffer == null) {
return this.wrotePosition.get();
}
if (this.isAbleToCommit(commitLeastPages)) {
if (this.hold()) {
//如果是堆外内存,那么需要做commit
commit0();
this.release();
}
...
}
return this.committedPosition.get();
}

查看commit0方法:

protected void commit0() {
...
//获取堆外内存
ByteBuffer byteBuffer = writeBuffer.slice();
//写入fileChannel
this.fileChannel.write(byteBuffer);
...
}

因此对于CommitRealTimeService,工作主要分2步:

1.判断是否是对外内存,如果不是那就不需要处理

2.如果是对外内存,则写入fileChannel

最后查看同步刷盘的GroupCommitService和异步刷盘FlushRealTimeService,查看其run方法,会发现其本质都是调用了如下方法:

CommitLog.this.mappedFileQueue.flush

当然在处理的逻辑上还有计算position等等逻辑,但这不是本文所关心的,所以就省略了。

同步和异步的区别体现在了执行刷盘操作的时间间隔,对于同步刷盘,固定间隔10ms:

this.waitForRunning(10);

而对于异步刷盘,时间间隔为配置值,默认500ms:

int interval = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushIntervalCommitLog();
...
if (flushCommitLogTimed) {
Thread.sleep(interval);
} else {
this.waitForRunning(interval);
}

最后查看mappedFileQueue.flush是如何刷盘的。最终会调用到mappedFile的flush方法:

public int flush(final int flushLeastPages) {
...
//如果是使用了堆外内存,那么调用的是fileChannel的刷盘
if (writeBuffer != null || this.fileChannel.position() != 0) {
this.fileChannel.force(false);
} else {
//如果非堆外内存,那么调用的是mappedByteBuffer的刷盘
this.mappedByteBuffer.force();
}
...
return this.getFlushedPosition();
}

因此最终的刷盘,工作主要分2步,正和前面的CommitRealTimeService工作对应:

1.如果是使用了堆外内存,那么调用fileChannel的刷盘

2.如果非堆外内存,那么调用mappedByteBuffer的刷盘

至此,整个rocketmq消息落盘的流程就完成了,接下去重新整理下整个流程:

1.CommitLog:存储的业务层,接收“保存消息”的请求,主要有2个功能:创建mappedFile、异步写入消息。

2.AllocateMappedFileService:异步创建mappedFile的服务,通过构建AllocateRequest对象和队列进行线程间的通讯。虽然MappedFile的实际创建是通过异步线程执行的,但是当前线程会等待创建完成后再返回,所以实际上是异步阻塞的。

3.MappedFile:存储的最底层对象,一个MappedFile对象就对应了一个实际的文件。在init方法中创建了fileChannel,并完成了mmap操作。如果启用了堆外内存,则会额外初始化writeBuffer,实现读写分离。

4.MappedFileQueue:管理MappedFile的容器。

5.写入消息的时候,会根据是否启用堆外内存,写入writeBuffer或者mappedByteBuffer。

6.实际落盘是通过异步的线程实现的,分为名义上的同步(GroupCommitService)和异步(FlushRealTimeService),不过主要区别在于执行落盘方法的时间间隔不同,最终都是调用mappedFile的flush方法

7.落盘会根据是否启用对外内存,分别调用fileChannel.force或者mappedByteBuffer.force

从源码分析RocketMq消息的存储原理的更多相关文章

  1. 源码分析RocketMQ消息轨迹

    目录 1.发送消息轨迹流程 1.1 DefaultMQProducer构造函数 1.2 SendMessageTraceHookImpl钩子函数 1.3 TraceDispatcher实现原理 2. ...

  2. 源码分析 RocketMQ DLedger(多副本) 之日志复制(传播)

    目录 1.DLedgerEntryPusher 1.1 核心类图 1.2 构造方法 1.3 startup 2.EntryDispatcher 详解 2.1 核心类图 2.2 Push 请求类型 2. ...

  3. 从SpringBoot源码分析 配置文件的加载原理和优先级

    本文从SpringBoot源码分析 配置文件的加载原理和配置文件的优先级     跟入源码之前,先提一个问题:   SpringBoot 既可以加载指定目录下的配置文件获取配置项,也可以通过启动参数( ...

  4. 并发编程学习笔记(9)----AQS的共享模式源码分析及CountDownLatch使用及原理

    1. AQS共享模式 前面已经说过了AQS的原理及独享模式的源码分析,今天就来学习共享模式下的AQS的几个接口的源码. 首先还是从顶级接口acquireShared()方法入手: public fin ...

  5. Guava 源码分析之Cache的实现原理

    Guava 源码分析之Cache的实现原理 前言 Google 出的 Guava 是 Java 核心增强的库,应用非常广泛. 我平时用的也挺频繁,这次就借助日常使用的 Cache 组件来看看 Goog ...

  6. 源码分析 RocketMQ DLedger 多副本存储实现

    目录 1.DLedger 存储相关类图 1.1 DLedgerStore 1.2 DLedgerMemoryStore 1.3 DLedgerMmapFileStore 2.DLedger 存储 对标 ...

  7. 源码分析 Kafka 消息发送流程(文末附流程图)

    温馨提示:本文基于 Kafka 2.2.1 版本.本文主要是以源码的手段一步一步探究消息发送流程,如果对源码不感兴趣,可以直接跳到文末查看消息发送流程图与消息发送本地缓存存储结构. 从上文 初识 Ka ...

  8. 源码分析Kafka 消息拉取流程

    目录 1.KafkaConsumer poll 详解 2.Fetcher 类详解 本节重点讨论 Kafka 的消息拉起流程. @(本节目录) 1.KafkaConsumer poll 详解 消息拉起主 ...

  9. 源码分析 RocketMQ DLedger 多副本之 Leader 选主

    目录 1.DLedger关于选主的核心类图 1.1 DLedgerConfig 1.2 MemberState 1.3 raft协议相关 1.4 DLedgerRpcService 1.5 DLedg ...

随机推荐

  1. haproxy https实现

    一.实验环境 一.准备后端服务器 # yum -y install nginx # echo "10.0.0.7" > /usr/share/nginx/html/index ...

  2. Linux系统安装(CentOS-7)详细操作步骤

    Linux系统安装(CentOS-7)详细操作步骤 一.创建虚拟机 首先创建一个虚拟机,然后才能在虚拟机里面安装系统. 第一步,创建虚拟机的向导 第二步,兼容性默认即可. 第三步,我们等创建完虚拟机后 ...

  3. opencv笔记--HOGDescriptor

    特征描述提取图像区域上有用信息而忽略无用信息,不同目标下有用信息与无用信息定义不同.这里提取的有用信息用于分类器输入并期望产生正确的分类. HOG(Histogram of Oriented Grad ...

  4. C++ 提高编程

    目录 C++ 提高编程 一. 模板 1. 概念 2. 函数模板 2.1 函数模板语法 2.2 注意事项 2.3 普通函数和函数模板的区别 2.4 普通函数和函数模板的调用规则 2.5 模板的局限性 3 ...

  5. suse 12 利用缓存创建本地源供内网服务使用

    文章目录 服务端获取 添加源 刷新源 清除缓存 安装软件 获取rpm包 客户端测试 zypper --help 前言: 其实,咱也不知道为啥写了这篇博客,咱就是想学一学suse,咱也不会,咱也只能学, ...

  6. v-model的修饰符

    .lazy 就是在用户输入时数据不会进行传输,在失去焦点后或者按回车,才会将数据将进行改变 .number 因为v-model所绑定的数据,都会默认转换为string类型,而.number就能将数据转 ...

  7. eBPF会成为服务网格的未来吗?

    服务网格现状 服务网格为服务提供了复杂的应用层网络管理,如服务发现.流量路由.弹性(超时/重试/断路).认证/授权.可观察性(日志/度量/追踪)等. 在分布式应用的早期,这些要求是通过直接将所需的逻辑 ...

  8. 【C# 集合】Hash哈希函数 |散列函数|摘要算法

    希函数定义 哈希函数(英語:Hash function)又称散列函数.散列函数.摘要算法.单向散列函数.散列函数把消息或数据压缩成摘要,使得数据量变小,将数据的格式固定下来.该函数将数据打乱混合,重新 ...

  9. Collection接口和Map接口的 size 方法和 isEmpty方法

    一.Collection接口的 size 方法和 isEmpty方法 int size(); 返回列表中元素的数目,如果这个列表包含超过Integer.MAX_VALUE,则返回Integer.MAX ...

  10. CDH5.16.2离线安装(详细)

    目录 01 Coudera Manager 02 环境准备 03 CM安装 01 Coudera Manager 概念:拥有集群自动化安装.中心化管理.集群监控.报警功能的一个工具,使集群安装从几天时 ...