MongoDB大量集合启动加载优化原理

本文涉及的产品
云数据库 MongoDB,独享型 2核8GB
推荐场景:
构建全方位客户视图
日志服务 SLS,月写入数据量 50GB 1个月
简介: ## 背景 启动数据加载时间对于很多数据库来说是一个不容忽视的因素,启动加载慢直接导致数据库恢复正常服务的RTO时间变长,影响服务可用性。比如Redis,启动时要加载RDB和AOF文件,把所有数据加载到内存中,根据节点内存数据量的不同,加载时间可能达到几十分钟甚至更长。MongoDB在启动时同样需要加载一些元数据,结合阿里云MongoDB云上运维的经验,在集合数量不多时,这个加载时间不会很长,但

背景

启动数据加载时间对于很多数据库来说是一个不容忽视的因素,启动加载慢直接导致数据库恢复正常服务的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第一次查找的结果里已经包含了这个信息。在优化前,这里用的是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: 0,
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"}的文档。
然后分别以优化前后的版本(完全相同的配置下)来启动加载准备好的数据,对比启动加载时间和初始内存占用情况。

优化前

启动日志:
1.png

加载完的日志:
2.png

启动后初始内存占用:

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

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

优化后

启动日志:
3.png

加载完的日志:
4.png

**
启动后初始内存占用:

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数据库入门
MongoDB数据库入门实验。
快速掌握 MongoDB 数据库
本课程主要讲解MongoDB数据库的基本知识,包括MongoDB数据库的安装、配置、服务的启动、数据的CRUD操作函数使用、MongoDB索引的使用(唯一索引、地理索引、过期索引、全文索引等)、MapReduce操作实现、用户管理、Java对MongoDB的操作支持(基于2.x驱动与3.x驱动的完全讲解)。 通过学习此课程,读者将具备MongoDB数据库的开发能力,并且能够使用MongoDB进行项目开发。 &nbsp; 相关的阿里云产品:云数据库 MongoDB版 云数据库MongoDB版支持ReplicaSet和Sharding两种部署架构,具备安全审计,时间点备份等多项企业能力。在互联网、物联网、游戏、金融等领域被广泛采用。 云数据库MongoDB版(ApsaraDB for MongoDB)完全兼容MongoDB协议,基于飞天分布式系统和高可靠存储引擎,提供多节点高可用架构、弹性扩容、容灾、备份回滚、性能优化等解决方案。 产品详情: https://www.aliyun.com/product/mongodb
相关文章
|
2月前
|
存储 监控 NoSQL
MongoDB优化的几点原则
这篇文章讨论了MongoDB优化的一些原则,包括查询优化、热数据大小、文件系统选择、硬盘选择、查询方式优化、sharding key设计和性能监控。
81 1
|
1月前
|
NoSQL MongoDB 数据库
MongoDB 删除集合
10月更文挑战第14天
27 1
|
1月前
|
存储 NoSQL MongoDB
掌握MongoDB索引优化策略:提升查询效率的关键
在数据库性能调优中,索引是提升查询效率的利器。本文将带你深入了解MongoDB索引的内部工作原理,探讨索引对查询性能的影响,并通过实际案例指导如何针对不同的查询模式建立有效的索引。不仅将涵盖单一字段索引,还会探讨复合索引的使用,以及如何通过分析查询模式和执行计划来优化索引,最终实现查询性能的最大化。
|
2月前
|
存储 缓存 NoSQL
MongoDB内部的存储原理
这篇文章详细介绍了MongoDB的内部存储原理,包括存储引擎WiredTiger的架构、btree与b+tree的比较、cache机制、page结构、写操作流程、checkpoint和WAL日志,以及分布式存储的架构。
74 1
MongoDB内部的存储原理
|
18天前
|
缓存 NoSQL MongoDB
|
1月前
|
存储 NoSQL MongoDB
MongoDB 创建集合
10月更文挑战第13天
30 1
|
1月前
|
存储 监控 NoSQL
TDengine 3.3.3.0 版本上线:优化监控、增强 MongoDB 支持
今天我们非常高兴地宣布,TDengine 3.3.3.0 版本正式发布。本次更新引入了多项重要功能和性能优化,旨在为用户提供更高效、更灵活的数据解决方案。
45 0
|
3月前
|
JSON NoSQL MongoDB
MongoDB Schema设计实战指南:优化数据结构,提升查询性能与数据一致性
【8月更文挑战第24天】MongoDB是一款领先的NoSQL数据库,其灵活的文档模型突破了传统关系型数据库的限制。它允许自定义数据结构,适应多样化的数据需求。设计MongoDB的Schema时需考虑数据访问模式、一致性需求及性能因素。设计原则强调简洁性、查询优化与合理使用索引。例如,在构建博客系统时,可以通过精心设计文章和用户的集合结构来提高查询效率并确保数据一致性。正确设计能够充分发挥MongoDB的优势,实现高效的数据管理。
64 3
|
3月前
|
安全 C# 数据安全/隐私保护
WPF安全加固全攻略:从数据绑定到网络通信,多维度防范让你的应用固若金汤,抵御各类攻击
【8月更文挑战第31天】安全性是WPF应用程序开发中不可或缺的一部分。本文从技术角度探讨了WPF应用面临的多种安全威胁及防护措施。通过严格验证绑定数据、限制资源加载来源、实施基于角色的权限管理和使用加密技术保障网络通信安全,可有效提升应用安全性,增强用户信任。例如,使用HTML编码防止XSS攻击、检查资源签名确保其可信度、定义安全策略限制文件访问权限,以及采用HTTPS和加密算法保护数据传输。这些措施有助于全面保障WPF应用的安全性。
52 0
|
4月前
|
存储 NoSQL MongoDB
MongoDB 索引原理与索引优化
MongoDB 索引原理与索引优化
101 1