如何高效地遍历 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。
参考
关于Fundebug
Fundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了10亿+错误事件,付费客户有Google、360、金山软件、百姓网等众多品牌企业。欢迎大家免费试用!
版权声明
转载时请注明作者Fundebug以及本文地址:
https://blog.fundebug.com/2019/03/21/how-to-visit-all-documents-in-a-big-collection-of-mongodb/
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Json反序列化与Java泛型
Java的JSON库有很多,本文分析google的Gson和alibaba的fastjson,在Java泛型场景反序列化的一些有意思的行为。考虑下面的json字符串: [ "2147483648", "2147483647" ] 用fastjson在不指定类型的情况下解析,下面的代码输出啥: JSON.parseArray(s).forEach(o -> { System.out.println(o.getClass()); }); 答案是: class java.lang.Long class java.lang.Integer 是不是感觉有点儿奇怪,两个都是数字啊,居然输出了不同的类型,原因我们下面细讲。再看看Gson, 用Gson解析并且不指定泛型类型的话,下面的代码输出啥: new Gson().fromJson(s, List.class).forEach(o -> { System.out.println(o.getClass()); }); 答案是: class java.lang.Double class java.lang.Double 这次两个都是Dou...
- 下一篇
我为什么推荐Prettier来统一代码风格
译者按: 关于代码风格,不同的人有不同的偏好,其实并没有什么绝对的对错。但是,有 2 条原则应该是对的: 少数服从多数;用工具统一风格。 原文: Why robots should format our code for us 译者: Fundebug 为了保证可读性,本文采用意译而非直译。另外,本文版权归原作者所有,翻译仅用于学习。 我曾经以为,程序员有自己独特的代码风格挺好的。因为,一个成熟的程序员应该清楚,好的代码应该是怎样的。 我的大学教授告诉我,他的学生在用我的代码,因为我的代码风格不一样。我想了一下,也许是因为我的代码至少是有风格的,而其他人的代码一团糟。 一些示例 示例 1: 读了The Programmers’ Stone之后,我把大括号这样写: if (food === 'pizza') { alert('Pizza ;-)'); } else { alert('Not pizza ;-('); } 但是,我意识到在前端社区里,也许只有我一个人这样写的。而其他人都是这样写的: if (food === 'pizza') { alert('Pizza ;-)'); } ...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- SpringBoot2整合Redis,开启缓存,提高访问速度
- CentOS8编译安装MySQL8.0.19
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Windows10,CentOS7,CentOS8安装Nodejs环境