市面上成熟的 Node.js 性能監控系統,監控的指標有很多。
  以開源的[Easy-Monitor](https://github.com/X-Profiler/xprofiler)為例,在[系統監控](http://www.devtoolx.com/easy-monitor#/app/1/instance?duration=24&tab=system&agentId=iZm5egr88rtfowtz6oo6qnZ)一欄中,指標包括內存、CPU、GC、進程、磁盤等。
  這些系統能全方位的監控著應用的一舉一動,并且可以提供安全提醒、在線分析、導出真實狀態等服務。
  本專題分為上下兩個篇章,會簡單分析下在 Node.js 環境中的幾個資源瓶頸,包括CPU 、內存和進程奔潰,并且會給出相應的監控方法。
  本系列所有的示例源碼都已上傳至Github,[點擊此處](https://github.com/pwstrick/node)獲取。
## 一、CPU
  在 Linux 系統中,可以通過 top 命令看到當前的 CPU 資源利用率、內存使用等信息,并且可按特定指標排序,類似于 Windows 的任務管理器。
  在 Node.js 中,提供了兩個方法可以讀取和計算出 CPU 負載和 CPU 使用率兩個指標。
  這兩個指標在一定程度上都可以反映一臺計算機的繁忙程度。
**1)CPU 負載**
  CPU 負載是指在一段時間內等待或占用 CPU 的進程數,進程是操作系統中資源分配的最小單位。
  平均負載(Load Average)就是那些進程數除以時間得到的平均數。
  假設一臺計算機只有一個 CPU 并且是一核,將 CPU 比作一座只有一條單向車道的橋,車比作進程。
* 當平均負載為 0 時,橋上沒有車。
* 當平均負載為 0.5 時,橋上一半路段有車。
* 當平均負載為 1 時,橋上所有路段都有車,雖然大橋已滿,但不會堵車。
* 當平均負載為 2 時,大橋已滿,并且還多了一樣多的車在橋外排隊等待。
  如果 CPU 每分鐘可以處理 100 個進程,那么當平均負載是 2 時,還有 100 個進程在排隊等待中。
  現在的芯片廠商往往會讓 1 個 CPU 包含多個核,并且還能將 1 個核虛擬成 2 個邏輯 CPU,CPU 負載建議的計算方式是:
~~~
(CPU個數 * 核數 * 2 * 0.8)或者(CPU個數 * 核數 * 2 * 0.7)
~~~
  不建議 CPU 長期滿負荷工作。對于平均負載的量化,會采用三個時間標準:1 分鐘,5 分鐘和 15 分鐘。
  1 分鐘的時間比較短,有時候峰值突然升高,有可能是暫時現象。
  5 分鐘和 15 分鐘是較為合適的評判指標,當這兩個時間段內的平均負載都大于 1,那就表明問題持續存在。
  這是一個危險的信號,CPU 上等待的進程在增多,若不及時清理,就會越堵越長,影響程序的正常運行。
  在 os 模塊中,提供了[loadavg()](https://nodejs.org/dist/latest-v18.x/docs/api/os.html#loadavg)方法,可以得到一個包含 1、5 和 15 分鐘的平均負載的數組。
~~~
const os = require("os");
os.loadavg(); // [ 1.9951171875, 1.951171875, 1.93359375 ]
~~~
  注意,平均負載是 Unix 特有的概念,在 Windows 上,返回值始終為 \[0, 0, 0\]。
**2)CPU 使用率**
  CPU 使用率是指程序在運行期間占用 CPU 的百分比,也就是說量化 CPU 的占用情況,計算方式如下:
~~~
CPU使用率 = (1 - CPU空閑時間 / CPU總時間) * 100
~~~
  CPU 使用率高,并不意味著 CPU 負載也高,例如當前任務很少,其中有一個需要大量的計算(CPU 密集型場景),那么使用率會很高,但負載很低。
  CPU 負載高,并不意味著 CPU 使用率也高,例如當前任務很多,在任務執行過程中因為等待 I/O 使得 CPU 非常空閑(I/O 密集型場景),那么使用率就會變低,但負載很高。
  在 os 模塊中,提供了[cpus()](https://nodejs.org/dist/latest-v18.x/docs/api/os.html#oscpus)方法,可得到以每個邏輯 CPU 內核信息組成的對象數組,如下所示。
~~~
[
{
model: 'Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz',
speed: 2300,
times: { user: 27207990, nice: 0, sys: 17891890, idle: 179286370, irq: 0 }
},
{
model: 'Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz',
speed: 2300,
times: { user: 294240, nice: 0, sys: 352550, idle: 223732290, irq: 0 }
},
]
~~~
  其中 times 屬性是一些時間信息,其中 nice 值僅適用于 POSIX 平臺。在 Windows 中,所有處理器的 nice 值始終為 0。
* user:CPU 在用戶模式下花費的毫秒數。
* nice:CPU 在良好模式下花費的毫秒數。
* sys:CPU 在系統模式下花費的毫秒數。
* idle:CPU 在空閑模式下花費的毫秒數。
* irq:CPU 在中斷請求模式下花費的毫秒數。
  下面用一個示例計算 CPU 使用率,遍歷 CPU 信息數組后,將各個時間依次累加,然后返回總時間和空閑時間,最后套用公式計算。
~~~
function getCPUInfo() {
const cpus = os.cpus();
let user = 0, nice = 0, sys = 0, idle = 0, irq = 0, total = 0;
// 遍歷 CPU
for (const cpu in cpus) {
const times = cpus[cpu].times;
user += times.user;
nice += times.nice;
sys += times.sys;
idle += times.idle;
irq += times.irq;
}
total += user + nice + sys + idle + irq;
return {
idle,
total,
};
}
const cpu = getCPUInfo();
// CPU 使用率
const usage = (1 - cpu.idle / cpu.total) * 100;
~~~
**3)v8-profiler**
  Node.js 是基于 V8 引擎運行的,而 V8 引擎內部實現了一個 CPU Profiler,并且開放了相關 API,[v8-profiler](https://github.com/node-inspector/v8-profiler)就是一個基于這些 API 收集一些運行時數據(例如 CPU 和內存)的庫。
  不過在安裝時,會報錯,因此需要換一個包:[v8-profiler-next](https://github.com/hyj1991/v8-profiler-next),基于 v8-profiler,兼容 Node.js V4 以上的所有版本。
~~~
../src/cpu_profiler.cc:6:9: error: no member named 'Handle' in namespace 'v8'; did you mean 'v8::CodeEventHandler::Handle'?
~~~
  在下面的示例中,是一段需要消耗 CPU 計算的加密代碼。
~~~
const crypto = require('crypto');
const password = 'test'
const salt = crypto.randomBytes(128).toString('base64')
crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex')
~~~
  在下面的示例中,會在 1 分鐘后導出一份 CPU 分析文件,運行后會在當前目錄生成 cpuprofile 后綴的文件。
~~~
const fs = require('fs');
const v8Profiler = require('v8-profiler-next');
const title = 'test';
// 兼容 vscode 中的 cpuprofile 解析
v8Profiler.setGenerateType(1);
v8Profiler.startProfiling(title, true);
// 1分鐘后運行
setTimeout(() => {
const profile = v8Profiler.stopProfiling(title);
// 導出CPU分析文件
profile.export(function (error, result) {
fs.writeFileSync(`${title}.cpuprofile`, result);
profile.delete();
});
}, 60 * 1000);
~~~
  點擊 Chrome DevTools 工具欄右側的更多按鈕,選擇 More tools -> JavaScript Profiler 進入到 CPU 的分析頁面。
:-: 
  將分析文件 Load 進來,首先看到的是 Heavy 視圖的分析結果,在圖中選中的下拉框中還可以選擇 Chart 和 tree。
  前者能顯示火焰圖,按時間順序排列;后者能顯示調用結構的總體狀況,從調用堆棧的頂端開始,即從最初調用的位置開始。
:-: 
  在 Heavy 視圖中,會按照對應用的性能影響程度從高到低排列,這其中有 3 個指標:
* Self Time:完成當前函數調用所用的時間,僅包括函數本身的語句,不包括它調用的任何子函數。
* Total Time:完成此函數的當前調用以及它調用的任何子函數所花費的總時間。
* Function:函數名及其全路徑,可展開查看子函數。
  切換到 Tree 視圖,逐層打開,就可以看到 pbkdf2Sync() 函數占據了 CPU 的大部分時間。
:-: 
  上圖中的 (program) 只計算了 native code 的時間,不包含執行腳本代碼的時間(即沒有在 JavaScript 的堆棧上),idle 也是 native 在執行 (program) 的一種。
## 二、垃圾回收器
  Node.js 是一個基于 V8 引擎的 JavaScript 運行時環境,而 Node.js 中的垃圾回收器(GC)其實就是 V8 的垃圾回收器。
  這么多年來,V8 的垃圾回收器(Garbage Collector,簡寫GC)從一個全停頓(Stop-The-World),慢慢演變成了一個更加并行,并發和增量的垃圾回收器。
  本節內容參考了 V8 團隊分享的文章:[Trash talk: the Orinoco garbage collector](https://v8.dev/blog/trash-talk)。
**1)代際假說**
  在垃圾回收中有一個重要術語:代際假說(The Generational Hypothesis),這個假說不僅僅適用于 JavaScript,同樣適用于大多數的動態語言,Java、Python 等。
  代際假說表明很多對象在內存中存在的時間很短,即從垃圾回收的角度來看,很多對象在分配內存空間后,很快就變得不可訪問。
**2)兩種垃圾回收器**
  在 V8 中,會將堆分為兩塊不同的區域:新生代(Young Generation)和老生代(Old Generation)。
  新生代中存放的是生存時間短的對象,大小在 1~ 8M之間;老生代中存放的生存時間久的對象。
  對于這兩塊區域,V8 會使用兩個不同的垃圾回收器:
* 副垃圾回收器(Scavenger)主要負責新生代的垃圾回收。如果經過垃圾回收后,對象還存活的話,就會從新生代移動到老生代。
* 主垃圾回收器(Full Mark-Compact)主要負責老生代的垃圾回收。
  無論哪種垃圾回收器,都會有一套共同的工作流程,定期去做些任務:
1. 標記活動對象和非活動對象,前者是還在使用的對象,后者是可以進行垃圾回收的對象。
2. 回收或者重用被非活動對象占據的內存,就是在標記完成后,統一清理那些被標記為可回收的對象。
3. 整理內存碎片(不連續的內存空間),這一步是可選的,因為有的垃圾回收器不會產生內存碎片。
**3)副垃圾回收器**
  V8 為新生代采用 Scavenge 算法,會將內存空間劃分成兩個區域:對象區域(From-Space)和空閑區域(To-Space)。
  副垃圾回收器在清理新生代時,會先將所有的活動對象移動(evacuate)到連續的一塊空閑內存中(這樣能避免內存碎片)。
  然后將兩塊內存空間互換,即把 To-Space 變成 From-Space。
:-: 
  接著為了新生代的內存空間不被耗盡,對于兩次垃圾回收后還活動的對象,會把它們移動到老生代,而不是 To-Space。
:-: 
  最后是更新引用已移動的原始對象的指針。上述幾步都是交錯進行,而不是在不同階段執行。
**4)主垃圾回收器**
  主垃圾回收器負責老生代的清理,而在老生代中,除了新生代中晉升的對象之外,還有一些大的對象也會被分配到此處。
  主垃圾回收器采用了 Mark-Sweep(標記清除)和 Mark-Compact(標記整理)兩種算法,其中涉及三個階段:標記(marking),清除(sweeping)和整理(compacting)。
  (1)在標記階段,會從一組根元素開始,遞歸遍歷這組根元素。其中根元素包括執行堆棧和全局對象,瀏覽器環境下的全局對象是 window,Node.js 環境下是 global。
  在這個遍歷過程中,會追溯每一個指向 JavaScript 對象的指針,將其標記為可訪問,同時追溯對象中每一個屬性的指針。
  這個過程會一直持續至找到并標記運行時可到達的所有對象,而那些追溯不到的就是垃圾數據。
  (2)在清除階段,會將非活動對象占用的內存空間添加到一個叫空閑列表的數據結構中。
  空閑列表中的內存塊由大小來區分,這是為了方便以后需要分配內存時,可以快速的找到大小合適的內存空間并分配給新的對象。
  下圖描繪了在將垃圾數據回收前后,內存占用的情況。
