我的Nodejs的有点奇怪(1)
Feb 20, 2014
前言
如果有人跟你说nodejs好棒, 他很有可能是个卖安利的, 但如果他说javascript好棒, 她一定是个心地善良的好人. ——NStal
作为一个nodejs的脑残粉, 经过许多尝试之后算是对nodejs的一些尿性特性有了一些理解。
网上主要都是讲Web开发的, 本文则主要介绍一些非Web的Nodejs的杂七杂八的坑和解决方案。
内存限制
V8毕竟不是为服务器设计的, 因此在nodejs的使用过程中总是会有一些限制。
Buffer大小限制
你是不能创建一个超过1GB的buffer的(早些时候是512MB)。 这是由Buffer::kMaxLength决定的. 主要原因是用unsigned int作为buffer length, 理论上可以再改大一点,但是不会超过~4G。
heap大小限制
64bit下, V8 默认的最大heap是~1.4G。 所有的V8 Object , 比如string array, 都在V8 heap上。
我们也可以通过添加V8 option --max-old-space-size=[size in MB]
来修改这一限制。
所有的C++ plugin都使用自己的heap, nodejs 的 Buffer Object的也是如此, 因此几乎不会占用V8 heap 大小,。
另一方面heap的实际使用大小严重的影响GC的频率和效果, 因此不希望GC参与的部分可以用C++的模块或者利用Buffer来实现。
当heap超过限制, 我们会收到这样一个不可屏蔽的错误。 FATAL ERROR: CALL_AND_RETRY_2 Allocation failed - process out of memory 注意这里和我们后面看到的另一个process out of memory的错误是不同的
字符串长度限制
你能读/写一个多大的JSON?
在批量处理信息的时候, 比如分词, 总是不希望所有的操作都是异步的。所以处理时总是希望数据以javascript Object的形式保留在内存中, 而不是每次都去数据库中读取。那么JSON就成了理想的数据存储格式。
在JSON没有超过递归限制的条件下,这个问题的答案其实取决于你能读写多大的string。其实这个数值在V8里是 String::kMaxLength….吗? 非也。 先让我们来看看下面3段神奇的代码:
str = ""
count = 1000 * 1000 * 300
for index in [0...count]
str+="0"
str2 = str + str
str3 = str.concat(str)
str = ""
count = 1000 * 1000 * 300
for index in [0...count]
str+="0"
//Should throw FATAL ERROR: JS Allocation failed - process out of memory
strViaArr = [str,str].join("")
str = ""
count = 1000 * 1000 * 300
for index in [0...count]
str+="0"
//Should throw FATAL ERROR: JS Allocation failed - process out of memory
strViaArr = "".concat str,str
同样是两段String变成一段,为什么第一段代码可以执行后面两段就OM了呢? 这要从V8::String 说起, V8::String有一个静态常量字段kMaxLength, 表示最大字符数, 默认是
static const int kMaxLength = (1 << (32 - 2)) - 1;
也就是1G个字符,顺带说一下ECMAscript的字符规定是UCS2 ,但V8内部用的是UTF16。 两者的区别是UTF16在大体兼容UCS2的基础上比原版更吊了,但UCS2是定长2Byte的串,UTF16则是变长最多4BYTE这就是为什么有的字我们能正确的看到却没办法得到正确的长度。这里用的int所以最大2G,每个字符2Byte那么就是最多1G字符。这解释了第一段字符为什么没有OM。因为第一段大概就600M上下,并没有达到String::kMaxLength的1G长度。至于后面两段的OM要从V8里String不同的品种说起了。
这里介绍几种主要的品种,下面是复制的V8一小段文档.
// - String
// - SeqString
// - SeqOneByteString
// - SeqTwoByteString
// - SlicedString
// - ConsString
SeqOneByteString 和SeqTwoByteString 是V8针对ascii字符串做的内存优化, 如果全部都是Ascii字符集那么就没有必要用双子字节这么浪费。SlicedString 是对别的字符串的局部引用, 我们调用String.prototype.substring
的时候得到的就是这种。ConsString是对两个字符的引用,我们调用str1+str2
的时候得到的就是这种。 也就是说在这3种String里面只有SeqString是在内存中有实际字符串的。其他的都是变相的引用。
V8里面对String::kMaxLength的限制是说理论上任何子类都不能够超过这个限制。实际上唯一能达到这个限制的子类必须是ConsString。ConsString可以是对别的两个字符进行引用。所以任何ConsString以外的String子类都不能拥有超过512M的kMaxLength。
...
// SeqOneByteString
// Maximal memory usage for a single sequential ASCII string.
static const int kMaxSize = 512 * MB - 1;
// Maximal length of a single sequential ASCII string.
// Q.v. String::kMaxLength which is the maximal size of concatenated strings.
static const int kMaxLength = (kMaxSize - kHeaderSize);
...
...
// SeqTwoByteString
// Maximal memory usage for a single sequential two-byte string.
static const int kMaxSize = 512 * MB - 1;
// Maximal length of a single sequential two-byte string.
// Q.v. String::kMaxLength which is the maximal size of concatenated strings.
static const int kMaxLength = (kMaxSize - kHeaderSize) / sizeof(uint16_t);
...
因此我们能在源代码里看到。SeqOneByteString/SeqTwoByteString除了kMaxLength外还有kMaxSize的限制均为512M。注意这里是大小限制而不是字符的数量限制。所以对中文这个数字要减半,哪怕里面只有一个中文,这真是一个件悲伤的事情。
我们回顾一下:
- String 最多1G个字符
- ConsString 对别的两个String引用但是总和不超过1G
- 其他的String大小不超过512MB
- SeqString有一个非ASCII的就会变成SeqTwoByteString
- SeqTwoByteString大小不超过512MB 因此长度不超过256M
最后再回来看看我们的代码
str2 = str + str
在V8中是生成的是ConsString
RUNTIME_FUNCTION(MaybeObject*, Runtime_StringAdd) {
HandleScope scope(isolate);
ASSERT(args.length() == 2);
CONVERT_ARG_HANDLE_CHECKED(String, str1, 0);
CONVERT_ARG_HANDLE_CHECKED(String, str2, 1);
isolate->counters()->string_add_runtime()->Increment();
// NStal Note: ConsString here
return *isolate->factory()->NewConsString(str1, str2);
}
str3 = str.concat(str)
当concat的参数数量为1个的时候会返回str+str
// ECMA-262, section 15.5.4.6
function StringConcat() {
CHECK_OBJECT_COERCIBLE(this, "String.prototype.concat");
var len = %_ArgumentsLength();
var this_as_string = TO_STRING_INLINE(this);
// NStal Note: only one argument just use Runtime_StringAdd
if (len === 1) {
return this_as_string + %_Arguments(0);
}
var parts = new InternalArray(len + 1);
parts[0] = this_as_string;
for (var i = 0; i < len; i++) {
var part = %_Arguments(i);
parts[i + 1] = TO_STRING_INLINE(part);
}
return %StringBuilderConcat(parts, len + 1, "");
}
因此这两种情况600M是可以接受的。 当concat的参数超过一个或者使用Array.prototype.join 的事后最终会代理给Runtime_StringBuilderConcat方法去操作。这个方法不会生成ConsString而是真的复制成了SeqString。生成字符串明显超过了512M得限制,所以挂了。
说了那么多屁话得出结论是:你不能够读写超过512M的JSON文件,除非你自己实现JSON.stringify/parse。
Share