NodeJS 自 2009 年顯露人間,到現在已經六個年頭了,由於各種原因,中間派生出了個兄弟,叫做 iojs,最近兄弟繼續合體,衍生出了 nodejs4.0 版本,這東西算是 nodejs new 1.0
版本,原班人馬都統一到一個戰線上。我沒有太關注 nodejs 背後的開發,但一直是它的忠實使用者,通讀了 v4.1.2 的 文件,感覺從開發者角度去看,也沒啥大的變化,所以這兩個兄弟分開這麼久,主要是在底層內建模組上做改造,上層建築尚未有大的變更,具體可以看 這篇文章。
如果你一直用著 nodejs,然而一直都在寫最基本的小 demo,很少深入的去剖析 nodejs 的效能問題,甚至連如何 debug 程式碼、如何發現效能問題都不知從哪裡下手,那麼趕緊往下讀吧!
命令列除錯
命令列中除錯 nodejs 程式碼,這是最基礎的除錯技能,如同我們在 Chrome 控制中除錯 JS 程式碼一般,然而卻用的很少,因為他太原始,顯得比較麻煩。
回顧下,我們平時的除錯方式:
- 在某個需要輸入的地方輸入 console.log() ,列印除錯結果
- 引入
asserts
模組,對除錯區域進行 debug
這兩種除錯方式,都需要我們顯式將各種 debug 資訊嵌入到我們的業務邏輯程式碼中,而熟悉了命令列除錯之後,我們可以更好地開啟自己的除錯之旅。NodeJS 給我們提供了 Debugger 模組,內建客戶端,通過 TCP 將命令列的輸入傳送到內建模組以達到除錯的目的。
在啟動檔案時,新增第二個引數 debug
:
1 2 3 4 5 6 7 8 |
➜ $ node debug proxy2.js < Debugger listening on port 5858 debug> . ok break in proxy2.js:1 > 1 var http = require('http'); 2 var net = require('net'); 3 var url = require('url'); debug> |
除錯程式碼的時候存在兩個狀態,一個是操作除錯的位置,比如下一步,進入函式,跳出函式等,此時為 debug 模式;另一個是檢視變數的值,比如進入迴圈中,想檢視迴圈計數器 i 的值,此時為 repl(read-eval-per-line) 狀態,在 debug 模式下輸入 repl
即可進入 repl 狀態:
1 2 3 4 5 |
debug> repl Press Ctrl + C to leave debug repl > http print something about http > |
按下 Ctrl+C
可以從 repl 狀態回到 debug 狀態下,我們也不需要記憶 debug 狀態下有多少除錯命令,執行 help 即可:
1 2 3 |
debug> help Commands: run (r), cont (c), next (n), step (s), out (o), backtrace (bt), setBreakpoint (sb), clearBreakpoint (cb), watch, unwatch, watchers, repl, restart, kill, list, scripts, breakOnException, breakpoints, version |
相關的命令不算很多:
命令 | 解釋 |
---|---|
cont, c | 進入下一個斷點 |
next, n | 下一步 |
step, s | 進入函式 |
out, o | 跳出函式 |
setBreakpoint(), sb() | 在當前行設定斷點 |
setBreakpoint(line), sb(line) | 在 line 行設定斷點 |
上面幾個是常用的,更多命令可以戳這裡。
NodeJS的除錯原理
我們平時開發都使用 IDE 工具,實際上很多 IDE 工具已經整合了 NodeJS 的除錯工具,比如 Eclipse、webStorm 等等,他們的原理依然是利用 Nodejs 的 Debugger 內建模組,在這個基礎上進行了封裝。
細心的同學會發現,當我們使用 debug 引數開啟一個 node 檔案時,會輸出這樣一行文案:
1 |
Debugger listening on port 5858 |
可以訪問下 http://localhost:5858
,會看到:
它告訴我們 nodejs 在開啟檔案的時候啟動了內建除錯功能,並且監聽埠 5858 過來的除錯命令。除了在命令列中直接除錯之外,我們還可以通過另外兩種方式去除錯這個程式碼:
-
node debug -p <pid> , 通過 URI 連線除錯,如
node debug localhost:5858
- node debug -p <pid> 通過 PID 連結除錯
如果我們使用 --debug
引數開啟檔案:
1 |
➜ $ node --debug proxy2.js |
此時,nodejs 不會進入到命令列模式,而是直接執行程式碼,但是依然會開啟內建除錯功能,這就意味著我們具備了遠端除錯 NodeJS 程式碼的能力,使用 --debug
引數開啟伺服器的 nodejs 檔案,然後通過:
1 |
➜ $ node debug <伺服器IP>:<除錯埠,預設5858> |
可以在本地遠端除錯 nodejs 程式碼。不過這裡需要區分下 --debug
和 --debug-brk
,前者會執行完所有的程式碼,一般是在監聽事件的時候使用,而後者,不會執行程式碼,需要等到外部除錯接入後,進入程式碼區。語言表述不會那麼生動,讀者可以自行測試下。
預設埠號是 5858,如果這個埠被佔用,程式會遞增埠號,我們也可以指定埠:
1 2 |
➜ node node --debug-brk=8787 proxy2.js Debugger listening on port 8787 |
更多的除錯方式
node-inspector
NodeJS 提供的內建除錯十分強大,它告訴 V8,在執行程式碼的時候中斷程度,等待開發者操控程式碼的執行進度。我們熟知的 node-inspector 也是用的這個原理。
1 |
➜ $ node-inspector --web-port 8080 --debug-port 5858 |
這裡的 --web-port
是 Chrome Devtools 的除錯頁面地址埠,--debug-port
為 NodeJS 啟動的內建 debug 埠,我們可以在 http://localhost:8080/debug?port=5858
開啟頁面,除錯使用 --debug(-brk)
引數開啟的程式。
更多設定可以查閱官方文件。
IDE除錯
Eclipse 和 webstorm 的工具欄中都有一個叫做 Run 的選擇欄,在這裡可以配置該檔案的執行方式,比如在 webstorm 中(Navigation>Run>Edit Configurations):
第一步,為程式新增一個啟動程式
如果沒有 Nodejs 的選項(如在 phpstorm 中),可以手動配置下。
第二步,配置執行項
Node interpreter
是你 node 程式的位置Node parameters
是開啟 nodejs 程式的選項,如果使用了 ES6 特性,需要開始--harmony
模式,如果需要遠端除錯程式,可以使用--debug
命令,我們採用控制檯除錯,顯然是不需要新增--debug
引數的。Working directory
是檔案的目錄Javascript file
是需要除錯的檔案
第三步,斷點,除錯
其他 IDE 工具的除錯大同小異,其原理也是通過 TCP 連線到 Nodejs 開啟的內建除錯埠。
發現程式的問題
上面介紹了 NodeJS 除錯需要掌握的幾個基本技能,掌握起來還是很輕鬆的,但是要自己去嘗試下。
Nodejs 相比 Java、PHP 這些老牌語言,其周邊設施還是有所欠缺的,如效能分析和監控工具等,加上它的單執行緒執行特性,在大型應用中,很容易讓系統的 CPU 或者記憶體達到瓶頸,從而導致程式崩潰。一旦發現程式警報 CPU 負載過高,或者記憶體飆高時,我們該如何深入排查 NodeJS 程式碼存在的問題呢?
首先來分析下問題,記憶體飆高存在哪些方面的因素呢:
- 快取,很多人在敲程式的時候把快取當記憶體用,比如使用一個物件儲存使用者的 session 資訊
- 閉包,作用域沒有被釋放掉
- 生產者和消費者存在速度差,比如資料庫忙不過來,Query 佇列堆積
CPU 負載過高預警可能因素:
- 垃圾回收頻率過高、量太大,這一般是因為記憶體或者快取暴漲導致的
- 密集型的長迴圈計算,比如大量遍歷資料夾、大量計算等
這些問題是最讓人頭疼的,一個專案幾十上百個檔案,收到這些警報如果沒有經驗,根本無從下手排查。
最直接的手段就是分析 GC 日誌,因為程式的一舉一動都會反饋到 GC 上,而上述問題也會一一指向 GC,如:
- 記憶體暴漲,尤其是 Old Space 記憶體的暴漲,會直接導致 GC 的次數和時間增長
- 快取增加,導致 GC 的時間增加,無用遍歷過多
- 密集型計算,導致 GC Now Space次數增加
這裡需要稍微插一段,NodeJS 的記憶體管理和垃圾回收機制。
V8 的記憶體分為 New Space 和 Old Space,New Space 的大小預設為 8M,Old Space 的大小預設為 0.7G,64位系統這兩個數值翻倍。
物件的生命週期是:首先進入 New Space,在這裡,New Space 被平均分為兩份,每次 GC 都會將一份中的活著的物件複製到另一份,所以它的空間使用率是 50%,這個演算法叫做 Cheney 演算法,這個操作叫做 Scavenge。過一段時間,如果 New Space 中的物件還活著,會被挪到 Old Space 中去,GC 會每隔一段時間遍歷 Old Space 中死掉的物件,然後整理碎片(這裡有兩種模式 mark-sweep 和 mark-compact,不祥述)。上面提到的”死掉“,指的是物件已經沒有被引用了,活著說被引用的次數為零了。
知道這些之後,我們就好分析問題了,如果快取增加(比如使用物件快取了很多使用者資訊),GC 是不知道這些快取死了還是活著的,他們會不停地檢視這個物件,以及這個物件中的子物件是否還存活,如果這個物件數特別大,那麼 GC 遍歷的時間也會特別長。當我們進行密集型計算的時候,會產生很多中間變數,這些變數往往在 New Space 中就死掉了,那麼 GC 也會在這裡多次地進行 New Space 區域的垃圾回收。
分析 GC 日誌
說了這麼多,如何去分析 GC 的日誌?
在啟動程式的時候新增 --trace_gc
引數,V8 在進行垃圾回收的時候,會將垃圾回收的資訊列印出來:
1 2 3 4 5 6 7 8 9 |
➜ $ node --trace_gc aa.js ... [94036] 68 ms: Scavenge 8.4 (42.5) -> 8.2 (43.5) MB, 2.4 ms [allocation failure]. [94036] 74 ms: Scavenge 8.9 (43.5) -> 8.9 (46.5) MB, 5.1 ms [allocation failure]. [94036] Increasing marking speed to 3 due to high promotion rate [94036] 85 ms: Scavenge 16.1 (46.5) -> 15.7 (47.5) MB, 3.8 ms (+ 5.0 ms in 106 steps since last GC) [allocation failure]. [94036] 95 ms: Scavenge 16.7 (47.5) -> 16.6 (54.5) MB, 7.2 ms (+ 1.3 ms in 14 steps since last GC) [allocation failure]. [94036] 111 ms: Mark-sweep 23.6 (54.5) -> 23.2 (54.5) MB, 6.2 ms (+ 15.3 ms in 222 steps since start of marking, biggest step 0.3 ms) [GC interrupt] [GC in old space requested]. ... |
V8 提供了很多程式啟動選項:
啟動項 | 含義 |
---|---|
–max-stack-size | 設定棧大小 |
–v8-options | 列印 V8 相關命令 |
–trace-bailout | 查詢不能被優化的函式,重寫 |
–trace-deopt | 查詢不能優化的函式 |
這些啟動項都可以讓我們檢視 V8 在執行時的各種 log 日誌,對於排查隱晦問題比較有用。然而這堆日誌並不太好看,我們可以將日誌輸出來之後交給專業的工具幫我們分析,相比很多人都用過 Chrome DevTools 的 JavaScript CPU Profile,它在這裡:
通過 Profile 可以找到具體函式在整個程式中的執行時間和執行時間佔比,從而分析到具體的程式碼問題,V8 也提供了 Profile 日誌匯出:
1 |
➜ $ node --prof test.js |
執行命令之後,會在該目錄下產生一個 *-v8.log
的日誌檔案,我們可以安裝一個日誌分析工具 tick:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
➜ $ sudo npm install tick -g ➜ $ node-tick-processor *-v8.log [Top down (heavy) profile]: Note: callees occupying less than 0.1% are not shown. inclusive self name ticks total ticks total 426 36.7% 0 0.0% Function: ~<anonymous> node.js:27:10 426 36.7% 0 0.0% LazyCompile: ~startup node.js:30:19 410 35.3% 0 0.0% LazyCompile: ~Module.runMain module.js:499:26 409 35.2% 0 0.0% LazyCompile: Module._load module.js:273:24 407 35.1% 0 0.0% LazyCompile: ~Module.load module.js:345:33 406 35.0% 0 0.0% LazyCompile: ~Module._extensions..js module.js:476:37 405 34.9% 0 0.0% LazyCompile: ~Module._compile module.js:378:37 ... |
我們也可以使用 headdump 之類的工具將日誌匯出,然後放到 Chrome 的 Profile 中去分析。
小結
本文主要從 NodeJS 程式的除錯手段上,以及除錯效能的入口上做了簡要的介紹,希望對你有所啟發,不到之處還請斧正!