概述

RocketMQ存储中主要用到以下知识点:

  • mmap 文件映射
  • 内存池
  • 异步刷盘
  • consumeQueue
    同时本节将介绍各个重要的类,本篇文章将介绍 mmap 文件映射的相关方法和内存池相关知识点,刷盘和 consumeQueue 相关知识点在下篇介绍。

MappedFile

mappedFile 对应着底层映射文件,主要的功能是

  • bytebuffer写入映射文件
  • 回刷回文件

重要字段

    public static final int OS_PAGE_SIZE = 1024 * 4;
protected static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); private static final AtomicLong TOTAL_MAPPED_VIRTUAL_MEMORY = new AtomicLong(0); private static final AtomicInteger TOTAL_MAPPED_FILES = new AtomicInteger(0);
//加了 final,值不能修改
protected final AtomicInteger wrotePosition = new AtomicInteger(0);
//ADD BY ChenYang
// 先commit 后 flush
protected final AtomicInteger committedPosition = new AtomicInteger(0);
private final AtomicInteger flushedPosition = new AtomicInteger(0);
protected int fileSize; protected FileChannel fileChannel;
/**
* Message will put to here first, and then reput to FileChannel if writeBuffer is not null.
* 消息先放到这里先,如果此时 writeBuffer 不为 null (此时有东西在写入)那么再次放入 fileChannel
*/
protected ByteBuffer writeBuffer = null;
//
protected TransientStorePool transientStorePool = null;
private String fileName;
private long fileFromOffset;
private File file;
//虚拟内存映射 buffer
private MappedByteBuffer mappedByteBuffer;
private volatile long storeTimestamp = 0;
private boolean firstCreateInQueue = false;

fileChannel 映射持久化的文件进来,使用原子类纪录 commit 和 flush 的节点。

init 方法

    public void init(final String fileName, final int fileSize,
final TransientStorePool transientStorePool) throws IOException {
init(fileName, fileSize);
this.writeBuffer = transientStorePool.borrowBuffer();
this.transientStorePool = transientStorePool;
} /**
* 初始化最主要就是文件映射
*/
private void init(final String fileName, final int fileSize) throws IOException {
this.fileName = fileName;
this.fileSize = fileSize;
this.file = new File(fileName);
this.fileFromOffset = Long.parseLong(this.file.getName());
boolean ok = false; ensureDirOK(this.file.getParent()); try {
// RandomAccessFile
// 参见 : https://blog.csdn.net/qq496013218/article/details/69397380
this.fileChannel = new RandomAccessFile(this.file, "rw").getChannel();
// fileChannel.map 返回的是堆外内存(java.nio.directByteBuffer)
this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize);
TOTAL_MAPPED_VIRTUAL_MEMORY.addAndGet(fileSize);
TOTAL_MAPPED_FILES.incrementAndGet();
ok = true;
} catch (FileNotFoundException e) {
log.error("create file channel " + this.fileName + " Failed. ", e);
throw e;
} catch (IOException e) {
log.error("map file " + this.fileName + " Failed. ", e);
throw e;
} finally {
if (!ok && this.fileChannel != null) {
this.fileChannel.close();
}
}
}

