Nodejs buffer.concat引出的bug(真可以打开,走传送门吧)
Nodejs buffer.concat引出的bug(真可以打开,走传送门吧)
最近有位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左右的内存消耗。
上个贴个图,发现和我一样理解错误的人不在少数,希望博主看见尽快修改,不要误导大众,呵呵 地址:http://blog.csdn.net/jklfjsdj79hiofo/article/details/7873065
Nodejs buffer.concat 引出的 bug(真可以打开,走传送门吧)
最近有位使用 rrestjs 框架的用户 YanQ 向我报告了一个错误,他提到在用户发布大量内容的文章时,服务端进程会崩溃并报出如下错误:
buffer.js:523
throw new RangeError('targetStart out of bounds');
具体的报错位置在:
var buf = Buffer.concat(rec_ary, rec_ary.length);
其中 rec_ary
是存放 Buffer 的数组。
这个 bug 具有一定的隐蔽性,因为它只在生产环境中出现,而在本地测试环境中没有问题。因此,一开始我一直以为是 Node 版本兼容性或系统兼容性等问题。
首先,我们来看一下官方文档中关于 Buffer.concat
方法的说明:
Class Method: Buffer.concat(list, [totalLength])
# list: 存放 Buffer 对象的数组
# totalLength: 总长度,即所有 Buffer 对象的总长度
...
如果未提供 `totalLength`,则会从 `list` 中读取。然而,这样做会增加额外的循环,因此最好显式地提供长度。
在官方文档中,“totalLength
是所有 Buffer 对象的总长度” 这句话很容易引起误解。许多人可能会误以为 totalLength
是 list
数组的长度,但实际上它指的是所有 Buffer 对象的总长度。
下面是 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;
}
}
... // 省略
}
这段代码表明,如果没有提供 totalLength
参数,Buffer.concat
方法会遍历 list
数组,计算所有 Buffer 对象的总长度。
正确的 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
方法时,注意传递 totalLength
参数,明确指定所有 Buffer 对象的总长度。如果你偷懒省略这个参数,虽然也不会出现问题,但会导致性能损失。
此外,我还顺便研究了一下 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。这意味着,如果有很多小的 Buffer,它们将共用这个 8KB 的存储空间。
为了更好地理解这一点,可以看下面的代码:
function Buffer(subject, encoding, offset) {
... // 省略参数初始化等操作
if (this.length > Buffer.poolSize) {
// 大 buffer,直接分配
this.parent = new SlowBuffer(this.length);
this.offset = 0;
} else if (this.length > 0) {
// 小 buffer,共用内存池
if (!pool || pool.length - pool.used < this.length) allocPool();
this.parent = pool;
this.offset = pool.used;
pool.used += this.length;
if (pool.used & 7) pool.used = (pool.used + 8) & ~7;
} else {
// 零长度 buffer
this.parent = new SlowBuffer(0);
this.offset = 0;
}
...
}
这段代码展示了如何根据 Buffer 的长度决定是否使用内存池。如果长度超过 8KB,则使用新的内存空间;否则,使用内存池中的空间。
总之,确保正确使用 Buffer.concat
方法和管理好 Buffer 的生命周期,以避免潜在的内存泄漏问题。
这难道也是buffer.concat引出的bug。。。呵呵
发到http://angularjs.cn/试一试
发了,也挂了,估计是markdown的问题
本地测试木有问题,可能是因为本地网速较快,数据一次就接收完毕了,没有触发多个data事件。
hoho~这个就不清楚啦,我的那篇文章写到cnode里,直接打不开了,不知道什么原因啊
关于 Node.js
中 Buffer.concat
方法引发的错误,主要在于传递给该方法的第二个参数 totalLength
的误解。这个参数应该表示所有待连接的 Buffer
对象的总长度,而不是数组的长度。
示例代码解释:
var bufAry = [];
var bufLen = 0;
// 创建5个不同长度的Buffer对象
for (var i = 0; i < 5; i++) {
bufAry[i] = new Buffer("str is " + i + '\n');
bufLen += bufAry[i].length; // 累加每个Buffer对象的长度
}
console.log(bufLen); // 输出总长度
// 使用Buffer.concat方法连接所有Buffer对象,并提供总长度作为参数
var newBuf = Buffer.concat(bufAry, bufLen);
console.log(newBuf.toString());
这段代码展示了如何正确使用 Buffer.concat
方法。首先,我们需要计算所有 Buffer
对象的总长度 bufLen
,然后使用这个总长度作为 Buffer.concat
的第二个参数。
错误原因:
错误发生是因为开发人员误解了 totalLength
参数的含义。他们认为 totalLength
应该是数组的长度,而不是所有 Buffer
对象的总长度。因此,当他们使用不正确的长度时,Buffer.concat
会尝试访问超出范围的内存位置,导致错误:
throw new RangeError('targetStart out of bounds');
解决方案:
确保在使用 Buffer.concat
方法时,传递正确的总长度。如果不确定总长度,可以先遍历数组计算总长度,然后再进行拼接操作。
总结:
Buffer.concat
方法的第一个参数是包含多个Buffer
对象的数组。- 第二个参数是所有
Buffer
对象的总长度,而不是数组的长度。 - 在使用
Buffer.concat
方法时,确保传递正确的总长度以避免错误。