背景
目前容器化和微服务是服务端开发的一个潮流和趋势,然而在这种微服务的架构下,我们在实际的企业开发中会遇到一些困境:趋向于越来越稳定的服务端 API 和多样化高灵活性的用户诉求间存在天然的矛盾。
更通俗地描述一些实际开发的场景:Android、IOS、PC 和 M 站对于同一个性质的接口需求的字段不一致,导致的前端开发和服务端开发间经常会因为增减字段产生的大量的沟通开销。
为了解决这样的一个困境,一些公司采取了在传统的前端和后端之间加入一层 BFF 层,进而达到谁使用谁开发维护的目的。很显然,对于前端比较熟悉的 Node.js 是这个 BFF 层实现的一个比较理想的语言。
但是这样做其实又引入了一些新的问题(典型的为了解决一个问题又引入了一个新的问题),相对于传统的比较成熟的 Java 语言来说,Node.js 的 runtime 对于绝大部分开发者来说是一个黑盒,没有对应的生态链工具来保障这个由 BFF 层运行的稳定————比如线上出现内存泄漏导致进程间歇性 OOM 了,我们应该怎么去处理定位。
这篇文章旨在这个大背景下对 Node.js 的开发中遇到内存泄漏问题做一些展开和探讨。
堆快照浅探
获取堆快照
想要分析定位内存泄漏问题,首先我们要去获取 Node.js 进程在发生泄漏时的堆上各个对象和它们间的引用关系,这个保存了堆上各个对象以及其引用关系的文件就是堆快照。V8 引擎提供了一个接口可以让我们很方便地实时获取到堆快照,下面我们介绍三种不同的方法来获取。
heapdump
首先可以执行如下命令安装 heapdump 模块:
npm install heapdump
此模块需要在代码中引入:
const heapdump = require('heapdump');
heapdump 模块提供了两种方式来获取进程当前的堆快照,第一种是在代码中通过自定义逻辑(可以是定时器定是获取,或者长连接开关热启动),下面是一个例子:
'use strict';const heapdump = require('heapdump');const path = require('path');setInterval(function() { let filename = Date.now() + '.heapsnapshot'; heapdump.writeSnapshot(path.join(__dirname, filename));}, 30 * 1000);
这里每隔 30s 输出一个堆快照到到当前目前下。
第二种是启动引入了 heapdump 模块的 Node.js 进程后,通过 usr2 这个信号量来触发堆快照:
kill -USR2 <需要获取堆快照的 node.js 进程 pid>需要获取堆快照的>
这种办法的好处是不需要在代码中植入相关逻辑,而仅在需要的时候 ssh 到服务器上通过信号量获取到堆快照。
v8-profiler
首先可以执行如下命令安装 heapdump 模块:
npm install v8-profiler
v8-profiler 提供了 transform 流的形式输出堆快照,对于一些比较大的堆快照文件能更好的进行生成处理:
'use strict';const v8Profiler = require('v8-profiler-node8');const snapshot = v8Profiler.takeSnapshot();// 获取堆快照数据流const transform = snapshot.export();// 流式处理堆快照transform.on('data', data => console.log(data));// 数据处理完毕后删除transform.on('finish', snapshot.delete.bind(snapshot));
v8-profiler 在 Node.js v6.x 之前的版本中通过 node-pre-gyp 可以直接下载到对应系统的 binary,无需进行本地编译,对于一些非 mac 类的开发环境还是比较友好的。