楔子

前一節中我們藉助於 Chrome devtools 實現了對線上 Node.js 應用的 CPU/Memory 問題的排查定位,但是在實際生產實踐中,大家會發現 Chrome devtools 更加偏向本地開發模式,因為顯然 Chrome devtools 不會負責去生成分析問題所需要的 Dump 文件,這意味著開發者還得額外在線上項目中設置好 v8-profiler 和 heapdump 這樣的工具,並且通過額外實現的服務來能夠去對線上運行的項目進行實時的狀態導出。

加上實際上預備章中除了 CPU/Memory 的問題,我們還會遇到一些需要分析錯誤日誌、磁碟和核心轉儲文件等才能定位問題的狀況,因此在這些場景下,僅僅靠 Chrome devtools 顯然會有一些力不從心。正是為瞭解決廣大 Node.js 開發者的這些痛點,我們在這裡推薦大家在使用 Node.js 性能平臺,即原來的 AliNode,它已經在阿里巴巴集團內部承載了幾乎所有的 Node.js 應用線上運行監控和問題排查,因此大家可以放心在生產環境部署使用。

本節將從 Node.js 性能平臺 的設計架構、核心能力以及最佳實踐等角度,幫助開發者更好地使用這一工具來解決前面提到的異常指標分析和線上 Node.js 應用故障定位。

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

架構

Node.js 性能平臺其實簡單的說由三部分組成:雲控制檯 +** AliNode runtime** + Agenthub,如下圖所示:

具體的部署步驟可以查看官方文檔:自助式部署 Runtime。藉助於 Node.js 性能平臺的整套解決方案,我們可以很方便地實現預備章節中提到的絕大部分異常指標的告警分析的能力。在生產實踐過程中,實際上在筆者看來,Node.js 性能平臺解決方案其實僅僅是提供了三個最核心卻也是最有效的能力:
  • 異常指標告警,即預備節中一些異常指標出現異常時能通過簡訊/釘釘通知到開發者
  • 導出線上 Node.js 應用狀態,包括但不限於即前面 Chrome devtools 一節中提到的 CPU/Memory 狀態導出
  • 在線分析結果和更好的 UI 展示,定製化解析應用導出狀態和展示,更符合國內開發者習慣

換言之,Node.js 性能平臺作為一個產品本身功能也在不斷迭代新增修改中,但是以上的三個核心能力一定是第一優先順序保障的,其它邊邊角角的功能則相對來說響應優先順序沒有那麼高。

實際上我們也理解作為使用平臺的開發者希望能在一個地方看到 Node.js 線上應用從底層到業務層的所有細節,然而我個人感覺不同的工具都有應該有其核心的能力輸出,很多時候不斷做加法容易讓產品本身定位模糊化以及泛而不精,Node.js 性能平臺實際上始終在致力於讓原本線上黑盒的運行時狀態,能更加直觀地反饋給開發者,讓 Node.js 應用的開發者面對一些偏向底層的線上疑難雜症能夠不再無所適從。

最佳實踐

I.配置合適的告警

線上應用的告警實際上是一種自我發現問題的保護機制,如果沒有告警能力,那每次都會等到問題暴露到用戶側導致其反饋才能發現問題,這顯然對用戶體驗非常的不友好。

因此部署完成一個項目後,開發者首先需要去配置合適的告警,而在我們的生產實踐中,線上問題通過錯誤日誌、Node.js 進程 CPU/Memory 的分析、核心轉儲(Core dump)的分析以及磁碟分析能夠得出結論,因此我們需要的基本的告警策略也是源自以上五個部分。幸運的是平臺已經給我們預設好了這些告警,大家只需要選擇一下即可完整這裡的告警配置,如下圖所示:

在 Node.js 性能平臺 的告警頁面上有 快速添加規則,點開選中後會自動生成告警規則的閾值表達式模板和報警說明模板,我們可以按照項目實際監控需求進行修改,比如想要對 Node.js 進程的堆內存進行監控,可以選中 Memory 預警 選項,如下圖所示:

此時點擊 添加報警項 即完整了對進程堆內存的告警,並且將出現告警時需要點擊 通知設置->添加到聯繫人列表 來添加的聯繫人加入此條規則,如下圖所示:

那麼在例子中的這條默認的規則裏,當我們的 Node.js 進程分配的堆內存超過堆上線的 80%(默認 64 位機器上堆上限是 1.4G)時會觸發簡訊通知到配置綁定到此條規則的聯繫人。

