背景

启动数据加载时间对于很多数据库来说是一个不容忽视的因素,启动加载慢直接导致数据库恢复正常服务的RTO时间变长,影响服务可用性。比如Redis,启动时要加载RDB和AOF文件,把所有数据加载到内存中,根据节点内存数据量的不同,加载时间可能达到几十分钟甚至更长。

MongoDB在启动时同样需要加载一些元数据,结合阿里云MongoDB云上运维的经验,在集合数量不多时,这个加载时间不会很长,但是对于大量集合场景、特别是MongoDB进程资源受限的情况下(比如虚机、容器、cgroup隔离场景),这个加载时间就变得无法预测,有可能会遇到节点本身内存小无法完成加载或者进程OOM的情况。经测试,在MongoDB 4.2.0之前(包括)的版本,加载10W集合耗时达到10分钟以上。

MongoDB 在最新开发版本里针对这个问题进行了优化,尤其是对于大量集合场景,效果非常明显。在完全相同的测试条件下,该优化使得启动加载10W集合的时间由10分钟降低到2分钟,并且启动后初始内存占用降低为之前的四分之一。这个优化目前已经backport到4.2和4.0最新版本,阿里云MongoDB 4.2也已支持。

鉴于该优化带来的效果和好处明显,有必要对其背后的技术原理和细节进行深入的探究和学习,本文主要基于MongoDB 4.2社区版优化前后的版本进行对比分析,对MongoDB的启动加载过程、具体优化点、优化原理进行阐述,希望和对MongoDB内部实现有兴趣的同学一起探讨和学习。

MongoDB启动加载过程

MongoDB在启动时,WiredTiger引擎层需要将所有集合/索引的元数据加载到内存中,而MongoDB的集合/索引实际上就是对应WiredTiger中的表,加载集合/索引就需要打开WiredTiger对应表的cursor。

WiredTiger Cursor介绍

WiredTiger是MongoDB的默认存储引擎,负责管理和存储MongoDB的各种数据,WiredTiger支持多种数据源(data sources),包括表、索引、列组(column groups)、LSM Tree、状态统计等,此外,还支持用户通过实现WiredTiger定义好的接口来扩展自定义的数据源。

WiredTiger对各个数据源中数据的访问和管理是由对应的cursor来提供的,WiredTiger内部提供了用于数据访问和管理的基本cursor类型(包括table cursor、column group cursor、index cursor、join cursor)、以及一些专用cursor(包括metadata cursor、backup cursor、事务日志cursor、以及用于状态统计的cursor),专用cursor可以访问由WiredTiger管理的数据,用于完成某些管理类任务。另外,WiredTiger还提供了两种底层cursor类型:file cursor和LSM cursor。

WiredTiger cursor的通用实现通常会包含:暂存数据源中数据存储位置的变量,对数据进行迭代遍历或查找的方法,对key、value各字段进行设置的getter/setters,对字段进行编码的方法(便于存储到对应数据源)。

MongoDB和WiredTiger数据组织方式介绍

为了能够管理所有的集合、索引,MongoDB将集合的Catalog信息(包括对应到WiredTiger中的表名、集合创建选项、集合索引信息等)组织存放在一个_mdb_catalog的WiredTiger表中(对应到一个_mdb_catalog.wt的物理文件)。因此,这个_mdb_catalog表可以认为是MongoDB的『元数据表』,普通的集合都是『数据表』。MongoDB在启动时需要先从WiredTiger中加载这个元数据表的信息,然后才能加载出其他的数据表的信息。

同样,在WiredTiger层,每张表也有一些元数据需要维护,这包括表创建时的相关配置,checkpoint信息等。这也是使用『元数据表』和『数据表』的管理组织方式。在WiredTiger中,所有『数据表』的元数据都会存放在一个WiredTiger.wt的表中,这个表可以认为是WiredTiger的『元数据表』。而这个WiredTiger.wt表本身的元数据,则是存放在一个WiredTiger.turtle的文本文件中。在WiredTiger启动时,会先从WiredTiger.turtle文件中加载出WiredTiger.wt表的数据,然后就能加载出其他的数据表了。

启动过程分析

再回到_mdb_catalog表,虽然对MongoDB来说,它是一张『元数据表』,但是在WiredTiger看来,它只是一张普通的数据表,因此启动时候,需要先等WiredTiger加载完WiredTiger.wt表后,从这个表中找到它的元数据。根据_mdb_catalog表的元数据可以对这个表做对应的初始化,并遍历出MongodB的所有数据表(集合)的Catalog信息元数据,对它们做进一步的初始化。

在上述这个过程中,对WiredTiger中的表做初始化,涉及到几个步骤,包括:

1)检查表的存储格式版本是否和当前数据库版本兼容;

2)确定该表是否需要开启journal,这是在该表创建时的配置中指定的。

这两个步骤都需要从WiredTiger.wt表中读取该表的元数据进行判断。

此外,结合目前的已知信息,我们可以看到,对MongoDB层可见的所有数据表,在_mdb_catalog表中维护了MongoDB需要的元数据,同样在WiredTiger层中,会有一份对应的WiredTiger需要的元数据维护在WiredTiger.wt表中。因此,事实上这里有两份数据表的列表,并且在某些情况下可能会存在不一致,比如,异常宕机的场景。因此MongoDB在启动过程中,会对这两份数据进行一致性检查,如果是异常宕机启动过程,会以WiredTiger.wt表中的数据为准,对_mdb_catalog表中的记录进行修正。这个过程会需要遍历WiredTiger.wt表得到所有数据表的列表。

综上,可以看到,在MongoDB启动过程中,有多处涉及到需要从WiredTiger.wt表中读取数据表的元数据。对这种需求,WiredTiger专门提供了一类特殊的『metadata』类型的cursor。

metadata cursor使用优化原理

metadata cursor简介

WiredTiger的metadata cursor是WiredTiger用于读取WiredTiger.wt表(元数据表)的cursor,它底层封装了用于查找WiredTiger.wt对应的内存btree结构的file cursor。File cursor实际上就是用于查找数据文件对应的btree结构的一种cursor,读取索引和集合数据文件也都是通过file cursor。

WiredTiger通过cursor的URI前缀来识别cursor的类型,对于metadata cursor类型,它的前缀是『metadata:』。根据cursor打开方式的不同,metadata cursor又可以分为metadata: cursor和metadata:create cursor两种。看下这两种cursor打开方式的区别:

  • 打开metadata:create cursor

WT_CURSOR* c = NULL;
int ret = session->open_cursor(session, "metadata:create", NULL, NULL, &c);
  • 打开metadata: cursor

WT_CURSOR* c = nullptr;
int ret = session->open_cursor(session, "metadata:", nullptr, nullptr, &c);

实际上,WiredTiger在打开metadata: cursor时,默认只需要打开一个读取WiredTiger.wt表的file cursor(源码里命名是file_cursor),对于metadata:create cursor,还需要再打开另一个读取WiredTiger.wt表的file cursor(源码里命名是create_cursor),虽然只多了一个cursor,但是metadata:create cursor的使用代价却比metadata: cursor高得多。从启动加载过程可以看到,主要有三处使用metadata cursor的地方,而MongoDB启动加载优化中一个主要的优化点,就是把前面两处使用『metadata:create』 cursor的地方改成了『metadata:』 cursor。接下来我们分析这背后的原因。

metadata cursor工作原理

metadata cursor的工作流程

以namesapce名称为db2.col1的集合为例,它在WiredTiger中的表名是db2/collection-11–4499452254973778892,来看看对于这个集合,是如何通过metadata cursor获取到实际的journal配置的,通过这个过程来说明metadata cursor的工作流程。下面具体来看:

  • 使用file_cursor查找WiredTiger.wt表的btree结构,查找的cursor key是:

table:db2/collection-11–4499452254973778892

获取到的元信息value:

app_metadata=(formatVersion=1),colgroups=,collator=,columns=,key_format=q,value_format=u

  • 其实到这里,metadata:create cursor和metadata: cursor做的事情是一样的,只不过对于metadata: cursor,则到这就结束了。如果是metadata:create cursor,接下来还需要通过create_cursor获取集合的creationString;因为这里要获取creationString中的journal配置,所以必须用metadata:create cursor。

  • 对于metadata:create cursor,使用create_cursor继续查找creationString配置。获取到creationString后,就可以从中拿到实际的journal配置了。create_cursor实际上有两次对WiredTiger.wt表的btree结构进行search的过程:

1)第一次search是为了从WiredTiger.wt表中拿到集合对应的source文件名,查找的cursor key是:

colgroup:db2/collection-11–4499452254973778892

