Node.js 性能調優之內存篇(一)——gcore+llnode
測試環境
$ 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 文件的方法:
- gcore 不停止程序 Core Dump
- 添加 --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
- 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+ 的話),對比發現增長的對象,再進行診斷。
參考鏈接
http://www.cnblogs.com/Anker/p/6079580.htmlhttps://www.zhihu.com/question/56806069http://www.brendangregg.com/blog/2016-07-13/llnode-nodejs-memory-leak-analysis.html
推薦閱讀:
※編寫 Node.js Rest API 的 10 個最佳實踐
※express4.x Request對象獲得參數方法小談
※4.2 目錄結構-博客後端Api-NodeJs+Express+Mysql實戰
※Node.js中request+response數據結構分解
