查看原文
其他

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

码匠 Nodejs技术栈 2022-07-06


作者@码匠 | 原文链接 https://mp.weixin.qq.com/s/vOXUW2EwMbJQoj91XoeYew

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

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

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

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

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

内存监控

咋一看,这是普通的不能再普通的内存泄漏问题,这对训练有素的士兵们已经不算什么。按照常规方法,取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。

未完待续。。。

- END -

敬请关注「Nodejs技术栈」微信公众号,获取优质文章,如需投稿可在后台留言与我取得联系。

往期精彩回顾
Node.js 文件句柄泄露导致进程假死案例
Node.js 性能监控解决方案 Easy-Monitor 3.0
通过 Node.js 小示例学习浏览器缓存策略
跨域(CORS)产生原因分析与解决方案
Node.js 在企业中的应用实践集锦

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存