為什麼需要效能監控?聊聊Node.js效能監控

2022-08-11 22:01:34
為什麼需要效能監控?下面本篇就來帶大家瞭解一下效能監控,希望對大家有所幫助!

為什麼需要效能監控

作為Javascript在伺服器端的一個執行時(Runtime),極大的豐富了Javascript的應用場景。

但是Node.js Runtime本身是一個黑盒,我們無法感知執行時的狀態,對於線上問題也難以復現

因此效能監控是Node.js應用程式「正常執行」的基石。不僅可以隨時監控執行時的各項指標,還可以幫助排查異常場景問題。

組成部分

效能監控可以分為兩個部分:

  • 效能指標的採集和展示

    • 程序級別的資料:CPU,Memory,Heap,GC等
    • 系統級別的資料:磁碟佔用率,I/O負載,TCP/UDP連線狀態等
    • 應用層的資料:QPS,慢HTTP,業務處理鏈路紀錄檔等
  • 效能資料的抓取和分析

    • Heapsnapshot:堆記憶體快照
    • Cpuprofile:CPU快照
    • Coredump:應用崩潰快照

方案對比

1.png

從上圖可以看到目前主流的三種Node.js效能監控方案的優缺點,以下是簡單介紹這三種方案的組成:

  • Prometheus

    • prom-client是prometheus的nodejs實現,用於採集效能指標
    • grafana是一個視覺化平臺,用來展示各種資料圖表,支援prometheus的接入
    • 只支援了效能指標的採集和展示,排查問題還需要其他快照工具,才能組成閉環
  • AliNode

    • alinode是一個相容官方nodejs的拓展執行時,提供了一些額外功能:

      • v8的執行時記憶體狀態監控
      • libuv的執行時狀態監控
      • 線上故障診斷功能:堆快照、CPU Profile、GC Trace等
    • agenthub是一個常駐程序,用來收集效能指標並上報

    • 整體從監控,展示,快照,分析形成閉環,接入便捷簡單,但是拓展執行時還是有風險

  • Easy-Monitor

    • xprofiler 負責進行實時的執行時狀態取樣,以及輸出效能紀錄檔(也就是效能資料的抓取)
    • xtransit 負責效能紀錄檔的採集與傳輸
    • 跟AliNode最大的區別在於使用了Node.js Addon來實現取樣器

效能指標

CPU

2.png

通過process.cpuUsage()可以獲取當前程序的CPU耗時資料,返回值的單位是微秒

  • user:程序執行時本身消耗的CPU時間
  • system:程序執行時系統消耗的CPU時間

Memory

3.png

通過process.memoryUsage()可以獲取當前程序的記憶體分配資料,返回值的單位是位元組

  • rss:常駐記憶體,node程序分配的總記憶體大小
  • heapTotal:v8申請的堆記憶體大小
  • heapUsed:v8已使用的堆記憶體大小
  • external:v8管理的C++所佔用的記憶體大小
  • arrayBuffers:分配給ArrayBuffer的記憶體大小

4.png

從上圖可以看出,rss包含程式碼段(Code Segment)、棧記憶體(Stack)、堆記憶體(Heap)

  • Code Segment:儲存程式碼段
  • Stack:儲存區域性變數和管理函數呼叫
  • Heap:儲存物件、閉包、或者其他一切

Heap

通過v8.getHeapStatistics()v8.getHeapSpaceStatistics()可以獲取v8堆記憶體和堆空間的分析資料,下圖展示了v8的堆記憶體組成分佈:

5.png

堆記憶體空間先劃分為空間(space),空間又劃分為頁(page),記憶體按照1MB對齊進行分頁。

  • New Space:新生代空間,用來存放一些生命週期比較短的物件資料,平分為兩個空間(空間型別為semi space):from spaceto space

    • 晉升條件:在New space中經過兩次GC依舊存活
  • Old Space:老生代空間,用來存放New Space晉升的物件

  • Code Space:存放v8 JIT編譯後的可執行程式碼

  • Map Space:存放Object指向的隱藏類的指標物件,隱藏類指標是v8根據執行時記錄下的物件佈局結構,用於快速存取物件成員

  • Large Object Space:用於存放大於1MB而無法分配到頁的物件

GC