commit 操作。

    /**
* 1. 判断是否达到commit 的要求
* 2. 获得锁
* 3. commit
* 4. 释放锁
*/
public int commit(final int commitLeastPages) {
if (writeBuffer == null) {
//no need to commit data to file channel, so just regard wrotePosition as committedPosition.
return this.wrotePosition.get();
}
if (this.isAbleToCommit(commitLeastPages)) {
if (this.hold()) {
commit0(commitLeastPages);
this.release();
} else {
log.warn("in commit, hold failed, commit offset = " + this.committedPosition.get());
}
}
//TODO 下面是什么操作
// All dirty data has been committed to FileChannel.
if (writeBuffer != null && this.transientStorePool != null && this.fileSize == this.committedPosition.get()) {
this.transientStorePool.returnBuffer(writeBuffer);
this.writeBuffer = null;
} return this.committedPosition.get();
} /**
* 使用 JAVA NIO 的 bytebuffer 创建子 buffer
* 然后写入到 filechannel 中去
* @param commitLeastPages
*/
protected void commit0(final int commitLeastPages) {
int writePos = this.wrotePosition.get();
int lastCommittedPosition = this.committedPosition.get(); if (writePos - this.committedPosition.get() > 0) {
try {
ByteBuffer byteBuffer = writeBuffer.slice();
byteBuffer.position(lastCommittedPosition);
byteBuffer.limit(writePos);
this.fileChannel.position(lastCommittedPosition);
this.fileChannel.write(byteBuffer);
this.committedPosition.set(writePos);
} catch (Throwable e) {
log.error("Error occurred when commit data to FileChannel.", e);
}
}
} /**
* 判断是否满了或是达到了最小的 commit 页数
* @param commitLeastPages 最小commit 页数
* @return 是否可以 commit
*/
protected boolean isAbleToCommit(final int commitLeastPages) {
int flush = this.committedPosition.get();
int write = this.wrotePosition.get(); if (this.isFull()) {
return true;
} if (commitLeastPages > 0) {
return ((write / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE)) >= commitLeastPages;
} return write > flush;
}
FileChannel,MappedByteBuffer:
这两个类代表的是Mmap 这样的内存映射技术,Mmap 能够将文件直接映射到用户态的内存地址,使得对文件的操作不再是 write/read,而转化为直接对内存地址的操作。 Mmap技术本身也有局限性,也就是操作的文件大小不能太大,因此RocketMQ 中限制了单文件大小来避免这个问题。也就是那个filesize定为1G的原因。

flush 回刷到文件中去

    /**
* @return The current flushed position
*/
public int flush(final int flushLeastPages) {
if (this.isAbleToFlush(flushLeastPages)) {
if (this.hold()) {
int value = getReadPosition(); try {
//我们只增加数据到 fileChannel 或是 mappedByteBuffer ,从不同时两者一起增加
//We only append data to fileChannel or mappedByteBuffer, never both.
if (writeBuffer != null || this.fileChannel.position() != 0) {
this.fileChannel.force(false);
} else {
this.mappedByteBuffer.force();
}
} catch (Throwable e) {
log.error("Error occurred when force data to disk.", e);
} this.flushedPosition.set(value);
this.release();
} else {
log.warn("in flush, hold failed, flush offset = " + this.flushedPosition.get());
this.flushedPosition.set(getReadPosition());
}
}
return this.getFlushedPosition();
} private boolean isAbleToFlush(final int flushLeastPages) {
int flush = this.flushedPosition.get();
int write = getReadPosition(); if (this.isFull()) {
return true;
} if (flushLeastPages > 0) {
return ((write / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE)) >= flushLeastPages;
} return write > flush;
}

其中 writebuffer 和 filechannel 什么情况会刷回磁盘呢?以下这种图回答了这个问题。

同时mappedFile还有预热处理,具体见 warmMappedFile 方法 。


TransientStorePool

该类的主要作用是创建内存池,而且是堆外内存,主要作用是消除了申请内存空间,回收的时间,提高了使用的性能。
字段

    private final int poolSize;
private final int fileSize;
private final Deque<ByteBuffer> availableBuffers;
private final MessageStoreConfig storeConfig;
public class TransientStorePool {
private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); private final int poolSize;//池的大小有多少,默认5
private final int fileSize;//每个commitLog文件大小,默认1G
private final Deque<ByteBuffer> availableBuffers;//双端队列记录可用的buffers
private final MessageStoreConfig storeConfig;//存储配置 public TransientStorePool(final MessageStoreConfig storeConfig) {
this.storeConfig = storeConfig;
this.poolSize = storeConfig.getTransientStorePoolSize();
this.fileSize = storeConfig.getMapedFileSizeCommitLog();
this.availableBuffers = new ConcurrentLinkedDeque<>();
} /**
* It's a heavy init method.
* 初始化函数,分配poolSize个fileSize的堆外空间
*/
public void init() {
for (int i = 0; i < poolSize; i++) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(fileSize);//虚拟机外内存中分配的空间 final long address = ((DirectBuffer) byteBuffer).address();
Pointer pointer = new Pointer(address);
LibC.INSTANCE.mlock(pointer, new NativeLong(fileSize)); availableBuffers.offer(byteBuffer);
}
} //销毁availableBuffers中所有buffer数据
public void destroy() {
for (ByteBuffer byteBuffer : availableBuffers) {
final long address = ((DirectBuffer) byteBuffer).address();
Pointer pointer = new Pointer(address);
LibC.INSTANCE.munlock(pointer, new NativeLong(fileSize));
}
} //用完了之后,返还一个buffer,对buffer数据进行清理
public void returnBuffer(ByteBuffer byteBuffer) {
byteBuffer.position(0);
byteBuffer.limit(fileSize);
this.availableBuffers.offerFirst(byteBuffer);
} //借一个buffer出去
public ByteBuffer borrowBuffer() {
ByteBuffer buffer = availableBuffers.pollFirst();
if (availableBuffers.size() < poolSize * 0.4) {
log.warn("TransientStorePool only remain {} sheets.", availableBuffers.size());
}
return buffer;
} //剩余可用的buffers数量
public int remainBufferNumbs() {
if (storeConfig.isTransientStorePoolEnable()) {
return availableBuffers.size();
}
return Integer.MAX_VALUE;
}
}

