I. 三种比较典型的内存泄漏

一. 闭包引用导致的泄漏

这段代码已经在很多讲解内存泄漏的地方引用了,非常经典,所以拿出来作为第一个例子,以下是泄漏代码:

'use strict';
const express = require('express');
const app = express(); //以下是产生泄漏的代码
let theThing = null;
let replaceThing = function () {
let leak = theThing;
let unused = function () {
if (leak)
console.log("hi")
}; // 不断修改theThing的引用
theThing = {
longStr: new Array(1000000),
someMethod: function () {
console.log('a');
}
};
}; app.get('/leak', function closureLeak(req, res, next) {
replaceThing();
res.send('Hello Node');
}); app.listen(8082);

js中的闭包非常有意思,通过打印heapsnapshot,在chrome的dev tools中展示,会发现闭包中真正存储本作用域数据的是类型为 closure 的一个函数(其__proto__指向的function)的 context 属性指向的对象。

这个例子中泄漏引起的原因就是v8对上述的 context 选择性持有本作用域的数据的两个特点:

  • 父作用域的所有子作用域持有的闭包对象是同一个。
  • 该闭包对象是子作用域闭包对象中的 context 属性指向的对象,并且其中只会包含所有的子作用域中使用到的父作用域变量。

二. 原生Socket重连策略不恰当导致的泄漏

这种类型的泄漏本质上node中的events模块里的侦听器泄漏,因为比较隐蔽,所以放在第二个例子,以下是泄漏代码:

const net = require('net');
let client = new net.Socket(); function connect() {
client.connect(26665, '127.0.0.1', function callbackListener() {
console.log('connected!');
});
} //第一次连接
connect(); client.on('error', function (error) {
// console.error(error.message);
}); client.on('close', function () {
//console.error('closed!');
//泄漏代码
client.destroy();
setTimeout(connect, 1);
});

泄漏产生的原因其实也很简单:event.js 核心模块实现的事件发布/订阅本质上是一个js对象结构(在v6版本中为了性能采用了new EventHandles(),并且把EventHandles的原型置为null来节省原型链查找的消耗),因此我们每一次调用 event.on 或者 event.once 相当于在这个对象结构中对应的 type 跟着的数组增加一个回调处理函数。

那么这个例子里面的泄漏属于非常隐蔽的一种:net 模块的重连每一次都会给 client 增加一个 connect事件 的侦听器,如果一直重连不上,侦听器会无限增加,从而导致泄漏。

三. 不恰当的全局缓存导致的泄漏

这个例子就比较简单了,但是也属于在失误情况下容易不小心写出来的,以下是泄漏代码

'use strict';
const easyMonitor = require('easy-monitor');
const express = require('express');
const app = express(); const _cached = []; app.get('/arr', function arrayLeak(req, res, next) {
//泄漏代码
_cached.push(new Array(1000000));
res.send('Hello World');
}); app.listen(8082);

如果我们在项目中不恰当的使用了全局缓存:主要是指只有增加缓存的操作而没有清除的操作,那么就会引起泄漏。

这种缓存引用不当的泄漏虽然简单,但是我曾经亲自排查过:Appium自动化测试工具中,某一个版本的日志缓存策略有bug,导致搭建的server跑一段时间就重启。

II. 常规排查方式

一. heapdump/v8-profiler + chrome dev tools

目前node上面用于排查内存泄漏的辅助工具也有一些,主要是:

  • heapdump
  • v8-profiler

这两个工具的原理都是一致的:调用v8引擎暴露的接口: v8::Isolate::GetCurrent()->GetHeapProfiler()->TakeHeapSnapshot(title, control) 然后将获取的c++对象数据转换为js对象。

这个对象中其实就是一个很大的json,通过chrome提供的dev tools,可以将这个json解析成可视化的树或者统计概览图,通过多次打印内存结构,compare出只增不减的对象,来定位到泄漏点。

二. Easy-Monitor工具自动定位疑似泄漏点

我之前项目中遇到疑似的内存泄漏基本都是这样排查的,但是排查的过程中也遇到了几个比较困扰的问题:

  • 只能在线下进行,而线上情况复杂,有些错误线下很难复现
  • 总是需要多次插工具打印,然后对比,比较麻烦

所以后面花了点时间,详细解析了下v8引擎输出的heapsnapshot里面的json结构,做了一个轻量级的线上内存泄漏排查工具,也是之前的Easy-monitor性能监控工具的一个补完。

对如何测试自己项目线上js代码性能,以及找出js函数可优化点感兴趣的朋友可以参看这一篇:

本文下一节主要是以第I节中的三种非常典型的内存泄漏状况,来使用新一版的Easy-Monitor进行简单的定位排查。

