开门见山,这是一段可以搞崩掉服务器的代码片段,如果你的代码也这样,那一定要注意啦~
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。
关注「淘系技术」微信公众号,一个有温度有内容的技术社区~