精华 浅析nodejs的buffer类
发布于 11 年前 作者 DoubleSpout 99889 次浏览 最后一次编辑是 8 年前

最近翻阅了node v0.10.4的buffer类的源代码,收获不少,也很久没有在cnode上发表文章了,想把一些收获分享给大家,有什么错误的地方希望大牛们指正啊。

前阵子有位rrestjs框架的使用者YanQ报告给我这样一个错误,跟我说在用户post很多内容的文章时会crash进程然后报如下错误:(热心的老雷帮我解决了问题)

buffer.js:523
    throw new RangeError('targetStart out of bounds');

错误的原因是api上Class Method: Buffer.concat(list, [totalLength])的第二个参数 totalLength 是list中所存储的所有buffer.length的最大小,而不是list的长度,这边大家需要注意下啊。

言归正传,简单总结一下吧: 1、什么时候该用buffer,什么时候不该用 我看一下如下的测试代码,分别是拼接各种不同长度的字符串,最后直接拼接了10MB的字符串

var string,string2,string3;
var bufstr,bufstr2,bufstr3;
var j;

console.time('write 100 string')
for(j=0;j<1000;j++){
	var x = j+'';
	string += x;
}
console.timeEnd('write 100 string')

console.time('write 100 buffer')
bufstr = new Buffer(100)
for(j=0;j<1000;j++){
	var x = j+'';
	bufstr.write(x,j);
}
console.timeEnd('write 100 buffer')


console.time('write 100000 string')
for(j=0;j<100000;j++){
	var x = j+'';
	string2 += x;
}
console.timeEnd('write 100000 string')

console.time('write 100000 buffer')
bufstr2 = new Buffer(100000)
for(j=0;j<100000;j++){
	var x = j+'';
	bufstr2.write(x,j);
}
console.timeEnd('write 100000 buffer')

console.time('write 1024*1024*10 string')
for(j=0;j<1024*1024*10;j++){
	var x = j+'';
	string3 += x;
}
console.timeEnd('write 1024*1024*10 string')

console.time('write 1024*1024*10 buffer')
bufstr3 = new Buffer(1024*1024*10)
for(j=0;j<1024*1024*10;j++){
	var x = j+'';
	bufstr3.write(x,j);
}
console.timeEnd('write 1024*1024*10 buffer')

接着是输出结果:

write 100 string: 0ms
write 100 buffer: 6ms
write 100000 string: 37ms
write 100000 buffer: 150ms
write 1024*1024*10 string: 4262ms
write 1024*1024*10 buffer: 8904ms

读取速度都不需要测试了,肯定string更快,buffer还需要toString()的操作。 所以我们在保存字符串的时候,该用string还是要用string,就算大字符串拼接string的速度也不会比buffer慢。 那什么时候我们又需要用buffer呢?没办法的时候,当我们保存非utf-8字符串,2进制等等其他格式的时候,我们就必须得使用了。

2、buffer不得不提的8KB

buffer著名的8KB载体,举个例子好比,node把一幢大房子分成很多小房间,每个房间能容纳8个人,为了保证房间的充分使用,只有当一个房间塞满8个人后才会去开新的房间,但是当一次性有多个人来入住,node会保证要把这些人放到一个房间中,比如当前房间A有4个人住,但是一下子来了5个人,所以node不得不新开一间房间B,把这5个人安顿下来,此时又来了4个人,发现5个人的B房间也容纳不下了,只能再开一间房间C了,这样所有人都安顿下来了。但是之前的两间房A和B都各自浪费了4个和3个位置,而房间C就成为了当前的房间。

具体点说就是当我们实例化一个新的Buffer类,会根据实例化时的大小去申请内存空间,如果需要的空间小于8KB,则会多一次判定,判定当前的8KB载体剩余容量是否够新的buffer实例,如果够用,则将新的buffer实例保存在当前的8KB载体中,并且更新剩余的空间。

我们做个简单的实验,模拟一个比较严重的内存泄露情况:

第一次我们将内存泄漏点那行代码注释掉,运行4分钟后,得到如下打印信息,V8已经自动把我分配的内存释放掉了,free men又回到了开始的数值,第二次我们将泄漏点那行代码放开,让全局变量 leak_buf_ary 始终引用着buffer,同样执行10分钟

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)

第一次结果:

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

第二次结果:

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左右的内存消耗。

3、buffer字符串的连接 我们接受post数据时,node是以流的形式发送上来的,会触发ondata事件,所以我们见到很多代码是这样写的:

var http = require('http');
 http.createServer(function (req, res) {
  
  var body = '';
  req.on('data',function(chunk){
	//console.log(Buffer.isBuffer(chunk))
	body +=chunk
  })
  req.on('end',function(){
	 console.log(body)
	 res.writeHead(200, {'Content-Type': 'text/plain'});
         res.end('Hello World\n');
  })
  
 
}).listen(8124);

console.log('Server running at http://127.0.0.1:8124/');

下面我们比较一下两者的性能区别,测试代码:

var buf = new Buffer('nodejsv0.10.4&nodejsv0.10.4&nodejsv0.10.4&nodejsv0.10.4&');
console.time('string += buf')
var s = '';
for(var i=0;i<10000;i++){
	s += buf;
}
s;
console.timeEnd('string += buf')


console.time('buf concat')
var list = [];
var len=0;
for(var i=0;i<10000;i++){
	list.push(buf);
	len += buf.length;
}
var s2 = Buffer.concat(list, len).toString();
console.timeEnd('buf concat')

输出结果,相差近一倍:

string += buf: 15ms
buf concat: 8ms

在1000次拼接过程中,两者的性能几乎相差一倍,而且当客户上传的是非UTF8的字符串时,直接+=还容易出现错误。