III. 使用Easy-Monitor快速定位泄漏点

一. 安装&嵌入项目

Easy-Monitor的使用非常简单,安装启动总共三步

1.安装模块

npm install easy-monitor

2.引入模块

const easyMonitor = require('easy-monitor');
easyMonitor('你的项目名称');

3.访问监控页面

打开你的浏览器,输入以下地址,即可看到进程相关信息:

http://127.0.0.1:12333

二. 内存泄漏排查使用方式

Easy-Monitor可以实时展示内存分析信息,所以在线上使用也是没有问题的,下面就来使用此工具分析第I节中出现的问题。

1.闭包泄漏

在闭包泄漏的代码中,按照上面的步骤引入easy-monitor,然后不停在浏览器中访问:

http://127.0.0.1:8082/leak

那么几次后通过top或者别的自带内存监控工具能看到内存明显上升:

这里我本地访问多次后,已经飙升到211MB。

此时,我们可以在Easy-Monitor的首页,点击对应Pid后面的 MEM 链接,即可自动进行当前业务进程的堆内内存快照打印以及泄漏点分析:

大约等待10s左右,页面即会呈现出解析的结果。最上面的 Heap Status 一栏呈现的内容是一个对当前堆内内存解析后的概览,大概看看就行了,比较重要的泄漏点定位在下面的 Memory Leak 一栏。

我对疑似的内存泄漏点推测是从计算得到的 retainedSize 着手的:泄漏的感知首先是内存无故增加,且只增不减,那么当前堆内内存结构中从 (GC roots) 节点出发开始,占据的 retainedSize 最大的就可能是疑似泄漏点的起始。

遵循这个规则,Memory Leak 第一个子栏目得到的是疑似泄漏点的概览:

这里按照 retainedSize 大小做了从大到小的排序,可以看到,这几个点基本上占据了90%以上的堆内内存大小。

好了,下面的子栏目则是对这里面的5个疑似泄漏点构建 引力图,来找出泄漏链条,原理和前面一样:占据总堆内内存 retainedSize 最大的对象下面一定也有占据其 retainedSize 最大的节点:

根据引力图可以很清晰看到 retainedSize 最大的疑似泄漏链条,颜色和大小的一部分含义:

  • 蓝色表示疑似的泄漏节点
  • 紫色表示普通节点
  • 最大的节点表示的是当前疑似泄漏链条的根节点

这里的展示用了Echarts2,所有的节点都可以点击展开/折叠。当我们把鼠标移动到疑似泄漏链条的最后一个子节点时,引力图下面会用文字显示出当前的泄漏链条的详细指向信息 Reference List ,这里简单的解析下其内容:

[object] (Route::@122187) ' stack
---> [object] (Array::@124261) ' [0]
---> [object] (Layer::@124265) ' handle
---> [closure] (closureLeak::@124169) ' context
---> [object] (system / Context::@84427) ' theThing
---> [object] (Object::@122271) ' someMethod
---> [closure] (someMethod::@122275) ' context
---> [object] (system / Context::@122269) ' leak
---> [object] (Object::@122113) ' someMethod
---> [closure] (someMethod::@122117) ' context
---> [object] (system / Context::@122111)

每一行表示一个节点:[类型] (名称::节点唯一id) ’ 属性名称或者index。 因为测试代码用了Express框架,熟悉Express框架源码的小伙伴都能看出来了:

  • 根节点是初始化express时构造的 Route 的实例。
  • 该 Route 实例的 stack 属性对应的数组的第一个元素,即这里的 [0] 对应的元素,其实也就是一个中间件,所以是 Layer 的一个实例。
  • 该中间件的 handle 属性指向 closureLeak 函数,这里开始出现我们自己编写的Express框架外的代码了,简单分析下也很容易明白这个中间件其实就是我们编写的app.get 部分。
  • closureLeak 函数持有了上级作用域产生的闭包对象,这个闭包对象中 retainedSize 最大的变量为 theThing
  • theThing 持有了 someMethod 的引用,someMethod 又通过上级作用域的闭包对象持有了 leak 变量,leak 变量又指向 theThing 变量指向的上一次的老对象,这个老对象中依旧包含了 someMethod …

通过这个引力图和下面提供的 Reference List 分析,其实很容易发现泄漏点和泄漏原因:正是因为第I节中提到的v8引擎作用域生成和持有闭包引用的规则,那么 unused 函数的存在,导致了 leak 变量被 replaceThing 函数作用域生成的闭包对象存储了,那么 theThing 每一次指向的新对象里面的 someMethod 函数持有了这个闭包对象,因此间接持有了上一次访问 theThing 指向的老对象。所以每一次访问后,老对象永远因为被持有永远无法得到释放,从而引起了泄漏。

