Node JS内存泄漏调查与修复

Node JS服务端内存泄漏问题修复很容易,但查找内存泄露的原因却很头痛。

网上很多文章介绍调试node js服务端的内存泄漏,大多先用artillery 之类的工具进行高负载测试,然后使用node --inspect进行调试。

但这种方法有一个问题,如果有数百个API,每个API有多个参数,这些参数会触发不同的代码路径。

在真实环境中,根本不知道导致内存泄漏的代码所在位置,如果要让内存胀满后进行泄漏调试,必须多次调用每个可能的参数。

这种方法很棘手,除非有goreplay 这类的工具,在测试服务器上记录并重放流量。

本文介绍另一种方法:对比新旧堆转储,查找导致内存泄露的代码。

堆转储

堆转储是当前堆的快照,包含堆中所有内部和用户定义的变量及分配。

堆是所有对象生活的地方,对象一直呆在那,直到GC认为它变成垃圾然后执行清除操作。

如果能够将新服务器的堆转储,与运行时间较长且有问题的服务器堆转储进行对比,那么就能通过差异对比识别未被GC回收的对象。

查看堆转储

heapdump一个npm库,能以编程方式获取服务器的heapdump。

安装:

npm i heapdump

express服务端示例代码:

const express = require('express');
const heapdump = require("heapdump");

const app = express();
const port = 3000;

const leaks = [];

app.get('/bloatMyServer', (req, res) => {
    const redundantObj = {
        memory: "leaked",
        joke: "meta"
    };

    [...Array(10000)].map(i => leaks.push(redundantObj));

    res.status(200).send({size: leaks.length})
});

app.get('/heapdump', (req, res) => {
    heapdump.writeSnapshot(`heapDump-${Date.now()}.heapsnapshot`, (err, filename) => {
        console.log("Heap dump of a bloated server written to", filename);

        res.status(200).send({msg: "successfully took a heap dump"})
    });
});

app.listen(port, () => {
    heapdump.writeSnapshot(`heapDumpAtServerStart.heapsnapshot`, (err, filename) => {
        console.log("Heap dump of a fresh server written to", filename);
    });
});

/heapdump接口负责执行堆转储。当内存消耗增加时,手动调用此接口执行堆转储

识别内存泄漏

假设应用已经运行了好几天,服务器的内存消耗开始激增(Express Status MonitorClinicPrometheus这类工具都能检测到)。

现在,调用上面定义的接口执行堆转储,该堆转储包含GC无法收集的对象:

curl --location --request GET 'http://localhost:3000/heapdump'

堆转储会触发GC,不必关注GC后的内存,重点关注当前堆的内存分配。

注:堆转储会占用大量内存且阻塞应用所有的操作,执行此操作需谨慎对待,注意操作时间及频率。

一旦获得了两个堆转储(新、旧服务器的),就可以开始比较了。

打开chrome浏览器按F12键进入开发者模式,在chrome控制台转到Memory选项卡,使用Load功能加载快照:

加载完两个快照后,将perspective改为Comparison,然后,点选运行时间较长的服务器快照(上文说的旧服务器):

通过Constructor浏览未被GC扫描的对象,大多是NodeJS的内部引用,通过Alloc. Size列对它们进行排序。

检查使用最频繁的内存分配,看到最频繁的对象是数组,且未被GC回收:

现在,可以将数组归结为内存消耗过的原因。

修复内存泄露

既然知道是数组导致的问题,检查调试代码并修复它。