MongoDB 初体验:存储引擎 MMAPv1 与高内存消耗及升级迁移

本文涉及的产品
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
云数据库 MongoDB,通用型 2核4GB
简介:

想不到我和MongoDB的第一次亲密接触竟然是这样开始的。

2dd14fe074fde4e4abef4a73def2cb0b73e03992

当我对公司的一个内部系统性能无可忍受时,意外发现在这个内存仅为 32G 的服务器上,运行着一个 MongoDB 数据库,其主进程 mongod 占用了 30.705 G的虚拟内存空间。这立刻引起了我的兴趣,必须要研究一下其工作原理。

1888fbffc55a9100fc702b927037647a63193730

这个数据库的版本是 3.0 :

[root@enmotech bin]# ./mongod --version

db version v3.0.12

那么,为什么 MongoDB 会消耗这么多内存呢?

通过数据库的状态查询,可以看到同样内存分配情况,Resident的固有内存分配了254M,Virtual的虚拟内存分配了 31,441M:

> db.serverStatus().mem;

{

"bits" : 64,

"resident" : 254,

"virtual" : 31441,

"supported" : true,

"mapped" : 15498,

"mappedWithJournal" : 30996

}

再看一下存储引擎,当前数据库使用的存储引擎是 mmapv1 :

> db.serverStatus().storageEngine;

{ "name" : "mmapv1" }

MMAPv1 实际上就是 MongoDB 在 3.0 以前原有的存储引擎,在 3.0 版本它也继续作为 MongoDB 的默认存储引擎,而在 MongoDB 3.2 版本默认存储引擎已经改为 WiredTiger。

0ef894afe4115f3ede47a43638f389ceb5c883b0

这个变化,就类似MySQL的存储引擎从 MyISAM 变化到 InnoDB 一样,性能是关键。在 3.0 版本之前,MMAPv1 对锁请求的做法是,以 Database 为单位加锁, 3.0 版本,MMAPv1 则开始使用以 Collection 为单位的加锁(collection-wise locking)。但是这仍然不够,WiredTiger 使用的实际为 Document 级的乐观锁机制,最终替代了 MMAPv1 .

这其中的主要原因是 2014 年 12 月,MongoDB 收购了 WiredTiger 公司,WiredTiger 为 MongoDB 3.0 开发了一个专用版本的存储引擎,我们不得不钦佩 MongoDB 的英明之处,想一想 MySQL 的历程,当 MySQL 的最佳存储引擎 InnoDB 被 Oracle 釜底抽薪收购(2006年)之后,MySQL 最后被 SUN 收购(2008年),辗转落入 Oracle 之手(2009年),当然自2009年 MySQL 5.5 开始 InnoDB 就成为了 MySQL 默认存储引擎。

通过下图和关系型数据库的对比,我们更好理解MongoDB的概念,虽然从 Database 级别锁到 Collection 级别,但是仍然不够:

bddd28dd3b13706a1c7815bfa7b824d20f38a4f0

回过头来,MMAPv1之所以叫 MMAP,是因为这个存储引擎会把数据直接映射到虚拟内存上,即 “memory mapping”。由于MMAPv1使用mmap来将数据库文件映射到内存中,MongoDB总是尽可能的多吃内存,以映射更多的数据文件,并且页面的换入换出基本交给OS控制。

根据官方文档描述:

With MMAPv1, MongoDB automatically uses all free memory on the machine as its cache. System resource monitors show that MongoDB uses a lot of memory, but its usage is dynamic. If another process suddenly needs half the server’s RAM, MongoDB will yield cached memory to the other process.

使用MMAPv1,MongoDB会自动将机器上的所有可用内存用作缓存。

Technically, the operating system’s virtual memory subsystem manages MongoDB’s memory. This means that MongoDB will use as much free memory as it can, swapping to disk as needed. Deployments with enough memory to fit the application’s working data set in RAM will achieve the best performance.

从技术上讲,操作系统的虚拟内存子系统管理MongoDB的内存。 这意味着MongoDB将使用尽可能多的空闲内存,并根据需要交换到磁盘。 具有足够内存的部署可适应应用程序在RAM中的工作数据集,从而实现最佳性能。

