震惊!这个代码片段竟然会让 V8 内存无法回收?!

开门见山,这是一段可以搞崩掉服务器的代码片段,如果你的代码也这样,那一定要注意啦~

  try {
        obj = JSON.parse(data);
    } catch (err) {
        // ignore
    }

你肯定很好奇,这段看似平淡的代码片段究竟是怎样搞崩掉服务器的?

这是一个"真实"的故事,就发生在几天前......

某晚一办公大楼警铃大作,电话那头某应用函数报告某应用系统异常, 从监控上看到,内存增长呈现阶梯式爆炸式增长,短短几个小时就消耗完了系统内存。

内存监控

咋一看,这是普通的不能再普通的内存泄漏问题,这对训练有素的士兵们已经不算什么。按照常规方法,取heapdump进行分析,占用最多的对象一般都能分析个八九不离十了。

但是 。。。

heapdump竟然看不出什么。。。只看到一个影子,一个吃了几百兆内存的影子,这是什么鬼?

Heapdump

此时,报警还在持续,办公室报警声不断,但又非常安静,弥漫着诡异的气氛。

监控上,应用一个个逼近系统极限,OOM一个个成为尸体,但是都留下相同的影子 。。。

时间在一分一秒的过去,"我们必须尽快抓到'影子',好给大家一个交代",数班长急促的声音透露着坚定。

"'影子'可能有个代号script_list,但是我们目前掌握的就只有那么多信息了",Y说到。

Y是班里最牛的信息兵,他有着最敏锐的洞察力,并掌握着最精准的信息,但是这一次,他也感到困惑。

"M,你跟我立刻去一趟基地,我们要进去抓'影子'" 班长说。"是,长官"。

作为特种兵的M,平时就接受了缺少粮食、缺少装备的高强度训练,他可以在极简的配置下,执行最底层的特殊任务。

M近照

"如果'影子'是个人,他应该还在基地里",M说。

"你能找到他么?",班长问。

"能!他只能从指定的门进去,并且注册登记,吃成这么胖,应该很容易被发现。"

"如果是妖呢?"

"下次好莱坞的电影可以用这个做题材,这是人类历史上首次捉到妖",

班长一脚踢向了M,"少TM扯淡,走!"

"带上这个,或许会用到。" 临走时,P塞给了M一卷图纸。走的匆忙,M也没来得及看一眼,就丢在了包里。

一卷图纸

班长和M离开了办公大楼,去往基地。

基地在不远的地方,门口有门卫守护,但是地方很大,要在基地找到'影子'并不是容易的事情。

基地内戒备森严,并还有巡逻的卫兵,巡视着基地内各个房间,并清理一些不必要的垃圾出来。

基地已经运作了很多很多年,可能有过一些异类后来被清理了,但是从来没有遇到过'妖怪'?

到了基地, "M,你进去吧,我还有个会议要参加,要给排长作简报,等你好消息哟~", "是,长官",M背着包就进了基地。

M的包里除了P塞的图纸,还有gdb和llnode两个工具。"真实的师傅领进门",M心里默想。

gdb 用来定位和分析v8/node的c++实现,大部分没啥用,但有把叉子总比啥都没有的强。

llnode 用来定位和分析v8的object,虽然绝大部分都是unkown,但能看个东西总比眼瞎的强。

基地内被分割了很多个营地,每个营地都有自己独立的管理人员。M面临的第一个问题,是如何找到各营地的管理人员,因为管理人员通常不固定在一个地方,而且他又没有电话号码可以联系。

但是每一个营地在建设的时候,都保留了一个设计图纸,里面标注了这个营地营长的办公室。

"P给我塞的难道是营地图纸",M嘀咕着, 

拿出图纸一看,真的是Isolate第一营地的地方标注,他径直走了进去。

关于进程内存中定位Isolate node支持多个Isolate,通过 node::per_process::v8_platform.platform_.per_isolate_ 可以获取到所有v8::Isolatenode binary会在固定内存的地址上存放了一些很重要的数据用以分析,比如下面的v8_platform

00000000029ae600 B node::per_process::v8_platform

除此之外还有 nodedbg、v8dbg开头的常量符号用于mdb(Modular Debugger), 被收进llnode中,用来给v8和node定位corefile,也被称作 postmortem (验尸)。

"长管,我是NODE特种兵M,请问您是Isolate的营长么?"

"我是"

"我受上级命令,来调查一个叫'影子'的人,这个人很危险关系到人民的利益,影响到群众用TB了"

"'影子'?从来没听过这个人",营长一脸困惑

"这个人可能很胖,你能给我讲一下我怎么能查到所有的人,我相信我能找到他"

"可以是可以,你得这样来 。。。",营长给M讲了一下营地的结构。