可以看到内存池在初始化的过程中,将内存用“lock”锁,防止CPU将进程在主存中的这一部分内存给交换回硬盘。

补充

内存池

在netty的过程在使用过程中,也会使用内存池,内存池的优势是集中管理内存的分配和释放,同时提高分配和释放内存的性能,很多框架会先预先申请一大块内存,然后通过提供响应的分配
和释放接口来使用内存,这样系统的性能也会打打提高。

随机读写,顺序读写

随机和顺序读写,是存储器的两种输入输出方式。存储的数据在磁盘中占据空间,对于一个新磁盘,操作系统会将数据文件依次写入磁盘,当有些数据被删除时,就会空出该数据原来占有的存储空间,时间长了,不断的写入、删除数据,就会产生很多零零散散的存储空间,就会造成一个较大的数据文件放在许多不连续的存贮空间上,读写些这部分数据时,就是随机读写,磁头要不断的调整磁道的位置,以在不同位置上的读写数据,相对于连续空间上的顺序读写,要耗时很多。
在开机时、启动大型程序时,电脑要读取大量小文件,而这些文件也不是连续存放的,也属于随机读取的范围。
随机读写:每一段数据有地址码,可以任意跳到某个地址读取该段数据
顺序读写:数据以一定长度连续存储,中间没有地址码,只能顺序读取

改善方法:做磁盘碎片整理,合并碎片文件,但随后还会再产生碎片造成磁盘读写性能下降,而且也解决不了小文件的随机存取的问题,这只是治标。更好的解决办法:更换电子硬盘(SSD),电子盘由于免除了机械硬盘的磁头运动,对于随机数据的读写极大的提高。
举个例子1: SSD的随机读取延迟只有零点几毫秒,而7200RPM的随机读取延迟有7毫秒左右,5400RPM硬盘更是高达9毫秒之多,体现在性能上就是开关机速度。

举个例子2:假设有1到1000笔的数据。
情况1:现在要读出第1000笔,顺序读写的方式是从第1笔开始读,一直找到第1000笔;随机读写是通过运算,很快的找到第1000笔。
情况2:要找出含“abc”的数据,顺序读写还是从第1笔开始读,一直找到第1000笔;随机读写是通过运算,很快的找到“abc”的数据。

总结

本节介绍了rocketmq中的存储细节,包括 mmap 相关,内存池相关知识点。

参考资料

  • http://silence.work/2019/05/03/RocketMQ-Broker
  • https://www.jianshu.com/p/771cce379994
  • https://blog.csdn.net/qq_33611327/article/details/81738195 (推荐一看)

