Node.js 環境性能監控
- 2019 年 11 月 14 日
- 筆記
作者:@LucasTwilight https://juejin.im/post/5c71324b6fb9a049d37fbb7c
隨著Node v11.0 release版本的發布,Node已經走過了很多年。基於Node產生了很多服務端框架,來幫助我們獨立於後端進行前端工程的開發和部署。
業務邏輯的遷移,以及各種MV*框架的服務端渲染模型的出現,讓基於Node的前端SSR策略更依賴伺服器性能。首屏直出性能以及Node服務的穩定性,直接關係影響著用戶體驗。Node作為服務端語言,相比於Java和PHP這種老服務端語言來說,對於整體性能的調控還是不夠完善。雖然有sentry這種報警平台來及時通知發生的錯誤,但是不能夠預防錯誤的發生。如何防患於未然,首先需要理解Node.js性能監控的主要指標。
下面的程式碼均是基於Egg框架的,如果對Egg不熟悉的小夥伴可以先去瀏覽一下文檔
指標
伺服器的資源瓶頸主要有下面幾個:
- CPU
- 記憶體
- 磁碟
- I/O
- 網路
考慮到不同的Node環境,其對於資源的需求類型也是不盡相同的。如果Node只是用於前端SSR的話,那麼CPU和網路就會成為主要的性能瓶頸。
當然如果你需要使用Node來進行數據持久化相關的工作,那麼I/O和磁碟也會有很高的佔用率。
即使是前端發展非常超前的公司,也很少會用Node作為業務數據的支撐。充其量當做BFF層來為前端提供數據服務,並不直接接觸持久化的數據。所以磁碟和I/O很難成為當下前端性能的瓶頸。
即使存在使用Node進行數據持久化平台,大多數也是實驗性質的平台或者是內部平台。不直接面向業務場景。
所以,在大多數場景下,CPU、記憶體以及網路就可以說是Node的主要性能瓶頸。
CPU指標
CPU負載和CPU使用率
顧名思義,這兩個指標都是用來評估系統當前CPU的繁忙程度的量化指標。CPU負載和CPU使用率是從兩個不同的角度來量化CPU的繁忙程度的。
- CPU負載:進程角度
- CPU使用率:CPU時間分配
進程是資源分配的最小單位。
這句話在作業系統的教科書上或者各位的考試卷上都多多少少出現過。也就是,系統按照進程級別來進行資源的分配,一個CPU核心在一個時刻只能夠為4個進程提供服務。
那麼, CPU的負載也就很好理解了。在某個時間段內,佔用以及等待CPU的進程總數就是CPU在這個時間段內的負載(load average),在大多數情況下,我們稱這個標準為loadavg。
而CPU利用率(cpu utilization),則是量化CPU時間佔用狀況的,一般我們認為CPU利用率 = 1 – 空閑CPU時間(idle time) / CPU總時間。
wiki上已經解釋的非常清楚了,請自備梯子(https://en.wikipedia.org/wiki/Load_(computing)#CPU_load_vs_CPU_utilization)
量化CPU指標
那麼這兩個指標到底哪個才最能代表的系統的實際狀態呢?
滑梯:CPU
人:進程
假如有4個滑梯。每個滑梯上最多可以塞得下10個人。我們假設所有的人的大小一致。那麼,可以得到如下的類比:
- Loadavg = 0,表示滑梯上一個人都沒有
- Loadavg = 0.5, 表示平均每個滑梯上的人都佔了滑梯的一半,也就是總共20個人在滑梯上,由於CPU調度策略,這些人一般會均勻分配(每個人都會挑人少的滑梯)
- Loadavg = 1,表示每個滑梯上都塞滿人了,沒有任何空閑空間
- Loadavg = 2, 表示不僅僅每個滑梯上都塞滿了人,還有40個人在後面等著
以上的類比都是基於瞬時的loadavg得到的。
一般對於loadavg的量化,我們都是採用3個不同的時間標準來進行的。1分鐘,5分鐘以及15分鐘。
1分鐘的指標是很難得到較為均衡的指標的。因為1分鐘時間太短,可能某一秒的峰值就能夠影響到1分鐘時間段內的平均指標。但是,1分鐘內,如果loadavg突然達到很高的值,也可能是系統崩潰的前兆,也是需要警惕的一個指標。
而5分鐘和15分鐘則是較為合適的評判指標。當CPU在5分鐘或者15分鐘內都保持高負荷運作,對於整個系統是非常危險的。遇到過堵車的人都應該知道,一旦發生了堵車,只要堵塞不及時清理,就會越堵越長。CPU也是這樣,如果CPU上等待的進程阻塞的較多,那麼後面進入隊列的任務就更加搶佔不到資源,也就會被一直阻塞了。
在MAC上可以在root許可權下,使用sysctl -n vm.loadavg來獲得。
// /app/lib/cpu.js const os = require('os'); // cpu核心數 const length = os.cpus().length; // 單核CPU的平均負載 os.loadavg().map(load => load / length);
而CPU利用率則是不太好作為直接評判標準的數值。由於進程阻塞在CPU上的原因不相同,對於CPU密集型任務來說,CPU利用率可以很好地表示當前CPU的工作情況,但是對於I/O密集型的任務來說,CPU空閑不代表CPU無事可做,可能是任務被掛起,去進行其他操作了。
但是,對於進行SSR的Node系統來說,渲染基本上可以理解為CPU密集型業務,所以這個指標在一定程度上可以體現出當前業務環境的CPU性能。
// /app/lib/cpu.js const os = require('os'); // 獲取當前的瞬時CPU時間 const instantaneousCpuTime = () => { let idleCpu = 0; let tickCpu = 0; const cpus = os.cpus(); const length = cpus.length; let i = 0; while(i < length) { let cpu = cpus[i]; for (let type in cpu.times) { tickCpu += cpu.times[type]; } idleCpu += cpu.times.idle; i++; } const time = { idle: idleCpu / cpus.length, // 單核CPU的空閑時間 tick: tickCpu / cpus.length, // 單核CPU的總時間 }; return time; } const cpuMetrics = () => { const startQuantize = instantaneousCpuTime(); return new Promise((resolve, reject) => { setTimeout(() => { const endQuantize = instantaneousCpuTime(); const idleDifference = endQuantize.idle - startQuantize.idle; const tickDifference = endQuantize.tick - startQuantize.tick; resolve(1 - (idleDifference / tickDifference)); }, 1000); }); }; cpuMetrics().then(res => { console.log(res); // 0.074999 });
結合上述兩個指標,可以大致得到系統的運行狀態,從而對於系統進行干預。比如將SSR降級為CSR。
記憶體指標
記憶體是一個非常容易量化的指標。記憶體佔用率是評判一個系統的記憶體瓶頸的常見指標。對於Node來說,內部記憶體堆棧的使用狀態也是一個可以量化的指標。
// /app/lib/memory.js const os = require('os'); // 獲取當前Node記憶體堆棧情況 const { rss, heapUsed, heapTotal } = process.memoryUsage(); // 獲取系統空閑記憶體 const sysFree = os.freemem(); // 獲取系統總記憶體 const sysTotal = os.totalmem(); module.exports = { memory: () => { return { sys: 1 - sysFree / sysTotal, // 系統記憶體佔用率 heap: heapUsed / headTotal, // Node堆記憶體佔用率 node: rss / sysTotal, // Node佔用系統記憶體的比例 } } }
對於process.memoryUsage()拿到的值有一些需要關注的地方:
我的Node啟蒙書《深入淺出Node.js》這本書,雖然版本已經落後了現在的Node.js很多release了,但是其中講到的關於V8引擎的GC機制的內容,仍然非常受用,推薦大家買正版支援一下朴靈老師。
- rss:表示node進程佔用的記憶體總量。
- heapTotal:表示堆記憶體的總量。
- heapUsed:實際堆記憶體的使用量。
- external:外部程式的記憶體使用量,包含Node核心的C++程式的記憶體使用量。
首先需要關注的是記憶體堆棧,也就是堆記憶體的佔用。在Node的單執行緒模式下,C++程式(V8引擎)會為Node申請一定的記憶體,來作為Node執行緒的記憶體資源heapTotal。而在我們Node的使用過程中,聲明的新的變數都會使用這些記憶體來進行存儲heapUsed。
Node的分代式GC演算法會在一定程度上浪費部分記憶體資源,所以當heapUsed達到heapTotal一半的時候,就可以強制觸發GC操作了global.gc()。gc操作相關可以看下這篇文章。對於系統記憶體的監控處理,不能夠僅僅像Node記憶體級別一樣,進行GC操作就可以,而同樣需要進行渲染降級。70% ~ 80%的記憶體佔用就是非常危險的情況了。具體的數值需要根據環境所在的宿主機來確定。
具體和Node記憶體GC策略以及分配規則相關的,可以看StrongLoop – Node.js Performance Tip of the Week: Managing Garbage Collection。
QPS
嚴格意義上來說,QPS不能夠作為web監控的直接標準。但是當伺服器在高負載的情況下,不能夠得到和壓測情況下接近的QPS的時候,就需要考慮是某些其他原因導致了伺服器的性能瓶頸。一般在進行Node環境下的SSR的時候,假設Node-Cluster最大執行緒數為10,那麼可以並行進行10個頁面的渲染,當然這也取決於宿主CPU的核心數。
在將Node作為SSR的宿主環境的情況下,可以很容易地記錄到當前機器在一段時間內響應的請求數。之前在做畢業論文的時候,有嘗試過對於web站點進行壓力測試的幾種方式。
ApacheBench
http_load
Seige
這三個web壓測工具大同小異,都能夠進行並發請求測試,對於web站點進行多用戶的並發訪問,並且記錄到所有請求過程的響應時間,並且重複進行請求,可以很好地模擬Node環境在壓力下的表現。
根據性能壓測的結果,以及對於需求的流量峰值的評估,可以大致計算出需要多少台機器才能夠保證web服務的穩定性,保證大多數用戶能夠在可接受的時間內得到響應。
測試
根據上述三個指標,對於本地啟動的環境進行壓測。
本地啟動的Node環境是基於Egg框架擴展的React SSR環境,實際線上環境由於很多靜態資源(包括javascript腳本、css、圖片等)都被推到了CDN上,所以這些資源不會直接對環境產生壓力,而且生產環境和開發環境也存在很多流程上的區別,所以實際性能要比本地啟動的好很多。這裡為了測試方便,所以直接在本地啟動了Egg工程。
測試環境本地可以使用PM2啟動Node工程,或者直接通過Node命令啟動,在本地測試環境盡量不要使用webpack-dev-server這樣的開發環境啟動,這樣可能會導致Node的Cluster模式不能夠很好地運行,監控執行緒阻塞掉頁面渲染的執行緒。基於Egg的環境可以使用schedule定時任務來定時列印環境監控日誌。具體使用可以看Egg的文檔,裡面會寫的比較詳細。然後自定義一個日誌類型,將監控日誌獨立於應用日誌存儲起來,便於分析和可視化。
// /app/schedule/monitor.js const memory = require('../lib/memory'); const cpu = require('../lib/cpu'); module.exports = app => { return { schedule: { interval: 10000, type: 'worker', }, async task(ctx) { ctx.app.getLogger('monitorLogger').info('你想列印的日誌結果') } } } // /config/config.prod.js const path = require('path'); // 自定義日誌,將日誌文件自定義到一個單獨的監控日誌文件中 module.exports = appInfo => { return { customLogger: { monitorLogger: { file: path.resolve(__dirname, '../logs/monitor.log') } } } }
然後準備siege進行壓測:Mac上安裝siege
或者在MAC上可以更簡單地使用brew來直接安裝siege。推薦使用這種方法,因為直接下載源碼包編譯的話,可能會發生libssl庫鏈接不上的問題,導致不能夠進行https請求。
測試和監控結果
- 在無請求訪問情況下:

- siege
配置siege的請求URL列表:我們可以將想要siege請求的URL放在文件裡面,通過siege命令進行讀取(這裡需要注意,siege只能夠訪問http站點,如果站點強制https的話可能需要考慮其他方法)。
- urls文件

urls 執行:siege -c 10 -r 5 -f urls -i -b
-c:模擬有n個用戶同時訪問
-r:重複測試n次
-f:指定測試URL的獲取文件
-I:指定隨機訪問URL獲取文件中的URL
-b:請求無需等待
上面的siege命令就表示,每次並發10個,分別請求urls文件中的隨機一個站點,然後這樣的並發一共執行5次,並且無需等待直接訪問。



可以看到,siege對於服務端進行了515次命中,因為服務端除了主頁面還有一些靜態資源需要請求,這些命中包含頁面,javascript腳本,圖片以及css等,平均每個資源的響應時間為0.83秒
請求結束時間為20:29:37,可以看到這個時間之後,cpu的各項指標都開始下降,而記憶體沒有非常明顯的變化。
再進行一次壓力較大的測試:
執行:siege -c 100 -r 5 -f urls -i -b,將並發數增加到10倍也就是100並發。


可以看到平均響應時間下降到了3.85秒,非常明顯。而且loadavg相比第一次壓測的時候,有著非常明顯的上升。記憶體使用的變化不大,
因為測試環境的機器是虛擬機,不會獨佔物理機的所有資源,但是獲取的CPU數卻是物理機的CPU數。由於之前我們對於每種參數都計算了單核的情況,所以這裡和CPU相關的結果需要和物理機核心數以及虛擬機佔用的核心數相關。
有興趣的小夥伴可以嘗試一下機器的極限ORZ。或者在物理機上嘗試一下壓測。我沒有敢這麼傷害我的小兄弟。
Conclusion
現在很多業務開始往前端進行遷移,BFF(backends for frontends)的概念有很多團隊已經開始逐漸嘗試去做了。讓後端專註於提供統一的數據模型,然後將業務邏輯遷移到基於Node.js的BFF層中,讓前端給自己提供api介面,這樣就剩下了很多前後端聯調的成本,讓後端提供的RPC或者HTTP介面更加通用,更少地修改後端工程,加快開發的效率。
但是這樣就非常依賴Node端的穩定性,在BFF架構中,一旦Node端發生錯誤導致阻塞,則所有前端頁面都會丟失服務,造成很嚴重的後果,所以Node端的監控越來越有意義。結合一些傳統平台比如sentry或者zabbix可以幫助構建一個穩定的前端部署環境。
參考
- 幾種web伺服器性能壓測工具
- Node.js Garbage Collection Explained
- Pattern: Backends For Frontends Node.js Performance Monitoring – Part 1: The Metrics to Monitor Node.js Performance Monitoring – Part 2: Monitoring the Metrics What is loadavg Using LoadAvg for Performance Optimization