v8的垃圾回收演演算法分為兩類:

  • Major GC:使用了Mark-Sweep-Compact演演算法,用於老生代的物件回收
  • Minor GC:使用了Scavenge演演算法,用於新生代的物件回收

Scavenge

7.gif

前提:New space分為fromto兩個物件空間

觸發時機:當New space空間滿了

步驟:

  • from space中,進行寬度優先遍歷

  • 發現存活(可達)物件

    • 已經存活過一次(經歷過一次Scavange),晉升到Old space
    • 其他的複製到to space
  • 當複製結束時,to space中只有存活的物件,from space就被清空了

  • 交換from spaceto space,開始下一輪Scavenge

適用於回收頻繁,記憶體不大的物件,典型的空間換時間的策略,缺點是浪費了多一倍的空間

Mark-Sweep-Compact

8.gif

三個步驟:標記、清除、整理

觸發時機:當Old space空間滿了

步驟:

  • Marking(三色標記法)

    • 白色:代表可回收物件
    • 黑色:代表不可回收物件,且其所產生的參照都已經掃描完畢
    • 灰色:代表不可回收物件,且其所產生的參照還沒掃描完
    • 將V8根物件直接參照的物件放進一個marking queue(顯式棧)中,並將這些物件標記為灰色
    • 從這些物件開始做深度優先遍歷,每存取一個物件,將該物件從marking queue pop出來,並標記為黑色
    • 然後將該物件參照下的所有白色物件標記為灰色,pushmarking queue上,如此往復
    • 直到棧上所有物件都pop掉為止,老生代的物件只剩下黑色(不可回收)和白色(可以回收)兩種了
    • PS:當一個物件太大,無法push到空間有限的棧時,v8會把這個物件保留灰色跳過,將整個棧標記為溢位狀態(overflowed),等棧清空後,再次進行遍歷標記,這樣導致需要額外掃描一遍堆
  • Sweep

    • 清除白色物件
    • 會造成記憶體空間不連續
  • Compact

    • 由於Sweep會造成記憶體空間不連續,不利於新物件進入GC
    • 把黑色(存活)物件移到Old space的一端,這樣清除出來的空間就是連續完整的
    • 雖然可以解決記憶體碎片問題,但是會增加停頓時間(執行速度慢)
    • 在空間不足以對新生代晉升過來的物件進行分配時才使用mark-compact

Stop-The-World

