我的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。