论文部分内容阅读
[摘 要]Node对内存泄漏十分敏感,一旦线上应用有成千上万的流量,哪怕是一个字节的内存泄漏也会造成堆积,垃圾回收过程中将耗费更多时间进行对象扫描,应用响应缓慢,直到进程内存溢出,应用崩溃。本文不仅分析了造成内存泄漏的原因,并介绍了几种主流的排查方案。
[关键词]Node 内存泄漏
中图分类号:TG294 文献标识码:A 文章编号:1009-914X(2017)46-0046-01
1 引言
内存泄漏通常产生于无意间,较难排查。尽管内存泄漏的情况不尽相同,但其实质只有一个,那就是应当回收的对象出现意外而没有被回收,变成了常驻在老生代中的对象。
2 引发内存泄漏的原因
通常,造成内存泄漏的原因主要有两个:缓存,队列消费不及时。
2.1 缓存
缓存在应用中的作用举足轻重,可以十分有效地节省资源。因为它的访问效率要比I/O的效率高,一旦命中缓存,就可以节省一次I/O的时间。
但是在Node中,缓存并非物美价廉。一旦一个对象被当作缓存来使用,那就意味着它将会常驻在老生代中。缓存中存储的键越多,长期存活的对象也就越多,这将导致垃圾回收在进行扫描和整理时,对这些对象做无用功。
另一个问题在于,JavaScript开发者通常喜欢用对象的键值对来缓存东西,但这与严格意义上的缓存又有着区别,严格意义的缓存有着完善的过期策略,而普通对象的键值对并没有。所以,在Node中,任何试图拿内存当缓存的行为都应当被限制。当然,这种限制并不是不允许使用的意思,而是要小心为之。
为了解决缓存中的对象永远无法释放的问题,需要加入一种策略来限制缓存的无限增长,例如将记录键记录在数组中,一旦超过数量,就以先进先出的方式进行淘汰,这种策略适合于小场景的案例中使用,在比较大型的应用场景一般使用的是基于LRU(最近最少使用)算法的策略。
除了限制缓存的大小外,还需要考虑到进程间是无法共享内存的。如果在进程内使用缓存,这些缓存不可避免地有重复,对物理内存的使用是一种浪费。目前较好的解决方案是采用进程外的缓存,进程自身不存储状态。外部的缓存软件有着良好的缓存过期淘汰策略以及自身的内存管理,不影响Node进程的性能。市面是较好的缓存有Redis和Memcached。
2.2 队列
在解决了缓存带来的内存泄漏问题后,另一个不经意产生的内存泄漏则是队列。在JavaScript中可以通过队列(数组对象)来完成许多特殊的需求,比如Bagpipe。队列在消费者-生产者模型中经常充当中间产物。这是一个容易忽略的情况,因为在大多数应用场景下,消费的速度远远大于生产的速度,内存泄漏不易产生。但是一旦消费速度低于生产速度,将会形成堆积。
举个例子,有的应用会收集日志。如果欠缺考虑,也许会采用数据库来记录日志。日志通常会是海量的,数据库构建在文件系统之上,写入效率远远低于文件直接写入,于是会形成数据库写入操作的堆积,而JavaScript中相关的作用域也不会得到释放,内存占用不会回落,从而出现内存泄漏。
遇到这种场景,表层的解决方案是换用消费速度更高的技术。在日志收集的案例中,换用文件写入日志的方式会更高效。但是,如果生产速度因为某些原因突然激增,或者消费速度因为突然的系统故障而降低,内存泄漏还是可能会出现。
深度的解决方案应该是监控队列的长度,一旦堆积,应当通过监控系统产生报警并通知相关人员。另一个解决方案是任意异步调用都应该包含超时机制,一旦在限定的时间内未完成响应,通过回调函数传递超时异常,使得任意异步调用的回调都具备可控的响应时间,给消费速度一个下限值。
对于Bagpipe而言,它提供了超时模式和拒绝模式。启用超时模式时,调用加入到队列中就开始计时,超时就直接响应一个超时错误。启用拒绝模式,当队列拥塞时,新到来的调用会直接响应拥塞错误。这两种模式都能够有效地防止队列拥塞导致的内存泄漏问题。
3 内存泄漏的排查方案
在Node中,由于V8的堆内存大小的限制,它对内存泄漏非常敏感。当在线服务的请求量变大时,哪怕是一个字节的泄露都会导致内存占用过高。
常见的用于排查Node应用内存泄露的有以下几个工具:
*v8-profiler,用于对V8堆内存抓取快照和对CPU进行分析;
*node-heapdump,允许对V8堆内存抓取快照,用于事后分析,這是Node核心贡献者编写的模块;
*node-mtrace,它使用了GCC的mtrace工具来分析堆的使用;
*node-memwatch,来自Mozilla的LloydHilaiel贡献的模块,采用WTFP许可发布。
4 总结
所谓知己知彼,百战不殆,只有深入了解了造成内存泄漏的主要原因,才能对症下药,尽可能地规避造成内存泄漏的行为,并采用主流的工具予以排查消除隐患。
[关键词]Node 内存泄漏
中图分类号:TG294 文献标识码:A 文章编号:1009-914X(2017)46-0046-01
1 引言
内存泄漏通常产生于无意间,较难排查。尽管内存泄漏的情况不尽相同,但其实质只有一个,那就是应当回收的对象出现意外而没有被回收,变成了常驻在老生代中的对象。
2 引发内存泄漏的原因
通常,造成内存泄漏的原因主要有两个:缓存,队列消费不及时。
2.1 缓存
缓存在应用中的作用举足轻重,可以十分有效地节省资源。因为它的访问效率要比I/O的效率高,一旦命中缓存,就可以节省一次I/O的时间。
但是在Node中,缓存并非物美价廉。一旦一个对象被当作缓存来使用,那就意味着它将会常驻在老生代中。缓存中存储的键越多,长期存活的对象也就越多,这将导致垃圾回收在进行扫描和整理时,对这些对象做无用功。
另一个问题在于,JavaScript开发者通常喜欢用对象的键值对来缓存东西,但这与严格意义上的缓存又有着区别,严格意义的缓存有着完善的过期策略,而普通对象的键值对并没有。所以,在Node中,任何试图拿内存当缓存的行为都应当被限制。当然,这种限制并不是不允许使用的意思,而是要小心为之。
为了解决缓存中的对象永远无法释放的问题,需要加入一种策略来限制缓存的无限增长,例如将记录键记录在数组中,一旦超过数量,就以先进先出的方式进行淘汰,这种策略适合于小场景的案例中使用,在比较大型的应用场景一般使用的是基于LRU(最近最少使用)算法的策略。
除了限制缓存的大小外,还需要考虑到进程间是无法共享内存的。如果在进程内使用缓存,这些缓存不可避免地有重复,对物理内存的使用是一种浪费。目前较好的解决方案是采用进程外的缓存,进程自身不存储状态。外部的缓存软件有着良好的缓存过期淘汰策略以及自身的内存管理,不影响Node进程的性能。市面是较好的缓存有Redis和Memcached。
2.2 队列
在解决了缓存带来的内存泄漏问题后,另一个不经意产生的内存泄漏则是队列。在JavaScript中可以通过队列(数组对象)来完成许多特殊的需求,比如Bagpipe。队列在消费者-生产者模型中经常充当中间产物。这是一个容易忽略的情况,因为在大多数应用场景下,消费的速度远远大于生产的速度,内存泄漏不易产生。但是一旦消费速度低于生产速度,将会形成堆积。
举个例子,有的应用会收集日志。如果欠缺考虑,也许会采用数据库来记录日志。日志通常会是海量的,数据库构建在文件系统之上,写入效率远远低于文件直接写入,于是会形成数据库写入操作的堆积,而JavaScript中相关的作用域也不会得到释放,内存占用不会回落,从而出现内存泄漏。
遇到这种场景,表层的解决方案是换用消费速度更高的技术。在日志收集的案例中,换用文件写入日志的方式会更高效。但是,如果生产速度因为某些原因突然激增,或者消费速度因为突然的系统故障而降低,内存泄漏还是可能会出现。
深度的解决方案应该是监控队列的长度,一旦堆积,应当通过监控系统产生报警并通知相关人员。另一个解决方案是任意异步调用都应该包含超时机制,一旦在限定的时间内未完成响应,通过回调函数传递超时异常,使得任意异步调用的回调都具备可控的响应时间,给消费速度一个下限值。
对于Bagpipe而言,它提供了超时模式和拒绝模式。启用超时模式时,调用加入到队列中就开始计时,超时就直接响应一个超时错误。启用拒绝模式,当队列拥塞时,新到来的调用会直接响应拥塞错误。这两种模式都能够有效地防止队列拥塞导致的内存泄漏问题。
3 内存泄漏的排查方案
在Node中,由于V8的堆内存大小的限制,它对内存泄漏非常敏感。当在线服务的请求量变大时,哪怕是一个字节的泄露都会导致内存占用过高。
常见的用于排查Node应用内存泄露的有以下几个工具:
*v8-profiler,用于对V8堆内存抓取快照和对CPU进行分析;
*node-heapdump,允许对V8堆内存抓取快照,用于事后分析,這是Node核心贡献者编写的模块;
*node-mtrace,它使用了GCC的mtrace工具来分析堆的使用;
*node-memwatch,来自Mozilla的LloydHilaiel贡献的模块,采用WTFP许可发布。
4 总结
所谓知己知彼,百战不殆,只有深入了解了造成内存泄漏的主要原因,才能对症下药,尽可能地规避造成内存泄漏的行为,并采用主流的工具予以排查消除隐患。