實際上快速添加規則列表中給大家提供的是最常見的一些預配置好的告警策略,如果這些尚不能滿足你的需求,更多定製化的自定義的服務告警策略配置方法可以看官方文檔 報警設置。並且除了簡訊告警,也支持釘釘機器人推送告警消息到羣,方便羣發感知線上 Node.js 應用態勢。

II. 按照告警類型進行分析

按照 I 節中配置完成合適的告警規則後,那麼當收到告警簡訊時就可以按照策略類型進行對應的分析了。本節將按照預備節中比較常見的五大類問題來逐一講解。

a. 磁碟監控

這個是比較好處理的問題,在快速添加的規則裏實際上我們會在伺服器的磁碟使用超過 85% 時進行告警,那麼收到磁碟告警後,可以連接到伺服器,使用如下命令查看那個目錄佔用比較高:

sudo du -h --max-depth=1 /

找到佔比比較高的目錄和文件後,看看是否需要備份後刪除來釋放出磁碟空間。

b. 錯誤日誌

收到特定的錯誤日誌告警後,只需要去對應的項目的 Node.js 性能平臺 控制檯找到問題 實例 去查看其 異常日誌 即可,如下圖所示:

這裡會按照錯誤類型進行規整,大家可以結合展示的錯誤棧信息來進行對應的問題定位。注意這裡的錯誤日誌文件需要你在部署 Agenthub 的時候寫入配置文件,詳細可以參見文檔 配置和啟動 Agenthub 一節中的 詳細配置 內容。

c. 進程 CPU 高

終於到了前一節中藉助 v8-profiler 導出 CPU Profile 文件再使用 Chrome devtools 進行分析的異常類型了。那麼在 Node.js 性能平臺 的整套解決方案下,我們並不需要額外的去依賴類似 v8-profiler 這樣的第三方庫來實現進程狀態的導出,與此相對的,當我們收到 Node.js 應用進程的 CPU 超過我們設置的閾值告警時,我們只需要在控制檯對應的 實例 點擊 CPU Profile 按鈕即可:

默認會給抓取的進程生成 3 分鐘的 CPU Profile 文件,等到結束後生成的文件會顯示在 文件 頁面:

此時點擊 轉儲 即可上傳到雲端以供在線分析展示了,如下圖所示:

這裡可以看到有兩個 分析 按鈕,其實第二個下標帶有 (devtools) 的分析按鈕實際上就是前一節中提到的 Chrome devtools 分析,這裡不再重複講解了,如果有遺忘的同學可以再去回顧下本大章前一節的內容。我們重點看下第一個 AliNode 定製的分析,點擊第一個分析按鈕後,可以在新頁面看到如下所示內容:

這裡其實也是火焰圖,但與 Chrome devtools 提供的火焰圖不一樣的地方在於,這裡是將抓取的 3 分鐘內的 JS 函數執行進行了聚合展示出來的火焰圖,在一些存在多次執行同一個函數(可能每次執行非常短)的情況下,聚合後的火焰圖可以很方便的幫助我們找到代碼的執行瓶頸來進行對應的優化。

值得一提的是,如果你使用的 AliNode runtime 版本在 v3.11.4 或者 v4.2.1 以上(包含這兩個版本)的話,當你的應用出現類死循環問題,比如由於異常的用戶參數導致的正則回溯(即執行完這個正則要十幾年,類似於 Node.js 進程發生了死循環)這類問題時,可以通過抓取 CPU Profile 文件來很方便地定位到問題代碼,詳細信息有興趣的同學可以看下 Node.js 性能平臺支持死循環和正則攻擊定位。

d. 內存泄漏

與 CPU 高的問題一樣,當我們收到 Node.js 應用進程的堆內存佔據堆上限的比率超過我們設置的閾值時,我們也不需要類似 heapdump 這樣的第三方模塊來導出堆快照進行分析,我們還是在控制檯對應的 實例 點擊 堆快照 按鈕即可生成對應 Node.js 進程的堆快照:

生成的堆快照文件同樣會顯示在 文件 列表頁面,點擊 轉儲 將堆快照上傳至雲端以供接下來的分析:

與上面一樣,下標帶有 (devtools) 的分析按鈕還是前一節中提到的 Chrome devtools 分析,這裡還是著重解析下 AliNode 定製的第一個分析按鈕,點擊後新頁面如下圖所示:

首先解釋下上面的總覽欄目的內容信息:
  • 文件大小: 堆快照文件本身的大小
  • Shallow Szie 總大小: 回顧下上一節中的內容,GC 根的 Retained Size 大小其實就是堆大小,也等於堆上分配的所有對象的 Shallow Size 總大小,因此這裡其實就是已使用的堆空間
  • 對象個數: 當前堆上分配的 Heap Object 總個數
  • 對象邊個數: 這個稍微抽象一些,假如 Object A.b 指向另一個 Object B,我們則認為表示指向關係的 b 屬性就是一條邊
  • GC Roots 個數: V8 引擎實現的堆分配,其實並不是我們之前為了幫助大家理解簡化的只有一個 GC 根的情況,在實際的運行模型下,堆空間內存在許多的 GC 根,這裡是真實的 GC 根的個數

這部分的信息旨在給大家一個概覽,部分信息需要深入解讀堆快照才能徹底理解,如果你實在無法理解其中的幾個概覽指標信息,其實也無傷大雅,因為這並不影響我們定位問題代碼。

簡單瞭解了概覽信息的含義後,接著我們來看對於定位 Node.js 應用代碼段非常重要的信息,第一個是默認展示的 可疑點 信息,上圖中的內容表示 @15249 這個對象佔據了堆空間 97.41% 的內存,那麼它很可能就是一個泄漏對象,這裡又存在兩種可能:

  • 此對象本身應該被釋放但是卻沒有釋放,造成堆空間佔用如此大
  • 此對象的某些屬性應該被釋放但是卻沒有釋放,造成表象是此對象佔據大量的堆空間

要判斷是哪一種情況,以及追蹤到對應的代碼段,我們需要點擊圖中的 簇視圖 鏈接進行進一步觀察:

這裡繼續解釋下什麼是簇視圖,簇視圖實際上是支配樹的一個別名,也就是這個視圖下我們看到的正是前面一節中提到的從可疑泄漏對象出發的支配樹視圖,它的好處是,在這個視圖下,父節點的 Retained Size 可以直接由其子節點的 Retained Size 累加後再加上父節點自身的 Shallow Size 得到,換言之,在這個視圖下我們層層展開即可以看到可疑泄漏對象的內存究竟被哪些子節點佔用了。

並且結合前一節的支配樹描述,我們可以知道支配樹下的父子節點關係,並不一定是真正的堆上空間內的對象父子關係,但是對於那些支配樹下父子關係在真正的堆空間內也存在父子節點關係的簇節點,我們將真正的 也用淺紫色標識出來,這部分的 信息對於我們映射到真正的代碼段非常有幫助。在這個簡單的例子中,我們可以很清晰的看到可疑泄漏對象 @15249 實際上是下屬的 test-alinode.js 中存在一個 array 變數,其中存儲了四個 45.78 兆的數組導致的問題,這樣就找到了問題代碼可以進行後續優化。

而在實際生產環境的堆快照分析下,很多情況下簇視圖下的父子關係在真實的堆空間中並不存在,那麼就不會有這些紫色的邊信息展示,這時候我們想要知道可疑泄漏對象如何通過 JavaScript 生成的對象間引用關係引用到後面真正佔據掉堆空間的對象(比如上圖中的 40 多兆的 Array 對象),我們可以點擊 可疑節點自身的地址鏈接

這樣就進入到以此對象為起點的堆空間內真正的對象引用關係視圖 Search 視圖

這個視圖因為反映的是堆空間內各個 Heap Object 之間真正的引用連接關係,因此父對象的 Retained Size 並不能直接由子節點的 Retained Size 累加獲取,如上圖紅框內的內容,顯然這裡的三個子節點 Retained Size 累加已經超過 100%,這也是 Search 視圖和簇視圖很大的不同點。藉助於 Search 視圖,我們可以根據其內反映出來的對象和邊之間的關係來定位可疑泄漏對象具體是在我們的 JavaScript 代碼中的哪一部分生成。

其實看到這邊,一些讀者應該意識到了這裡的 Search 視圖實際上對應的就是前一節中提到的 Chrome devtools 的 Containment 視圖,只不過這裡的起始點是我們選中的對象本身罷了。

最後就是需要提一下 Retainers 視圖,它和前一節中提到的 Chrome devtools 中解析堆快照展示結果裡面的 Retainers 含義是一致的,它表示對象的父引用關係鏈,我們可以來看下:

這裡 globa@1279 對象的 clearImmediate 屬性指向 timers.js()@15325,而 timers.js()@15325 的 context 屬性指向了可疑的泄漏對象 system / Context@15249。

