NodeJS的代碼調試和性能調優


本文轉自我的個人博客

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

➜  $ 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 狀態:

debug> repl
Press Ctrl + C to leave debug repl
> http
print something about http
>

按下 Ctrl+C 可以從 repl 狀態回到 debug 狀態下,我們也不需要記憶 debug 狀態下有多少調試命令,執行 help 即可:

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 文件時,會輸出這樣一行文案:

Debugger listening on port 5858

可以訪問下 http://localhost:5858,會看到: 

它告訴我們 nodejs 在打開文件的時候啟動了內建調試功能,並且監聽端口 5858 過來的調試命令。除了在命令行中直接調試之外,我們還可以通過另外兩種方式去調試這個代碼:

  • node debug <URI>, 通過 URI 連接調試,如 node debug localhost:5858
  • node debug -p <pid> 通過 PID 鏈接調試

如果我們使用 --debug 參數打開文件:

➜  $ node --debug proxy2.js

此時,nodejs 不會進入到命令行模式,而是直接執行代碼,但是依然會開啟內建調試功能,這就意味着我們具備了遠程調試 NodeJS 代碼的能力,使用 --debug 參數打開服務器的 nodejs 文件,然后通過:

➜  $ node debug <服務器IP>:<調試端口,默認5858>

可以在本地遠程調試 nodejs 代碼。不過這里需要區分下 --debug--debug-brk,前者會執行完所有的代碼,一般是在監聽事件的時候使用,而后者,不會執行代碼,需要等到外部調試接入后,進入代碼區。語言表述不會那么生動,讀者可以自行測試下。

默認端口號是 5858,如果這個端口被占用,程序會遞增端口號,我們也可以指定端口:

➜  node  node --debug-brk=8787 proxy2.js
Debugger listening on port 8787

更多的調試方式

node-inspector

NodeJS 提供的內建調試十分強大,它告訴 V8,在執行代碼的時候中斷程度,等待開發者操控代碼的執行進度。我們熟知的 node-inspector 也是用的這個原理。

➜  $ 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 在進行垃圾回收的時候,會將垃圾回收的信息打印出來:

➜  $ 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 日志導出:

➜  $ node --prof test.js

執行命令之后,會在該目錄下產生一個 *-v8.log 的日志文件,我們可以安裝一個日志分析工具 tick:

➜  $ 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 程序的調試手段上,以及調試性能的入口上做了簡要的介紹,希望對你有所啟發,不到之處還請斧正!

本文轉自我的個人博客

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM