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 space
to 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的效能監控系統有所了解。