:-: 
  可以看出,在執行清除算法后,會產生大量不連續的內存碎片。
  (3)在整理階段,會讓所有活動的對象都向一端移動,然后直接清理掉端邊界以外的內存,如下圖所示。
:-: 
**5)垃圾回收機制**
  在本節開頭提到了并行(parallel)、增量(incremental)和并發(concurrent)三種垃圾回收機制。
  (1)并行是指主線程和協助線程同時執行同樣的工作,這仍然是一種全停頓。
  但垃圾回收所耗費的時間等于總時間除以參與的線程數量(加上一些同步開銷)。
:-: 
  (2)增量是指主線程間歇性的去做少量的垃圾回收,而不是花一整段時間去執行。
  雖然沒有減少主線程暫停的時間,但 JavaScript 的執行都能得到及時的響應。

  (3)并發是指主線程一直執行 JavaScript,而輔助線程在后臺執行垃圾回收,這種實現起來最難,需要處理很多復雜的場景。
  例如 JavaScript 堆上的任何東西都可以隨時更改,使之前所做的工作無效。 況且現在有讀/寫競爭,輔助線程和主線程有可能同時在更改同一個對象。
:-: 
  V8 在新生代垃圾回收中會使用并行清理,每個協助線程會將所有的活動對象都移動到 To-Space。
  主垃圾回收器主要使用并發標記,當堆的動態分配接近最高閾值時,會啟動并發標記任務。
  V8 會利用主線程上的空閑時間主動的去執行垃圾回收,在 Chrome 中,大約有 16.6 毫秒的時間去渲染動畫的每一幀。
  如果動畫提前完成,那么就能在下一幀之前的空閑時間去觸發垃圾回收。
