nodejs CorkedRequest导致内存泄露

2016-10-19 15:33:02

0x00 状态

有一个websocket长连接服务原先是用php的workerman框架跑的,但是近期出现了一些莫名其妙的bug,用Nodejs重写后接替了原先的全部流量。
最初还没什么问题。
跑了一段时间后发现内存泄露问题比较严重,服务器有8G内存,用cluster方式跑了8个进程,几乎24小时内就会吃满8g内存,导致进程被系统kill。

0x01 堆内存分析

遇到内存泄露,第一时间想到的就是把内存dump下来看看,于是发布了一个能够实时dump内存的版本,拿到heapdump文件后发现文件只有60M不到,而当时的进程内存占用达到了1.6G,于是能够知道大量的内存开销都不是堆内存,而是堆外内存占用。

将dump文件交给webstorm分析,得到的结果非常诡异

有大量的Buffer对象是已经猜到的,但是其中大量的Buffer有着成千上万的引用深度,点开Webstorm直接卡死,分析只有几百的引用深度的对象,发现引用树几乎一直在WriteReq.next()方法之间自己调用自己,几乎没我业务代码什么事。

Google关键字"nodejs writereq memory leak"。通常来说,Google也解决不了的,只有两种情况,一是低级bug,二是真的前无古人的bug,而大部分都是低级bug。
但是过了几天也依旧没想到其中原因。

0x02 线索

又过了几天,发现一个规律,在高峰期带宽不太足的时候内存增长非常快。

但是按正常情况下,带宽不足时会在内存中缓存发送的数据是很正常的,但是如果有将近8G的数据堆在内存里等待发送的话,客户端应该会有明显的感觉。但是事实是客户端几乎没有太多的延迟感。

0x03 源码

最终没有办法了,决定阅读nodejs的源码。

WriteReq的定义在_stream_writeable.js中,是一个nodejs内部私有模块https://github.com/nodejs/node/blob/master/lib/_stream_writable.js




WriteableStream的write方法在调用时如果发现上次的write还未结束,会构建WriteReq对象链,WriteReq.next指向本次写入新生成的WriteReq。于是就形成了

qq20161019-0

到这里为止还算正常,那么接下来应该看看如果WriteReq被写了会怎么样


当一个上WriteReq对象在写入完成后会调用socket.state.onwrite,socket.write是一个WritableState对象,他的onwrite方法是


在这个方法里可以看到,只有stream缓冲流被清空,没有任何等待发送的数据,this.bufferedRequest == null时,才会调用clearBuffer方法

而clearBuffer干了什么呢


clearBuffer函数里清理掉了整条WriteReq链,并且调用了socket.write()的回调

那么就会有一个问题,如果程序数据生成的速度大于网络带宽时,由于流永远不会退出缓冲模式,所以clearBuffer方法也就永远不会被调用到,也就永远不会有人清理WriteReq链,于是造成了WriteReq链上持有的buffer对象会一直堆积在内存里无法释放。

Buffer对象存储数据所用的内存是不在V8的堆内存内的,也就吻合了1.6G的内存占用dump只有60M的情况。

0x04 解决方案

解决方案其实也很简单暴力……在node前面用nginx跑一层反代,不要让node把请求缓存起来就OK了

https://cnodejs.org/topic/57e90fb256898f231a526f7b#5806efe027a1d99178a98fa5