这里也把关键词整理出来,方便大家项目全局搜索排查:Leak Key

2.Socket重连泄漏

同样的方式,第I节中的代码保存后执行,注意 connect 操作的端口填写一个本地不存在的端口,来模拟触发客户端的断线重连。

那么这段代码跑大概一分钟左右,即开始产生比较明显的泄漏现象。同样打开easy-monitor监控页面进行堆内存分析,得到如下结果:

这个图很容易看出来,占据 retainedSize 最大的对象正是 socket 对象,几乎占到了堆内总内存的 50% 以上。

接着往下看引力图,如下所示:

其中的 Reference List 如下:

[object] (Socket::@97097) ' _events
---> [object] (EventHandlers::@97101) ' connect
---> [object] (Array::@102511)

这里熟悉Node核心模块 events 的小伙伴就能感到熟悉,_events 正是存储订阅事件/事件回调函数的属性,那么这边很显然是原生的socket触发断线重连时,会不停增加 connect 事件的处理,如果服务器一直挂掉,即客户端无法断线重连成功,那么内存就会不断增加导致泄漏。

题外插一句,我翻了下net.js的代码,这里的 connect 事件是以 once 的方式添加的,所以只要重连过程中能够连上一次,这部分侦听器增加的内存就能够被回收掉。

3.全局缓存泄漏

这个是最简单的原因了,大家可以使用Easy-Monitor自行尝试一番~

IV. 如何修改避免泄漏

一. 断掉闭包中的泄漏变量引用链条

根据第III节中的解析,明白了这种泄漏的原理,就比较容易对代码进行修改了,断掉 unused 函数对 leak 变量的引用,那么 replaceThing 函数作用域的闭包对象中就不会有 leak 变量了,这样 someMethod 即不会再对老对象间接产生引用导致泄漏,修改后代码如下:

'use strict';
const express = require('express');
const app = express();
const easyMonitor = require('easy-monitor');
easyMonitor('Closure Leak'); let theThing = null;
let replaceThing = function () {
let leak = theThing;
//断掉leak的闭包引用即可解决这种泄漏
let unused = function (leak) {
if (leak)
console.log("hi")
}; theThing = {
longStr: new Array(1000000),
someMethod: function () {
console.log('a');
}
};
}; app.get('/leak', function closureLeak(req, res, next) {
replaceThing();
res.send('Hello Node');
}); app.listen(8082);

二. 断线重连时去掉老侦听器

修改主要目的是在重连时去掉连接失败时添加的 connect 事件,修改后代码如下:

const net = require('net');
const easyMonitor = require('easy-monitor');
easyMonitor('Socket Leak');
let client = new net.Socket(); function callbackListener() {
console.log('connected!');
}); function connect() {
client.connect(26665, '127.0.0.1', callbackListener} connect(); client.on('error', function (error) {
// console.error(error.message);
}); client.on('close', function () {
//console.error('closed!');
//断线时去掉本次侦听的connect事件的侦听器
client.removeListener('connect', callbackListener);
client.destroy();
setTimeout(connect, 1);
});

三.

修改和测试大家可以自行尝试一番。

V. 结语

做这个工具也让自己对于v8的内存管理有了更深入的认识,收获挺大的,下一步的计划是优化代码逻辑和前台呈现界面,提高易用性和开发者的体验。

Easy-Monitor新版本下依旧支持线上部署和多项目cluster部署,最后项目的git地址在:

Easy-Monitor

如果大家觉得有帮助或者不错,欢迎给个star

轻松排查线上Node内存泄漏问题的更多相关文章

  1. 线上服务内存OOM问题定位[转自58沈剑]

    相信大家都有感触,线上服务内存OOM的问题,是最难定位的问题,不过归根结底,最常见的原因: 本身资源不够 申请的太多 资源耗尽 58到家架构部,运维部,58速运技术部联合进行了一次线上服务内存OOM问 ...

  2. 线上服务内存OOM问题定位

    转自:架构师之路,http://mp.weixin.qq.com/s/iOC1fiKDItn3QY5abWIelg 相信大家都有感触,线上服务内存OOM的问题,是最难定位的问题,不过归根结底,最常见的 ...

  3. 线上服务内存OOM问题定位三板斧

    相信大家都有感触,线上服务内存OOM的问题,是最难定位的问题,不过归根结底,最常见的原因: 本身资源不够 申请的太多 资源耗尽 58到家架构部,运维部,58速运技术部联合进行了一次线上服务内存OOM问 ...

  4. Java虚拟机性能管理神器 - VisualVM(6) 排查JAVA应用程序内存泄漏【转】

    Java虚拟机性能管理神器 - VisualVM(6) 排查JAVA应用程序内存泄漏[转] 标签: javajvm内存泄漏监控工具 2015-03-11 18:30 1870人阅读 评论(0) 收藏  ...

  5. Linux命令排查线上问题常用的几个

    排查线上问题常用的几个Linux命令 https://www.cnblogs.com/cjsblog/p/9562380.html top 相当于Windows任务管理器 可以看到,输出结果分两部分, ...

  6. [实战] Flutter 上的内存泄漏监控

    一.前言 Flutter 所使用的 Dart 语言具有垃圾回收机制,有垃圾回收就避免不了会内存泄漏. 在 Android 平台上有个内存泄漏检测工具 LeakCanary, 它可以方便地在 debug ...

  7. 记一次linux通过jstack定位CPU使用过高问题或排查线上死锁问题

    一.java定位进程 在服务器中终端输入命令:top 可以看到进程ID,为5421的cpu这列100多了. 记下这个数字:5421 二.定位问题进程对应的线程 然后在服务器中终端输入命令:top -H ...

  8. Node 内存泄漏排查案例

    背景 在阿里云上看到我运行了一段时间的程序,发现 memory 一项基本是在稳步提升,就知道有内存泄漏的情况出现.如下图 近三日从 35% 升到 40%,缓慢而坚定的提升. 代码 排查此问题需要分析其 ...

  9. JVM jmap dump 分析dump文件 / 如何使用Eclipse MemoryAnalyzer MAT 排查线上问题

    jhat简介 jhat用来分析java堆的命令,可以将堆中的对象以html的形式显示出来,包括对象的数量,大小等等,并支持对象查询语言 这个工具并不是想用于应用系统中而是用于"离线" ...

随机推荐

  1. 图像的点运算----底层代码与Halcon库函数

    最基本的图像分析工具----灰度直方图.使用直方图辅助,可以实现4大灰度变换,包括线性灰度变换(灰度拉伸).灰度对数变换.灰度伽马变换.灰度分段线性变换:使用直方图修正技术,可以实现2大变换,包括直方 ...

  2. C#导入Excel|读取Excel方法

    OleDbConnection读取 /// <summary>       /// 返回Excel数据源       /// </summary>       /// < ...

  3. 第2天:JavaScript基础(运算符、案例、循环、冒泡以及prompt提示输入框)

    一元运算在前在后的区别 加加 var num1 = 10; //++在后面 先参与运算 再自加1 var sum1 = num1++ +10; console.log("sum1的值:&qu ...

  4. Node.js学习笔记(四) --- fs模块的使用

    目录 . fs.stat 检测是文件还是目录 . fs.mkdir 创建目录 . fs.writeFile 创建写入文件 . fs.appendFile 追加文件 . fs.readFile 读取文件 ...

  5. Java接口和抽象类理解(New)

    一. 抽象类和接口的特点  包含抽象方法的类称为抽象类,但并不意味着抽象类中只能有抽象方法,它和普通类一样,同样可以拥有成员变量和普通的成员方法.注意,抽象类和普通类的主要有三点区别: 1)抽象方法必 ...

  6. Spring AOP注解通过@Autowired,@Resource,@Qualifier,@PostConstruct,@PreDestroy注入属性的

    本文介绍了使用spring注解注入属性的方法. 使用注解以前,注入属性通过类以及配置文件来实现.现在,注入属性可以通过引入@Autowired注解,或者@Resource,@Qualifier,@Po ...

  7. sql: T-SQL parent-child function script

    --Parent-Child reationship --涂聚文 2014-08-25 --得位置的子節點函數表(包含本身) if exists (select * from dbo.sysobjec ...

  8. HTML颜色代码

    记录十种个人比较喜欢的颜色: #19CAAD   #8CC7B5  #A0EEE1  #BEE7E9  #BEEDC7 #D6D5B7  #D1BA74  #E6CEAC  #ECAD9E  #F46 ...

  9. 医药箱APP静态小项目

    花费了10天时间,纯手写一个医药箱APP静态小项目,里面有上拉加载.左右滑动.弹出层淡入淡出等效果,主要是练习. 以下是一部分页面效果图: 我用的是谷歌的开发者工具的手机端模拟器. 里面需要优化的地方 ...

  10. HADOOP背景介绍

    1. HADOOP背景介绍 1.1 什么是HADOOP 1. HADOOP是apache旗下的一套开源软件平台 2. HADOOP提供的功能:利用服务器集群,根据用户的自定义业务逻辑,对海量数据进行分 ...