最近项目中用到了长连接,昨天和后端讨论一个问题就是:在前端程序中和后端建立长连接,如果相同帐号的两个长连接会导致互踢,那么一个浏览器开两个tab会不会互踢呢?

    此时突然想起来之前看到的一个知识点,在浏览器中一个tab其实是一个进程,而每个进程的堆和栈都是独立的。所以建立长连接的时候只是在堆中分配了一个对象来调用操作系统的网络资源进行通信,而两个独立的进程中这两个控制长连接的对象可以看成是独立的。那么在浏览器中开两个tab理论上其实和在两个电脑中分别登录是没有区别的。当然除了浏览器内部可能会提供的进程间通信的一些机制。

      这个思考引发了笔者对了解进程内部存储的极大兴趣,所以接下来我们以node后端进程为例来分析一下。在之前阅读过的《大规模Web服务开发技术》一书中提到过,业务后台系统的设计要着眼于两个瓶颈,一个是反向代理服务器把请求发送到应用程序服务器时CPU的负载,还有就是应用程序服务器从数据库服务器取数据,产生I/O请求时的负载。可以适当通过增加数据库服务器的缓存来提高I/O访问效率。但是在作为一个独立存在的进程我们进行故障排查的时候思路是怎样的呢?

    当然就是会导致进程崩溃和卡死的两个参数:内存占用率和cpu占用率,内存我们主要分析空间,cpu主要分析的是时间。本次讲解我们主要关注的是内存占用。

    分析内存的时候我们主要是通过堆内存快照来进行分析,V8引擎提供了内部接口可以直接把堆中的JS对象导出来供开发者分析。我们采用heapdump这个模块,执行如下命令安装:

npm install heapdump --save


    接着在代码中添加如下语句来执行此模块:

const heapdump = require('heapdump');
heapdump.writeSnapshot('./test' + '.heapsnapshot');

        

    为了便于分析我们写了一段express的node服务demo:

var app = require('express')();
var http = require('http').Server(app);
var heapdump = require('heapdump');

var leakobjs = [];
function LeakClass(){
    this.x = 1;
}

app.get('/'function(req, res){
    console.log('get /');
    for(var i = 0; i < 1000; i++){
        leakobjs.push(new LeakClass());
    }
    res.send('<h1>Hello world</h1>');
});

setInterval(function(){
    heapdump.writeSnapshot('./' + Date.now() + '.heapsnapshot');
}, 3000);

http.listen(3000function(){
    console.log('listening on port 3000');
});


        这段程序中,全局域中我们定义了leakobjs数组和LeakClass,每个请求到来的时候,都会往leakobjs数组中去添加对象,由于leakobjs是在全局域中所以请求结束不会自动释放,就会造成内存泄露。

    我们对这个服务请求10次左右,可以看到项目目录里面出现了多个heapsnapshot文件,我们用文本编辑器来查看一下,可以看到里面的内容主要是一个大json,主要字段有snapshot、nodes、edges、strings其中nodes表示点,edges表示连接点的边。snapshot中node_fields字段主要描述了nodes中每个节点那6个数字代表的含义,同理edge_fields也是表示edges字段中每个边那3个数字的含义。strings字段描述的其实是点和边的名称,其中可以看到node中内存垃圾回收管理的跟节点"GC root"。

node故障排查从原理到实操-内存泄露_java

node故障排查从原理到实操-内存泄露_java_02

node故障排查从原理到实操-内存泄露_java_03

node故障排查从原理到实操-内存泄露_java_04


        堆快照描述的就是点和它们之间的边的关系。那么内存堆和这里描述的点和边又有什么联系呢?

    我们可以把node中的GC root作为根节点,从全局域可以访问2和3对象,2和3都能访问4对象。3可以访问5,4可以访问6。左图中如果去掉了节点2,还是可以从GC跟节点访问到4的,所以2就不是4的支配节点。而去掉节点4,就无法从根节点访问到6节点,那么4就是节点6的支配节点。按照图论中的算法,把左边的引用图转换成支配树,即可得到从GC到每个节点的支配关系。

node故障排查从原理到实操-内存泄露_java_05


    那么我们了解了支配关系图有什么用处呢?这里就要介绍两个概念:Shallow Size和Retained Size。Shallow Size就是对象自身被创建时所需要内存的大小,Retained Size就是当把对象从支配树上拿掉,对象和它的所有下级节点一共能释放的内存大小。

    由于node和浏览器中javascript的运行时环境都是v8,我们把之前获取到的堆快照heapsnapshot文件放到浏览器中进行分析:在chrome中打开调试者模式,在memory选项卡中选中Heap snapshot,点击load上传heapsnapshot文件,即可看到浏览器分析出来的支配树。

node故障排查从原理到实操-内存泄露_java_06


    浏览器处理完后,调到summary模式,我们可以看到支配树就是这个样子。右边调到Retained Size按照递减排序,那么这时我们就比较清楚了,一般发生内存泄漏的地方,都会引起内存不合理的变大,所以我们只要从Retained Size由大到小来确定哪个顶层节点下所包含的对象占用内存过大,然后再逐级确定内存泄露的对象,就可以定位内存泄漏的地方了。通过观察(closure)下的三个红框中的对象都是比其他的节点大很多的:

node故障排查从原理到实操-内存泄露_java_07


    我们先打开app()对象,可以看到_router对象的大小也是比较异常。

node故障排查从原理到实操-内存泄露_java_08


    所以我们来继续逐级观察,最后真的定位到是因为leakobjs数组占用内存过大所以导致父级节点的Retained Size过大:

node故障排查从原理到实操-内存泄露_java_09


    如果我们继续分析router()和()也能得到同样的结果。


思考到这里,突然对之前看过的操作系统那本书有了“蓦然回首,那人却在灯火阑珊处”的感觉。可能我们学过的理论知识如果不在工作中进行应用和思考,都只是死的知识点而已,经过应用和思考,才能活起来吧。