消息队列(五)--- RocketMQ-消息存储2的更多相关文章

  1. 消息队列之-RocketMQ入门

    简介 RocketMQ是阿里开源的消息中间件,目前已经捐献个Apache基金会,它是由Java语言开发的,具备高吞吐量.高可用性.适合大规模分布式系统应用等特点,经历过双11的洗礼,实力不容小觑. 官 ...

  2. [分布式学习]消息队列之rocketmq笔记

    文档地址 RocketMQ架构 哔哩哔哩上的视频 mq有很多,近期买了<分布式消息中间件实践>这本书,学习关于mq的相关知识.mq大致有有4个功能: 异步处理.比如业务端需要给用户发送邮件 ...

  3. 消息队列之--RocketMQ

    序言 资料 https://github.com/alibaba/RocketMQ http://rocketmq.apache.org/

  4. 消息队列中间件 RocketMQ 源码分析 —— Message 存储

  5. 阿里消息队列中间件 RocketMQ源码解析:Message发送&接收

  6. 阿里消息队列中间件 RocketMQ 源码分析 —— Message 拉取与消费(上)

  7. IPC之消息队列详解与使用

    一.    概念 消息队列就是一个消息的链表.对消息队列有写权限的进程可以向其中按照一定的规则添加新消息:对消息队列有读权限的进程可以从消息队列中读出消息.消息队列是随内核持续的.下面介绍三个概念: ...

  8. C#消息队列(RabbitMQ)零基础从入门到实战演练

    一.课程介绍 如果您从工作中之听过但未有接触过消息对队列(MQ),如果你接触过一点关于MQ的知识,如果没有这么的多如果的话......,那么阿笨将通过本次<C#消息队列零基础从入门到实战演练&g ...

  9. 【windows 操作系统】进程间通信(IPC)简述|无名管道和命名管道 消息队列、信号量、共享存储、Socket、Streams等

    一.进程间通信简述 每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进 ...

  10. IM开发基础知识补课(五):通俗易懂,正确理解并用好MQ消息队列

    1.引言 消息是互联网信息的一种表现形式,是人利用计算机进行信息传递的有效载体,比如即时通讯网坛友最熟悉的即时通讯消息就是其具体的表现形式之一. 消息从发送者到接收者的典型传递方式有两种: 1)一种我 ...

随机推荐

  1. 在PDB级别中如何切换或重建UNDO表空间

    Oracle 12.1版本中,UNDO表空间仅存在CDB级别(共享UNDO),来自于AskScuti博客园. Oracle 12.2版本开始,UNDO表空间同时可以存在每个PDB级别(本地UNDO). ...

  2. 跨站跟踪攻击(CST/XST)

    XSS与httponly 正常情况下,客户端脚本(如JS脚本)是可以通过document.cookie函数获得,这样如果有XSS跨站漏洞,cookie很容易被盗取.浏览器有一个安全策略,通过设置coo ...

  3. CentOS之service iptables stop 显示not loaded

    停止firewalld服务停止防火墙,并禁用这个服务 sudo systemctl stop firewalld.servicesudo systemctl disable firewalld.ser ...

  4. 题解【loj537】「LibreOJ NOIP Round #1」DNA 序列

    题目描述 \(NOIP\)复赛之前\(HSD\)桑进行了一项研究,发现人某条染色体上的一段\(DNA\)序列中连续的\(k\)个碱基组成的碱基序列与做题的 \(AC\) 率有关!于是他想研究一下这种关 ...

  5. 题解【洛谷P5248】 [LnOI2019SP]快速多项式变换(FPT)

    题目描述 这是一道构造题. 诗乃在心中想了一个n+1项的多项式f(x).第i项的次数为i,系数为ai: f(x)=a0​+a1​*x+a2​*x2+a3​*x3+⋯+an*​xn 给定m以及f(m)的 ...

  6. Android Socket 通信

    Android socket 通信 安卓编写Socket客户端,实现连接Socket服务端通信. 创建Socket连接并获取服务端数据 先创建几个全局变量吧 private BufferedWrite ...

  7. RTMP服务器搭建(nginx+rtmp)

    参考文章:https://obsproject.com/forum/resources/how-to-set-up-your-own-private-rtmp-server-using-nginx.5 ...

  8. 传奇服务端添加双倍经验卷的方法 双倍经验卷轴DB示例展示

    第一步我们在DBC数据库中添加好双倍经验卷轴DB,以下是现成的双倍经验卷DB,导入到DB里面就可以了. 222;双倍经验卷;31;0;1;20;0;0;265;0;0;0;0;0;0;0;0;0;0; ...

  9. Laravel 部署到阿里云 / 腾讯云

    首先你需要一台阿里云/腾讯云服务器 安装系统选择 ubuntu 16.04 然后通过 ssh 登录远程服务器按下列步骤进行配置: 更新列表 apt-get update 安装语言包 sudo apt- ...

  10. MyBatis知识点整理

    1.MyBatis一般使用步骤 1.1获取Configuration实例或编写配置文件 //获取Configuration实例的样例 TransactionFactory transactionFac ...