Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >MongoDB Mmap 引擎分析

MongoDB Mmap 引擎分析

原创
作者头像
孔德雨
修改于 2017-06-19 11:29:40
修改于 2017-06-19 11:29:40
11.4K21
代码可运行
举报
文章被收录于专栏:孔德雨的专栏孔德雨的专栏
运行总次数:1
代码可运行

MongoDB在3.0之前一直使用mmap引擎作为默认存储引擎,本篇从源码角度对mmap引擎作分析,业界一直以来对10gen用mmap实现存储引擎褒贬不一,本文对此不作探讨。

存储按照db来分目录, 每个db目录下有 .ns文件 {dbname}.0, {dbname}.1 等文件。journal 目录下存放的是WAL(write ahead log) 用于故障恢复。 目录结构如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
db
|------journal
           |----_j.0
           |----_j.1
           |----lsn
|------local
           |----local.ns
           |----local.0
           |----local.1
|------mydb
           |----mydb.ns
           |----mydb.0
           |----mydb.1

这三类文件构成了mmap引擎的持久化单元。本文主要从代码层次分析每类文件的结构。

Namespace元数据管理

.ns文件映射

mmap引擎加载某个database时,首先初始化namespaceIndex,namespaceIndex相当于database的元数据入口。 mongo/db/storage/mmap_v1/catalog/namespace_index.cpp

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 89    DurableMappedFile _f{MongoFile::Options::SEQUENTIAL};      
 90    std::unique_ptr<NamespaceHashTable> _ht;               
154    const std::string pathString = nsPath.string(); 
159    _f.open(pathString);
232    p = _f.getView();
242    _ht.reset(new NamespaceHashTable(p, (int)len, "namespace index"));

如上,创建对.ns文件的mmap,将内存的view直接映射到hashtable上(不不进行任何解析)。因此.ns文件是一个hashtable的内存镜像。

hashtable的key-value关系string->NamespaceDetails(namespace_details.h),采用的是开放寻址hash。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
39 int NamespaceHashTable::_find(const Namespace& k, bool& found) const {        
46     while (1) {        
47         if (!_nodes(i).inUse()) {        
48             if (firstNonUsed < 0)
49                 firstNonUsed = i;        
50         }       
51        
52         if (_nodes(i).hash == h && _nodes(i).key == k) {        
53             if (chain >= 200) 
54                 log() << "warning: hashtable " << _name << " long chain " << std::endl;        
55             found = true; 
56             return i;        
57         }        
58         chain++;        
59         i = (i + 1) % n; 
60         if (i == start) {        
62             log() << "error: hashtable " << _name << " is full n:" << n << std::endl;        
63             return -1;
64         }       
65         if (chain >= maxChain) {        
66             if (firstNonUsed >= 0)        
67                 return firstNonUsed;
68             log() << "error: hashtable " << _name << " max chain reached:" << maxChain << std::endl;
69             return -1;
70         }       
71     }       
72 }

上述过程是开放式寻址hash的经典的查找过程,如果有冲突,向后跳一格,如果跳到查找的起点依然没有找到可用的空槽,则说明hashtable满了。

元数据内容窥探

一个NamespaceDetails对象对应该db下的某张表的元数据(namespace_index.h),大小为496bytes,mongod默认为.ns文件分配16MB的空间,且.ns文件唯一且不可动态伸缩空间,可以推断出一个mongod实例至多可建表大概30000个。该类有22个字段,重要字段有如下6个。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct NamespaceDetails {
// extent对应于一个内存连续块,由于mmap,也是文件连续区域。一张表有多个extent。
// 以双向链表的形式组织,firstExtent和lastExtent分别对应extent的首尾指针
DiskLoc firstExtent;  
DiskLoc lastExtent;
// 有若干种(26种)按照最小尺寸划分的freelist,
// 表中删除掉的行对应的数据块放到freelist中,按照数据块的尺寸划分为若干规则的freelist。
DiskLoc deletedListSmall[SmallBuckets];
// 兼容旧版本mmap引擎的废弃字段
DiskLoc deletedListLegacyGrabBag;
// 该表是否是capped,capped-table是ring-buffer类型的table,MongoDB中用来存放oplog
int isCapped;
// 和deletedListSmall字段一样,都是freelist的一部分,只是大小不同
DiskLoc deletedListLarge[LargeBuckets];
}

