Node.js 應用故障排查手冊 —— 正確打開 Chrome devtools

楔子

前面的預備章節中咱們大體瞭解瞭如何在服務器上的 Node.js 應用出現問題時,從常規的錯誤日誌、系統/進程指標以及兜底的核心轉儲這些角度來排查問題。這樣就引出了下一個問題:咱們知道進程的 CPU/Memory 高,或者拿到了進程 Crash 後的核心轉儲,要如何去進行分析定位到具體的 JavaScript 代碼段。node

其實 Chrome 自帶的 Devtools,對於 JavaScript 代碼的上述 CPU/Memory 問題有着很好的原生解析展現,本節會給你們作一些實用功能和指標的介紹(基於 Chrome v72,不一樣的版本間使用方式存在差別)。git

本書首發在 Github,倉庫地址:https://github.com/aliyun-node/Node.js-Troubleshooting-Guide,雲棲社區會同步更新。github

CPU 飆高問題

I. 導出 JS 代碼運行狀態算法

當咱們經過第一節中提到的系統/進程指標排查發現當前的 Node.js 應用的 CPU 特別高時,首先咱們須要去經過一些方式將當前 Node.js 應用一段時間內的 JavaScript 代碼運行情況 Dump 出來,這樣子才能分析知道 CPU 高的緣由。幸運的是,V8 引擎內部實現了一個 CPU Profiler 可以幫助咱們完成一段時間內 JS 代碼運行狀態的導出,目前也有很多成熟的模塊或者工具來幫咱們完成這樣的操做。npm

v8-profiler 是一個老牌的 Node.js 應用性能分析工具,它能夠很方便地幫助開發者導出 JS 代碼地運行狀態,咱們能夠在項目目錄執行以下命令安裝此模塊:緩存

npm install v8-profiler --save

接着能夠在代碼中按照以下方式獲取到 5s 內地 JS 代碼運行狀態:服務器

'use strict';

const v8Profiler = require('v8-profiler');
const title = 'test';
v8Profiler.startProfiling(title, true);
setTimeout(() => {
    const profiler = v8Profiler.stopProfiling(title);
    profiler.delete();
    console.log(profiler);
}, 5000);

那麼咱們能夠看到,v8-profiler 模塊幫我導出的代碼運行狀態其實是一個很大的 JSON 對象,咱們能夠將這個 JSON 對象序列化爲字符串後存儲到文件:test.cpuprofile 。注意這裏的文件名後綴必須爲 .cpuprofile ,不然 Chrome devtools 是不識別的。ide

注意:v8-profiler 目前也處於年久失修的狀態了,在 Node.js 8 和 Node.js 10 上已經沒法正確編譯安裝了,若是你在 8 或者 10 的項目中想進行使用,能夠試試看 v8-profiler-next函數

II. 分析 CPU Profile 文件工具

藉助於 v8-profiler 拿到咱們的 Node.js 應用一段時間內的 JS 代碼運行狀態後,咱們能夠將其導入 Chrome devtools 中進行分析展現。

在 Chrome 72 中,分析咱們 Dump 出來的 CPU Profile 的方法已經和以前有所不一樣了,默認工具欄中也不會展現 CPU Profile 的分析頁面,咱們須要經過點擊工具欄右側的 更多 按鈕,而後選擇 More tools -> JavaScript Profiler 來進入到 CPU 的分析頁面,以下圖所示:

選中 JavaScript Profiler 後,在出現的頁面上點擊 Load 按鈕,而後將剛纔保存獲得的 test.cpuprofile 文件加載進來,就能夠看到 Chrome devtools 的解析結果了:

這裏默認的視圖是 Heavy 視圖,在這個視圖下,Devtools 會按照對你的應用的影響程度從高到低,將這些函數列舉出來,點擊展開可以看到這些列舉出來的函數的全路徑,方便你去代碼中對應的位置進行排查。這裏解釋兩個比較重要的指標,以便讓你們能更有針對性地進行排查:

  • Self Time: 此函數自己代碼段執行地時間(不包含任何調用)
  • Total Time: 此函數包含了其調用地其它函數總共的執行時間

像在上述地截圖例子中,ejs 模塊在線上都應該開啓了緩存,因此 ejs 模塊的 compile 方法不該該出如今列表中,這顯然是一個很是可疑的性能損耗點,須要咱們去展開找到緣由。

除了 Heavy 視圖,Devtools 實際上還給咱們提供了火焰圖來進行更多維度的展現,點擊左上角能夠切換:

火焰圖按照咱們的 CPU 採樣時間軸進行展現,那麼在這裏咱們更容易看到咱們的 Node.js 應用在採樣期間 JS 代碼的執行行爲,新增的兩個指標這邊也給你們解釋一下其含義:

  • Aggregated self time: 在 CPU 採樣期間聚合後的此函數自己代碼段的執行總時間(不包含其餘調用)
  • Aggregated total time: 在 CPU 採樣期間聚合後的此函數包含了其調用地其它函數總共的執行總時間

綜上,藉助於 Chrome devtools 和可以導出當前 Node.js 應用 Javascript 代碼運行狀態的模塊,咱們已經能夠比較完備地對應用服務異常時,排查定位到相應的 Node.js 進程 CPU 很高的狀況進行排查和定位分析了。在生產實踐中,這部分的 JS 代碼的性能的分析每每也會用到新項目上線前的性能壓測中,有興趣的同窗能夠更深刻地研究下。

內存泄漏問題

I. 判斷是否內存泄漏

在筆者的經歷中,內存泄漏問題是 Node.js 在線上運行時出現的問題種類中的重災區。尤爲是三方庫自身的 Bug 或者開發者使用不當引發的內存泄漏,會讓不少的 Node.js 開發者感到一籌莫展。本節首先向讀者介紹下,什麼狀況下咱們的應用算是有很大的可能在發生內存泄漏呢?

實際上判斷咱們的線上 Node.js 應用是否有內存泄漏也很是簡單:藉助於你們各自公司的一些系統和進程監控工具,若是咱們發現 Node.js 應用的總內存佔用曲線 處於長時間的只增不降 ,而且堆內存按照趨勢突破了 堆限制的 70%  了,那麼基本上應用 很大可能 產生了泄漏。

固然事無絕對,若是確實應用的訪問量(QPS)也在一直增加中,那麼內存曲線只增不減也屬於正常狀況,若是確實由於 QPS 的不斷增加致使堆內存超過堆限制的 70% 甚至 90%,此時咱們須要考慮的擴容服務器來緩解內存問題。

II. 導出 JS 堆內存快照

若是確認了 Node.js 應用出現了內存泄漏的問題,那麼和上面 CPU 的問題同樣,咱們須要經過一些辦法導出 JS 內存快照(堆快照)來進行分析。V8 引擎一樣在內部提供了接口能夠直接將分配在 V8 堆上的 JS 對象導出來供開發者進行分析,這裏咱們採用 heapdump 這個模塊,首先依舊是執行以下命令進行安裝:

npm install heapdump --save

接着能夠在代碼中按照以下方法使用此模塊:

'use sytrict';

const heapdump = require('heapdump');
heapdump.writeSnapshot('./test' + '.heapsnapshot');

這樣咱們就在當前目錄下獲得了一個堆快照文件:test.heapsnapshot ,用文本編輯工具打開這個文件,能夠看到其依舊是一個很大的 JSON 結構,一樣這裏的堆快照文件後綴必須爲 .heapsnapshot ,不然 Chrome devtools 是不識別的。

III. 分析堆快照

在 Chrome devtools 的工具欄中選擇 Memory 便可進入到分析頁面,以下圖所示:

而後點擊頁面上的 Load 按鈕,選擇咱們剛纔生成 test.heapsnapshot 文件,就能夠看到分析結果,以下圖所示:

默認的視圖實際上是一個 Summary 視圖,這裏的 Constructor 和咱們編寫 JS 代碼時的構造函數並沒有不一樣,都是指代此構造函數建立的對象,新版本的 Chrome devtools 中還在構造函數後面增長 * number 的信息,它表明這個構造函數建立的實例的個數。

實際上在堆快照的分析視圖中,有兩個很是重要的概念須要你們去理解,不然極可能拿到堆快照看着分析結果也無所適從,它們是 Shallow Size 和 Retained Size ,要更好地去理解這兩個概念,咱們須要先了解 支配樹。首先咱們看以下簡化後的堆快照描述的內存關係圖:

這裏的 1 爲根節點,即 GC 根,那麼對於對象 5 來講,若是咱們想要讓對象 5 回收(即從 GC 根不可達),僅僅去掉對象 4 或者對象 3 對於對象 5 的引用是不夠的,由於顯然從根節點 1 能夠分別從對象 3 或者對象 4 遍歷到對象 5。所以咱們只有去掉對象 2 才能將對象 5 回收,因此在上面這個圖中,對象 5 的直接支配者是對象 2。照着這個思路,咱們能夠經過必定的算法將上述簡化後的堆內存關係圖轉化爲支配樹:

對象 1 到對象 8 間的支配關係描述以下:

  • 對象 1 支配對象 2
  • 對象 2 支配對象 3 、4 和 5
  • 對象 4 支配對象 6
  • 對象 5 支配對象 7
  • 對象 6 支配對象 8

好了,到這裏咱們能夠開始解釋什麼是 Shallow Size 和 Retained Size 了,實際上對象的 Shallow Size 就是對象自身被建立時,在 V8 堆上分配的大小,結合上面的例子,即對象 1 到 8 自身的大小。對象的 Retained Size 則是把此對象從堆上拿掉,則 Full GC 後 V8 堆可以釋放出的空間大小。一樣結合上面的例子,支配樹的葉子節點對象 三、對象 7 和對象 8 由於沒有任何直接支配對象,所以其 Retained Size 等於其 Shallow Size。

將剩下的非葉子節點能夠一一展開,爲了篇幅描述方便,SZ_5表示對象 5 的 Shallow Size,RZ_5 表示對象 5 的 Retained Size,那麼能夠獲得以下結果:

  • 對象 3 的 Retained Size:RZ_3 = SZ_3
  • 對象 7 的 Retained Size:RZ_7 = SZ_7
  • 對象 8 的 Retained Size:RZ_8 = SZ_8
  • 對象 6 的 Retained Size:RZ_6 = SZ_6 + RZ_8 = SZ_6 + SZ_8
  • 對象 5 的 Retained Size:RZ_5 = SZ_5 + RZ_7 = SZ_5 + SZ_7
  • 對象 4 的 Retained Size:RZ_4 = SZ_4 + RZ_6 = SZ_4 + SZ_6 + SZ_8
  • 對象 2 的 Retained Size:RZ_2 = SZ_2 + RZ_3 + RZ_4 + RZ_5 = SZ_2 + SZ_3 + SZ_4 + SZ_5 + SZ_6 + SZ_7 + SZ_8
  • GC 根 1 的 Retained Size:RZ_1 = SZ_1 + RZ_2 = SZ_1 + SZ_2 + RZ_3 + RZ_4 + RZ_5 = SZ_2 + SZ_3 + SZ_4 + SZ_5 + SZ_6 + SZ_7 + SZ_8

這裏能夠發現,GC 根的 Retained Size 等於堆上全部今後根出發可達對象的 Shallow Size 之和,這和咱們的理解預期是相符合的,畢竟將 GC 根從堆上拿掉的話,本來就應當將今後根出發的全部對象都清理掉。

理解了這一點,回到咱們最開始看到的默認總覽視圖中,正常來講,可能的泄漏對象每每其 Retained Size 特別大,咱們能夠在窗口中依據 Retained Size 進行排序來對那些佔據了堆空間絕大部分的對象進行排查:

假如確認了可疑對象,Chrome devtools 中也會給你自動展開方便你去定位到代碼段,下面以 NativeModule 這個構造器生成的對象 vm 爲例:

這裏上半部分是順序的引用關係,好比 NativeModule 實例 @45655 的 exports 屬性指向了對象 @45589,filename 屬性則指向一個字符串 "vm.js";下半部分則是反向的引用關係:NativeModule 實例 @13021 的 _cache 屬性指向了 Object 實例 @41103,而 Object 實例 @41103 的 vm 屬性指向了 NativeModule 實例 @45655。

若是對這部分展現圖表比較暈的能夠仔細看下上面的例子,由於找到可疑的泄漏對象,結合上圖能看到此對象下的屬性和值及其父引用關係鏈,絕大部分狀況下咱們就能夠定位到生成可疑對象的 JS 代碼段了。

實際上除了默認的 Summary 視圖,Chrome devtools 還提供了 Containment 和 Statistics 視圖,這裏再看下 Containment 視圖,選擇堆快照解析頁面的左上角能夠進行切換,以下圖所示:

這個視圖其實是堆快照解析出來的內存關係圖的直接展現,所以相比 Summary 視圖,從這個視圖中直接查找可疑的泄漏對象相對比較困難。

結尾

Chrome devtools 其實是很是強大的一個工具,本節也只是僅僅介紹了對 CPU Profile 和堆快照解析能力的介紹和經常使用視圖的使用指南,若是你仔細閱讀了本節內容,面對服務器上定位到的 Node.js 應用 CPU 飆高或者內存泄漏這樣的問題,想必就能夠作到心中有數不慌亂了。



本文做者:奕鈞

閱讀原文

本文爲雲棲社區原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索