Node.js 性能調優之內存篇(一)——gcore+llnode

在開始之前,我們先了解下什麼是 Core 和 Core Dump。

測試環境

$ uname -anLinux shimo-develop 4.4.0-62-generic #83~14.04.1-Ubuntu SMP Wed Jan 18 18:10:30 UTC 2017 x86_64 x86_64 x86_64 GNU/Linuxn

什麼是 Core?

在使用半導體作為內存材料前,人類是利用線圈當作內存的材料(發明者為王安),線圈就叫作 core ,用線圈做的內存就叫作 core memory。如今 ,半導體工業澎勃發展,已經沒有人用 core memory 了,不過在許多情況下, 人們還是把記憶體叫作 core 。

什麼是 Core Dump?

當程序運行的過程中異常終止或崩潰,操作系統會將程序當時的內存狀態記錄下來,保存在一個文件中,這種行為就叫做 Core Dump(中文有的翻譯成「核心轉儲」)。我們可以認為 Core Dump 是「內存快照」,但實際上,除了內存信息之外,還有些關鍵的程序運行狀態也會同時 dump 下來,例如寄存器信息(包括程序指針、棧指針等)、內存管理信息、其他處理器和操作系統狀態和信息。Core Dump 對於編程人員診斷和調試程序是非常有幫助的,因為對於有些程序錯誤是很難重現的,例如指針異常,而 Core Dump 文件可以再現程序出錯時的情景。

開啟 Core Dump

在終端中輸入:

$ ulimit -cn

查看允許 Core Dump 生成的文件的大小,如果是 0 則表示關閉了 Core Dump,即程序異常終止時,也不會生成 Core Dump 文件。使用以下命令開啟 Core Dump 功能,並且不限制 Core Dump 生成的文件大小:

$ ulimit -c unlimitedn

以上命令只針對當前終端環境有效,如果想永久生效,可以修改 /etc/security/limits.conf 文件,如下:

gcore

不一定非要等程序崩潰時產生 Core Dump 文件,我們也可以用 gcore 不重啟程序而 dump 出對應進程的 core 文件。gcore 使用方法:

$ gcore [-o filename] pidn

如:

$ sudo gcore 17151n[New LWP 17181]n[New LWP 17180]n[New LWP 17179]n[New LWP 17178]n[New LWP 17155]n[New LWP 17154]n[New LWP 17153]n[New LWP 17152]n[Thread debugging using libthread_db enabled]nUsing host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".nsyscall () at ../sysdeps/unix/sysv/linux/x86_64/syscall.S:38n38t../sysdeps/unix/sysv/linux/x86_64/syscall.S: No such file or directory.nwarning: Memory read failed for corefile section, 8192 bytes at 0x7ffcd8fd4000.nSaved corefile core.17151n

Core Dump 時,默認會在執行 gcore 命令的目錄生成諸如 core.PID 的文件。

llnode

llnode 的 GitHub 主頁說明:

Node.js v4.x-v6.x C++ plugin for LLDB - a next generation, high-performance debugger.

LLDB 官方主頁說明:

LLDB is a next generation, high-performance debugger. It is built as a set of reusable components which highly leverage existing libraries in the larger LLVM Project, such as the Clang expression parser and LLVM disassembler.

安裝 llnode + lldb:

$ sudo apt-get updatenn# Clone llnoden$ git clone https://github.com/nodejs/llnode.git && cd llnodenn# Install lldb and headersn$ sudo apt-get install lldb-3.8 lldb-3.8-devnn# Initialize GYP,如果 timeout,嘗試 git clone https://github.com/bnoordhuis/gyp.git tools/gypn$ git clone https://chromium.googlesource.com/external/gyp.git tools/gypnn# Configuren$ ./gyp_llnode -Dlldb_dir=/usr/lib/llvm-3.8/nn# Buildn$ make -C out/ -j9nn# Installn$ sudo make install-linuxn

注意:如果 sudo apt-get update 遇到這種錯誤:

W: GPG error: xxx stable Release: The following signatures couldnt be verified because the public key is not available: NO_PUBKEY 6DA62DE462C7DA6Dn

用以下命令解決:

$ sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 6DA62DE462C7DA6Dn

--recv-keys 後面跟的是前面報錯提示的 PUBKEY。

Node.js 測試代碼

下面用一個典型的全局變數緩存導致的內存泄漏的例子來講解 llnode 的用法。

代碼如下:

// test/app.jsnuse strict;nnconst leaks = [];nnfunction LeakingClass() {n this.name = Math.random().toString(36);n this.age = Math.floor(Math.random() * 100);n}nnsetInterval(function() {n for (let i = 0; i < 100; i++) {n leaks.push(new LeakingClass);n }nn console.error(Leaks: %d, leaks.length);n}, 1000);n

一個窗口運行該程序:

$ node app.js &n

打開另一個窗口運行 gcore:

$ ulimit -c unlimitedn$ sudo gcore PIDn

生成 core.PID 文件。

--abort-on-uncaught-exception

Node.js 中通過添加 --abort-on-uncaught-exception 參數啟動,當程序 crash 的時候,會自動 Core Dump,方便『死後驗屍』。

分析 Core 文件

上面講到兩種生成 core 文件的方法:

  1. gcore 不停止程序 Core Dump
  2. 添加 --abort-on-uncaught-exception 參數,程序意外 crash 自動 Core Dump

顯然第一種更好一些,但問題來了,lldb-3.8 有個 bug,導致用 lldb 載入活著的進程生成的 core 文件(比如用 gcore)會一直掛起,而載入 crash 後生成的 core 文件則沒有這個問題。這個 bug 應該在 3.9 以上的版本中修復了,而我用的測試機器是 [email protected],最新的 lldb 只能用到 3.8(嘗試了其他辦法安裝 3.9,但依賴的許多庫需要升級,為了保證測試環境的穩定性,遂放棄了)。如果在高版本的 ubuntu 下,讀者也可以嘗試安裝使用 lldb-3.9 及以上版本。

