实战线上内存泄漏问题
发布于 6 年前 作者 zy445566 4559 次浏览 来自 分享

故事背景

由于我们线上验证码突然出现假死,记录最后一次node崩溃的线上node内存使用达到了1.36GB,所以基本断定是内存泄漏了

工具

heapdump(npm包) chrome浏览器中的开发者工具(memory)

准备

先在入口文件中加入

const heapdump = require('heapdump');

我们的环境用的是k8s服务,所以每个prod都有自己的虚拟域名(所以下面的域名是不存在的), 我们获取验证码的接口是http://valve-tdbase/valve/verification/get 下面是我的请求的测试脚本

const http = require("http");
for(let i=0;i<500;i++)
{
    http.get('http://valve-tdbase/valve/verification/get', (res) => {});
}

开干

然后使用node启动入口文件(请勿用pm2启动服务,pm2开的子进程容易造成数据的迷惑) 启动后,查出node的PID,如图 pid 接下来每运行一次测试脚本就执行一次下面的命令(2981是我启动node服务的PID)

kill -USR2 2981

生成出内存快照后,进入chrome开发者工具进行对比,使用对比模式(comparision),将两次执行测试脚本后生成的快照进行对比,发现CAP类型的数据回收数量为0,并且新增数量巨大,如下图 comparision

并且发现绑定在reg_ary中,遂查找调用链。发现以下可能存在问题的代码(有兴趣可以先看一下)

以下是我们业务代码(每一次调用都会走,存在删减):

_M.prototype.get = function* () {
    try{
        this.log.info('request get default verification code');
        var ccap = require('ccap');
        var captcha = ccap(default_cfg.style);
        var ary = captcha.get();
        var code = ary[0];
        var buffer = ary[1];
        var pid = randomWord(false,16);
        yield this.redis_client.setex(pid,default_cfg.expire_date,code.toLowerCase());
        return {pid:pid,img:'data:'+default_cfg.type+';base64,'+buffer.toString('base64')}
    }catch(e){
        this.log.error('get default verification code error:',e);
        return null;
    }
};

以下是ccap库的代码(存在删减): hcap.js:

...
var timer = require('./timer.js');
...
var CAP = function(args){
...
}
module.exports = function(args){
	var cap = CAP(args);
	ins_count++;
	timer.reg_ary.push(cap);//将实例化之后的对象注册到timer定时器中
	return cap;
};

dirObj.dir();//设置路径权限,清空历史文件夹
dirObj.setSchedule();//开启定时器,每天凌晨2点清理文件夹
timer.timer();//启动计时器,每分钟更新缓存

timer.js:

var timer = {
	timeout:1000*60,//更新验证码时间1分钟
	reg_ary:[],
	isRunning:0,
}
timer.timer = function(notFirst){
...
		
		var rary = timer.reg_ary,
			len = rary.length;

		for(var i=0;i<len;i++){
			try{
				rary[i]._timer();
			}
			catch(e){
				console.error(e)
			}
		}
...
		
}
module.exports = timer;

发现问题

业务代码中存在这段,每次请求都会调用

var ccap = require('ccap');
var captcha = ccap(default_cfg.style);

而运行ccap,会把CAP这个方法不断推送到timer.js文件的timer变量的reg_ary数组中! 重点reg_ary是绑定在require(’./timer.js’)的,所以不会回收,而一直缓存的!

module.exports = function(args){
	var cap = CAP(args);
	ins_count++;
	timer.reg_ary.push(cap);//将实例化之后的对象注册到timer定时器中
	return cap;
};

导致绑定在reg_ary的数据越来越多!

解决

将ccap(default_cfg.style);放到当前文件上面即可。

var ccap = require('ccap');
var captcha = ccap(default_cfg.style);
_M.prototype.get = function* () {
    try{
        this.log.info('request get default verification code');
        var ary = captcha.get();
        var code = ary[0];
        var buffer = ary[1];
        var pid = randomWord(false,16);
        yield this.redis_client.setex(pid,default_cfg.expire_date,code.toLowerCase());
        return {pid:pid,img:'data:'+default_cfg.type+';base64,'+buffer.toString('base64')}
    }catch(e){
        this.log.error('get default verification code error:',e);
        return null;
    }
};

总结

虽然这份业务代码不是我也写的,但自认如果自己写,也有可能会遇到这样的坑,所以希望ccap的作者有时间能够改良一下,虽然可能我有时间也会PR,嘿嘿!@DoubleSpout

11 回复

简化一下成这样

// 文件 a
const obj = {
  array: []
};

// 文件 b
const obj = require("./a"); // 文件a是被缓存的,不会被垃圾回收
function createUser(username) {
  // 如果频繁调用这个函数,数组长度不断增长,内存不断增加,直到内存溢出
  obj.array.push({
    name: username
  });
}

这个故事告诉我们,数组不能随便push,对象不能随便挂载,另外上alinode

@axetroy 666,这简化可以。准确的说使用某个库前,必须先了解这个库怎么用会内存溢出。 其实这个最大的坑还不是内存泄露,而是这个库会遍历这个不断增加的数组,导致最后的效果和死循环一样。

没有用 alinode 定位吗

@axetroy 动态类型一时爽,就是有人不信邪。js的动态性总是被很多人滥用,我刚刚就又被坑了 if (Date.now() - body.ttl > 60000) 在用go语言重写时没注意到这个ttl传的居然是个字符串,结果上线运行几天才发现

我也见过一个同事的代码在每一个controller当中实例化一个redis实例。你们之前的写法明显是有问题的,初始化就只应该发生一次。ccap没什么可改的吧。

严格来说,不算是内存泄露吧?因为 reg_ary 本身就不应该被回收,只能说是类库使用有问题。统一在文件头部 require 并初始化就能避免

@zengming00

所以你需要上 Typescript.

但是港真,即便上 Typescript,也不能解决,还需要手动校验.

毕竟 Body 是动态传入的, 即便你定义了 interface body.ttl 是 number, 但人家硬是传给你 string

用 Go 写的话就舒服得多了,序列化映射到结构体的时候,类型不对就已经抛出错误了

@JacksonTian 没有,主要是公司这边的机器都是是独立的,没有使用任何外部服务,上这个运维比较担心信息泄露等一系列问题,而且机器都是通过vpn链接到外面,即使使用了,应该是不能链接到阿里云这边的服务。所以我也没太大把握去说服和推动这件事。

@alsotang 主要是不明真相的群众,一不小心误用了,一点提示都没有。我觉得实例太多了还是要给出一些提示的,至少。

@xiedacon 内存泄露主要就是因为写法存在问题导致的。跟require无关的,主要是初始化的问题

回到顶部