Nodejs 看看能打开吗,能打开吗
Nodejs 看看能打开吗,能打开吗
最近有位rrestjs框架的使用者YanQ报告给我这样一个错误,跟我说在用户post很多内容的文章时会crash进程然后报如下错误:(热心的老雷帮我解决了问题)
buffer.js: 523 throw new RangeError(‘targetStart out of bounds’); 具体报错的位置在:
var buf = Buffer.concat(rec_ary, rec_ary.length); 其中rec_ary就是存放buffer的数组。
当然这个bug是具有一定隐蔽性的,因为这个bug被报告为本地测试环境没有问题,而线上会有。所以一开始一直以为是node版本兼容性,系统兼容性等等的问题。
我们先看下官方api里buffer.concat方法的说明吧:
Class Method: Buffer.concat(list, [totalLength])# list Array List of Buffer objects to concat totalLength Number Total length of the buffers when concatenated … If totalLength is not provided, it is read from the buffers in the list. However, this adds an additional loop to the function, so it is faster to provide the length explicitly. 我个人觉得很具有迷惑性,total length of the buffers这句话怎么理解?
我理解为list的长度,需要连接的buffers的数量,但是真实的情况却是这样的,下面代码是node v0.10.2 / lib / buffer.js中的片段:
Buffer.concat = function (list, length) {… //省略
if (typeof length !== ‘number’) { length = 0; for (var i = 0; i < list.length; i++) { var buf = list[i]; length += buf.length; } }… //省略 }
原来这句话的正确理解是,需要连接的所有buffers的长度,也就是buffers的总长度,理解出现偏差造成了这个bug。正确的buffer.concat代码如下:
var bufAry = [] var bufLen = 0; for (var i = 0; i < 5; i++) { bufAry[i] = new Buffer("str is " + i + ‘\n’); bufLen += bufAry[i].length } console.log(bufLen) var newBuf = Buffer.concat(bufAry, bufLen) console.log(newBuf.toString()) 将输出:
45 str is 0 str is 1 str is 2 str is 3 str is 4 所以希望今后大家用到buffer.concat方法时要注意以下,第二个参数务必传递buffers的总长度,当然你也可以偷懒省略第二个参数,这样也不会有问题。
这次顺便也看了看buffer.js的源码,发现挺有意思,SlowBuffer这个类是直接调用c++的接口类,所以我们尽量不要去调用这个类,除非我们想自己维护一个独立buffer池。我们看如下代码:
Buffer.poolSize = 8 * 1024; var pool;
function allocPool() { pool = new SlowBuffer(Buffer.poolSize); pool.used = 0; } 分配buffer池的函数会将模块变量pool赋值为slowbuffer的实例,同时默认大小为8KB,那这个8KB对我们有什么意义呢?其实8KB只是一个存储的空间,如果我们有很多小的buffer,将会共用这8KB的存储空间,我们看代码如下:
function Buffer(subject, encoding, offset) { //buffer类 … //省略以上是一些参数的初始化,switch判断等等
if (this.length > Buffer.poolSize) { //this.length就是之前初始化的本buffer实例需要分配的内存bytes空间,跟8KB进行比较
// Big buffer, just alloc one.
this.parent = new SlowBuffer(this.length); //如果大于8KB则使用c++的api重新为它分配存储空间,并且将返回的实例赋值给this.parent,这样我们就知道了此buffer实例保存在了哪块内存中
this.offset = 0; //因为这块内存是此buffer实例第一个独享的,所以记录其偏移为0
} else if (this.length > 0) { //如果此buffer实例大于0但是小于8KB ,node认为是小的buffer
// Small buffer.
if (!pool || pool.length - pool.used < this.length) allocPool(); //如果当前8KB内存池不够存储了,则重新分配一个slowbuffer让此buffer实例存储
this.parent = pool; //将当前分配的内存buffer池赋值给 this.parent 方便之后的读取和剪切等
this.offset = pool.used; //将偏移赋值给此buffer实例
pool.used += this.length; //并且更新此buffer池的使用byte
if (pool.used & 7) pool.used = (pool.used + 8) & ~7;
//将pool.used按位与7,如果pool.used是8或8的倍数,则表达式pool.used & 7是false,否则为true
// (pool.used + 8) & ~7; 这里将返回大于pool.used最近的8的倍数
//比如我们pool.used的大小为9byte,则执行这行代码之后,pool.used会调整为16,6byte的内存浪费了,这也是很多文章建议我们申请小于8KB的内存的时候最好是8的倍数,避免造成浪费
} else { //如果是0长度的buffer,所以所有长度0的buffer都是 var zeroBuffer = new SlowBuffer(0);
// Zero-length buffer
this.parent = zeroBuffer;
this.offset = 0;
}… //处理一些数据,包括调用wrtie将buffer写入
SlowBuffer.makeFastBuffer(this.parent, this, this.offset, this.length);
//最后将这些参数通过slowbuffer的makeFastBuffer将内存地址和js的buffer实例做好关联引用
} 打开node_buffer.h,我们看到声明了Buffer类的static静态成员函数
static v8::Handle < v8::Value > MakeFastBuffer(const v8::Arguments & args); 打开node_buffer.cc,找到MakeFastBuffer的实现:
Handle < Value > Buffer::MakeFastBuffer(const Arguments & args) { HandleScope scope;
if (!Buffer::HasInstance(args[0])) {
return ThrowTypeError("First argument must be a Buffer"); //判断第一个函数是否是buffer类的实例
}
Buffer * buffer = ObjectWrap::Unwrap < Buffer > (args[0] - > ToObject()); //创建一个Buffer指针指向this.parent
Local < Object > fast_buffer = args[1] - > ToObject();; //定义V8的local<object>类型的fast_buffer指向this
uint32_t offset = args[2] - > Uint32Value(); //将偏移量和this.length保存成C++的uint32
uint32_t length = args[3] - > Uint32Value();
if (offset > buffer - > length_) { //如果偏移量比this.parent总长度还要长,则抛出异常
return ThrowRangeError(“offset out of range”);
}
if (offset + length > buffer - > length_) { //如果偏移量加上本buffer的长度大于this.parent的总长度,则抛出异常
return ThrowRangeError(“length out of range”);
}
// Check for wraparound. Safe because offset and length are unsigned.
if (offset + length < offset) { //如果偏移量或者length加起来小于偏移量,则抛出异常,一般不会出现
return ThrowRangeError(“offset or length out of range”);
}
fast_buffer - > SetIndexedPropertiesToExternalArrayData(buffer - > data_ + offset,
kExternalUnsignedByteArray,
length);
//最后调用v8接口的SetIndexedPropertiesToExternalArrayData方法将数据与js的对象建立起引用关系
return Undefined();
} node是通过new char[length]来申请内存空间保存buffer的,调用v8的SetIndexedPropertiesToExternalArrayData来建立引用关系,因为一块8KB的内存可能公用,所以要根据buffer - > data和偏移量取得指定的内存地址。C++中buffer的data属性就是指向这块slowbuffer的指针。值得注意的是V8的手册上有这样的说明:
Note: The embedding program still owns the data and needs to ensure that the backing store is preserved while V8 has a reference. 总结一下: 1. 大家盛传的8KB内存空间池是小buffer的载体,所以如果很悲催的每次保存都是4097byte(略大于4096byte),那么内存可能就会造成很大的浪费,每个4097byte的块都将占用8KB。 2. slowbuffer并不是如字面意思那样,不是慢速的buffer,只是大于8KB的或者当buffer池不够用的情况下会用slowbuffer来申请空间,大于8KB或者直接用new slowbuffer出来的这个空间将是独享的,小于8KB的buffer将会共享内存空间。 3. 一定要确保buffer的正确释放,不然可能存在内存泄露。
最后我们做个简单的实验,模拟一个比较严重的内存泄露情况:
var os = require(‘os’); var leak_buf_ary = []; var show_memory_usage = function () { //打印系统空闲内存 console.log('free mem : ’ + Math.ceil(os.freemem() / (1024 * 1024)) + ‘mb’); }
var do_buf_leak = function () { var leak_char = ‘l’; //泄露的几byte字符 var loop = 100000; //10万次 var buf1_ary = [] while (loop–) { buf1_ary.push(new Buffer(4096)); //申请buf1,占用4096byte空间,会得到自动释放
//申请buf2,占用几byte空间,将其引用保存在外部数据,不会自动释放
//*******
leak_buf_ary.push(new Buffer(loop + leak_char));
//*******
}
console.log("before gc")
show_memory_usage();
buf1_ary = null;
return;
}
console.log(“process start”) show_memory_usage()
do_buf_leak();
var j = 10000; setInterval(function () { console.log(“after gc”) show_memory_usage() }, 1000 * 60) 第一次我们将内存泄漏点那行代码注释掉,运行4分钟后,得到如下打印信息,V8已经自动把我分配的内存释放掉了,free men又回到了开始的数值,很遗憾我们无法手动去对buffer进行gc
process start free mem: 5362mb before gc free mem: 5141mb after gc free mem: 5163mb after gc free mem: 5151mb after gc free mem: 5148mb after gc free mem: 5556mb 第二次我们将泄漏点那行代码放开,让全局变量leak_buf_ary始终引用着buffer,同样执行10分钟我们看结果:
process start free mem: 5692mb before gc free mem: 4882mb after gc free mem: 4848mb after gc free mem: 4842mb after gc free mem: 4843mb after gc free mem: 4816mb after gc free mem: 4822mb after gc free mem: 4816mb after gc free mem: 4809mb after gc free mem: 4810mb after gc free mem: 4831mb after gc free mem: 4830mb 虽然我们释放了4096byte的buffer,但是由于那几byte的字节没有释放掉,将会造成整个8KB的内存都无法释放,如果继续执行循环最终我们的系统内存将耗尽,程序将crash。同样由于我们是依次循环分配4096 + 几byte内存的,所以每块8KB的内存空间都将浪费409Xbyte,在执行循环之后,我们明显发现第二次的内存占用比第一次要大很多。这里我们将近多出了300MB左右的内存消耗。
Nodejs 看看能打开吗,能打开吗
问题背景
最近有一位使用restjs
框架的开发者YanQ报告了一个问题,他在用户提交大量内容的文章时遇到了进程崩溃的问题。具体错误信息如下:
buffer.js: 523
throw new RangeError('targetStart out of bounds');
错误发生在以下代码行:
var buf = Buffer.concat(rec_ary, rec_ary.length);
其中rec_ary
是一个存放Buffer
对象的数组。
问题分析
起初,这个问题被认为可能是由于Node版本兼容性或系统兼容性问题导致的。然而,经过排查后发现,问题的核心在于Buffer.concat
方法的使用上。
Buffer.concat
方法解析
Buffer.concat
方法用于将多个Buffer
对象合并成一个新的Buffer
对象。其API定义如下:
Class Method: Buffer.concat(list, [totalLength])
list
: 存放Buffer
对象的数组。totalLength
: 合并后的Buffer
对象的总长度。
官方文档中提到totalLength
表示所有Buffer
对象的总长度。但在实际应用中,很多人可能会误解为list
数组的长度。实际上,totalLength
应该是指所有Buffer
对象的总长度,而不是数组的长度。
示例代码及解释
var bufAry = [];
var bufLen = 0;
for (var i = 0; i < 5; i++) {
bufAry[i] = new Buffer("str is " + i + '\n');
bufLen += bufAry[i].length;
}
console.log(bufLen); // 输出: 45
var newBuf = Buffer.concat(bufAry, bufLen);
console.log(newBuf.toString());
上述代码创建了5个Buffer
对象,并计算它们的总长度。然后使用Buffer.concat
方法将这些Buffer
对象合并成一个新的Buffer
对象,并输出结果。
关键点总结
Buffer.concat
方法:使用时必须提供所有Buffer
对象的总长度作为第二个参数。- 内存管理:Node.js通过内部的内存池机制来管理小的
Buffer
对象,避免频繁的内存分配和释放。但需要注意的是,如果存在内存泄漏,可能导致系统内存耗尽。
内存泄漏实验
为了演示内存泄漏的影响,可以进行如下实验:
var os = require('os');
var leak_buf_ary = [];
var show_memory_usage = function () {
console.log('free mem : ' + Math.ceil(os.freemem() / (1024 * 1024)) + 'mb');
};
var do_buf_leak = function () {
var leak_char = 'l'; // 泄露的字符
var loop = 100000; // 循环次数
var buf1_ary = [];
while (loop--) {
buf1_ary.push(new Buffer(4096)); // 申请4096字节的Buffer,会被自动释放
leak_buf_ary.push(new Buffer(loop + leak_char)); // 申请少量字节的Buffer,不会自动释放
}
console.log("before gc");
show_memory_usage();
buf1_ary = null;
return;
};
console.log("process start");
show_memory_usage();
do_buf_leak();
var j = 10000;
setInterval(function () {
console.log("after gc");
show_memory_usage();
}, 1000 * 60);
这段代码展示了如何模拟内存泄漏,并观察内存使用情况的变化。通过对比内存使用情况,可以看到内存泄漏对系统资源的影响。
结论
在使用Buffer.concat
方法时,确保提供正确的totalLength
参数非常重要。此外,注意内存管理和避免内存泄漏,以防止潜在的系统资源耗尽问题。
针对标题“Nodejs 看看能打开吗,能打开吗”,这个问题看起来是关于buffer.concat
方法导致的错误。从提供的背景信息来看,该问题涉及到如何正确使用buffer.concat
方法,特别是如何正确传递参数以避免错误。
根据上述内容,如果rec_ary
数组中存放了许多Buffer
对象,并且想要将它们合并成一个新的Buffer
对象,需要注意buffer.concat
方法的第二个参数应该传递的是所有Buffer
对象的总长度。如果这个长度不正确,可能会导致诸如RangeError
的错误,如“targetStart out of bounds”。
示例代码
// 创建几个 Buffer 对象并存入数组
var rec_ary = [];
for (var i = 0; i < 5; i++) {
rec_ary.push(new Buffer("str is " + i + '\n'));
}
// 计算 Buffer 数组的总长度
var totalLength = 0;
for (var i = 0; i < rec_ary.length; i++) {
totalLength += rec_ary[i].length;
}
// 使用 buffer.concat 方法合并 Buffer 对象
try {
var newBuf = Buffer.concat(rec_ary, totalLength);
console.log(newBuf.toString());
} catch (err) {
console.error('Error:', err.message);
}
解释
- 创建多个Buffer对象:首先,创建一系列
Buffer
对象并存入数组rec_ary
中。 - 计算总长度:计算所有
Buffer
对象的总长度,这是为了正确调用buffer.concat
方法。 - 合并Buffer对象:使用
buffer.concat
方法合并这些Buffer
对象,并确保传递正确的总长度。这样可以避免“targetStart out of bounds”的错误。
正确理解totalLength
参数的意义是解决这一问题的关键。如果忘记提供正确的总长度,Node.js会尝试遍历所有Buffer
对象来计算总长度,但这种行为在某些情况下可能会引发错误。