nodejs应用部署完毕后,需要实时监控应用的情况,除了应用的业务问题,内存泄漏以及导致的性能问题也需要关注,本文为分析的整体流程做下梳理

如何确定部署机器及该应用的内存使用情况

一、 机器内存使用

查询内存的使用情况在windows或者mac电脑上都会有进程/任务管理器,打开就可以看到具体的使用情况,而在linux上需要通过 free 或者 top 命令操作即可

xxx@xxxxx:~# free
              total        used        free      shared  buff/cache   available
Mem:        4039456     1669408     1110168        3024     1259880     2102916
Swap:        969964           0      969964

由于一台机器上可能存在多个应用同时在运行,我们需要查看到具体某一个应用的内存使用情况,如下通过指定pid来查看具体进程的相关运行情况


xxx@xxx:~# top  -p 19341
top - 12:06:23 up 76 days,  1:23,  1 user,  load average: 0.00, 0.00, 0.00
Tasks:   1 total,   0 running,   1 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.5 us,  0.2 sy,  0.0 ni, 99.3 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  4039456 total,  1109976 free,  1669616 used,  1259864 buff/cache
KiB Swap:   969964 total,   969964 free,        0 used.  2102708 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
193** user      20   0  9465**  491**  209** S   0.0  1.2   0:07.12 node

对于企业来说都有自己的监控平台,不需要自己登录机器去操作,只需要在启动服务的机器上启动一个额外的监控服务,通过轮询请求等操作获取机器上的应用运行情况,比如守护进程等等....

以上是对于机器的内存使用等情况的监控,对于应用的运行情况甚至某段代码运行后的内存变化等情况的监控,如果针对内存的变化监控,可以简单使用process.memoryUsage() 或者 os模块获取内存使用情况来判断是否内存溢出

var theThing = null;
var replaceThing = function () {
    var originalThing = theThing;
    var unused = function () {
        if (originalThing)
        console.log("hi");
    };
    theThing = {
        longStr: new Array(1000000).join('*'),
        someMethod: function () {
            console.log('someMessage');
        }
    };
};

app.get('/leak', (ctx) => {
  let beforeMemoryInfo = process.memoryUsage();
    replaceThing();
    let memoryInfo = process.memoryUsage();
    ctx.body = {before: beforeMemoryInfo, after: memoryInfo};
})

返回结果类似


{
    rss: 4935680, // 表示 Resident Set 的大小
    heapTotal: 1826816, // 表示堆的总大小
    heapUsed: 650472, // 表示堆的实际使用大小
    external: 49879
}

当持续请求 /leak接口时,发现返回的结果heapUsed持续增加时就意味着内存未释放,会发生内存溢出

以上是针对具体代码的运行情况监控,但是对于线上应用,内存溢出总是让人难以排查,只能根据整体情况来进行深入分析,我们来模拟线上运行的情况来监控运行时内存使用情况,可以使用对应的模块来截取当前运行时的快照,然后进行分析,对于nodejs可以使用heapdump模块截取快照

var heapdump = require('heapdump');
var theThing = null;
var replaceThing = function () {
    var originalThing = theThing;
    var unused = function () {
        if (originalThing)
        console.log("hi");
    };
    theThing = {
        longStr: new Array(1000000).join('*'),
        someMethod: function () {
            console.log('someMessage');
        }
    };
};

app.get('/leak', (ctx) => {
    replaceThing();
    ctx.body = 'ok';
});

app.listen(3000);

启动服务之后,可用通过以下两种方式记录快照

  • 在指定代码调用API记录快照,都是在运行replaceThing运行前后

heapdump.writeSnapshot('/var/local/' + Date.now() + '.heapsnapshot');
  • 在UNIX 平台上,可以使用命令行发送指令记录快照,按照以上代码,可以在/leak接口调用前后执行两次该命令,默认在引用heapdump模块的文件目录下生成.snapshot文件

kill -USR2 <pid>

通过以上的操作,可以得到2份快照文件,建议是在运行前运行少量次数运行多次这三种情况下记录快照,这样对比起来比较好分析

注意,记录快照写入文件是同步操作,会阻塞程序运行,所以尽量在流量少时操作

如何分析

借助chrome的dev-tool,可以把记录的snapshot文件导入进行分析,如下图

分析上图几个方面

  • 左边Profiles snapshot问价的大小,如果前后相差太大,且后续快照没有平缓的迹象,基本判断内存溢出
  • Distance表示从跟对象global对改对象的路径层级,一般情况下distance过大,就意味着嵌套层级太深且可能出现循环引用问题
  • Shallow SIze 表示改对象本身所占内存大小
  • Retained Size 表示该对象本身已经其所引用的对象的总大小,意味着只要将其回收,就可以释放 Retained Size大小的内存

以上代码由于闭包问题,出现循环引用导致内存溢出,上图的引用嵌套只是一部分,具体可以自己试试,至于以上代码循环引用的问题,可以参考这篇文章An interesting kind of JavaScript memory leak;

chrome dev-tool的更多使用方式,可以参考以下文章