Node Buffer/Stream 内存策略分析
2015-09-01 00:40
866 查看
在Node 中,Buffer 是一个广泛用到的类,本文将从以下层次来分析其内存策略:
User 层面,即Node lib/*.js 或用户自己的Js 文件调用 new Buffer
Socekt read/write
File read/write
SlowBuffer 为 src/node_buffer.cc 导出,当用户调用new Buffer时 ,如果你要申请的空间大于8K,Node 会直接调用SlowBuffer ,如果小于8K ,新的Buffer 会建立在当前slab 之上:
新创建的Buffer的 parent成员变量会指向这个slab ,
offset 变量指向在这个slab 中的偏移:
比如当你需要2K 的空间时 : new Buffer(21024),它会检查这个slab 的剩余空间,如果有剩余,则分配给你这段可用空间,并把当前 slab 的已用空间 used += 21024 比如当我们连续两次调用new Buffer(2*1024)时 :
![](http://static.data.taobaocdn.com/up/nodeclub/2011/11/bs1.png)
当我们再次申请一个5K 的空间时,当前的pool 仅有4K 可用,所以这时node会再次申请一个8K 的slab ,并把当前的pool 指向它 ,注意此时原先的slab 会有4K空间被浪费:
![](http://static.data.taobaocdn.com/up/nodeclub/2011/11/bs2.png)
此时原先的slab 被两个2K 的 Buffer 所引用,所以当这两个Buffer 引用都变为null 后,V8 认为可以销毁这个slab。
注意,假如我们的某一个slab被一个1Byte 的Buffer 所引用 ,那么,即使其他所有的引用都已经变为null ,这块8K 的slab 也不会被回收:
![](http://static.data.taobaocdn.com/up/nodeclub/2011/11/bs3.png)
首先让我们看stream read 的情况: 在stream_wrap 当中,此时的策略与用户层的 new Buffer 相似,只是slab 的 size 变为 1MB ,此时我们需要考虑socket “读操作” 缓冲区大小问题,设想以下,假如我们数据长度为30K,而我们的缓冲区大小仅为2K,这意味着我们至少调用15次socket read操作,要触发15次 on(“data”) 事件,每次都需要把这个事件及数据从libuv 层次传递到用户js 层次,这是极其低效的,所以我们需要设置一个较大的缓冲区,在libuv 的 unix/stream.c ,当绑定socket 的 watcher read 事件被触发时,会调用uv__read 函数,其固化了buffer 大小为64*1024
alloc_cb 定义在 stream_wrap.cc 中
![](http://static.data.taobaocdn.com/up/nodeclub/2011/11/bs4.png)
但事实上我们知道,我们socket read 一般很少会有64K 大小,比如假如nread 仅为 2k,此时我们为了避免浪费,可以重设slab_used :
![](http://static.data.taobaocdn.com/up/nodeclub/2011/11/bs51.png)
敬请注意,我们之所以能够这么做,是因为当检测到socket 上read事件时才分配缓冲区, alloc_cb →socket read → read callback 这一过程是顺序进行的,没有外来的干扰!(我不明白为何node 还要加上一次判断 if (handle_that_last_alloced == handle) ,深究的可以告诉我)
我们看到,在socket read 的情况下,缓冲区的管理在stream_wrap 中控制,uv steram.c 执行读操作,返回的回调函数也是在stream_wrap 中定义,然后把读取到的Buffe 层层传递给user 的js当中,即我们的on(“data”) 事件,这个过程中没有额外的内存拷贝,还是相当高效的, 不过有个问题:假使你持久引用了一个有stream.read 上浮的Buffer ,你将导致其所引用的那个1M 的slab 得不到释放!
我们在来看 Socket.prototype.write ,当你传入一个 string 时,node 会自动生成一个Buffer ,如果你本身就是Buffer ,那就省了这一步 (注意调用的是user 层面的 new Buffer):
然后这个Buffer 对应的指针会层层传递,直至 uv 的stream.c 的相应的 write 函数,这个过程也不会再有额外的拷贝操作,尤其要注意的是:当你直接传入一个Buffer 时,直至socket.write 回调返回表示结束,此过程中你不应该再修改它,因为底层正在或将要操作它!
关于IO 操作时bufsize 大小的重要性,上文已有介绍,记得APUE 中 steven 老先生也有专门的测试结果,此处不再赘述,
在 fs.ReadStream 时,我们可以传入一些参数:
默认bufsize 为 64K ,但在 lib/fs.js 中,还有一个poolSize 控制变量:
当node 最终实际调用fs.read 时:
Node 会对用户传入的bufsize 与 当前pool 的剩余空间作比较,取其小者而用之,所以默认的64*1024 大小其实是永远不会生效的。
好吧,40K 大小也可以接受,但如果你要读取的文件比较小,比如1K ,2K 级别的比较多,这时我们预留40K 的buf ,当读返回时,其实只用到了1K 或 2K ,这时候,Node 不会再像socket.read 那样,再把 pool.used 减去 39K 或 38K ,因为我们实际的fs.read 操作是在另一独立线程中执行的,即 buf alloc → fs read → read cb 这一个过程不是顺序的,我们不能再像socket.read 那样重新设置pool used !这种情况下内存的浪费相当严重!
所以当你想缓存大量小文件时,如静态服务器,我的建议是:自己分配大块Buffer ,然后把从fs.readStream 上浮的Buffer 拷贝到我们自己的大块Buffer 中,然后在这个大块Buffer 上做 slice生成相应的小Buffer ,这样我们就没有引用readStream 上浮的Buffer ,使其可以被V8 回收,当然如果你内存足够你挥霍,当我啥都没说…
这个函数的内存操作很单纯:
其实通过上面分析可知,一个繁忙的网络服务器,很可能会频繁的new/delete 8K / 1M 的内存块,如果是静态文件服务,可能还会有频繁的40K 内存块的操作,所以我试着对node 添加了 8K 内存块的内存池控制,服务繁忙时命中率无限接近100%,可惜总体性能提升没有达到预期,在此就不现拙了,有兴趣的同学可以自己hack 玩玩,有成果了可以知会我一声(http://weibo.com/windyrobin)…
不要轻易持久引用由 socket.readStream 或 fs.readStream 上浮的Buffe
当你调用stream.write 并直接传递Buffer 进去时,在此操作返回之前,你不应该再修改它
当调用fs.readStream 时,如果你对文件大小有估值,尽量传入较接近的bufsize
当你持久引用一个Buffer 时,哪怕它只有一个字节,也可能导致其依赖的slab (可能是8K /1M…)得不到释放
附:以上分析基于node 0.6 系列,就这方面的问题,我已提交了几个Issue 给 Node 官方,开发人员正在对以上暴露的问题就行改进:
转自:https://cnodejs.org/topic/4f16442ccae1f4aa27001067
User 层面,即Node lib/*.js 或用户自己的Js 文件调用 new Buffer
Socekt read/write
File read/write
User Buffer
在 lib/buffer.js 模块中,有个模块私有变量 pool, 它指向当前的一个8K 的slab :Buffer.poolSize = 8 * 1024; var pool; function allocPool() { pool = new SlowBuffer(Buffer.poolSize); pool.used = 0; }
SlowBuffer 为 src/node_buffer.cc 导出,当用户调用new Buffer时 ,如果你要申请的空间大于8K,Node 会直接调用SlowBuffer ,如果小于8K ,新的Buffer 会建立在当前slab 之上:
新创建的Buffer的 parent成员变量会指向这个slab ,
offset 变量指向在这个slab 中的偏移:
if (!pool || pool.length - pool.used < this.length) allocPool(); this.parent = pool; this.offset = pool.used; pool.used += this.length;
比如当你需要2K 的空间时 : new Buffer(21024),它会检查这个slab 的剩余空间,如果有剩余,则分配给你这段可用空间,并把当前 slab 的已用空间 used += 21024 比如当我们连续两次调用new Buffer(2*1024)时 :
![](http://static.data.taobaocdn.com/up/nodeclub/2011/11/bs1.png)
当我们再次申请一个5K 的空间时,当前的pool 仅有4K 可用,所以这时node会再次申请一个8K 的slab ,并把当前的pool 指向它 ,注意此时原先的slab 会有4K空间被浪费:
![](http://static.data.taobaocdn.com/up/nodeclub/2011/11/bs2.png)
此时原先的slab 被两个2K 的 Buffer 所引用,所以当这两个Buffer 引用都变为null 后,V8 认为可以销毁这个slab。
注意,假如我们的某一个slab被一个1Byte 的Buffer 所引用 ,那么,即使其他所有的引用都已经变为null ,这块8K 的slab 也不会被回收:
![](http://static.data.taobaocdn.com/up/nodeclub/2011/11/bs3.png)
首先让我们看stream read 的情况: 在stream_wrap 当中,此时的策略与用户层的 new Buffer 相似,只是slab 的 size 变为 1MB ,此时我们需要考虑socket “读操作” 缓冲区大小问题,设想以下,假如我们数据长度为30K,而我们的缓冲区大小仅为2K,这意味着我们至少调用15次socket read操作,要触发15次 on(“data”) 事件,每次都需要把这个事件及数据从libuv 层次传递到用户js 层次,这是极其低效的,所以我们需要设置一个较大的缓冲区,在libuv 的 unix/stream.c ,当绑定socket 的 watcher read 事件被触发时,会调用uv__read 函数,其固化了buffer 大小为64*1024
... buf = stream->alloc_cb((uv_handle_t*)stream, 64 * 1024); ...
alloc_cb 定义在 stream_wrap.cc 中
uv_buf_t StreamWrap::OnAlloc(uv_handle_t* handle, size_t suggested_size)
![](http://static.data.taobaocdn.com/up/nodeclub/2011/11/bs4.png)
但事实上我们知道,我们socket read 一般很少会有64K 大小,比如假如nread 仅为 2k,此时我们为了避免浪费,可以重设slab_used :
if (handle_that_last_alloced == handle) { slab_used -= (buf.len - nread); }
![](http://static.data.taobaocdn.com/up/nodeclub/2011/11/bs51.png)
敬请注意,我们之所以能够这么做,是因为当检测到socket 上read事件时才分配缓冲区, alloc_cb →socket read → read callback 这一过程是顺序进行的,没有外来的干扰!(我不明白为何node 还要加上一次判断 if (handle_that_last_alloced == handle) ,深究的可以告诉我)
我们看到,在socket read 的情况下,缓冲区的管理在stream_wrap 中控制,uv steram.c 执行读操作,返回的回调函数也是在stream_wrap 中定义,然后把读取到的Buffe 层层传递给user 的js当中,即我们的on(“data”) 事件,这个过程中没有额外的内存拷贝,还是相当高效的, 不过有个问题:假使你持久引用了一个有stream.read 上浮的Buffer ,你将导致其所引用的那个1M 的slab 得不到释放!
我们在来看 Socket.prototype.write ,当你传入一个 string 时,node 会自动生成一个Buffer ,如果你本身就是Buffer ,那就省了这一步 (注意调用的是user 层面的 new Buffer):
// Change strings to buffers. SLOW if (typeof data == 'string') { data = new Buffer(data, encoding); }
然后这个Buffer 对应的指针会层层传递,直至 uv 的stream.c 的相应的 write 函数,这个过程也不会再有额外的拷贝操作,尤其要注意的是:当你直接传入一个Buffer 时,直至socket.write 回调返回表示结束,此过程中你不应该再修改它,因为底层正在或将要操作它!
文件读写
regular file 的write 和 socket 比较类似,没什么亮点,我们重点来看 file read。关于IO 操作时bufsize 大小的重要性,上文已有介绍,记得APUE 中 steven 老先生也有专门的测试结果,此处不再赘述,
在 fs.ReadStream 时,我们可以传入一些参数:
{ flags: 'r', encoding: null, fd: null, mode: 0666, bufferSize: 64 * 1024 }
默认bufsize 为 64K ,但在 lib/fs.js 中,还有一个poolSize 控制变量:
var kPoolSize = 40 * 1024;
当node 最终实际调用fs.read 时:
var thisPool = pool; var toRead = Math.min(pool.length - pool.used, this.bufferSize); var start = pool.used;
Node 会对用户传入的bufsize 与 当前pool 的剩余空间作比较,取其小者而用之,所以默认的64*1024 大小其实是永远不会生效的。
好吧,40K 大小也可以接受,但如果你要读取的文件比较小,比如1K ,2K 级别的比较多,这时我们预留40K 的buf ,当读返回时,其实只用到了1K 或 2K ,这时候,Node 不会再像socket.read 那样,再把 pool.used 减去 39K 或 38K ,因为我们实际的fs.read 操作是在另一独立线程中执行的,即 buf alloc → fs read → read cb 这一个过程不是顺序的,我们不能再像socket.read 那样重新设置pool used !这种情况下内存的浪费相当严重!
所以当你想缓存大量小文件时,如静态服务器,我的建议是:自己分配大块Buffer ,然后把从fs.readStream 上浮的Buffer 拷贝到我们自己的大块Buffer 中,然后在这个大块Buffer 上做 slice生成相应的小Buffer ,这样我们就没有引用readStream 上浮的Buffer ,使其可以被V8 回收,当然如果你内存足够你挥霍,当我啥都没说…
内存池
再来看底层的node_buffer :void Buffer::Replace(char *data, size_t length, free_callback callback, void *hint)
这个函数的内存操作很单纯:
…. delete [] data_; …. data_ = new char[length_];
其实通过上面分析可知,一个繁忙的网络服务器,很可能会频繁的new/delete 8K / 1M 的内存块,如果是静态文件服务,可能还会有频繁的40K 内存块的操作,所以我试着对node 添加了 8K 内存块的内存池控制,服务繁忙时命中率无限接近100%,可惜总体性能提升没有达到预期,在此就不现拙了,有兴趣的同学可以自己hack 玩玩,有成果了可以知会我一声(http://weibo.com/windyrobin)…
小节:
由以上分析,我们可知不要轻易持久引用由 socket.readStream 或 fs.readStream 上浮的Buffe
当你调用stream.write 并直接传递Buffer 进去时,在此操作返回之前,你不应该再修改它
当调用fs.readStream 时,如果你对文件大小有估值,尽量传入较接近的bufsize
当你持久引用一个Buffer 时,哪怕它只有一个字节,也可能导致其依赖的slab (可能是8K /1M…)得不到释放
附:以上分析基于node 0.6 系列,就这方面的问题,我已提交了几个Issue 给 Node 官方,开发人员正在对以上暴露的问题就行改进:
转自:https://cnodejs.org/topic/4f16442ccae1f4aa27001067
相关文章推荐
- Node复制文件
- 【leetcode每日一题】24.Swap Nodes in Pairs
- [leetcode] 222 Count Complete Tree Nodes
- leetcode 237: Delete Node in a Linked List
- How to Install Node.js
- ClippingNode的使用之为节点添加动态光效
- hadoop集群配置datanode无法启动的原因
- Node.js游戏框架
- Aspose.words编程指南之DOM树结构初识,Node类继承关系及说明
- hadoop2.X如何将namenode与SecondaryNameNode分开配置
- LeetCode_Merge k Sorted Lists
- node.js中util模块介绍
- Node.js和testacular的安装与配置
- Remove Nth Node From End of List
- Node.js实现RESTful api,express or koa?
- Node.js 开发指南笔记
- *Count Complete Tree Nodes
- **Lowest Common Ancestor of Two Nodes in a Binary Tree
- 安装supervisor调试nodejs
- 安装node-inspector来调试node