一次node服务的内存泄漏排查记录(DevTool使用)

背景:

公司的一个node服务在某台ECS服务器上经常发生内存或CPU告警(部署到其它服务器的不存在),且有一定概率会造成机器宕机。本文记录一下笔者排查的思路。

分析:

既然只在某台服务器上发生,那么自然会想到是不是这台服务器的环境存在什么问题, 遂进行排查: 正常服务器的配置(内存8G): 发生内存告警的服务器配置(内存4G): 内存确实是存在差距的,但是转念一想,我们在业务上的体量也是不一样的呀,8G服务器所在地区的业务体量是4G服务器所在的N倍(N > 2)。 而且,我们的这个node服务本质是一个网关,不会处理太多业务逻辑,也就不需要太多的常驻内存。也就是说,一定存在严重的内存泄漏问题

找运维同学,要了一下出问题时候机器的运行情况。 这张图是内存报警时候的图,机器还没挂掉。挂掉的时候,CPU、内存、BPS全部打满了...

我们使用devTools分析一波。 开启测试环境Node服务的inspect模式,便于观测内存的变化。 node --inspect-brk=0.0.0.0:9229 dist/index.js :::danger 一定要保证在网络安全的环境下使用这种方式,如果你的服务器是外网可访问的,那么任何人都可以调试你的服务。 ::: 在chrome打开 chrome://inspect/#devices 配置好之后,你会看到 Remote Target 下方出现了一条,点击inspect即刻打开devTool 点击后,切换到Memory,你会看到如下图的面板 三个选项的区别和使用场景: **Heap snapshot(堆快照): ** 将当前的Node服务的堆栈情况生成一个快照。通过快照我们可以清晰某一刻,系统中都有哪些数据在内促中。我们也可以通过隔一段时间打一个快照,对比两个快照的内存变化,从而分析系统的内存监控情况。

当你对系统内存泄漏的信息不多时,可以使用打快照的方式,对比两个时间节点的内存变化。

Allocation instrumentation on timeline(内存仪表盘):Heap snapshot不同的是,启用这个模式后,分析面板会多一个时间线,且分析会一直进行下去。指点你点击终止。 Timeline 中,蓝色柱代表未被回收的内存(可能存在内存泄漏),灰色代表被GC已经回收的内存。 选中对应的柱状图,即可看到当时的内存情况。

当你知道某些操作可能会引发内促泄漏的时候,可以用这个用来验证。

Allocation sampling(分配抽样): 像内存仪表盘一样,它也会一直运行。 不同的是,它记录的是内存分配的过程,而不是每个时间点内存的所有情况。 它周期性的对内存分配进行抽样,对性能的损耗更小,适合需要长时间收集的场景。

当你想观测系统的内存分配情况时候,可以使用这种方式。

有了分析工具,笔者决定先使用 Heap snapshot 模式,先对刚启动的服务打一个快照,并模拟用户,在前端页面点一点(为了更加真实,笔者这里使用了压测工具,将事先写好的接口并行调用)。调用了100次之后再次打了一个快照,神奇的事情出现了,内促竟然从33.1MB变成了37.4MB 查看两个快照之间分配的对象发现,有 (string)的占用了3.4MB的内存(Shallow Size),占用了内存增长的大部分。

Memory面板名词解释,部分引用自zhuanlan.zhihu.com/p/80792297

(string) 字符串原始值。指字符串的引用。除了明确使用 new String创建的字符串外,其它的均属于(string)

(array) 通常指的是数组索引属性的集合, 它是指那些由于元素增加或删除而发生变化的索引.

Array 代表实际的Array对象实例,指数组对象的整个生命周期变化------新数组的创建或现有数组的回收

Contructor - 表示使用此构造函数创建的所有对象

Distance - 显示使用节点最短简单路径时距根节点的距离

Shallow Size - 显示通过特定构造函数创建的所有对象浅层大小的总和。浅层大小是指对象自身占用的内存大小(一般来说,数组和字符串的浅层大小比较大)

Retained Size - 显示同一组对象中最大的保留大小。某个对象删除后(其依赖项不再可到达)可以释放的内存大小称为保留大小。

New - Comparison 特有 - 新增项

Deleted - Comparison 特有 - 删除项

Delta - Comparison 特有 - 增量 (Delta = New - Deleted)

Alloc. Size - Comparison 特有 - 内存分配大小

Freed Size - Comparison 特有 - 释放大小

Size Delta - Comparison 特有 - 内存增量 (Size Delta = Alloc.Size - Freed Size)

点开进一步分析,发现这个string正是我们刚才接口请求的响应体。定位到之指示的apiProxy.service.js对应的代码片段:

typescript 复制代码
public static proxy(ctx: KoaContext, path: string, method: string, headers: AnyObject): Promise<any> {
        const {
            port,
            hostname,
        } = ApiProxyService.getRoute(path, headers);

        return new Promise((resolve, reject) => {
            const options: RequestOptions = {
                hostname: hostname,
                path: path,
                method: method,
                port: port,
                headers: headers,
            };

            const request = http.request(options, (res) => {
                if (res.statusCode !== 200) {
                    ...
                    return;
                }
                let data = '';
                res.on('data', (chunk) => {
                    data = data + chunk;
                });
                res.on('end', () => {
                  ...
                });
            });

            request.on('error', (e) => {
              ...
            });

            request.end();
            // 自动定位到了这里
            process.on('uncaughtException', function (err) {
                console.warn(err.stack);
                console.warn('NOT exit...');
            });

        });
    }

从代码上看,在 proxy中增加了 一个 uncaughtException事件,相当于每个请求,都会增加一个全局事件,当这个事件触发时候,会将之前监听的所有事件全部触发。笔者将监听 uncaughtException 的事件放置到了全局,确保应用启动只触发一次,再次inspect Memory后,发现一切恢复了正常!

遗留问题:

Memory面板来看,每次请求后,内存中都会将接口响应的字符串存下来。字符串的内存泄漏是本次的罪魁祸首,重复监听只是次要的。 但是从代码层面分析

typescript 复制代码
process.on('uncaughtException', function (err) {
                console.warn(err.stack);
                console.warn('NOT exit...');
            });

uncaughtException事件只会造成重复监听,它的回调函数中也没有对外部的引用,不会触发闭包, 理论上来讲不会和响应体字符串产生关联。 求各位看官答疑解惑。

相关推荐
没事别瞎琢磨41 分钟前
十、统一 Runner 入口——能力检测与模式回退
人工智能·node.js
没事别瞎琢磨1 小时前
八、环境隔离——构建安全的子进程环境
人工智能·node.js
没事别瞎琢磨2 小时前
六、输出捕获与截断
人工智能·node.js
没事别瞎琢磨2 小时前
七、敏感路径预检——Protected Paths
人工智能·node.js
没事别瞎琢磨2 小时前
五、进程执行——spawn、超时与进程树清理
人工智能·node.js
没事别瞎琢磨2 小时前
四、命令风险分级与审批策略
人工智能·node.js
没事别瞎琢磨3 小时前
三、配置系统——默认值与解析
人工智能·node.js
右耳朵猫AI4 小时前
Node.js周刊2026W22 | Node.js 26、Deno 2.8、Rolldown 1.0、TypeORM 1.0、Bun v1.3.14
node.js
没事别瞎琢磨4 小时前
二、类型系统——给所有概念起名字
人工智能·node.js
Java.熵减码农7 小时前
Hermes Agent 安装踩坑记录:DNS 解析失败 & Node.js 幽灵文件冲突
node.js·ai编程·hermes