【RocketMQ源码分析】深入消息存储(3)

CommitLog篇 ——【RocketMQ源码分析】深入消息存储(1)

ConsumeQueue篇 ——【RocketMQ源码分析】深入消息存储(2)

前面两篇已经说过了消息如何存储到CommitLog,以及ConsumeQueue的构建流程,到了第三篇,我们有一个不得不跨过的坎儿,MappedFile —— 内存文件映射。

MappedFile的存在是RocketMQ选择将消息直接存储到磁盘的关键因素,在第一篇CommitLog存储流程开篇中,我就写过一个思路。

【RocketMQ源码分析】深入消息存储(3)

即用到内存又用到本地磁盘

填充和交换

文件映射到内存

随机读接口去访问

这里出现的几个关键句,都离不开本篇要说的MappedFile。

RocketMQ既然要去与磁盘交互存储文件,不同IO方法在性能差距上都是千差万别的,怎么高效的与磁盘/内存进行交互,是很多涉及存储的中间件强大与否的重要标志。

实现一个进程内基于队列的消息持久化存储引擎

这是几年前天池中间件大赛的题目,目标就是设计一个利用有限内存、较多磁盘空间来实现一个消息队列,这样看其实思路在第一篇就已经说过了,重点是他要求这个队列支持聚合操作。

【RocketMQ源码分析】深入消息存储(3)

这让我想到ElasticSearch的聚合场景,如果要实现那么复杂的聚合功能,也太南了吧。

不过好在题目只是要求做指定时间段的消息加和,这无非就是维护一个消息存储的偏移量与时间的存储就好了。

为了深入了解内存文件映射,我们可以来读读它的源码,这里相对于CommitLog、ConsumeQueue更加底层,更多涉及的是IO、Buffer、PageCache等知识。

从页表谈到零拷贝

在我过去学习汇编语言的时候,有两个寻址相关的寄存器。

段寄存器、变址寄存器。

在8086的年代,地址总线是20位,但寄存器16位,寻址能力有限,为了保证1M的寻址能力,是将两个16位寄存器一起使用,以段基址和偏移地址的形式,达到1M寻址能力。

这个思想在操作系统保护模式下也是一样的,假如我们有一台32位操作系统,内存4GB。

我们来思考一下它的内存布局,内核空间和用户空间这是我们熟知的概念了,假如内存空间不做任何操作,按顺序性让我们去访问,首先一个大问题就是内存隔离,两个进程之间如何做到内存互不污染,这也引出了Java虚拟机内存分配的一个问题,分配之后的内存空间被垃圾回收器清理,剩下的空间大大小小可能不连续,后续一个需要占据大内存的对象可能无法存储,JVM可以选择回收-清理的方式保证没有碎片,这是因为有栈上的引用指向堆,一个大对象就算被移动也不用担心,但操作系统不同,如果想用类似JVM回收-清理的方式减少碎片内存,首先一个要面对的问题就是地址变更,后续进程在寻址时可能找不到目标。

此处需要注意地址变更,因为后面我们也会提到,操作系统的PageCache操作不当也会引起这个问题。

还有一个问题是,这种循序的空间并不安全,所有进程之间都可以互相访问到对方的地址,这是一些修改器的常用手段。

基于以上问题,操作系统映入了保护模式,基于页表将内存空间调整为虚拟内存,与实际的物理内存区分开。

现在的页表通常是二级页表,所谓两级页表就是对页表再进行分页,一个页表内的所有页表项是连续存放的,页表本质上是一堆数据,也是以页为单位存放在内存。

第一级称为页目录表。每个页表的物理地址在页目录表中都以页目录项(PDE)的形式来存储,4MB的页表再次分页可以分为1K(4MB/4KB)个页,对每个页的描述需要4个字节,所以页目录表占用4K大小,正好是一个标准页的大小,其指向第二级表。线性地址的高10位产生第一级的索引,由索引得到的表项中,指定并选择了1K个二级表中的一个页表。

第二级称为页表,存放在一个4K大小的页面中,包含1K个表项,每个表项包含一个页的物理基地址。线性地址的中间10位产生第二级索引,可以获得包含页的物理地址的页表项。这个物理地址的高20位与线性地址的低12位形成了最终的物理地址。

【RocketMQ源码分析】深入消息存储(3)

有了页表就能很好的划分进程空间,以及减少碎片空间了,对于一个进程而言,理论上最大可使用空间为4GB。基于此,操作系统的内存操作大多都是基于页(4KB).

虚拟内存的映入使得操作系统管理划分内存更加方便,实际进行虚拟地址映射到物理地址的单元是MMU,mmap内存文件映射也是一样,通过MMU映射到文件。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zwpyzd.html