探讨Node内存机制和大文件处理

2019-09-11 15:25:18

关于node的内存溢出,有人提出  node --max-old-space-size 8000  yourserver.js    

但是我是做node md5 大文件的,发现这样并不生效,依然错误,不知道其他的通过这个方法得到了改变吗。

下面看我的分心。

同时参考地址  探讨Node内存机制和大文件处理


前言

在以前,javascript还运行在浏览器端的时候,js程序员根本不会想到有一天他们会直面文件io和内存。直到Ryan Dahl把Node带到了这个世界上。

一切都变得不一样了,原本被认为逐渐趋于固定的后端语言,仿佛突然间出现了黑体辐射之于经典物理学一般的阴云。虽然后面的事情没有紫外线灾难那样让人瞠目结舌却也给当时趋于同质化的后端语言带来了不小的震撼。

Node无疑是成功的,笔者写这篇文章的目的是巩固自己在学习Node过程中一些难以理解的点,并附加一些自己的思考与大家分享,如有错误和纰漏请尽情指教不必拘束。

内存机制

总体上来讲,我认为Node的内存应该分为两个部分。

由ChromeV8管理的部分(通过Javascript使用的部份)

由系统底层管理的部分(通过C++/C使用的部分)

二者实际上应该处于一种包含关系。即ChromeV8的部分应当包含在系统底层管理的部分当中。

ChromeV8的内存管理

ChormeV8中所有的javascript对象实际上是以堆的方式存储在内存当中,当以分配给对象的内存不够时,继续向堆申请内存直到满足对象或者达到堆的空间上限。

通过相关资料可以知道的是

Node中通过javascript使用内存的上限:

  • 64位环境下约为1.7GB

  • 在32位环境下约为0.7GB
    也就是说,如果需要引入一个非常大的对象的时候虽然不常见,但是这样一个限制是确实存在的。

ChromeV8的内存回收机制

ChromeV8的内存分配中,使用了一种分代方法,堆中分为两个部分,新生代和老生代。

其中新生代采用Scavenge算法进行垃圾回收,又将其分为了两个部分。

Scavenge算法:

将内存空间分为两个部分From和To,从From中开始分配内存,当触发垃圾回收之后,检索From中的活跃对象。之后将活跃对象复制到To中,清空From部分,之后这次的To内存空间,将会变为新的From直到再次触发垃圾回收,进入一个循环的过程。
而对于老生代Chrome采用了Mark-Sweep和Mark-Compact两种模式的混合,追求垃圾回收速度和碎片化两者之间的平衡。

但实际上,这仍然是一个非常缓慢的过程。

内存C/C++的部分

这是Node的原生部分也是根本上区别与前端js的部分,包括核心运行库,在一些核心模块的加载过程中,Node会调用一个名为js2c的工具。这个工具会将核心的js模块代码以C数组的方式存储在内存中,用以提升运行效率。

在这个部分,我们也不会有内存的使用限制,但是作为C/C++扩展来使用大量内存的过程中,风险也是显而易见的。

  • C/C++没有内存回收机制

作为大部分没有C/C++功底的纯js程序员,我不建议贸然的去使用这个部分,因为C/C++模块非常强大,如过对于对象生命周期的理解不够到位,而又使用大内存对象的情境中,很容易就造成内存溢出导致整个Node的崩溃甚至是系统的崩溃。

有没有安全的使用大内存的方法?

答案是肯定的,那是就是用Buffer对象。

我们使用Buffer对象的时候使用的是JavaScripts语法

var buffer = new Buffer(size);//8.0.0之后的Node版本请使用Buffer.alloc(size[, fill[, encoding]])

之前不是说过使用javascript的部分是由ChromeV8接管的吗?那为什么仍然可以使用大量内存创立缓存呢?