获取到的元信息value:

app_metadata=(formatVersion=1),collator=,columns=,source=“file:db2/collection-11–4499452254973778892.wt”,type=file

2)第二次search是通过上一步获取到的表元信息中的数据文件名称,继续从WiredTiger.wt表中拿到该表的creationString信息。

cursor key:
file:db2/collection-11–4499452254973778892.wt

获取到的元信息value:

access_pattern_hint=none,allocation_size=4KB,app_metadata=(formatVersion=1),assert=(commit_timestamp=none,durable_timestamp=none,read_timestamp=none),block_allocation=best,block_compressor=snappy,cache_resident=false,checksum=on,colgroups=,collator=,columns=,dictionary=0,encryption=(keyid=,name=),exclusive=false,extractor=,format=btree,huffman_key=,huffman_value=,ignore_in_memory_cache_size=false,immutable=false,internal_item_max=0,internal_key_max=0,internal_key_truncate=true,internal_page_max=4KB,key_format=q,key_gap=10,leaf_item_max=0,leaf_key_max=0,leaf_page_max=32KB,leaf_value_max=64MB,log=(enabled=true),lsm=(auto_throttle=true,bloom=true,bloom_bit_count=16,bloom_config=,bloom_hash_count=8,bloom_oldest=false,chunk_count_limit=0,chunk_max=5GB,chunk_size=10MB,merge_custom=(prefix=,start_generation=0,suffix=),merge_max=15,merge_min=0),memory_page_image_max=0,memory_page_max=10m,os_cache_dirty_max=0,os_cache_max=0,prefix_compression=false,prefix_compression_min=4,source=“file:db2/collection-11–4499452254973778892.wt”

可以看到这实际上就是wiredTiger在创建表时的schema元信息,可以通过db.collection.stats()命令输出的wiredTiger.creationString字段来查看。获取到creationString信息,就可以从中解析出log=(enabled=true)这个配置了。

metadata:create cursor代价为什么高

从上面的分析可以看出,对于metadata: cursor,只有一次对WiredTiger.wt表的btree search过程。而对于metadata:create cursor,一次元数据配置查找其实会有三次对WiredTiger.wt表的btree进行search的过程,并且每次都是从btree的root节点去查找(因为每次要查找的元数据在btree结构中的存储位置上互相是没有关联的),开销较大。

启动加载优化细节

优化1:获取集合的存储格式版本号

这里最终目的就是要获取集合元数据中"app_metadata=(formatVersion=1)"里的formatVersion的版本号,从metadata cursor的工作流程可以看到,file_cursor第一次查找的卖二手QQ平台结果里已经包含了这个信息。在优化前,这里用的是metadata:create cursor,是不必要的,所以这里改用一个metadata: cursor就可以了,每个集合的初始化就少了两次『从WiredTiger.wt表对应的btree的root节点开始search』的过程。

优化2:获取所有集合的数据文件名称

以db2.col1集合为例,查找的cursor key是:
colgroup:db2/collection-11–4499452254973778892

获取到的元信息value:
app_metadata=(formatVersion=1),collator=,columns=,source=“file:db2/collection-11–4499452254973778892.wt”,type=file

获取集合的数据文件名称,实际上就是要获取元信息里的source="file:db2/collection-11–4499452254973778892.wt"这个配置。优化后,这里改成了metadata: cursor,只要一次file cursor的next调用就好,并且下个集合在获取数据文件名时cursor已经是就位(positioned)的。在优化前,这里用的是metadata:create cursor,多了两次file cursor的search调用过程,并且每次都是从WiredTiger.wt表对应的btree的root节点开始search,开销大得多。

延迟打开cursor优化

MongoDB最新版本中,还有一个针对大量集合/索引场景的特定优化,那就是『延迟打开Cursor』。在优化前,MongoDB在启动时,需要为每个集合都打开对应的WiredTiger表的cursor,这是为了获取NextRecordId。这是干什么的呢?先要说一下RecordId。

我们知道,MongoDB用的是WiredTiger的key-value行存储模式,一个MongoDB中的文档会对应到WiredTiger中的一条KV记录,记录的key被称为RecordId,记录的value就是文档内容。WiredTiger在查找、更新、删除MongoDB文档时都是通过这个RecordId去找到对应文档的。

对于普通数据集合,RecordId就是一个64位自增数字。而对于oplog集合,MongoDB按照时间戳+自增数字生成一个64位的RecordId,高32位代表时间戳,低32位是一个连续增加的数字(时间戳相同情况下)。

比如,下面是针对普通数据集合和oplog集合插入一条数据的记录内容:

  • 普通数据集合中连续插入一条{a:1}和{b:1}的文档

record id:1, record value:{ _id: ObjectId('5e93f4f6c8165093164a940f'), a: 1.0 }
record id:2, record value:{ _id: ObjectId('5e93f78f015050efdb4107b4'), b: 1.0 }
  • oplog中插入一条的记录(向db1.col1这个集合插入一个{c:1}的新文档触发)

record id:6815068270647836673,
record value:{ ts: Timestamp(1586756732, 1), t: 24, h: ,
v: 2, op: "i", ns: "db1.col1", ui: UUID("ae7cfb6f-8072-4475-b33a-78b88ab72c6c"), wall: new Date(1586756748564), o: { _id: ObjectId('5e93fc7c7dc2edf0b11837ad')
, c: 1.0 } }
注:6815068270647836673实际上就是1586756732 << 32 + 1

优化细节

MongoDB在内存中为每个集合都维护了一个NextRecordId变量,用来在下次插入新的文档时分配RecordId。因此这里在启动时为每个集合都都打开对应的WiredTiger表的cursor,并通过反向遍历到第一个key(也就是最大的一个key),并对其值加一,来得到这个NextRecordId。

而在MongoDB最新版本中,MongoDB把启动时为每个集合获取NextRecordId这个动作给推迟到了该集合第一次插入新文档时才进行,这在集合数量很多的时候就减少了许多开销,不光能提升启动速度,还能减少内存占用。

优化效果

下面我们通过测试来看下实际优化效果如何。

测试条件

事先准备好测试数据,写入10W集合,每个集合包含一个{“a”:“b”}的文档。
然后分别以优化前后的版本(完全相同的配置下)来启动加载准备好的数据,对比启动加载时间和初始内存占用情况。

优化前

启动日志:

加载完的日志:

启动后初始内存占用:

db.serverStatus().mem
{ “bits” : 64, “resident” : 4863, “virtual” : 6298, “supported” : true }

可以看到优化前版本启动加载10W集合的时间约为 10分钟 左右,启动后初始内存(常驻)占用为4863M。

优化后

启动日志:

加载完的日志:

启动后初始内存占用:

db.serverStatus().mem
{ “bits” : 64, “resident” : 1181, “virtual” : 2648, “supported” : true }

可以看到优化后版本启动加载10W集合的时间约为 2分钟 左右。启动后初始内存(常驻)占用为1181M。

结论

在同样的测试条件下,优化后版本启动加载时间约为优化前的1/5,优化后版本启动后初始内存占用约为优化前的1/4。

总结

最后,我们来简要总结下MongoDB最新版本对启动加载的优化内容:

1)优化启动时集合加载打开cursor的次数,用metadata:类型cursor替代不必要的metadata:create cursor(代价比较高),将metadata:create cursor的调用次数由每个表3次降到1次。

2)采用『延迟打开cursor』机制,启动时不再为所有集合都打开cursor,将打开cursor的动作延后进行。

可以看到,这个优化本身并没有对底层WiredTiger引擎实现有任何改动,对于上层MongoDB的改动也不大,而是通过深挖底层存储引擎WiredTiger cursor使用上的细节,找到了关键因素,最终取得了非常显著的效果,充分证明了“细节决定成败”这个真理,很值得学习。

尽管已经取得了如此大的优化效果,事实上MongoDB启动加载还有进一步的优化空间,由于启动数据加载目前还是单线程,瓶颈主要在CPU,官方已经有计划将启动数据加载流程并行化,进一步优化启动时间,我们后续也会持续关注。

作者简介

李竟成(花名腾峰),阿里云高级技术专家,目前主要负责阿里云MongoDB的内核开发工作,曾参与阿里云Redis数据库的开发,专注于数据库和存储方向,致力于做最好的云数据库产品。