MongoDB 采用mmap将数据文件映射到内存,同时带来的好处是,当MongoDB重启时,这些映射的内存并不会清除,相对于其它自己维护Cache的数据库,MongoDB在重启后并不需要进行缓存重建与预热。

[root@enmotech mongodb]# bin/mongod -f mongod.conf

about to fork child process, waiting until server is ready for connections.

forked process: 31489

child process started successfully, parent exiting

[root@enmotech mongodb]# top

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND

31489 root 20 0 30.610g 163112 132612 S 0.7 0.5 0:01.65 mongod

那么在 MMAPv1 存储引擎下,数据库是怎么组织的呢?到底这个数据库占用了多少空间呢?

每个 Database 由一个 .ns文件 及若干个数据文件组成,以下这个示范数据库,只有一个数据文件,大小是 64M:

[root@enmotech data]# ls -l en*

-rw------- 1 root root 67108864 May 31 09:36 enmotech.0

-rw------- 1 root root 16777216 May 31 09:36 enmotech.ns

如果空间继续扩展,MongoDB MMAPv1 引擎的扩展规则是,每次存储文件容量翻倍,直至增加到 2G 大小,下图是我们一个较大的数据库,其数据文件的扩展情况,可以清楚的看到从 64M、128M、256M、512M、1204M、2048M的扩展历程:

0bbaa78816b02fc2f448e8f89e6c9e7e4a26ffca

每个数据库中包含一系列的 collection ,.ns 文件事实上就用于记录不同 collection 的位置,相当于是元数据。那么如何实现快速的HASH查找呢?

在源码中可以看到非常详细的设计,『我们在这里用全零填充剩余空间,因为它们是确定性的对于给定的操作序列,它们具有的字节数。 这使得测试和调数据文件更容易。如果分析表明这种方法是一个重大瓶颈,我们可以有一个版本我们用于不填零的读取,并保持调零行为写入。

f776fbcf7bc3ad2f0304ec983cfc63994b76d45d

不论如何,MMAPv1 存储引擎已经渐渐属于过去时,而在网上同样发现 Wiredtiger 存储引擎,缺省的同样会尽量使用更多的内存来缓存数据,很多朋友同样遇到内存耗尽的问题。

在MongoDB源码中(wiredtiger_init.cpp),有这样一段,说明了其使用Cache的方式。注释说明,当用户不未提供cache Size时,我们选择为系统和二进制文件保留 1GB 内存。在程序算法中,使用 memSizeMB - 1G 之后的 60% 作为缓存。

4dc6a4a3482008d55610bc65161cf4fb46a73069

在 Wiredtiger 存储引擎下,合理的规划MongoDB的内存使用,可以通过参数设置 wiredTigerCacheSizeGB 来限制其使用的Cache大小。

以下是一个设置示范:

 
mongod
--storageEngine wiredTiger
--dbpath <path>
--journal --wiredTigerCacheSizeGB <value>
--wiredTigerJournalCompressor <compressor>
--wiredTigerIndexPrefixCompression < boolean >
--wiredTigerCollectionBlockCompressor <compressor>

在经过分析之后,我想我需要更换存储引擎,从 MMAPv1 更换到 wiredTiger 上去了。由于 MongoDB 3.0.12 版本,本身就支持 wiredTiger 存储引擎,所以这一步不变更版本。

首先导出数据:

[root@enmotech mongodb-rhel-3.0.12]# ./bin/mongodump -h 127.0.0.1 --port 12088 -o mongbkp

17:36:41.150 writing enmo.UsOp to mongbkp/enmo/UsOp.bson

17:36:41.150 writing CU_ENMOTECH.MicroPost to mongbkp/CU_ENMOTECH/MicroPost.bson

。。。。

