如何高效地遍历 MongoDB 超大集合?
- GitHub 仓库:Fundebug/loop-mongodb-big-collection

本文使用的编程语言是 Node.js,连接 MongoDB 的模块用的是mongoose。但是,本文介绍的方法适用于其他编程语言及其对应的 MongoDB 模块。
错误方法:find()
也许,在遍历 MongoDB 集合时,我们会这样写:
const Promise = require("bluebird");
function findAllMembers() {
return Member.find();
}
async function test() {
const members = await findAllMembers();
let N = 0;
await Promise.mapSeries(members, member => {
N++;
console.log(`name of the ${N}th member: ${member.name}`);
});
console.log(`loop all ${N} members success`);
}
test();
注意,我们使用的是 Bluebird 的mapSeries而非map,members 数组中的元素是一个一个处理的。这样就够了吗?
当 Member 集合中的 document 不多时,比如只有 1000 个时,那确实没有问题。但是当 Member 集合中有 1000 万个 document 时,会发生什么呢?如下:
<--- Last few GCs --->
rt of marking 1770 ms) (average mu = 0.168, current mu = 0.025) finalize [5887:0x43127d0] 33672 ms: Mark-sweep 1398.3 (1425.2) -> 1398.0 (1425.7) MB, 1772.0 / 0.0 ms (+ 0.1 ms in 12 steps since start of marking, biggest step 0.0 ms, walltime since start of marking 1775 ms) (average mu = 0.088, current mu = 0.002) finalize [5887:0x43127d0] 35172 ms: Mark-sweep 1398.5 (1425.7) -> 1398.4 (1428.7) MB, 1496.7 / 0.0 ms (average mu = 0.049, current mu = 0.002) allocation failure scavenge might not succeed
<--- JS stacktrace --->
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
1: 0x8c02c0 node::Abort() [node]
2: 0x8c030c [node]
3: 0xad15de v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [node]
4: 0xad1814 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [node]
5: 0xebe752 [node]
6: 0xebe858 v8::internal::Heap::CheckIneffectiveMarkCompact(unsigned long, double) [node]
7: 0xeca982 v8::internal::Heap::PerformGarbageCollection(v8::internal::GarbageCollector, v8::GCCallbackFlags) [node]
8: 0xecb2b4 v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [node]
9: 0xecba8a v8::internal::Heap::FinalizeIncrementalMarkingIfComplete(v8::internal::GarbageCollectionReason) [node]
10: 0xecf1b7 v8::internal::IncrementalMarkingJob::Task::RunInternal() [node]
11: 0xbc1796 v8::internal::CancelableTask::Run() [node]
12: 0x935018 node::PerIsolatePlatformData::FlushForegroundTasksInternal() [node]
13: 0x9fccff [node]
14: 0xa0dbd8 [node]
15: 0x9fd63b uv_run [node]
16: 0x8ca6c5 node::Start(v8::Isolate*, node::IsolateData*, int, char const* const*, int, char const* const*) [node]
17: 0x8c945f node::Start(int, char**) [node]
18: 0x7f84b6263f45 __libc_start_main [/lib/x86_64-linux-gnu/libc.so.6]
19: 0x885c55 [node]
Aborted (core dumped)
可知,内存不足了。
打印find()返回的 members 数组可知,集合中所有元素都返回了,哪个数组放得下 1000 万个 Object?
正确方法:find().cursor()与 eachAsync()
将整个集合 find()全部返回,这种操作应该避免,正确的方法应该是这样的:
function findAllMembersCursor() {
return Member.find().cursor();
}
async function test() {
const membersCursor = await findAllMembersCursor();
let N = 0;
await membersCursor.eachAsync(member => {
N++;
console.log(`name of the ${N}th member: ${member.name}`);
});
console.log(`loop all ${N} members success`);
}
test();
使用cursor()方法返回 QueryCursor,然后再使用eachAsync()就可以遍历整个集合了,而且不用担心内存不够。
QueryCursor是什么呢?不妨看一下 mongoose 文档:
A QueryCursor is a concurrency primitive for processing query results one document at a time. A QueryCursor fulfills the Node.js streams3 API, in addition to several other mechanisms for loading documents from MongoDB one at a time.
总之,QueryCursor 可以每次从 MongoDB 中取一个 document,这样显然极大地减少了内存使用。
如何测试?
这篇博客介绍的内容很简单,但是也很容易被忽视。如果大家测试一下,印象会更加深刻一些。
测试代码很简单,大家可以查看Fundebug/loop-mongodb-big-collection。
我的测试环境是这样的:
- ubuntu 14.04
- mongodb 3.2
- nodejs 10.9.0
1. 使用 Docker 运行 MongoDB
sudo docker run --net=host -d --name mongodb daocloud.io/library/mongo:3.2
2. 使用mgodatagen生成测试数据
使用 mgodatagen,1000 万个 document 可以在 1 分多钟生成!
下载 mgodatagen:https://github.com/feliixx/mgodatagen/releases/download/0.7.3/mgodatagen_linux_x86_64.tar.gz
解压之后,复制到/usr/local/bin 目录即可:
sudo mv mgodatagen /usr/local/bin
mgodatagen 的配置文件mgodatagen-config.json如下:
[
{
"database": "test",
"collection": "members",
"count": 10000000,
"content": {
"name": {
"type": "string",
"minLength": 2,
"maxLength": 8
},
"city": {
"type": "string",
"minLength": 2,
"maxLength": 8
},
"country": {
"type": "string",
"minLength": 2,
"maxLength": 8
},
"company": {
"type": "string",
"minLength": 2,
"maxLength": 8
},
"email": {
"type": "string",
"minLength": 2,
"maxLength": 8
}
}
}
]
执行mgodatagen -f mgodatagen-config.json命令,即可生成 10000 万测试数据。
mgodatagen -f mgodatagen-config.json
Connecting to mongodb://127.0.0.1:27017
MongoDB server version 3.2.13
collection members: done [====================================================================] 100%
+------------+----------+-----------------+----------------+
| COLLECTION | COUNT | AVG OBJECT SIZE | INDEXES |
+------------+----------+-----------------+----------------+
| members | 10000000 | 108 | _id_ 95368 kB |
+------------+----------+-----------------+----------------+
run finished in 1m12.82s
查看 MongoDB,可知新生成的数据有 0.69GB,其实很小,但是使用 find()方法遍历会报错。
show dbs
local 0.000GB
test 0.690GB
3. 执行测试代码
两种不同遍历方法的代码分别位于test1.js和test2.js。
参考
如何高效地遍历 MongoDB 超大集合?的更多相关文章
- 如何高效的遍历Map?你常用的不一定是最快的
微信公众号:大黄奔跑 关注我,可了解更多有趣的面试相关问题. 写在之前 如文章标题所言,遍历Map是开发过程中比较常见的行为,实现的方式也有多种方式,本文带领大家一起看看更加高效的遍历 Map. 『茴 ...
- MongoDB固定集合
固定集合 MongoDB 固定集合(Capped Collections)是性能出色且有着固定大小的集合,对于大小固定,我们可以想象其就像一个环形队列,当集合空间用完后,再插入的元素就会覆盖最初始的头 ...
- MongoDB固定集合(Capped Collections)
MongoDB 固定集合(Capped Collections)是性能出色且有着固定大小的集合,对于大小固定,我们可以想象其就像一个环形队列,当集合空间用完后,再插入的元素就会覆盖最初始的头部的元素! ...
- mongoDB之集合操作
mongoDB之集合操作 mongoDB中的集合相当于mysql中的表. mongoDB中集合的创建: 第一种方式:不限制集合大小 db.createCollection("集合名称&q ...
- MongoDB 固定集合
MongoDB 固定集合(Capped Collections)是性能出色且有着固定大小的集合,对于大小固定,我们可以想象其就像一个环形队列,当集合空间用完后,再插入的元素就会覆盖最初始的头部的元素! ...
- mongoDB 删除集合后,空间不释放
mongoDB 删除集合后,空间不释放,添加新集合,没有重新利用之前删除集合所空出来的空间,也就是数据库大小只增不减. 方法有: 1.导出导入 dump & restore 2.修复数据库 r ...
- C#中遍历各类数据集合的方法总结
C#中遍历各类数据集合的方法总结: 1.枚举类型 //遍历枚举类型Sample的各个枚举名称 foreach (string sp in Enum.GetNames(typeof(Sample))) ...
- 关于mongodb删除集合后磁盘空间不释放的问题
mongodb删除集合后磁盘空间不释放,只有用db.repairDatabase()去修复才能释放. 但是在修复的过程中如果出现了非正常的mongodb的挂掉,再次启动时启动不了的,需要先修复才可以, ...
- mongodb的集合操作
MongoDB 创建集合 1.手动创建: 语法格式: db.createCollection(name, options) 参数说明: name: 要创建的集合名称 options: 可选参数, 指定 ...
随机推荐
- 性能测试学习 第八课--LR12中针对WebServices协议的三种脚本开发模式
一,webservices协议简介 webservices是建立可交互操作的分布式应用程序的新平台,它通过一系列的标准和协议来保证程序之间的动态连接, 其中最基本的协议包括soap,wsdl,uddi ...
- ASP.NET Core 2.1对GDPR的支持
欧盟的<通用数据保护条例>(General Data Protection Regulation,以下简称 GDPR)已经于 2018 年 5 月 25 日正式施行.GDPR 涵盖了包括数 ...
- Java实现堆排序和计数排序
堆排序代码: 思想:每次都取堆顶的元素,将其放在序列最后面,然后将剩余的元素重新调整为最小堆,依次类推,最终得到排序的序列. import java.util.Arrays; /** * 思路:首先要 ...
- JDK设计模式之—动态代理
代理模式的特点 代理模式是常用的java设计模式,它的特征是代理类与委托类有同样的接口.代理类主要负责为委托类预处理消息.过滤消息.把消息转发给委托类. 代理类的对象并不是真正实现服务,而是通过调用委 ...
- [Swift]LeetCode416. 分割等和子集 | Partition Equal Subset Sum
Given a non-empty array containing only positive integers, find if the array can be partitioned into ...
- Mysql的两种“超过多少次”写法(力扣596)
题目: 有一个courses 表 ,有: student (学生) 和 class (课程). 请列出所有超过或等于5名学生的课. 例如,表: +---------+------------+ | s ...
- MySQL5.7.25(解压版)Windows下详细的安装过程
大家好,我是浅墨竹染,以下是MySQL5.7.25(解压版)Windows下详细的安装过程 1.首先下载MySQL 推荐去官网上下载MySQL,如果不想找,那么下面就是: Windows32位地址:点 ...
- scala读取解析json文件
import scala.util.parsing.json.JSON._ import scala.io.Source object ScalaJsonParse { def main(args: ...
- CDN边缘节点容器调度实践(下)
5月27日,OSC 源创会在上海成功举办.又拍云系统开发高级工程师黄励博在大会分享了<CDN 边缘节点容器调度的实践>.主要介绍又拍云自主开发的边缘节点容器调度方案,从 0 到 1 ,实现 ...
- 【java设计模式】(5)---装饰者模式(案例解析)
设计模式之装饰者模式 一.概念 1.什么是装饰者模式 装饰模式是在不使用继承和不改变原类文件的情况下,动态的扩展一个对象的功能.它是通过创建一个包装对象,也就是装饰来包裹真实的对象. 这一个解释 ...