4、独享的空间 如果你想创建一个独享的空间,独立的对这块内存空间进行读写,有两种办法,1是实例化一个超过8KB长度的buffer,另外一个就是使用slowbuffer类。

5、buffer的释放 很遗憾,我们无法手动对buffer实例进行GC,只能依靠V8来进行,我们唯一能做的就是解除对buffer实例的引用。

6、清空buffer 刷掉一块buffer上的数据最快的办法是buffer.fill

最后如果也想看看buffer源码,希望我的博客对你有帮助: 浅析node的buffer模块(一创建) 浅析node的buffer模块(二写入) 浅析node的buffer模块(三读取)

24 回复

好帖,顶起来!!!

支持,标记,收藏。

用process.memoryUsage()不是能更准确说明nodejs的内存占用情况吗。用os.freemem()的话,万一运行测试期间操作系统的其他进程释放/占用内存,测试结果就受到影响。例如buffer内存占用测试的第一次结果5362mb-5556mb<0,怎么可能nodejs跑起来后空闲内存反而多100多mb。

仔细看了并运行的你的实例 有一个疑问: console.time(‘write 100 buffer’) bufstr = new Buffer(100) for(j=0;j<1000;j++){ var x = j+’’; bufstr.write(x,j); } console.timeEnd(‘write 100 buffer’)

这段代码new的buffer是100byte,但是循环了1000次,虽然程序能正常,但是我想问一下当循环到101次的时候write是怎样去操作的呢?

buffer.fill() ?

确实,使用process.memoryUsage().rss更加直观,这里的测试主要是要展示内存被buffer抓住不释放的问题,至于你说的5362mb-5556mb<0这个问题有可能是其他应用程序的退出,我是在windows下测试的。 谢谢你的提醒~

snoopy的文章质量一直很高!

@snoopy 一打开cnodejs就显示 snoppy回复于1秒前

这个问题好,事实上当101次的时候确实写入了内存了,因为buf.write返回值是1,但是由于当前buf实例的长度只有100,所以你是无法访问那块内存区域的,并且由于8KB的限制,当你的偏移offset超过1024*8这个数值后,就会报"Offset is out of bounds"错误,所以还真得注意不要这么做

@leizongmin 打了两个错别字,删掉了。。hoho~

高质量啊!

@snoopy 我用v0.10.13,当运行到101次的时候就开始报 attempt to write beyond buffer bound的错误,我看了一下源码,确实对offset做了判断,但是对此做的判断我不知道是不是一个bug

var bufstr = new Buffer(‘abcde’) var x = ‘x’; var y = ‘y’; var z = ‘z’; bufstr.write(x,4); console.log(bufstr.toString());

bufstr.write(y,5); console.log(bufstr.toString());

bufstr.write(y,6); console.log(bufstr.toString());

当offset为6的时候,开始报错,但是offset应该是从0-4,所以我认为应该从5就应该开始报错,不知道为什么没有报错…

@nodehugo 至于为什么写 write(‘y’,6) 抛错,看下面的buffer.js的源码就

  Buffer.prototype.write = function(string, offset, length, encoding) {
  ...
  offset = +offset || 0;
  //offset=6
  var remaining = this.length - offset;
  //remaining =-1
  ...
  if (string.length > 0 && (length < 0 || offset < 0));
  //length<0 为true所以执行if的条件
      throw new RangeError('attempt to write beyond buffer bounds');
  ..
  }

那么我们在执行write(‘y’,5)时为什么不抛错,做了哪些事情呢?

   case 'utf-8':
      ret = this.parent.utf8Write(string, this.offset + offset, length);
      //这里string  = 'y'
      //this.offset + 5
      //length = 0
      break;

我们看node_buffer.cc大约337行

   template <encoding encoding>
   Handle<Value> Buffer::StringWrite(const Arguments& args) {
     ...
     size_t offset = args[1]->Int32Value();
     size_t max_length = args[2]->IsUndefined() ? buffer->length_ - offset
                                         : args[2]->Uint32Value();
     //第二个参数此时传递进来时0,所以max_length 为 args[2]->Uint32Value()
     max_length = MIN(buffer->length_ - offset, max_length);

     //这边取两者的小的一方,结果为0   

     if (max_length == 0) {
     // shortcut: nothing to write anyway
     //这里发现长度为0,没有什么东西要写入,所以返回0
     Local<Integer> val = Integer::New(0);
     constructor_template->GetFunction()->Set(chars_written_sym, val);
     return scope.Close(val);
     }
     ...
    }

所以将你的代码改写下:

     var bufstr = new Buffer('abcde')
     var x = 'x';
     var y = 'y';
     var z = 'z';

     var ret = bufstr.write(y,5);
     console.log(ret);

将会打印出:0 表示没有东西写入buffer,所以建议不要去这么写代码,绕了一大圈,没有意义

学习学习··

@snoopy 你分析的很正确,但我想说的是业务逻辑,它是否应该在5的时候就报错,因为当为5的时候,没有东西写入,这个操作相当于没有作用,而他之所以在后面版本加入错误,就是不希望进行一些无用offset的操作,我认为5也应该算在内,加一个=号应该就可以了。

if (string.length > 0 && (length < = 0 || offset < 0)) throw new RangeError(‘attempt to write beyond buffer bounds’);

@nodehugo 这个就要坐等官方的fixed了,或者提交pull request去~

膜拜大神佳作

@snoopy 最新版本直接就报错了 RangeError: attempt to write beyond buffer bounds

吴老师和老雷是好基友

按照楼主的方法测试buffer与string谁更快,谁更慢时,出现了不定的结果。多次执行,有时后buffer会更快些,有时候string会更快。node.js现在buffer更加高效了吗?

回到顶部