onvno / pokerface

日常技术文章阅读整理
3 stars 0 forks source link

20190717 - Node - 内存泄漏方案 #47

Open onvno opened 5 years ago

onvno commented 5 years ago

好文

可参考文章

依赖包

onvno commented 5 years ago

轻松排查线上Node内存泄漏问题 一. 闭包引用导致的泄漏 这段代码已经在很多讲解内存泄漏的地方引用了,非常经典,所以拿出来作为第一个例子,以下是泄漏代码:

'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跑一段时间就重启。

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);
});

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