这便是Node运行在服务端和Chrome运行在前端的区别,Chrome和Node都采用ChromeV8作为JS的引擎,但是实际上他们所面对的对象是不同的,Node面对的是数据提供,逻辑和I/O,而Chrome面对的是界面的渲染,数据的呈现。因此在Chrome上,几乎不会遇到大内存的情况,作为为Chrome的而生的V8引擎自然也不会考虑这种情况,因此才会出现上文所述的内存限制。而现在,Node面对这样的情况是不可以接受的,所以Buffer对象,是一个特殊的对象,它由更低层的模块创建,存储在V8引擎以外的内存空间上。

在内存的层面上讲Buffer和V8是平级的。

问题的提出

之前的讲解是我接下来遇到的问题的一个小小铺垫,现在我正在面对一个问题,需要将一个足足有3.3GB的JSON ARRAY导入到我的项目中,然后并发的对数组内的元素进行处理。我知道使用Node进行这种操作并不是明智之举,但是作为学习的一个部分,我仍然想要尝试完成这个工作。

首先由于V8的限制直接读入对象的方法是不可用的。

解决思路:

1.直接使用Require方式引入JSON文件
2.使用C/C++原生模块
3.将JSON文件以流的形式写入Buffer

直接使用Require方式引入JSON文件

Require是Node的核心组成,它将模块加载进整个Node的运行时中,同时也可以直接引入JSON数据。

根据Require的机制,JSON文件本质上也是创建一个Buffer对象,存储在ChromeV8以外的内存空间中。

理论上,和第三种本质上是相同的方法。

在确保我本机内存足够的情况下,尝试使用这种解决方案。

var bigArr = require('/home/data.json');

//根据require获得模块的机制,使用绝对路径可以加快加载速度。
运行

$ node index.js

获得了一个很有意思的错误

buffer.js:220
    throw err;
    ^RangeError: "size" argument must not be larger than 2147483647
    at Function.Buffer.allocUnsafe (buffer.js:247:3)
    at tryCreateBuffer (fs.js:531:21)
    at Object.fs.readFileSync (fs.js:571:14)
    at Object.Module._extensions..json (module.js:641:20)
    at Module.load (module.js:545:32)
    at tryModuleLoad (module.js:508:12)
    at Function.Module._load (module.js:500:3)
    at Module.require (module.js:568:17)
    at require (internal/module.js:11:18)
    at Object.<anonymous> (/home/kanoyami/bigDataTest/index.js:1:74)

这个错误的大意是在新建Buffer对象的时候,向构造器输入对象的大小参数超过了 2147483647。
这个数字大家都不会陌生是32位操作系统中最大的符号型整型常量。
也就是说这创建的缓存大小还是超出了限制。
查阅了官方文档之后,得到了这样的信息

buffer.constants.MAX_LENGTH#
Added in: 8.2.0
<integer> The largest size allowed for a single Buffer
instance.
On 32-bit architectures, this value is (2^30)-1
(~1GB). On 64-bit architectures, this value is (2^31)-1
(~2GB).
This value is also available as buffer.kMaxLength

也就是说这里有一个最大限制为2^31-1。
然后再看具体的错误中提到的方法:
Buffer.allocUnsafe()

Class Method: Buffer.allocUnsafe(size)#
size
<integer> The desired length of the new Buffer
Allocates a new Bufferof size
bytes. If the sizeis larger than buffer.constants.MAX_LENGTH
or smaller than 0,a RangeError
will be thrown.
//如果size参数大于了之前提到的最大数字,或者小于0则会抛出一条RangeError错误
A zero-length Buffer will be created if sizeis 0.The underlying memory for Buffer
instances created in this way is not initialized.

在8.0.0以后的版本中出于安全考虑,原本的Buffer构造器被系统的删除了,取而代之的是一系列新的API方法。
本次实验中提到的allocUnsafe就是其中之一,这里也提到了这个错误的来源。由此可以知道,方法3应该也是行不通的了。而且由于fs返回的也是Buffer对象,自然也受这个限制,因此自然是不可使用的。

分析

由于Buffer自身的特性,在文档中有关于它的一条描述:

Instances of the Buffer class are similar to arrays of integers but correspond to fixed-sized, raw memory allocations outside the V8 heap. The size of the Buffer is established when it is created and cannot be resized.

