大數據

震驚!這個代碼片段竟然會讓 V8 內存無法回收?!

開門見山,這是一段可以搞崩掉服務器的代碼片段,如果你的代碼也這樣,那一定要注意啦~

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

你肯定很好奇,這段看似平淡的代碼片段究竟是怎樣搞崩掉服務器的?

這是一個"真實"的故事,就發生在幾天前......

某晚一辦公大樓警鈴大作,電話那頭某應用函數報告某應用系統異常, 從監控上看到,內存增長呈現階梯式爆炸式增長,短短几個小時就消耗完了系統內存。

image.png

內存監控

咋一看,這是普通的不能再普通的內存洩漏問題,這對訓練有素的士兵們已經不算什麼。按照常規方法,取heapdump進行分析,佔用最多的對象一般都能分析個八九不離十了。

但是 。。。

heapdump竟然看不出什麼。。。只看到一個影子,一個吃了幾百兆內存的影子,這是什麼鬼?

image.png

Heapdump

此時,報警還在持續,辦公室報警聲不斷,但又非常安靜,瀰漫著詭異的氣氛。

監控上,應用一個個逼近系統極限,OOM一個個成為屍體,但是都留下相同的影子 。。。

時間在一分一秒的過去,"我們必須儘快抓到'影子',好給大家一個交代",數班長急促的聲音透露著堅定。

"'影子'可能有個代號script_list,但是我們目前掌握的就只有那麼多信息了",Y說到。

Y是班裡最牛的信息兵,他有著最敏銳的洞察力,並掌握著最精準的信息,但是這一次,他也感到困惑。

"M,你跟我立刻去一趟基地,我們要進去抓'影子'" 班長說。"是,長官"。

作為特種兵的M,平時就接受了缺少糧食、缺少裝備的高強度訓練,他可以在極簡的配置下,執行最底層的特殊任務。

image.png

M近照

"如果'影子'是個人,他應該還在基地裡",M說。

"你能找到他麼?",班長問。

"能!他只能從指定的門進去,並且註冊登記,吃成這麼胖,應該很容易被發現。"

"如果是妖呢?"

"下次好萊塢的電影可以用這個做題材,這是人類歷史上首次捉到妖",

班長一腳踢向了M,"少TM扯淡,走!"

"帶上這個,或許會用到。" 臨走時,P塞給了M一卷圖紙。走的匆忙,M也沒來得及看一眼,就丟在了包裡。

image.png

一卷圖紙

班長和M離開了辦公大樓,去往基地。

基地在不遠的地方,門口有門衛守護,但是地方很大,要在基地找到'影子'並不是容易的事情。

基地內戒備森嚴,並還有巡邏的衛兵,巡視著基地內各個房間,並清理一些不必要的垃圾出來。

基地已經運作了很多很多年,可能有過一些異類後來被清理了,但是從來沒有遇到過'妖怪'?

到了基地, "M,你進去吧,我還有個會議要參加,要給排長作簡報,等你好消息喲~", "是,長官",M揹著包就進了基地。

M的包裡除了P塞的圖紙,還有gdb和llnode兩個工具。"真實的師傅領進門",M心裡默想。

gdb 用來定位和分析v8/node的c++實現,大部分沒啥用,但有把叉子總比啥都沒有的強。

llnode 用來定位和分析v8的object,雖然絕大部分都是unkown,但能看個東西總比眼瞎的強。

基地內被分割了很多個營地,每個營地都有自己獨立的管理人員。M面臨的第一個問題,是如何找到各營地的管理人員,因為管理人員通常不固定在一個地方,而且他又沒有電話號碼可以聯繫。

但是每一個營地在建設的時候,都保留了一個設計圖紙,裡面標註了這個營地營長的辦公室。

"P給我塞的難道是營地圖紙",M嘀咕著,

拿出圖紙一看,真的是Isolate第一營地的地方標註,他徑直走了進去。

關於進程內存中定位Isolate node支持多個Isolate,通過

