今日文章由作者 @林乐扬 投稿分享。
背景
团队最近将两个项目迁移至 degg 2.0
中,两个项目均出现比较严重的内存泄漏问题,此处以本人维护的埋点服务为例进行排查。服务上线后内存增长如下图,其中红框为 degg 2.0
线上运行的时间窗口,在短短 36 小时内,内存已经增长到 50%,而平时内存稳定在 20%-30%,可知十之八九出现了内存泄漏。
排查思路
由于两个接入 degg 2.0
的服务均出现内存泄漏问题,因此初步将排查范围锁定在 degg 2.0
引入或重写的基础组件上,重点怀疑对象为 nodex-logger
组件;同时为了排查内存泄漏,我们需要获取服务运行进程的堆快照(heapsnapshot),获取方式可参看文章 hyj1991:Node 案发现场揭秘 —— 快速定位线上内存泄漏https://zhuanlan.zhihu.com/p/36340263 。
排查过程
一、获取堆快照
使用 alinode 获取堆快照,服务启动后,使用小流量预热一两分钟便记录第1份堆快照(2020-4-16-16:52),接着设置 qps
为 125 对服务进行施压,经过大约一个小时(2020-4-16-15:46)获取第2份堆快照。使用 Chrome dev
工具载入两份堆快照,如下图所示,发现服务仅短短运行一小时,其堆快照文件就增大了 45MB,而初始大小也不过 39.7MB;我们按 Retained Size
列进行排序,很快就发现了一个『嫌疑犯』,即 generator
;该项占用了 55% 的大小,同时 Shallow Size
却为 0%,一项一项地展开,锁定到了图中高亮的这行,但是继续展开却提示 0%,线索突然断了。
盯着 generator
进入思考,我的服务代码并没有generator
语法,为什么会出现 generator
对象的内存泄漏呢?此时我把注意力转到 node_modules
目录中,由于最近一直在优化 nodex-kafka
组件,有时直接在 node_modules
目录中修改该组件的代码进行调试,因此几乎每个文件头部都有的一段代码引起了我的注意:
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
这个代码是 typescript 源码编译后的产出,由于代码使用了 async/await
语法,因此都编译成 __awaiter
的形式,在源码中使用 async
函数的地方,在编译后都使用 __awaiter
进行包裹:
// 编译前
(async function() {
await Promise.resolve(1);
await Promise.resolve(2);
})()
// 编译后
(function () {
return __awaiter(this, void 0, void 0, function* () {
yield Promise.resolve(1);
yield Promise.resolve(2);
});
})();
同时一个关于 generator
内存泄漏的 #3075