在最開始v8進行垃圾回收時,需要停止程式的執行,掃描完整個堆,回收完記憶體,才會重新執行程式。這種行為就叫全停頓(Stop-The-World

雖然新生代活動物件較小,回收頻繁,全停頓,影響不大,但是老生代存活物件多且大,標記、清理、整理等造成的停頓就會比較嚴重。

優化策略

  • 增量回收(Incremental Marking):在Marking階段,當堆達到一定大小時,開始增量GC,每次分配了一定量的記憶體後,就暫停執行程式,做幾毫秒到幾十毫秒的marking,然後恢復程式的執行。

這個理念其實有點像React框架中的Fiber架構,只有在瀏覽器的空閒時間才會去遍歷Fiber Tree執行對應的任務,否則延遲執行,儘可能少地影響主執行緒的任務,避免應用卡頓,提升應用效能。

  • 並行清除(Concurrent Sweeping):讓其他執行緒同時來做 sweeping,而不用擔心和執行程式的主執行緒衝突
  • 並行清除(Parallel Sweeping):讓多個 Sweeping 執行緒同時工作,提升 sweeping 的吞吐量,縮短整個 GC 的週期

空間調整

由於v8對於新老生代的空間預設限制了大小

  • New space 預設限制:64位元系統為32M,32位元系統為16M
  • Old space 預設限制:64位元系統為1400M,32位元系統為700M

因此node提供了兩個引數用於調整新老生代的空間上限

  • --max-semi-space-size:設定New Space空間的最大值
  • --max-old-space-size:設定Old Space空間的最大值

檢視GC紀錄檔

node也提供了三種檢視GC紀錄檔的方式:

  • --trace_gc:一行紀錄檔簡要描述每次GC時的時間、型別、堆大小變化和產生原因
  • --trace_gc_verbose:展示每次GC後每個V8堆空間的詳細狀況
  • --trace_gc_nvp:每次GC的詳細鍵值對資訊,包含GC型別,暫停時間,記憶體變化等

由於GC紀錄檔比較原始,還需要二次處理,可以使用AliNode團隊開發的v8-gc-log-parser

快照工具

Heapsnapshot

對於執行程式的堆記憶體進行快照取樣,可以用來分析記憶體的消耗以及變化

生成方式

生成.heapsnapshot檔案有以下幾種方式:

9.png

10.png

  • 使用nodejs內建的v8模組提供的api

    • v8.getHeapSnapshot()

    11.png

    • v8.writeHeapSnapshot(fileName)

    12.png

  • 使用v8-profiler-next

13.png

分析方法

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

14.png

預設的檢視是Summary檢視,在這裡我們要關注最右邊兩欄:Shallow SizeRetained Size

  • Shallow Size:表示該物件本身在v8堆記憶體分配的大小
  • Retained Size:表示該物件所有參照物件的Shallow Size之和

當發現Retained Size特別大時,該物件內部可能存在記憶體漏失,可以進一步展開去定位問題

還有Comparison檢視是用於比較分析兩個不同時段的堆快照,通過Delta列可以篩選出記憶體變化最大的物件

15.png

Cpuprofile

對於執行程式的CPU進行快照取樣,可以用來分析CPU的耗時及佔比

生成方式

生成.cpuprofile檔案有以下幾種方式:

  • v8-profiler(node官方提供的工具,不過已經無法支援node v10以上的版本,並不再維護)
  • v8-profiler-next(國人維護版本,支援到最新node v18,持續維護中)

這是採集5分鐘的CPU Profile樣例

16.png

分析方法

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

17.png

預設的檢視是Heavy檢視,在這裡我們看到有兩欄:Self TimeTotal Time

  • Self Time:代表此函數本身(不包含其他呼叫)的執行耗時
  • Total Time:代表此函數(包含其他呼叫函數)的總執行耗時

當發現Total TimeSelf Time偏差較大時,該函數可能存在耗時比較多的CPU密集型計算,也可以展開進一步定位排查

Codedump

當應用意外崩潰終止時,系統會自動記錄下程序crash掉那一刻的記憶體分配資訊,Program Counter以及堆疊指標等關鍵資訊來生成core檔案

生成方式

生成.core檔案的三種方法:

  • ulimit -c unlimited開啟核心限制
  • node --abort-on-uncaught-exceptionnode啟動新增此引數,可以在應用出現未捕獲的異常時也能生成一份core檔案
  • gcore <pid>手動生成core檔案

分析方法

獲取.core檔案後,可以通過mdb、gdb、lldb等工具實現解析診斷實際程序crash的原因

  • llnode `which node` -c /path/to/core/dump

案例分析

觀察

18.png

從監控可以觀察到堆記憶體在持續上升,因此需要堆快照進行排查

分析

19.png

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

排查

20.png

從程式碼中可以看到雖然unused方法沒有呼叫,但是newThing物件是參照自theThing,導致其一直存在於replaceThing這個函數的執行上下文中,沒有被釋放,這就是典型的由於閉包產生的記憶體漏失案例

小結

常見的記憶體漏失有以下幾種情況:

  • 全域性變數
  • 閉包
  • 定時器
  • 事件監聽
  • 快取

因此在上述這幾種情況時,一定要謹慎考慮物件在記憶體中是否會被自動回收,不會被自動回收的話,需要手動進行回收,比如手動把物件設定為null、移除定時器、解綁事件監聽等

總結

至此,本文已經對整個Node.js的效能監控體系進行了詳細的介紹。

首先,介紹了效能監控解決的問題,組成部分以及主流方案的優缺點對比。

然後,針對兩大部分效能指標和快照工具進行了具體的介紹,

  • 效能指標主要關注CPU、記憶體、堆空間、GC幾個指標,同時介紹了v8的GC策略和GC優化方案,
  • 快照工具主要有堆快照、CPU快照以及崩潰時的Coredump

最後,從觀察、分析、排查再現一個簡單的記憶體漏失案例,並總結了常見記憶體漏失的情況和解決方案。

希望這一篇文章能夠幫助大家對整個Node.js的效能監控體系有所瞭解。

更多node相關知識,請存取:!

以上就是為什麼需要效能監控?聊聊Node.js效能監控的詳細內容,更多請關注TW511.COM其它相關文章!