image.png

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年就退伍轉業了,有些新兵則可能留在部隊晉升到老兵區了。
  • 老兵區,老兵通常有著更豐富的經驗,並且比新兵更加沉穩,願意效忠,退伍意願並不強烈。
  • 還有器械區,擺放了各種武器,雖然武器最後會分發給各個士兵,但是都存放在這裡。

image.png

Node內存

"每一個人,每一把槍,都在賬本上有登記,你也可以查看宿舍和倉庫。我現在帶你去見H長官",營長說。

H長官負責所有營地的人或物件的管理,任何進出都需由H長官許可。

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

在H長官的帶領下,M檢查了新兵區和老兵區的登記,沒有發現任何異樣,完全沒有異常體重的人。

M走進了大型器械倉庫,看到一個超級大的架子,

"這是什麼?",M問道,

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

"這有多長",M接著問道,

"700多m",

"你們有多少武器"

"10w件",

"那要這麼大的架子麼?",M表示疑問。

image.png

從 0xbec56a80138 - 0xbec81f55660,存放了一個LargeObject,佔用了726M內存空間

M拿出了GDB仔細檢查了這個架子,發現700m的架子上,只有頭上和中間部分集中擺放了一些武器,其餘部分都是空的。

"為什麼會這樣?",M問H長官。

"這是按規定的,我們有一個賬本,記錄了進來的武器,每次進來一件,我就會從架子上分配一個格子,如果沒有格子了,我就問上級需求一個新的架子。我們這裡需求很大,你看,現在已經分配到66626945格了。"

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

"放心,GC衛兵會來清點的,如果架子後面都是空的,他會標註最後一個有武器的格子,然後我會從下一個空格子分配。這個系統已經運作很久了,從來沒有出過問題",H長官有些不耐煩。

"這個架子有代號麼?",

"有,叫script_list"。

image.png

中間 (0x00002090 - 0x056dc5c0), ( 0x056dc610 - 0x1fc48d60) 都是0x0000000000000003(v8空指針) 空洞佔了絕大多內存空間,由於v8指針壓縮技術的存在,寫髒的頁面導致很大的內存開銷。每一個js都會創建一個script添加到script_list上。

聽到這,M已經理解為啥這個營地需要那麼多的架子存放武器了。

因為只要架子後面有一把武器沒有被拿走,新來的武器只能存放在他的後面。所以這個架子已經接到700多m,並且600多m都是空的。

M走到架子中間,隨手拿起中部架子上第一部武器,是一把手槍。M拿出了LLNODE,仔細檢查了這把手槍。M注意到手槍上面印有"[object Object]"的字樣。

image.png

這個script含有特徵字符 "[object Object]"和3個smi數字(1,2,6596938),但無法判斷是什麼script

"這是誰的槍?",M問道,

"士兵使用不同的槍械,這種類型的'[object Object]'手槍屬於很多個兵種,一排二排都是,但是不知道具體誰的。",長官答道,

M拍了拍上面的灰塵說,"這把槍應該很久沒有人來拿過了,要不現在開始,所有的入庫都需要檢查一下,看看誰還有這把手槍?"

沒過多久,有個叫Json士兵來到架子前,M用GDB查看了他的手槍,上面寫著"[object Object]"。

"有個長官讓我更換這個槍的槍托,我更換時發現這個槍托根本拆不開,按照部隊規定我就給送到這裡來了",Json解釋到,

"這把是不是也你的?",M問道,

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

"你們有沒有流程記錄送到這裡的槍械,然後會全部取回麼?",M問道,

"沒有,忙起來就忘了"。

image.png

利用gdb的數據斷點,可以捕獲向script_list添加script的調用棧。

這個捕獲的調用棧顯示了在處理JSON異常時,會向v8::script_list增加script,並且這個script含有特徵字符串"[object Object]"。

JS的代碼呢?你沒看錯,就是片頭的範例。

image.png

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。

關注「淘系技術」微信公眾號,一個有溫度有內容的技術社區~

image.png

Leave a Reply

Your email address will not be published. Required fields are marked *