17:37:53.150 [######################..] enmo.UsOp 17657623/18819781 (93.8%)

17:37:56.151 [#######################.] enmo.UsOp 18593062/18819781 (98.8%)

17:37:56.854 writing enmo.UsOp metadata to mongbkp/enmo/UsOp.metadata.json

17:37:56.854 done dumping enmo.UsOp (18819781 documents)

设置新的参数文件,注意,我在 8888 端口启用了一个新的实例,隔离原数据库,防范问题,当升级完成之后,进行调整,应用再连接到这个新的数据库:

[root@enmotech mongodb-rhel-3.0.12]# cat moneygle.conf

port = 8888

bind_ip = 172.16.26.129

dbpath = eygle

logpath = logs/eygle.log

logappend = true

fork = true

storageEngine = wiredTiger

启动新的数据库:

[root@enmotech mongodb-rhel-3.0.12]# ./bin/mongod -f moneygle.conf

about to fork child process, waiting until server is ready for connections.

forked process: 13673

child process started successfully, parent exiting

执行数据恢复:

[root@enmotech mongodb-rhel-3.0.12]# ./bin/mongorestore -h 172.16.26.129 --port 8888 mongbkp

building a list of dbs and collections to restore from mongbkp dir

reading metadata file from mongbkp/enmo/UsOp.metadata.json

....

restoring indexes for collection cclog.UsOp from metadata

finished restoring cclog.UserOperationLog (18819781 documents)

done

完成存储引擎的更换,接下来我想把MongoDB升级到新版本3.6上来。

发现如果直接从3.0升级到3.6,会出现错误:

about to fork child process, waiting until server is ready for connections.

forked process: 16204

ERROR: child process failed, exited with error number 62

在日志中会记录明确的提示:

** IMPORTANT: UPGRADE PROBLEM:

The data files need to be fully upgraded to version 3.4

before attempting an upgrade to 3.6;

see http://dochub.mongodb.org/core/3.6-upgrade-fcv for more details.

也就是说,3.0 版本,要先升级到3.4,再升级到3.6

这其中还有几个小步骤,首先从官方网站下载相应的版本,用 3.4 版本启动数据库,然后在 admin下执行必要的命令,将兼容性版本设置为3.4:

[root@enmotech mongodb-rhel-3.4.15]# ./bin/mongo 127.0.0.1:8888

MongoDB shell version v3.4.15

connecting to: mongodb://127.0.0.1:8888/test

MongoDB server version: 3.4.15

Server has startup warnings:

> use admin

switched to db admin

> db.adminCommand( { setFeatureCompatibilityVersion: "3.4" } )

{ "ok" : 1 }

> db.shutdownServer();

此时再用 3.6 版本的程序启动数据库,就一切正常了:

./bin/mongod --port 8888 -dbpath=./eygle/ -logpath=./logs/365.log --logappend --fork


about to fork child process, waiting until server is ready for connections.

forked process: 19098

child process started successfully, parent exiting

接下来修改兼容性版本,设置为3.6 ,现在我们已经拥有了一个 3.6 版本,使用 WireTiger 存储引擎的MongoDB数据库了。

[root@enmotech ]# ./mongodb-rhel-3.6.5/bin/mongo 127.0.0.1:8888

MongoDB shell version v3.6.5

connecting to: mongodb://127.0.0.1:8888/test

MongoDB server version: 3.6.5

Server has startup warnings:

> use admin

switched to db admin

> db.adminCommand( { setFeatureCompatibilityVersion: "3.6" } )

{ "ok" : 1 }

接下来修改兼容性版本,设置为3.6 ,现在我们已经拥有了一个 3.6 版本,使用 WireTiger 存储引擎的MongoDB数据库了。

在 WireTiger 网站上,至今还张挂着『We've Joined MongoDB!』的声明:

3706a3a825dd814eaa9585f16b9d73e47264d6e2

我们必须说,Oracle 的设计理念,在各种数据库中都可以看到类似的影子。比如 WireTiger 同样使用 『优先写日志 - Write-ahead Transaction Log 』原则。

这和 Oracle 的 Redo 日志原理相似,MongDB 在数据更新时,先将数据更新写入到日志文件,然后在 Checkpoint 操作开始时,将日志文件中记录的操作,刷新到数据文件。WiredTiger 日志文件会持续记录自上一次 Checkpoint 操作之后发生的所有数据变化,在 MongoDB 系统崩溃时,通过日志文件就能够还原从上次Checkpoint之后发生的数据更新。

以下是当前我的数据库中日志信息:

ls -l journal/

total 307200

Jun 14 17:15 WiredTigerLog.0000000087

Jun 14 17:10 WiredTigerPreplog.0000000001

Jun 14 17:10 WiredTigerPreplog.0000000002

Journal Files是存储在硬盘的日志文件,每个Journal File大约是100MB。

WiredTiger使用检查点在磁盘上提供一致性数据视图,并允许MongoDB从上一个检查点恢复。 但是,如果MongoDB在检查点之间意外退出,则需要使用日志记录来恢复上次检查点之后发生的信息。

通过日志记录,恢复过程:

查看数据文件以查找上一个检查点的标识符。

在日志文件中搜索与上一个检查点的标识符相匹配的记录。

自上次检查点以来,在日志文件中应用这些操作。

MongoDB WiredTiger 首先使用内存缓冲来存储日志记录,直到超过128 kB,才写入磁盘。根据以下时间间隔或条件,WiredTiger将缓冲的日记记录同步到磁盘:

  1. 3.2版新增功能:每50毫秒。

  2. 版本3.6中:MongoDB 设置检查点以60秒的间隔执行。

  3. 如果写入操作包含 j:true 的写入指令,则WiredTiger会强制WiredTiger日志文件同步。

  4. 由于MongoDB使用的日志文件大小限制为100 MB,WiredTiger大约每隔100 MB创建一个新的日志文件。当WiredTiger创建新的日志文件时,WiredTiger将同步前一个日志文件。

在写操作之间,当日志记录保留在WiredTiger缓冲区中时,在mongod强制关闭时更新可能会丢失。


原文发布时间为:2018-06-18

本文作者:盖国强

本文来自云栖社区合作伙伴“数据和云”,了解相关信息可以关注“数据和云”。

相关实践学习
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
相关文章
|
1月前
|
SQL DataWorks NoSQL
DataWorks报错问题之datax mongodb全量迁移报错如何解决
DataWorks是阿里云提供的一站式大数据开发与管理平台,支持数据集成、数据开发、数据治理等功能;在本汇总中,我们梳理了DataWorks产品在使用过程中经常遇到的问题及解答,以助用户在数据处理和分析工作中提高效率,降低难度。
|
Docker 容器
CPU内存不足分析Gitlab的内存消耗
CPU内存不足分析Gitlab的内存消耗
CPU内存不足分析Gitlab的内存消耗
|
5月前
|
存储 缓存 NoSQL
MongoDB 存储引擎
MongoDB 存储引擎
|
5月前
|
存储 算法 芯片
关于内存芯片的电流消耗机制的介绍
关于内存芯片的电流消耗机制的介绍
49 0
|
5月前
|
XML NoSQL Redis
如何检测出redis的哪些key在消耗内存
如何检测出redis的哪些key在消耗内存
53 0
|
5月前
|
存储 数据库 C语言
Hawkeyes: x86软件迁移Arm的弱内存序问题解决方案
本文介绍了x86软件迁移到Arm过程中可能遇到的弱内存序问题的解决方案,解析了弱内存序问题的根因,介绍了Hawkeyes的架构和实现原理。欢迎有需求的团队发送邮件咨询
622 0
|
6月前
|
JavaScript API
使用 Node.js Stream API 减少服务器端内存消耗的一个具体例子
使用 Node.js Stream API 减少服务器端内存消耗的一个具体例子
59 0
|
7月前
|
SQL 数据库
使用 SAP ABAP Memory Inspector 对应用程序消耗内存进行检测时常犯的错误试读版
使用 SAP ABAP Memory Inspector 对应用程序消耗内存进行检测时常犯的错误试读版
66 0
|
7月前
ABAP Memory Inspector 里对动态内存对象的内存消耗度量方式
ABAP Memory Inspector 里对动态内存对象的内存消耗度量方式
46 0