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

相关推荐
理想不理想v3 小时前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
暮毅7 小时前
10.Node.js连接MongoDb
数据库·mongodb·node.js
~甲壳虫13 小时前
说说webpack中常见的Plugin?解决了什么问题?
前端·webpack·node.js
~甲壳虫13 小时前
说说webpack中常见的Loader?解决了什么问题?
前端·webpack·node.js
~甲壳虫13 小时前
说说webpack proxy工作原理?为什么能解决跨域
前端·webpack·node.js
熊的猫14 小时前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
前端青山1 天前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
GDAL1 天前
npm入门教程1:npm简介
前端·npm·node.js
郑小憨1 天前
Node.js简介以及安装部署 (基础介绍 一)
java·javascript·node.js
lin-lins2 天前
模块化开发 & webpack
前端·webpack·node.js