添加 --abort-on-uncaught-exception 參數重起測試程序:

$ ulimit -c unlimitedn$ node --abort-on-uncaught-exception app.js &n[1] 26506nLeaks: 100nLeaks: 200nLeaks: 300nLeaks: 400nLeaks: 500nLeaks: 600nLeaks: 700nLeaks: 800n

另一個窗口運行:

$ kill -BUS 26506n

第一個終端窗口會顯示:

[1]+ Bus error (core dumped) node --abort-on-uncaught-exception app.jsn

我們還需要創建一個 memory ranges 文件才能使用 findjsobjects(這個也在 3.9 版本中修復了),然後設置環境變數,如下:

$ ../llnode/scripts/readelf2segments.py ./core.26506 > core.26506.rangesn$ export LLNODE_RANGESFILE=core.26506.rangesn

然後啟動 lldb,載入剛才生成的 Core 文件:

$ lldb-3.8 -c ./core.26506n(lldb) target create --core "./core.26506"nCore file /data/dev_app/test/./core.26506 (x86_64) was loaded.n(lldb) n

輸入 v8 查看使用文檔,有以下幾條命令:

  • bt
  • findjsinstances
  • findjsobjects
  • findrefs
  • inspect
  • nodeinfo
  • print
  • source

運行 v8 findjsobjects 查看所有對象實例及總共占內存大小:

(lldb) v8 findjsobjectsn Instances Total Size Namen ---------- ---------- ----n 1 24 (anonymous)n 1 24 JSONn 1 24 processn 1 32 Signaln 1 32 Timern 1 32 WriteWrapn 1 56 DefineError.aVn 1 56 MathConstructorn 1 56 RangeErrorn 1 96 Consolen 1 136 Modulen 1 144 Timeoutn 2 64 TTYn 2 208 WriteStreamn 2 224 CorkedRequestn 2 576 ReadableStaten 2 576 WritableStaten 6 336 Errorn 7 560 (ArrayBufferView)n 8 320 TickObjectn 13 416 Argumentsn 17 2312 PropertyDescriptorn 21 672 ContextifyScriptn 23 1288 NativeModulen 24 584 (Object)n 96 3072 (Array)n 426 23256 Objectn 800 32000 LeakingClassn 7103 23040 (String)n

可以看出:LeakingClass 有 800 個實例,占內存最多。使用 v8 findjsinstances LeakingClass 查看所有 LeakingClass 實例:

(lldb) v8 findjsinstances LeakingClassn0x00002a16f00dd039:<Object: LeakingClass>n0x00002a16f00dd0a1:<Object: LeakingClass>n0x00002a16f00dd109:<Object: LeakingClass>n...n

使用 v8 i 檢索實例具體內容:

(lldb) v8 i 0x00002a16f00dd039n0x00002a16f00dd039:<Object: LeakingClass properties {n .name=0x00002a16f00dd071:<String: "0.1ovw652ac1y8pv...">,n .age=<Smi: 7>}>n(lldb) v8 i 0x00002a16f00dd0a1n0x00002a16f00dd0a1:<Object: LeakingClass properties {n .name=0x00002a16f00dd0d9:<String: "0.6jebmtyldt7nl8...">,n .age=<Smi: 72>}>n(lldb) v8 i 0x00002a16f00dd109n0x00002a16f00dd109:<Object: LeakingClass properties {n .name=0x00002a16f00dd141:<String: "0.owy49plxmtfhia...">,n .age=<Smi: 35>}>n

可以看到每個 LeakingClass 實例的 name 和 age 欄位的值。

使用 -F 顯示字元串原始值:

(lldb) v8 i -F 0x00002a16f00dd141n0x00002a16f00dd141:<String: "0.owy49plxmtfhia4i">n

使用 findrefs 查看引用:

(lldb) v8 findrefs 0x00002a16f00dd039n0x2a16f00883c1: (Array)[785]=0x2a16f00dd039n

通過這個 LeakingClass 實例的內存地址,使用 findrefs 找到一個引用它的數組,我們繼續看這個數組裡有什麼東西:

(lldb) v8 i 0x2a16f00883c1n0x00002a16f00883c1:<Array: length=800 {n [0]=0x00002a16f008cfb9:<Object: LeakingClass>,n [1]=0x00002a16f008d241:<Object: LeakingClass>,n [2]=0x00002a16f008d409:<Object: LeakingClass>,n ...n [799]=0x00002a16f00dd5e1:<Object: LeakingClass>}>n

可以看出這個數組長度 800,每一項是一個 LeakingClass 實例,這不就是我們代碼中的 leaks 數組嘛。

Tips: v8 i 是 v8 inspect 的縮寫,v8 p 是 v8 print 的縮寫。

總結

以上測試代碼沒有引用任何第三方模塊,如果項目較大引用的模塊較多,v8 findjsobjects 的結果將難以甄別,這個時候可以多次使用 gcore Core Dump(如果你用的 llnode-3.9+ 的話),對比發現增長的對象,再進行診斷。

參考鏈接

cnblogs.com/Anker/p/607zhihu.com/question/5680brendangregg.com/blog/2
推薦閱讀:

編寫 Node.js Rest API 的 10 個最佳實踐
express4.x Request對象獲得參數方法小談
4.2 目錄結構-博客後端Api-NodeJs+Express+Mysql實戰
Node.js中request+response數據結構分解

TAG:Nodejs | 内存泄露 |