在絕大部分的情況下,通過結合 Search 視圖Retainers 視圖 我們可以定位到指定對象在 JavaScript 代碼中的生成位置,而 簇視圖 下我們又可以比較方便的知道堆空間被哪些對象佔據掉了,那麼綜合這兩部分的信息,我們就可以實現對線上內存泄漏的問題進行分析和代碼定位了。

e. 出現核心轉儲

最後就是收到伺服器生成核心轉儲文件(Core dump 文件)的告警了,這表示我們的進程已經出現了預期之外的 Crash,如果你的 Agenthub 配置正常的話,在 文件 -> Coredump 文件 頁面會自動將生成的核心轉儲文件信息展示出來:

和之前的步驟類似,我們想要看到服務端分析和結果展示,首先需要將伺服器上生成的核心轉儲文件轉儲到雲端,但是與之前的 CPU Profile 和堆快照的轉儲不一樣的地方在於,核心轉儲文件的分析需要我們提供對應 Node.js 進程的啟動執行文件,即 AliNode runtime 文件,這裡簡化處理為只需要設置 Runtime 版本即可:

點擊 設置 runtime 版本 即可進行設置,格式為 alinode-v{x}.{y}.{z} 的形式,比如 alinode-v3.11.5,版本會進行校驗,請務必填寫你的應用真實在使用的 AliNode runtime 版本。版本填寫完成後,我們就可以點擊 轉儲 按鈕進行文件轉儲到雲端的操作了:

顯然對於核心轉儲文件來說,Chrome devtools 是沒有提供解析功能的,所以這裡只有一個 AliNode 定製的分析按鈕,點擊這個 分析 按鈕,即可以看到結果:

這裡第一欄的概覽信息看文字描述就能理解其含義,所以這裡就不再多做解釋了,我們來看下比較重要的默認視圖 BackTrace 信息視圖,此視圖下展示的實際上是 Node.js 應用在 Crash 時刻的線程信息,許多開發者認為 Node.js 是單線程的運行模型,其實這句話也不是完全錯誤,更準確的說法是 單主 JavaScript 工作線程,因為實際上 Node.js 還會開啟一些後臺線程來處理諸如 GC 裏的部分任務。

絕大部分的情況下,應用的 Crash 都是由 JavaScript 工作線程引發的,因此我們需要關注的也僅僅是這個線程,這裡顯然 BackTrace 信息視圖中將 JavaScript 工作線程做了標紅和置頂處理,展開後可以看到 Node.js 應用 Crash 那一刻的錯誤堆棧信息:

因為就算在 JavaScript 的工作線程中,也會存在 Native C/C++ 代碼的穿透,但是在問題排查中我們往往只需要去看同樣標紅的 JavaScript 棧信息即可,像在這個簡單的例子中,顯然 Crash 是因為視圖去啟動一個不存在的 JS 文件導致的。

值得一提的是,核心轉儲文件的分析功能非常的強大,因為在預備節中我們提到其生成的途徑除了 Node.js 應用 Crash 的時候由系統內核控制輸出外,還可以由 gcore 這樣的命令手動強制輸出,而本小節我們又看到核心轉儲文件的分析實際上可以看到此刻的 JavaScript 棧信息以及其入參,結合這兩點,我們可以在線上出現 CPU Profile 一節中提到的類死循環問題時直接採用 gcore 生成核心轉儲文件,然後上傳至平臺雲端進行分析,這樣不僅可以看到我們的 Node.js 應用是阻塞在哪一行的 JavaScript 代碼,甚至引發阻塞的參數我們也能完整獲取到,這對本地復現定位問題的幫助無疑是無比巨大的。

結尾

本節其實給大家介紹了 Node.js 性能平臺 的整套面向 Node.js 應用開發的監控、告警、分析和定位問題的解決方案的架構和最佳實踐,希望能讓大家對平臺的能力和如何更好地結合自身項目進行使用有一個整體的理解。

限於篇幅,最佳實踐中的 CPU Profile、堆快照和核心轉儲文件的分析例子都非常的簡單,這部分的內容也更多的是旨在幫助大家理解平臺提供的工具如何使用以及其分析結果展示的指標含義,那麼本書的第三節中,我們會通過一些實際的生產遇到的案例問題藉助於 Node.js 性能平臺 提供的上述工具分析過程,來幫助大家更好的理解這部分信息,也希望大家在讀完這些內容後能有所收穫,能對 Node.js 應用在生產中的使用更有信心。

本文作者:奕鈞

原文鏈接

更多技術乾貨敬請關注云棲社區知乎機構號:阿里云云棲社區 - 知乎

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


推薦閱讀:
相關文章