MongoDB大量集合启动加载优化原理相关推荐

  1. webpack实现懒加载原理以及如何使用懒加载优化项目

    参考: 清晰易懂,可以先看这个:webpack的异步加载原理及分包策略 webpack中的懒加载代码原理解析:https://segmentfault.com/a/1190000020233387 自 ...

  2. 网页性能优化02-懒加载工作原理

    懒加载工作原理 1.1-懒加载介绍(以图片懒加载为例) 1.为什么要有懒加载技术 (1)img标签特点:不管图片隐藏还是显示 有src属性都会去加载 例如电商类网站,一个页面有几百张图片.有时候假设用 ...

  3. spring 启动加载数据_12个很棒的Spring数据教程来启动您的数据项目

    spring 启动加载数据 Spring Data的任务是为数据访问提供一个熟悉且一致的,基于Spring的编程模型,同时仍保留基础数据存储的特​​殊特征. 它使使用数据访问技术,关系和非关系数据库, ...

  4. 前端性能优化学习 08 资源加载优化

    图片延迟加载 什么是延迟加载 首先来想象一个场景,当浏览一个内容丰富的网站时,比如电商的商品列表页.主流视频网站的节目列表等,由于屏幕尺寸的限制,每次只能查看到视窗中的那部分内容,而要浏览完页面所包含 ...

  5. RocketMQ源码(4)—Broker启动加载消息文件以及恢复数据源码【一万字】

    详细介绍了Broker启动加载消息文件以及恢复数据源码. 此前我们学习了Broker的启动源码:RocketMQ源码(3)-Broker启动流程源码解析[一万字],Broker的启动过程中,在Defa ...

  6. 记前端项目首屏加载优化(网络篇)

    继之前的一篇<记前端项目首屏加载优化(打包篇)>之后,这次来讲一讲我的首屏加载在网络方面的优化?. 写在前面 资源加载是一个网站的展示在用户浏览器的必经之路,资源的请求次数和响应时间决定了 ...

  7. springboot集成mybatis源码分析-启动加载mybatis过程(二)

    springboot集成mybatis源码分析-启动加载mybatis过程(二) 1.springboot项目最核心的就是自动加载配置,该功能则依赖的是一个注解@SpringBootApplicati ...

  8. jvm加载class原理

    转载地址 : http://hxraid.iteye.com/blog/747625 当Java编译器编译好.class文件之后,我们需要使用JVM来运行这个class文件.那么最开始的工作就是要把字 ...

  9. (七)传输加载优化(前沿技术解决高访问量网站性能优化问题)

    传输加载优化 启用压缩Gzip[必会的传输压缩方案] Gzip 安装ngnix 启用Keep Alive[通过一个参数提速连接] Keep Alive HTTP资源缓存[必会的HTTP缓存方法] 看看 ...

最新文章

  1. 如何快速融入一个团队?
  2. 卷积神经网络模型如何辨识裸体图片
  3. 简书 java jvm_详细介绍Java虚拟机(JVM)
  4. matlab 控制实验指导,智能控制系统-实验指导书-实验一-BP算法的MATLAB实现
  5. UESTC 250 windy数 数位dp
  6. 价值6000的信息分类系统源码
  7. 【消息队列之rabbitmq】Rabbitmq之消息可靠性投递和ACK机制实战
  8. bat中的“多线程”处理代码
  9. javascript arraybuffer
  10. Python实用技法第26篇:定义实现最短匹配的正则表达式
  11. file处理的一些内容
  12. 转载--numpy中的ravel()、flatten()、squeeze()的用法与区别
  13. Ubuntu18.04操作系统sudo apt-get update报错
  14. FFmpeg下载无损截取HLS视频流
  15. Unity——退出程序代码
  16. 线程Signaled nonsignaled状态的意思
  17. Armstrong 一个n位数等于各个位数n次方之和
  18. 差分隐私若干基本知识点介绍(一)
  19. C#中ref和out关键字的应用以及区别。
  20. 招沿实业学生怎样才能做好投资理财工作

热门文章

  1. 苹果WWDC 2021新产品预测
  2. android之在线音乐播放器
  3. 100天学会VR游戏开发
  4. java计算机毕业设计南京传媒学院门户网源码+系统+mysql数据库+lw文档
  5. hive:常见日期函数
  6. 教你手写DMA传输数据(看完这篇你就会手动写啦,保姆级讲解)---- 2020.3.31
  7. strsep函数用法
  8. javassm奥运会志愿者管理系统
  9. 瑞星FOR VISTA 仍然不能升级!!!报错“抱歉,此安装包不能在当前版本的Windows上运行”...
  10. 计算机网络技术 校园网规划,大学校园网的IP地址规划