
Node作為Javascript在服務端的一個執行時(Runtime),極大的豐富了Javascript的應用程式場景。
但是Node.js Runtime本身就是一個黑盒,我們無法感知運行時的狀態,對於線上問題也難以復現。
因此效能監控是Node.js應用程式「正常運作」的基石。不僅可以隨時監控執行時期的各項指標,還可以幫助排除異常場景問題。
效能監控可分為兩個部分:
效能指標的收集和展示
效能資料的抓取和分析

從上圖可以看到目前主流的三種Node.js效能監控方案的優缺點,以下是簡單介紹這三種方案的組成:
Prometheus
AliNode
alinode是一個相容官方nodejs的拓展運行時,提供了一些額外功能:
agenthub是一個常駐進程,用來收集性能指標並上報
整體從監控,展示,快照,分析形成閉環,接入便捷簡單,但是拓展運行時還是有風險
Easy-Monitor
Node.js Addon來實現採樣器
透過process.cpuUsage()可以取得目前進程的CPU耗時數據,回傳值的單位是微秒

透過process.memoryUsage()可以獲取當前進程的內存分配數據,返回值的單位是字節

從上圖可以看出, rss包含程式碼段( Code Segment )、堆疊記憶體( Stack )、堆疊記憶體( Heap )
透過v8.getHeapStatistics()和v8.getHeapSpaceStatistics()可以取得v8堆記憶體和堆空間的分析數據,下圖展示了v8的堆記憶體組成分佈:

堆記憶體空間先劃分為空間(space),空間再劃分為頁(page),記憶體依1MB對齊進行分頁。
New Space:新生代空間,用來存放一些生命週期比較短的物件數據,平分為兩個空間(空間類型為semi space ): from space , to space
Old Space:老生代空間,用來存放New Space晉升的物件
Code Space:存放v8 JIT編譯後的可執行程式碼
Map Space:存放Object指向的隱藏類別的指針對象,隱藏類別指標是v8根據執行時間記錄下的物件佈局結構,用於快速存取物件成員
Large Object Space:用於存放大於1MB而無法分配到頁的物件
v8的垃圾回收演算法分為兩類:
Mark-Sweep-Compact演算法,老生代的物件回收Scavenge演算法,用於新生代的物件回收
前提: New space分為from和to兩個物件空間
觸發時機:當New space空間滿了
步驟:
在from space中,進行寬度優先遍歷
發現存活(可達)物件
Old spaceto space中當複製結束時, to space中只有存活的對象, from space就被清空了
交換from space和to space ,開始下一輪Scavenge
適用於回收頻繁,內存不大的對象,典型的空間換時間的策略,缺點是浪費了多一倍的空間

三個步驟:標記、清除、整理
觸發時機:當Old space空間滿了
步驟:
Marking(三色標記法)
marking queue (顯式棧)中,並將這些對象標記為灰色marking queue pop出來,並標記為黑色push到marking queue上,如此往復Sweep
Compact
Old space的一端,這樣清除出來的空間就是連續完整的在最開始v8進行垃圾回收時,需要停止程式的運行,掃描完整個堆,回收完內存,才會重新運行程式。這種行為就叫全停頓( Stop-The-World )
雖然新生代活動對象較小,回收頻繁,全停頓,影響不大,但是老生代存活對像多且大,標記、清理、整理等造成的停頓就會比較嚴重。
這個理念其實有點像React框架中的Fiber架構,只有在瀏覽器的空閒時間才會去遍歷Fiber Tree執行對應的任務,否則延遲執行,盡可能少地影響主執行緒的任務,避免應用卡頓,提升應用性能。
由於v8對於新舊生代的空間預設限制了大小
New space預設限制:64位元系統為32M,32位元系統為16MOld space預設限制:64位元系統為1400M,32位元系統為700M因此node提供了兩個參數用於調整新老生代的空間上限
--max-semi-space-size :設定New Space空間的最大值--max-old-space-size :設定Old Space空間的最大值node也提供了三種查看GC日誌的方式:
--trace_gc :一行日誌簡要描述每次GC時的時間、類型、堆大小變化和產生原因--trace_gc_verbose :展示每次GC後每個V8堆空間的詳細狀況--trace_gc_nvp :每次GC的詳細鍵值對信息,包含GC類型,暫停時間,內存變化等由於GC日誌比較原始,還需要二次處理,可以使用AliNode團隊開發的v8-gc- log-parser
對於運行程式的堆記憶體進行快照採樣,可以用來分析記憶體的消耗以及變化
產生.heapsnapshot檔案有以下幾種方式:
使用heapdump

