背景
近期,為了評估服務(wù)性能,測試同學(xué)對關(guān)鍵業(yè)務(wù)接口進行了壓測,單臺NodeJS服務(wù)開啟3個進程的情況下,QPS最高達320多。為了確認服務(wù)是否還有優(yōu)化空間,我們使用阿里云的 NodeJS性能平臺 對服務(wù)進行分析,定位了服務(wù)的瓶頸,并在阿里云的同學(xué)幫助下采取了相應(yīng)的措施,優(yōu)化了服務(wù)的性能。
問題排查與分析
Step1 獲取與分析CPU Profile
當我們以400并發(fā)量,對單一業(yè)務(wù)接口進行壓測,發(fā)現(xiàn)QPS為320時,服務(wù)器CPU被打滿。為了找到是什么原因?qū)е翪PU達到了性能瓶頸,我們使用了阿里云的「NodeJS性能平臺」,抓取了壓測時的 CPU Profile 信息。

經(jīng)過分析,我們發(fā)現(xiàn) _tickDomainCallback 和 garbage collector 在CPU占比很大,其中 _tickDomainCallback占了50%多,GC 也占了27%的比例。通過展開 _tickDomainCallback 里的內(nèi)容,發(fā)現(xiàn)CPU占比高的邏輯主要是TypeORM 和4處業(yè)務(wù)邏輯。

Step2 排查數(shù)據(jù)庫性能
當我們看到TypeORM時,我們以為是數(shù)據(jù)庫消費不過來(生產(chǎn)與消費能力不匹配,Query隊列產(chǎn)生大量堆積),導(dǎo)致TypeORM消耗大量CPU資源。后來,我們進行了第二次壓測,并在服務(wù)器CPU打滿時獲取了RDS的性能分析報告。報告顯示:
- 數(shù)據(jù)庫CPU使用了15%的資源
- 平均查詢響應(yīng)速度小于15ms
- 無慢查詢記錄
- 無死鎖記錄
因此,我們排除了RDS導(dǎo)致TypeORM消耗CPU資源。我們推測可能與TypeORM本身的代碼有關(guān),我們使用了一個非常早期的TypeORM版本(v0.0.11)。阿里云的同學(xué)推薦我們升級TypeORM的版本試試,看看會不會有所改善。但是最新的TypeORM版本與早期的版本API已經(jīng)發(fā)生了變化,無法進行平滑升級。因此,放棄了對TypeORM優(yōu)化。
Step3 排查業(yè)務(wù)邏輯代碼
我們將可能影響性能的業(yè)務(wù)代碼進行了Review,發(fā)現(xiàn)優(yōu)化空間并不是很大,代碼本身已經(jīng)經(jīng)過了精簡和優(yōu)化。無法進行進一步提升,我們將優(yōu)化重點放在了占比高達27%的 GC 上。
Step4 GC 信息抓取與分析
為了獲得詳細的GC信息,我們再次進行了壓測,并獲取了 GC Trace 信息。結(jié)果如下圖:

從圖中,我們可以獲取到一些重要信息:
- GC時間占比為26.87%
- 3分鐘內(nèi),GC暫時時間為47.8s,且scavenge占了大多數(shù)
- 平均GC暫停時間為50~60ms
根據(jù)這些信息,我們可以得出 scavenge 非常頻繁,導(dǎo)致了CPU資源的占用。
scavenge 發(fā)生在新生代的內(nèi)存回收階段,這個階段觸發(fā)條件是, semi space allocation failed(半空間分配失敗)??梢酝茰y出,壓測期間我們的代碼邏輯頻繁的生成大量的小對象,導(dǎo)致 semi space很快被分配滿,從而導(dǎo)致了 scavenge 回收和CPU資源的占用。既然這樣,我們可不可通過調(diào)整 semi space(半空間)的大小,減少GC的次數(shù)來優(yōu)化對CPU的占用。
Step5 GC 調(diào)優(yōu)與測試
NodeJS在64位系統(tǒng)上,默認的semi space大小為16M。
我們將 semi space 進行了3次調(diào)整,分別設(shè)為64M、128M、256M,對不同值情況下的服務(wù)進行了壓測并獲取了對應(yīng) GC Trace 和 CPU Profile。
修改 semi space 方法
對于普通node服務(wù):
node index.js --max_semi_space_size=64
對于PM2啟動的服務(wù),在pm2的config文件中添加:
node_args: '--max_semi_space_size=64',
1) 64M
將 semi space 修改為64M,并進行線上壓測,獲取壓測時的 GC Trace 和 CPU Profile信息:


對比修改前的數(shù)據(jù),我們發(fā)現(xiàn):
- GC的CPU占比從27.5%下降到了7.14%;
- 3分鐘內(nèi)GC次數(shù),從1008次降到了312次。其中,Scavenge的次數(shù)從988次下降到了294次;
- GC時間,從原來的47.7s下降到了11.8s
- GC平均暫停時間在40ms左右
GC時間從47.7s下降到了11.8s,相應(yīng)的,QPS提升了10%。
2) 128M
將 semi space 調(diào)整到128M,得到的 GC Trace 和 CPU Profile信息:


對比64M時的數(shù)據(jù),我們可以發(fā)現(xiàn):
- 與64M時GC次相比,GC次數(shù)從312下降到了145;
- Scavenge算法回收時間,增加了1倍。從平均50ms漲到了100ms;
- Mark-sweep的次數(shù)沒有發(fā)生變化
- CPU占比略微下降,從7.14降到了6.71
可以看出,將 semi space從64M調(diào)整到了128M,性能并沒有很大的提升。相反,Scavenge算法回收時間幾乎增長了一倍。
3) 256M
將 semi space 調(diào)整到256M,得到的 GC Trace 和 CPU Profile信息:


可以觀察到:
- 與128M時相比,GC次數(shù)下降了一倍
- 但是Scavenge回收的時間,波動到了150ms。
- CPU占比,也略微下降了一點,降到了5.99
可以看出,將 semi space調(diào)整到了 256M,性能并沒有顯著提升,且增加了 Scavenge 的回收時間。
小結(jié)
將 semi space 從16M調(diào)整到64M時,GC的CPU占比從27.5%下降到了7.14%,Scavenge算法平均回收耗時減少,QPS提升了10%。繼續(xù)調(diào)大 semi space,性能并沒有顯著提升,且Scavenge算法回收時間增加。semi space本身用于新生代對象快速分配,不適合調(diào)整過大。因此,semi space 設(shè)置為64M較為合適。
總結(jié)
通過將semi space調(diào)大,觸發(fā) Scavenge算法回收的概率降低,GC的次數(shù)也隨之減少。且 Scavenge算法回收內(nèi)存的時間也較為合理,因而可以降低GC在CPU中的占比。
本文主要介紹了線上服務(wù)的性能瓶頸的排查與GC調(diào)優(yōu),并沒有介紹V8 垃圾回收機制的原理。推薦感興趣的同學(xué),閱讀樸靈老師的《深入淺出Node.js》中關(guān)于《V8的垃圾回收機制》一節(jié)。其中,詳細了介紹了V8用到的各種算法,非常有助于理解性能調(diào)優(yōu)的原理。
最后,感謝一下阿里的奕鈞同學(xué),在他的幫助下,幫我解決了問題。