为了便于下文阐述,结合上述对namespaceIndex构建过程的描述与对元数据的注解,笔者先勾勒出如下的元数据结构

单表结构

上文我们讨论了单表元数据(NamespaceDetails)中重要字段的含义,接下来进行深入探讨。

Extent的组织形式

每张表由若干extent组成,每个extent为一块连续的内存区域(也即连续的硬盘区域),由firstExtent 和 lastExtent 记录首尾位置,每个extent的结构为

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/*extents are datafile regions where all the records within the region belong to the same namespace.*/
struct Extent {
    DiskLoc myLoc;
    DiskLoc xnext; //双向链表中前节点指针
    DiskLoc xprev; //双向链表中后节点指针
    Namespace nsDiagnstic;
    int length;
    // 一个Record对应表中的一行,每个extent在物理上由若干地址连续的
    // Record组成,但是这些record在逻辑上的前后关系并不等价于物理上
    // 的前后关系,first/last Record维护了逻辑上的先后关系,在维护游
    // 表迭代时使用
    DiskLoc firstRecord;
    DiskLoc lastRecord;
    char _extentData[4];
}

上述描述的组织结构如下图所示:

Extent 的分配与回收由ExtentManger管理,ExtentManager 首先尝试从已有文件中分配一个满足条件的连续块,如果没有找到,则生成一个新的{dbname}.i 的文件。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
143 void DataFile::open(OperationContext* txn,                                                       
144                     const char* filename,                                                         
145                     int minSize,                                                                 
146                     bool preallocateOnly) {                                                       
147     long size = _defaultSize();                                                                   
148                                                                                                   
149     while (size < minSize) {                                                                     
150         if (size < maxSize() / 2) {                                                               
151             size *= 2;                                                                           
152         } else {                                                                                 
153             size = maxSize();                                                                     
154             break;                                                                               
155         }                                                                                         
156     }                                                                                             
157                                                                                                   
158     if (size > maxSize()) {                                                                       
159         size = maxSize();                                                                         
160     }                                                                                             
161                                                                                                   
162     invariant(size >= 64 * 1024 * 1024 || mmapv1GlobalOptions.smallfiles);

文件的大小 {dbname}.0的大小默认为64MB。 之后每次新建会扩大一倍,以maxSize(默认为2GB)为上限。

一个extent被分为若干Records,每个Record对应表中的一行(一个集合中的文档),每一张表被RecordStore类封装,并对外提供出CRUD的接口。

Record分配

首先从已有的freelist(上文中提到的deletedBuckets)中分配,每张表按照内存块尺寸维护了不同规格的freelist,每个freelist是一个单向链表,当删除Record时,将record放入对应大小的freelist中。

如下按照从小到大的顺序遍历DeletedBuckets,如果遍历到有空闲且符合大小的空间,则分配:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
107         for (myBucket = bucket(lenToAlloc); myBucket < Buckets; myBucket++) {
108             // Only look at the first entry in each bucket. This works because we are either
109             // quantizing or allocating fixed-size blocks.
110             const DiskLoc head = _details->deletedListEntry(myBucket);
111             if (head.isNull())
112                 continue;
113             DeletedRecord* const candidate = drec(head);
114             if (candidate->lengthWithHeaders() >= lenToAlloc) {
115                 loc = head;
116                 dr = candidate;
117                 break;
118             }
119         }

上述代码分配出一块尺寸合适的内存块,但是该内存块依然可能比申请的尺寸大一些。mmap引擎在这里的处理方式是:将多余的部分砍掉,并归还给freelist。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
133     const int remainingLength = dr->lengthWithHeaders() - lenToAlloc;
134     if (remainingLength >= bucketSizes[0]) {
135         txn->recoveryUnit()->writingInt(dr->lengthWithHeaders()) = lenToAlloc;
136         const DiskLoc newDelLoc = DiskLoc(loc.a(), loc.getOfs() + lenToAlloc);
137         DeletedRecord* newDel = txn->recoveryUnit()->writing(drec(newDelLoc));
138         newDel->extentOfs() = dr->extentOfs();       
139         newDel->lengthWithHeaders() = remainingLength;
140         newDel->nextDeleted().Null();
141         
142         addDeletedRec(txn, newDelLoc);
143     }

上述分片内存的过程如下图所示:

如若从已有的freelist中分配失败,则会尝试申请新的extent,并将新的extent加到尺寸规则最大的freelist中。并再次尝试从freelist中分配内存。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 59 const int RecordStoreV1Base::bucketSizes[] = {
  ...
 83     MaxAllowedAllocation,      // 16.5M
 84     MaxAllowedAllocation + 1,  // Only MaxAllowedAllocation sized records go here.
 85     INT_MAX,                   // "oversized" bucket for unused parts of extents.
 86 };
 87

上述过程为mmap引擎对内存管理的概况,可见每个record在分配时不是固定大小的,申请到的内存块要将多出的部分添加到deletedlist中,record释放后也是链接到对应大小的deletedlist中,这样做时间久了之后会产生大量的内存碎片,mmap引擎也有针对碎片的compact过程以提高内存的利用率。

碎片Compact

compact以命令的形式,暴露给客户端,该命令以collection为维度,在实现中,以extent为最小粒度。

compact整体过程分为两步,如上图,第一步将extent从freelist中断开,第二步将extent中已使用空间copy到新的extent,拷贝过去保证内存的紧凑。从而达到compact的目的。

  1. orphanDeletedList 过程

将collection 对应的namespace 下的deletedlist 置空,这样新创建的record就不会分配到已有的extent。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
443         WriteUnitOfWork wunit(txn);
444         // Orphaning the deleted lists ensures that all inserts go to new extents rather than
445         // the ones that existed before starting the compact. If we abort the operation before
446         // completion, any free space in the old extents will be leaked and never reused unless
447         // the collection is compacted again or dropped. This is considered an acceptable
448         // failure mode as no data will be lost.
449         log() << "compact orphan deleted lists" << endl;
450         _details->orphanDeletedList(txn);
  1. 对于每个extent,每个extent记录了首尾record,遍历所有record,并将record插入到新的extent中,新的extent在插入时由于空间不足而自动分配(参考上面的过程),extent重新设置从最小size开始增长。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
452     // Start over from scratch with our extent sizing and growth
453     _details->setLastExtentSize(txn, 0);
454
455     // create a new extent so new records go there
456     increaseStorageSize(txn, _details->lastExtentSize(txn), true);
467     for (std::vector<DiskLoc>::iterator it = extents.begin(); it != extents.end(); it++) {
468         txn->checkForInterrupt();
469         invariant(_details->firstExtent(txn) == *it);
470         // empties and removes the first extent
471         _compactExtent(txn, *it, extentNumber++, adaptor, options, stats);
472         invariant(_details->firstExtent(txn) != *it);
473         pm.hit();
474     }
  1. 在_compactExtent的过程中,该extent的record逐渐被插入到新的extent里,空间逐步释放,当全部record都清理完后,该extent又变成崭新的,没有使用过的extent了。如下图
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
324         while (!nextSourceLoc.isNull()) {
325             txn->checkForInterrupt();
326
327             WriteUnitOfWork wunit(txn);
328             MmapV1RecordHeader* recOld = recordFor(nextSourceLoc);
329             RecordData oldData = recOld->toRecordData();
330             nextSourceLoc = getNextRecordInExtent(txn, nextSourceLoc);
371             CompactDocWriter writer(recOld, rawDataSize, allocationSize);
372             StatusWith<RecordId> status = insertRecordWithDocWriter(txn, &writer);
398             _details->incrementStats(txn, -(recOld->netLength()), -1);
              }

上述即是_compactExtent函数中遍历该extent的record,并插入到其他extent,并逐步释放空间的过程(398行)。

mmap数据回写

上面我们介绍.ns文件结构时谈到.ns文件是通过mmap 映射到内存中的一个hashtable上,这个映射过程是通过DurableMappedFile 实现的。我们看下该模块是如何做持久化的。

在mmap 引擎的 finishInit中

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
252 void MMAPV1Engine::finishInit() {

253     dataFileSync.go();

这里调用 DataFileSync类的定时任务,在backgroud线程中定期落盘

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 67     while (!inShutdown()) {
 69         if (storageGlobalParams.syncdelay == 0) {
 70             // in case at some point we add an option to change at runtime
 71             sleepsecs(5);
 72             continue;
 73         }
 74
 75         sleepmillis(
 76             (long long)std::max(0.0, (storageGlobalParams.syncdelay * 1000) - time_flushing));

 83         Date_t start = jsTime();
 84         StorageEngine* storageEngine = getGlobalServiceContext()->getGlobalStorageEngine();
 85
 86         dur::notifyPreDataFileFlush();
 87         int numFiles = storageEngine->flushAllFiles(true);
 88         dur::notifyPostDataFileFlush();
 97         }
 98     }

flushAllFiles最终会调用每个memory-map-file的flush方法

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
245 void MemoryMappedFile::flush(bool sync) {                                                         
246     if (views.empty() || fd == 0 || !sync)                                                       
247         return;                                                                                   
248                                                                                                   
249     bool useFsync = !ProcessInfo::preferMsyncOverFSync();                                         
250                                                                                                   
251     if (useFsync ? fsync(fd) != 0 : msync(viewForFlushing(), len, MS_SYNC) != 0) {               
252         // msync failed, this is very bad                                                         
253         log() << (useFsync ? "fsync failed: " : "msync failed: ") << errnoWithDescription()       
254               << " file: " << filename() << endl;                                                 
255         dataSyncFailedHandler();                                                                 
256     }                                                                                             
257 }

fsync vs msync

不管调用fsync 还是msync落盘,我们的预期都是内核会高效的查找出数据中的脏页执行写回,但是根据https://jira.mongodb.org/browse/SERVER-14129 以及下面的代码注释中。

在有些操作系统上(比如SmartOS与 Solaris的某些版本), msync并不能高效的寻找脏页,因此mmap引擎在这里对操作系统区别对待了。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
208         // On non-Solaris (ie, Linux, Darwin, *BSD) kernels, prefer msync.
209         // Illumos kernels do O(N) scans in memory of the page table during msync which
210         // causes high CPU, Oracle Solaris 11.2 and later modified ZFS to workaround mongodb
211         // Oracle Solaris Bug:                                                                   
212         //  18658199 Speed up msync() on ZFS by 90000x with this one weird trick
213         bool preferMsyncOverFSync;

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
2 条评论
热度
最新
mark
mark
回复回复点赞举报
感谢作者分享
感谢作者分享
回复回复点赞举报
推荐阅读
编辑精选文章
换一批
【赵渝强老师】MongoDB的MMAPv1存储引擎
在MongoDB 3.2版本以前,MongoDB使用MMAPv1作为默认的存储引擎。在MMAPv1的存储引擎中,包含以下的组成部分:
赵渝强老师
2024/08/31
1320
【赵渝强老师】MongoDB的MMAPv1存储引擎
一次Oracle性能诊断过程
问题分析:已经大概知道该项目在性能优化这方面基本毫无准备,之前提到的一些解决方案完全未得到落实,对于索引只停留在可以加速概念上,对于索引的类型、结构、数据分布情况没有任何概念。登到监控平台看了一下,虚拟机的CPU使用率20%左右,内存使用率也是20%左右,和DBA同事探讨了一下大叶内存,认为在虚拟机上操作,会有一定风险。
python与大数据分析
2022/03/11
3770
MongoDB数据存储-深入了解
最近运维中时常涉及到MongoDB的维护工作,今天详细梳理下MongoDB数据存储方面的内容。首先需要深入了解的一个概念:Memeory-Mapped Files 通过上图可以看出MongoDB数据库
洗尽了浮华
2018/01/23
5.5K0
MongoDB数据存储-深入了解
ptmalloc、tcmalloc与jemalloc对比分析
在开发微信看一看期间,为了进行耗时优化,基础库这层按照惯例使用tcmalloc替代glibc标配的ptmalloc做优化,CPU消耗和耗时确实有所降低。但在晚上高峰时期,在CPU刚刚超过50%之后却出现了指数上升,服务在几分钟之内不可用。最终定位到是tcmalloc在内存分配的时候使用自旋锁,在锁冲突严重的时候导致CPU飙升。为了弄清楚tcmalloc到底做了什么,仔细了解各种内存管理库迫在眉睫。
233333
2024/02/23
2.1K0
ptmalloc、tcmalloc与jemalloc对比分析
C++ ⾼性能内存池
当前项⽬是实现⼀个⾼并发的内存池,他的原型是google的⼀个开源项⽬tcmalloc,tcmalloc全称 Thread-Caching Malloc,即线程缓存的malloc,实现了⾼效的多线程内存管理,⽤于替代系统的内存分配相关的函数(malloc、free)。
ljw695
2025/02/27
790
C++ ⾼性能内存池
自底向上分析boltdb源码之精简版
boltdb是一个纯go编写的磁盘型kv数据库、支持事务,底层采用b+树来组织数据。目前主要的用途是做分布式组件的wal,或者单机磁盘型数据存储。对数据库感兴趣的小伙伴,非常值得一读boltdb的源码。代码量不大只有3k~4k,但功能很强大,从中可以学到不少知识。boltdb项目还是蛮出名的,现在由etcd团队在维护,etcd维护的组件叫bbolt,从boltdb fork而来,此外还有其他的一些知名的开源项目在生产环境使用boltdb。本文最初是本着好奇心和兴趣的驱使,最后通过一种自底向上的方式对boltdb内部实现一探究竟。
jaydenwen123
2021/07/19
9350
自底向上分析boltdb源码之精简版
mmap 分析
1、进程在用户空间调用库函数mmap,原型:void mmap(void addr, size_t len, int prot, int flags,
花落花相惜
2021/12/07
6120
Linux内存映射——mmap
所谓的内存映射就是把物理内存映射到进程的地址空间之内,这些应用程序就可以直接使用输入输出的地址空间,从而提高读写的效率。Linux提供了mmap()函数,用来映射物理内存。在驱动程序中,应用程序以设备文件为对象,调用mmap()函数,内核进行内存映射的准备工作,生成vm_area_struct结构体,然后调用设备驱动程序中定义的mmap函数。
全栈程序员站长
2022/07/02
6K0
Linux内存映射——mmap
【Binder 机制】Native 层 Binder 机制分析 ( 注册 Binder 服务 | svcmgr_handler | do_add_service | find_svc )
在上一篇博客 【Binder 机制】Native 层 Binder 机制分析 ( binder_loop | svcmgr_handler | binder.c | binder_parse ) 中 , 简单介绍了 在 service_manager.c 中的 main 函数中调用了 binder_loop 方法 , 在 binder_loop 方法中 , 传入了 svcmgr_handler 方法作为回调函数 , svcmgr_handler 中可以接收不同的消息 , 处理不同的业务 ;
韩曙亮
2023/03/29
4150
MongoDB性能分析之WiredTiger引擎
MongoDB的WiredTiger引擎可以设置其可使用的最大内存,由cacheSizeGB参数控制,单位为G,表示WiredTiger引擎能够缓存的最大热数据量。每一个连接数都会消耗内存使用,可通过命令行db.serverStatus().connections查看当前连接数,如果连接数较大可通过连接池的方式控制mongodb的连接数。
Power
2025/03/02
1320
Binder: ServiceManager的创建
承接Binder: addService初探这篇文章,我们已经知道Client端通过BpBinder的transact方法与service端进行通信,在BpBinder的transact方法中又通过IPCThreadState的transact方法将数据传递到service端。
Rouse
2021/02/23
3700
【腾讯云 MongoDB】 基于snapshot的从库读优化
导语 我们发现腾讯云上一些腾讯云MongoDB实例在主库写压力比较大的情况下,这时从库上会出现很多慢查询,经过调查发现,从库在回放oplog的时候加了全局锁,阻塞了所有的读直到回放结束。经过我们的优化
腾讯云数据库 TencentDB
2017/11/24
2.9K0
【腾讯云 MongoDB】 基于snapshot的从库读优化
一份DBA试题
注:以下题目,可根据自己情况挑选题目作答,不必全部作答您也可以就相关问题直接找负责面试人员面述而不笔答。 一:SQL tuning 类 1:列举几种表连接方式 hash join/merge join/nest loop(cluster join)/index join 2:不借助第三方工具,怎样查看sql的执行计划 set autot on explain plan set statement_id = &item_id for &sql; select * from table(dbms_xplan
赵腰静
2018/03/09
1K0
Oracle案例:SMON回滚异常导致实例Crash
当SMON重新发起并行回滚时,实例被PMON终止,这里有一个隐藏错误,常常被忽视,PMON (ospid: 100111): terminating the instance due to error 474
数据和云
2022/02/25
1.9K0
一文读懂 Linux mmap 内存映射
mmap(memory map)即内存映射,用于将一个文件或设备映射到进程的地址空间,或者创建匿名的内存映射。
恋喵大鲤鱼
2024/05/24
6.8K0
一文读懂 Linux mmap 内存映射
Databus Relays
抓取Oracle数据是通过给源表添加一个触发器,在新增和修改的时候记录SCN号作为查询的依据,通过relay定期的查询获取变化的数据。删除和查询不受影响。
全栈程序员站长
2022/11/03
4970
文件操作之 FileChannel 与 mmap
Java 中原生读写方式大概可以被分为三种:普通 IO,FileChannel(文件通道),mmap(内存映射)。
leobhao
2023/03/11
1.5K0
文件操作之 FileChannel 与 mmap
CVE-2018-18955:较新Linux内核的提权神洞分析
鉴于目前还没有针对这个漏洞的详细分析,原作者的advisory对新手来说也很不友好,我就写了这篇文章。
FB客服
2019/05/09
1.5K0
CVE-2018-18955:较新Linux内核的提权神洞分析
【Linux 内核 内存管理】物理分配页 ⑦ ( __alloc_pages_slowpath 慢速路径调用函数源码分析 | 判断页阶数 | 读取 mems_allowed | 分配标志位转换 )
在 【Linux 内核 内存管理】物理分配页 ② ( __alloc_pages_nodemask 函数参数分析 | __alloc_pages_nodemask 函数分配物理页流程 ) 博客中 , 分析了 __alloc_pages_nodemask 函数分配物理页流程如下 :
韩曙亮
2023/03/30
1.4K0
【Linux 内核 内存管理】物理分配页 ⑦ ( __alloc_pages_slowpath 慢速路径调用函数源码分析 | 判断页阶数 | 读取 mems_allowed | 分配标志位转换 )
MongoDB 复制集原理
孔德雨
2016/10/11
10.8K2
MongoDB 复制集原理
相关推荐
【赵渝强老师】MongoDB的MMAPv1存储引擎
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验