原来营地分为很多个区域,

  • 新兵区,刚来的新兵都在这个区域进行训练,有些新兵呆满2年就退伍转业了,有些新兵则可能留在部队晋升到老兵区了。

  • 老兵区,老兵通常有着更丰富的经验,并且比新兵更加沉稳,愿意效忠,退伍意愿并不强烈。

  • 还有器械区,摆放了各种武器,虽然武器最后会分发给各个士兵,但是都存放在这里。

Node内存

"每一个人,每一把枪,都在账本上有登记,你也可以查看宿舍和仓库。我现在带你去见H长官",营长说。

H长官负责所有营地的人或物件的管理,任何进出都需由H长官许可。

Isolate->heap_ 管理了v8所有的对象。

在H长官的带领下,M检查了新兵区和老兵区的登记,没有发现任何异样,完全没有异常体重的人。

M走进了大型器械仓库,看到一个超级大的架子,

"这是什么?",M问道,

"这是武器架,任何武器都存放在这个架子上,每个武器存放一格"。

"这有多长",M接着问道,

"700多m",

"你们有多少武器"

"10w件",

"那要这么大的架子么?",M表示疑问。

从 0xbec56a80138 - 0xbec81f55660,存放了一个LargeObject,占用了726M内存空间

M拿出了GDB仔细检查了这个架子,发现700m的架子上,只有头上和中间部分集中摆放了一些武器,其余部分都是空的。

"为什么会这样?",M问H长官。

"这是按规定的,我们有一个账本,记录了进来的武器,每次进来一件,我就会从架子上分配一个格子,如果没有格子了,我就问上级需求一个新的架子。我们这里需求很大,你看,现在已经分配到66626945格了。"

"那些取出的武器呢?",M问

"放心,GC卫兵会来清点的,如果架子后面都是空的,他会标注最后一个有武器的格子,然后我会从下一个空格子分配。这个系统已经运作很久了,从来没有出过问题",H长官有些不耐烦。

"这个架子有代号么?",

"有,叫script_list"。

中间 (0x00002090 - 0x056dc5c0), ( 0x056dc610 - 0x1fc48d60) 都是0x0000000000000003(v8空指针) 空洞占了绝大多内存空间,由于v8指针压缩技术的存在,写脏的页面导致很大的内存开销。每一个js都会创建一个script添加到script_list上。

听到这,M已经理解为啥这个营地需要那么多的架子存放武器了。

因为只要架子后面有一把武器没有被拿走,新来的武器只能存放在他的后面。所以这个架子已经接到700多m,并且600多m都是空的。

M走到架子中间,随手拿起中部架子上第一部武器,是一把手枪。M拿出了LLNODE,仔细检查了这把手枪。M注意到手枪上面印有"[object Object]"的字样。

这个script含有特征字符 "[object Object]"和3个smi数字(1,2,6596938),但无法判断是什么script

"这是谁的枪?",M问道,

"士兵使用不同的枪械,这种类型的'[object Object]'手枪属于很多个兵种,一排二排都是,但是不知道具体谁的。",长官答道,

M拍了拍上面的灰尘说,"这把枪应该很久没有人来拿过了,要不现在开始,所有的入库都需要检查一下,看看谁还有这把手枪?"

没过多久,有个叫Json士兵来到架子前,M用GDB查看了他的手枪,上面写着"[object Object]"。

"有个长官让我更换这个枪的枪托,我更换时发现这个枪托根本拆不开,按照部队规定我就给送到这里来了",Json解释到,

"这把是不是也你的?",M问道,

"可能是我上次忘了吧,", Json答道,

"你们有没有流程记录送到这里的枪械,然后会全部取回么?",M问道,

"没有,忙起来就忘了"。

利用gdb的数据断点,可以捕获向script_list添加script的调用栈。

这个捕获的调用栈显示了在处理JSON异常时,会向v8::script_list增加script,并且这个script含有特征字符串"[object Object]"。

JS的代码呢?你没看错,就是片头的范例。

M拨通了数班长的电话,"我找到'影子'了"。

几个月后,

node基地从v12.18.2开始,对script_list的入库,都采用了新账本来管理这些入库的武器, 那些freed的格子都被填满了武器。

终——

这是一个复合型的内存泄漏案例。

v8::script_list的实现是在WeakArrayList的末尾添加新的script,并在执行完成之后由GC回收缩短队列, 

JSON.parse()在遇到异常时,会有少量的内存泄漏并可能遗留script的对象在script_list中, 

泄漏的script对象造成了v8::script_list出现空洞而无法回缩,从而放大了对内存的消耗。

  • node-v12.18.2以前所有的v12版本都受这个问题影响。

  • 但v10不受这个问题影响。

最后:如你的应用已经遇到类似的内存泄漏问题,请尽快升级到最新的nodejs或alinode。

✿  拓展阅读

作者|赵磊(码匠)

编辑|橙子君

出品|阿里巴巴新零售淘系技术

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值