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. kubeadm/flannel/dashboard/harbor部署以及服务发布

    kubeadm/flannel/dashboard/harbor部署以及服务发布 目录 kubeadm/flannel/dashboard/harbor部署以及服务发布 一.部署kubeadm 1. ...

  2. java-数据库操作(JDBC)

    前言:JDBC用到的API和常用方法 DriverMannager类 加载驱动并创建与数据库的连接,通常使用Class类的静态方法forName()来实现加载驱动,使用getConnection(St ...

  3. 鸟哥的Linux私房菜学习笔记——文件权限与目录配置

    Linux的文件权限和目录配置 在linux中的每个用户必需属于一个组,不能独立于组外.在linux中每个文件有所有者.所在组.其它组的概念. (1)所有者 一般为文件的创建者,谁创建了该文件,就是天 ...

  4. k8s集群节点ping不通其他主机的ip

    文章目录 排查过程 本地宿主机网络检查 pod网络检查 tcpdump检查网络 检查flannel网卡 检查宿主机网卡 iptables检查 解决方法 测试环境服务出现问题,服务一直报错认证超时,检查 ...

  5. Dubbo SPI机制之一JDK中的SPI

    首先简单阐述下什么是SPI:SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制.目前有不少框架用它来做服务的扩展发现,简单来说,就是一种动态 ...

  6. Node 模块规范鏖战:难以相容的 CJS 与 ESM

    自 13.2.0 版本开始,Node.js 在保留了 CommonJS(CJS)语法的前提下,新增了对 ES Modules(ESM)语法的支持. 天下苦 CJS 久已,Node 逐渐拥抱新标准的规划 ...

  7. 实战 | 关于随手一点就发现XSS漏洞这件事

    起因 今天正在做一个项目时,需要找一个云接码平台接码去登录系统.当我随手从百度找了一个接码平台时,一个偶然的发现,有了今天这篇文章. 正文 当我进入这个接码平台随便找了个手机号,然后等了好几分钟都没啥 ...

  8. CSS代码示例-背景属性(background)

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  9. k8s管理平台:rancher

    简介 中文官网:https://docs.rancher.cn/ github:https://github.com/rancher/rancher 基础环境 https://www.cnblogs. ...

  10. 把SQLAlchemy查询对象转换成字典

    1-假设查出来的为单个对象 1-1 在model.py中为模型对象添加字典转换函数: from exts import db class User(db.Model): __tablename__ = ...