大概意思是,Buffer类型的实例非常类似于整数数组,它虽然创建在V8外部,但是它的大小和V8堆的原始大小相对应。Buffer一但被创建,它的大小就无法再次修改。
这和我们在C/C++中使用malloc不同,Buffer一创建,就不能对他的大小再次进行修改。
这里注意,文档提到的是实例,也就说并非所有的Buffer对象共享这2G的内存空间,在某些需求下我们可以开辟多个Buffer,分次别存储一个大文件。
但是我这里是一个完整的JSON ARRAY,而且我希望并发的处理内部的数据,显然分多个Buff是不能满足要求的。

只能选择C/C++了吗?

大概是我需求太奇葩,查阅了很多资料也没有找到能使用js解决这个问题的答案,不过有一个地方值得注意,那就是官方文档内对Buffer实例大小上限的描述是buffer.constants.MAX_LENGTH这个整数,是一个固定值。而另一个描述中提到的是和V8的堆总内存一致。

我们知道可以使用max_old_space_size来提升V8老生代的内错空间,总体上看就是提高了整个V8堆的内错容量。
如果这个值是启动时定义的,而且buffer.constants.MAX_LENGTH是引用了V8堆的最大可用内存,那么这个问题是不是就可以解决了呢?
$ node index.js -max_old_space_size = 4096

  • 然后我获得了一样的错误返回

总结

至此,可以得到的是:
1. Node的Buffer对象不是无限制的内存使用,其具有最大值
2. 最大值理论等同于V8堆的内存总量在32位约1G,64位约2G
3. 最大值buffer.constants.MAX_LENGTH是预定义在Node中的,不会因为V8的内存增加而增加。

那么接下来便只有两条路可走,那就是使用C/C++编写原生模块或者修改Node的源代码。
Buffer对象实际上是一个由C++和Js共同完成的特殊对象,修改它的代码,实际上还是在使用C++。

那么,接下来,我会选择这唯一的方法,去编写一个原生模块,允许使用大量的内存创建缓存,并暴露方法给JavaScript使用。

谈谈我的想法

笔者做这个东西的本质是在学习,运行这个实验的平台是运行CentOS7且可用内存22G以上的服务器。
或许没什么太大意义,但是对自身的提高和学习是一个不错的实践。
我觉得既然学习Node,就不应该限于应用层的使用,对于内部的理解是学习的最快途径。

生命在于折腾。


  • 2018-04-25 00:46:48

    Android开发笔记——SharedPreferences 存储实体类以及任意类型

    我们常常要用到保存数据,Android中常用的存储方式有SQLite,sharedPreferences 等,当然也有各自的应用场景,前者适用于保存较多数据的情形,后者责倾向于保存用户偏好设置比如某个checkbox的选择状态,用户登录的状态等等,都是以键值对的形式进行的文件读取,可以存储String,int,booean等一些基本数据类型等等。

  • 2018-04-25 11:48:44

    Java泛型详解

    泛型是Java中一个非常重要的知识点,在Java集合类框架中泛型被广泛应用。本文我们将从零开始来看一下Java泛型的设计,将会涉及到通配符处理,以及让人苦恼的类型擦除。

  • 2018-05-05 20:31:52

    StringUtils就这1张图,必备(二)

    StringUtils是工作中使用最频繁的一个工具类,提供了大量丰富的字符串操作方法,下面是所有方法的一个蓝图:

  • 2018-05-06 00:41:36

    设置EditText不自动聚焦

    如果界面中有EditText的时候,用户打开界面的话EditText就会自动聚焦。如果想取消这种一打开界面EditText就聚焦效果,可在EditText的上级父容器中加入如下代码:

  • 2018-05-21 13:54:06

    laravel-nestedset:多级无限分类正确姿势

    Nested Set Model 是一种实现有序树的高明的方法,它快速且不需要递归查询,例如不管树有多少层,你可以仅使用一条查询来获取某个节点下的所有的后代,缺点是它的插入、移动、删除需要执行复杂的sql语句,但是这些都在这个插件内处理了!