:-: 
  在《[綜合性 GC 問題和優化](https://github.com/aliyun-node/Node.js-Troubleshooting-Guide/blob/master/0x08_%E5%AE%9E%E8%B7%B5%E7%AF%87_%E7%BB%BC%E5%90%88%E6%80%A7%20GC%20%E9%97%AE%E9%A2%98%E5%92%8C%E4%BC%98%E5%8C%96.md)》一文中提到,絕大部分的 GC 引發的問題會表現在 CPU 上,而本質上這類問題卻是 GC 引起的內存問題。
  一般產生的流程是:先在堆內存不斷達到觸發 GC 的預設條件,然后不斷觸發 GC,最后 CPU 飆高。
參考資料:
[Node.js 環境性能監控探究](https://juejin.cn/post/6844903781889474567)
[Nodejs中的內存管理和V8垃圾回收機制](https://www.nodejs.red/#/nodejs/memory)
[深入 Nodejs 源碼探究 CPU 信息的獲取與實時計算](https://www.nodejs.red/#/nodejs/modules/os-cpu-usage)
[「譯」Orinoco: V8的垃圾回收器](https://zhuanlan.zhihu.com/p/55917130)
[Node.js 調試指南](https://www.bookstack.cn/read/node-in-debugging/README.md)
[CPU負載和 CPU使用率](https://www.cnblogs.com/muahao/p/6492665.html)
[CPU負載](https://github.com/autowebkit/tech/wiki/CPU%E8%B4%9F%E8%BD%BD)
[Difference between 'self' and 'total' in Chrome CPU Profile of JS](https://stackoverflow.com/questions/7127671/difference-between-self-and-total-in-chrome-cpu-profile-of-js)
[Deep understanding of chrome V8 garbage collection mechanism](https://developpaper.com/deep-understanding-of-chrome-v8-garbage-collection-mechanism/)
[怎么獲取Node性能監控指標?獲取方法分](https://www.php.cn/js-tutorial-491060.html)
*****
> 原文出處:
[博客園-Node.js精進](https://www.cnblogs.com/strick/category/2154090.html)
[知乎專欄-前端性能精進](https://www.zhihu.com/column/c_1611672656142725120)
已建立一個微信前端交流群,如要進群,請先加微信號freedom20180706或掃描下面的二維碼,請求中需注明“看云加群”,在通過請求后就會把你拉進來。還搜集整理了一套[面試資料](https://github.com/pwstrick/daily),歡迎瀏覽。

推薦一款前端監控腳本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不僅能監控前端的錯誤、通信、打印等行為,還能計算各類性能參數,包括 FMP、LCP、FP 等。
- ES6
- 1、let和const
- 2、擴展運算符和剩余參數
- 3、解構
- 4、模板字面量
- 5、對象字面量的擴展
- 6、Symbol
- 7、代碼模塊化
- 8、數字
- 9、字符串
- 10、正則表達式
- 11、對象
- 12、數組
- 13、類型化數組
- 14、函數
- 15、箭頭函數和尾調用優化
- 16、Set
- 17、Map
- 18、迭代器
- 19、生成器
- 20、類
- 21、類的繼承
- 22、Promise
- 23、Promise的靜態方法和應用
- 24、代理和反射
- HTML
- 1、SVG
- 2、WebRTC基礎實踐
- 3、WebRTC視頻通話
- 4、Web音視頻基礎
- CSS進階
- 1、CSS基礎拾遺
- 2、偽類和偽元素
- 3、CSS屬性拾遺
- 4、浮動形狀
- 5、漸變
- 6、濾鏡
- 7、合成
- 8、裁剪和遮罩
- 9、網格布局
- 10、CSS方法論
- 11、管理后臺響應式改造
- React
- 1、函數式編程
- 2、JSX
- 3、組件
- 4、生命周期
- 5、React和DOM
- 6、事件
- 7、表單
- 8、樣式
- 9、組件通信
- 10、高階組件
- 11、Redux基礎
- 12、Redux中間件
- 13、React Router
- 14、測試框架
- 15、React Hooks
- 16、React源碼分析
- 利器
- 1、npm
- 2、Babel
- 3、webpack基礎
- 4、webpack進階
- 5、Git
- 6、Fiddler
- 7、自制腳手架
- 8、VSCode插件研發
- 9、WebView中的頁面調試方法
- Vue.js
- 1、數據綁定
- 2、指令
- 3、樣式和表單
- 4、組件
- 5、組件通信
- 6、內容分發
- 7、渲染函數和JSX
- 8、Vue Router
- 9、Vuex
- TypeScript
- 1、數據類型
- 2、接口
- 3、類
- 4、泛型
- 5、類型兼容性
- 6、高級類型
- 7、命名空間
- 8、裝飾器
- Node.js
- 1、Buffer、流和EventEmitter
- 2、文件系統和網絡
- 3、命令行工具
- 4、自建前端監控系統
- 5、定時任務的調試
- 6、自制短鏈系統
- 7、定時任務的進化史
- 8、通用接口
- 9、微前端實踐
- 10、接口日志查詢
- 11、E2E測試
- 12、BFF
- 13、MySQL歸檔
- 14、壓力測試
- 15、活動規則引擎
- 16、活動配置化
- 17、UmiJS版本升級
- 18、半吊子的可視化搭建系統
- 19、KOA源碼分析(上)
- 20、KOA源碼分析(下)
- 21、花10分鐘入門Node.js
- 22、Node環境升級日志
- 23、Worker threads
- 24、低代碼
- 25、Web自動化測試
- 26、接口攔截和頁面回放實驗
- 27、接口管理
- 28、Cypress自動化測試實踐
- 29、基于Electron的開播助手
- Node.js精進
- 1、模塊化
- 2、異步編程
- 3、流
- 4、事件觸發器
- 5、HTTP
- 6、文件
- 7、日志
- 8、錯誤處理
- 9、性能監控(上)
- 10、性能監控(下)
- 11、Socket.IO
- 12、ElasticSearch
- 監控系統
- 1、SDK
- 2、存儲和分析
- 3、性能監控
- 4、內存泄漏
- 5、小程序
- 6、較長的白屏時間
- 7、頁面奔潰
- 8、shin-monitor源碼分析
- 前端性能精進
- 1、優化方法論之測量
- 2、優化方法論之分析
- 3、瀏覽器之圖像
- 4、瀏覽器之呈現
- 5、瀏覽器之JavaScript
- 6、網絡
- 7、構建
- 前端體驗優化
- 1、概述
- 2、基建
- 3、后端
- 4、數據
- 5、后臺
- Web優化
- 1、CSS優化
- 2、JavaScript優化
- 3、圖像和網絡
- 4、用戶體驗和工具
- 5、網站優化
- 6、優化閉環實踐
- 數據結構與算法
- 1、鏈表
- 2、棧、隊列、散列表和位運算
- 3、二叉樹
- 4、二分查找
- 5、回溯算法
- 6、貪心算法
- 7、分治算法
- 8、動態規劃
- 程序員之路
- 大學
- 2011年
- 2012年
- 2013年
- 2014年
- 項目反思
- 前端基礎學習分享
- 2015年
- 再一次項目反思
- 然并卵
- PC網站CSS分享
- 2016年
- 制造自己的榫卯
- PrimusUI
- 2017年
- 工匠精神
- 2018年
- 2019年
- 前端學習之路分享
- 2020年
- 2021年
- 2022年
- 2023年
- 2024年
- 日志
- 2020