使用v8的heap-profile

使用nodejs內建的v8模組提供的api
v8.getHeapSnapshot()

v8.writeHeapSnapshot(fileName)

使用v8-profiler-next

產生的.heapsnapshot文件,可以在Chrome devtools工具列的Memory,選擇上傳後,展示結果如下圖:

預設的視圖是Summary視圖,在這裡我們要專注於最右邊兩欄: Shallow Size和Retained Size
Shallow Size :表示該物件本身在v8堆記憶體分配的大小Retained Size :表示該物件所有引用物件的Shallow Size總和當發現Retained Size特別大時,該物件內部可能存在記憶體洩漏,可以進一步展開去定位問題
還有Comparison視圖是用於比較分析兩個不同時段的堆快照,透過Delta列可以篩選出記憶體變化最大的對象

對於運行程式的CPU進行快照採樣,可以用來分析CPU的耗時及佔比
生成.cpuprofile檔案有以下幾種方式:
這是採集5分鐘的CPU Profile樣本

產生的.cpuprofile文件,可以在Chrome devtools工具列的Javascript Profiler (不在預設tab,需要在工具列右側的更多中開啟顯示),選擇上傳文件後,展示結果如下圖:

預設的視圖是Heavy視圖,在這裡我們看到有兩欄: Self Time和Total Time
Self Time :代表此函數本身(不包含其他呼叫)的執行耗時Total Time :代表此函數(包含其他呼叫函數)的總執行耗時當發現Total Time和Self Time偏差較大時,函數可能存在耗時比較多的CPU密集型計算,也可以展開進一步定位排查
當應用意外崩潰終止時,系統會自動記錄下進程crash掉那一刻的記憶體分配信息,Program Counter以及堆疊指標等關鍵資訊來產生core檔
產生.core檔的三種方法:
ulimit -c unlimited開啟內核限制node --abort-on-uncaught-exception node啟動加入此參數,可在套用出現未擷取的例外狀況時也能產生一份core檔gcore <pid>手動產生core檔取得.core檔後,可以透過mdb、gdb、lldb等工具實現解析診斷實際進程crash的原因
llnode `which node` -c /path/to/core/dump
從監控可以觀察到堆內存在持續上升,因此需要堆快照進行排查

根據heapsnapshot可以分析排查到有一個newThing的物件一直保持著比較大的記憶體

從程式碼中可以看到雖然unused方法沒有調用,但是newThing物件是引用自theThing ,導致其一直存在於replaceThing這個函數的執行上下文中,沒有被釋放,這就是典型的由於閉包產生的內存洩漏案例
常見的記憶體洩漏有以下幾種情況:
因此在上述這幾種情況時,一定要謹慎考慮物件在記憶體中是否會被自動回收,不會被自動回收的話,需要手動進行回收,例如手動把物件設定為null 、移除計時器、解綁事件監聽等
至此,本文已經對整個Node.js的效能監控系統進行了詳細的介紹。
首先,介紹了效能監控解決的問題,組成部分以及主流方案的優缺點對比。
然後,針對兩個大部分效能指標和快照工具進行了具體的介紹,
最後,從觀察、分析、排查再現一個簡單的記憶體洩漏案例,並總結了常見記憶體洩漏的情況和解決方案。
希望這篇文章能幫助大家對整個Node.js的效能監控系統有所了解。