Node 性能優化

前言

這篇文章也一樣發表在個人我的博客中,歡迎訪問:Node 性能優化javascript

沒有 profile 談優化都是耍流氓,性能優化的大前提是 profile ,有數據才能找出程序慢在哪裏了。
本篇文章主要介紹 Node 後端的性能優化,前端的同窗能夠看看 Chrome 的 devtools https://github.com/CN-Chrome-...html

1、Web 應用優化

性能的瓶頸每每在 IO前端

IO 層優化

磁盤 IO 爲何慢

計算機裏的常見 IO 有 :java

  • CPU 一二級緩存node

  • 內存mysql

  • 硬盤nginx

  • 網絡git

硬盤的 IO 開銷是很是昂貴的,硬盤 IO 花費的 CPU 時鐘週期是內存的 41000000/250 = 164000 倍。github

全部在通常應用中,優化要首先考慮數磁盤 IO , 一般也就是數據層的優化,說到數據庫優化,不少人第一時間會想到加索引,可是什麼加了索引查詢會變快呢?索引要怎麼加才合適呢?web

爲何索引快

關於索引的原理能夠看看這篇文章,索引原理。索引快主要的緣由是:

  • 索引佔用空間更小,能夠有效減小磁盤 IO 次數。

  • 索引可使用方便快速查詢的數據結構,如b+樹

索引怎麼加

回到咱們的主題,沒有 profile 談優化都是耍流氓
以 mongo 爲例,mongo 是帶有慢查詢功能的。
MongoDB 查詢優化分析 這篇文章介紹瞭如何開啓和使用 mongo 的慢查詢功能。
開啓慢查詢收集功能後,使用 db.system.profile.find().pretty() 語句能夠查詢到哪些語句的查詢比較慢。如下面這個查詢語句爲例:

query new_koala.llbrandomredpackage query: { user_id: "56ddb33e23db696f89fdae2a", status: { $ne: 1 } }

查詢條件是 user_id、status 兩個,因此給這兩個字段加上索引能夠提升查詢速度。
固然,若是 mongo 沒有是先開啓慢查詢,掃描一下 mongo.log 也是個辦法。

grep '[0-9][0-9][0-9]ms' /var/log/mongodb/mongodb.log

這樣就能夠找出全部查詢耗時大於100 ms 的記錄。而後再對症下藥便可。

緩存大法好,有選擇地用。

上文有說到,內存 IO 比磁盤 IO 快很是多,因此使用內存緩存數據是有效的優化方法。經常使用的工具如 redis、memcached 等。
緩存效果顯著,因此不少時候一談到優化,不少人就會想到加緩存,可是使用緩存是有代價的,你須要維護緩存的更新和失效,這是個繁瑣的事情,用上了緩存後你會常常碰到緩存沒有及時更新帶來的問題。
重要的事情說多幾遍:
緩存有反作用
緩存有反作用
緩存有反作用

並非全部數據都須要緩存,訪問頻率高,生成代價比較高的才考慮是否緩存,也就是說影響你性能瓶頸的考慮去緩存。

並且緩存還有 緩存雪崩緩存穿透 等問題要解決。見 緩存穿透與緩存雪崩

靜態文件緩存

靜態文件如圖片、js 文件等具備不變性,是很是適合作緩存的。
常見的靜態文件緩存服務有 nginx、vanish 等。

代碼層面優化。

合併查詢

在代碼這一塊,常作的事情是將屢次的查詢合併爲一次,消滅 for 循環,實際上仍是減小數據庫查詢。例如

for user_id in userIds 
     var account = user_account.findOne(user_id)

這類代碼實際上能夠改寫成:

var user_account_map = {}   // 注意這個對象將會消耗大量內存。
user_account.find(user_id in user_ids).forEach(account){
    user_account_map[account.user_id] =  account
}
for user_id in userIds 
    var account = user_account_map[user_id]

這樣就把 N 次的查詢合併爲一次。
實際上仍是爲了減小 IO。

關於過早優化

性能優化的工做作多了之後,每每會陷入一個什麼都想着去優化的狀態,這樣就可能陷入過早優化的深坑中。
這裏引用一下其餘人的觀點
https://www.zhihu.com/questio...
e603afaf28c751376c79b3c11e8950c0.png

2、內存泄露排查

Node 是基於 V8 這個 js 引擎的,這裏咱們瞭解下 V8 裏的內存相關的知識。

V8 的 GC 垃圾回收機制

V8 的內存分代

在 V8 中,主要將內存分爲新生代和老生代兩代。新生代的對象爲存活時間比較短的對象,老生代中的對象爲存活時間較長的或常駐內存的對象。

默認狀況下,新生代的內存最大值在 64 位系統和 32 位系統上分別爲 32 MB 和 16 MB。V8 對內存的最大值在 64 位系統和 32 位系統上分別爲 1464 MB 和 732 MB。

爲何這樣分兩代呢?是爲了最優的 GC 算法。新生代的 GC 算法 Scavenge 速度快,可是不合適大數據量;老生代針使用 Mark-Sweep(標記清除) & Mark-Compact(標記整理) 算法,合適大數據量,可是速度較慢。分別對新舊兩代使用更適合他們的算法來優化 GC 速度。

詳情參見《深刻淺出 nodejs》5.1 V8 的垃圾回收機制與內存限制

V8 的 GC log

在啓動程序的時候添加 --trace_gc 參數,V8 在進行垃圾回收的時候,會將垃圾回收的信息打印出來:

➜  $ node --trace_gc aa.js
...
[94036]       68 ms: Scavenge 8.4 (42.5) -> 8.2 (43.5) MB, 2.4 ms [allocation failure].
[94036]       74 ms: Scavenge 8.9 (43.5) -> 8.9 (46.5) MB, 5.1 ms [allocation failure].
[94036] Increasing marking speed to 3 due to high promotion rate
[94036]       85 ms: Scavenge 16.1 (46.5) -> 15.7 (47.5) MB, 3.8 ms (+ 5.0 ms in 106 steps since last GC) [allocation failure].
[94036]       95 ms: Scavenge 16.7 (47.5) -> 16.6 (54.5) MB, 7.2 ms (+ 1.3 ms in 14 steps since last GC) [allocation failure].
[94036]      111 ms: Mark-sweep 23.6 (54.5) -> 23.2 (54.5) MB, 6.2 ms (+ 15.3 ms in 222 steps since start of marking, biggest step 0.3 ms) [GC interrupt] [GC in old space requested].
...

V8 提供了不少程序啓動選項:

啓動項 含義
–max-stack-size 設置棧大小
–v8-options 打印 V8 相關命令
–trace-bailout 查找不能被優化的函數,重寫
–trace-deopt 查找不能優化的函數

使用 memwatch 模塊來檢測內存泄露

npm模塊 memwatch 是一個很是好的內存泄漏檢查工具,讓咱們先將這個模塊安裝到咱們的app中去,執行如下命令:

npm install --save memwatch

而後,在咱們的代碼中,添加:

var memwatch = require('memwatch');

而後監聽 leak 事件

memwatch.on('leak', function(info) {
 console.error('Memory leak detected: ', info);
});

這樣當咱們執行咱們的測試代碼,咱們會看到下面的信息:

{
 start: Fri Jan 02 2015 10:38:49 GMT+0000 (GMT),
 end: Fri Jan 02 2015 10:38:50 GMT+0000 (GMT),
 growth: 7620560,
 reason: 'heap growth over 5 consecutive GCs (1s) - -2147483648 bytes/hr'
}
mem

memwatch 發現了內存泄漏!memwatch 斷定內存泄漏事件發生的規則以下:

當你的堆內存在5個連續的垃圾回收週期內保持持續增加,那麼一個內存泄漏事件被派發

瞭解更加詳細的內容,查看 memwatch

使用 heapdump dump 出 Node 應用內存快照

檢測到了內存泄露的時候,咱們須要查看當時內存的狀態,heapdump 能夠抓下當時內存的快照。

memwatch.on('leak', function(info) {
 console.error(info);
 var file = '/tmp/myapp-' + process.pid + '-' + Date.now() + '.heapsnapshot';
 heapdump.writeSnapshot(file, function(err){
   if (err) console.error(err);
   else console.error('Wrote snapshot: ' + file);
  });
});

運行咱們的代碼,磁盤上會產生一些 .heapsnapshot 的文件到/tmp目錄下。

使用 Chrome 的開發者工具分析內存消耗

heapdump 提供的內存快照是能夠用 Chrome 的開發者工具來查看的。把 .heapsnapshot 文件導入到 Chrome Developer Tools
576c50513fa2ffff1268ac6e5cb17478.png
0f6a3bbdbc7e9be80de0514f91d40f19.png

怎麼使用內存分析工具呢?
Chrome開發者工具之JavaScript內存分析
這篇文件詳細介紹瞭如何使用開發者工具來分析內存的使用狀況。能夠參考,這裏就不細說了。

摘取個例子,使用對比視圖。
對比視圖 demo
這個例子展現了經過對比先後的內存變化來找出內存泄露的緣由,看起來仍是很簡單方便的。

可是,理想很美好,現實很殘酷。下面展現下平常開發中 dump 下的數據。
使用對比視圖:
09befe50b36e6f61279a081e5599ed46.png

能夠看出 array 是內存增加的主要元兇,但也只能獲得這個線索,那具體是那些 array 消耗了內存呢?
點開 array 查看詳細信息:

081c75666cbe8c61a9d89a9e52dc47bf.png

一大堆的匿名數組,沒法準確查到具體那些 array 消耗了內存。
主要緣由是後端使用了 sails 這個 web 框架,框架裏的代碼量比較多,干擾項太多,沒法準確地判斷是哪些 function 出現了問題。

內存泄露緣由

一般,形成內存泄露的緣由有以下幾個。

  • 慎用內存當緩存,非用的話控制好緩存的大小和過時時間,防止出現永遠沒法釋放的問題

  • 隊列消費不及時,數組、回調,生產者的速度比消費者速度快,堆積了大量生產者致使沒法釋放做用域或變量

  • 做用域未釋放,沒法當即回收的內存有全局變量和閉包,儘可能使用變量賦值爲 null|undefined 來觸發回收

這部分的詳細解釋請參考《深刻淺出 nodejs》5.4 內存泄露。

3、優化應用 CPU 瓶頸

上面介紹了 IO 優化,內存優化,使用 Node 作後端的話還會常常碰到 CPU 瓶頸。總所周知,Node 是單線程的,因此對 CPU 密集的運算不是太勝任,因此應該避免使用 Node 來進行 CPU 密集的運算。
那麼若是出現了 CPU 類的問題要怎麼處理呢?

V8log:

加入 --prof 參數能夠在應用結束是收集 log,執行命令以後,會在該目錄下產生一個 *-v8.log 的日誌文件,咱們能夠安裝一個日誌分析工具 tick

tick 工具分析 log

能夠分析每一個 function 的處理時間。

➜  $ sudo npm install tick -g
➜  $ node-tick-processor *-v8.log
[Top down (heavy) profile]:
  Note: callees occupying less than 0.1% are not shown.

  inclusive      self           name
  ticks   total  ticks   total
    426   36.7%      0    0.0%  Function: ~<anonymous> node.js:27:10
    426   36.7%      0    0.0%    LazyCompile: ~startup node.js:30:19
    410   35.3%      0    0.0%      LazyCompile: ~Module.runMain module.js:499:26
    409   35.2%      0    0.0%        LazyCompile: Module._load module.js:273:24
    407   35.1%      0    0.0%          LazyCompile: ~Module.load module.js:345:33
    406   35.0%      0    0.0%            LazyCompile: ~Module._extensions..js module.js:476:37
    405   34.9%      0    0.0%              LazyCompile: ~Module._compile module.js:378:37
...

前端的同窗能夠直接在 chrome 裏收集 cpu profile 用於分析。

4、使用第三方平臺

alinode,基於 Node 運行時的應用性能管理解決方案,筆者沒有體驗過,不預評價。

5、總結

文章主要介紹的仍是後端開發中如何作性能優化的幾種方式:

  • 添加索引

  • 接口緩存

  • 靜態文件緩存

  • 合併查詢
    這幾種方法的目的其實都是爲了減小 IO。看來 IO 太高是 Node 應用反應慢的主要緣由。

此外,文章也介紹瞭如何排查處理內存泄露和 CPU 太高的問題。這兩類問題是也是影響 Node 性能的一大緣由。

參考:

《深刻淺出 nodejs》樸靈著
MySQL索引原理及慢查詢優化
MongoDB 查詢優化分析
如何用redis/memcache作Mysql緩存層?
緩存穿透與緩存雪崩
http://www.barretlee.com/blog...
http://www.w3ctech.com/topic/842
https://addyosmani.com/blog/t...
http://m.oschina.net/blog/270248
http://www.cnblogs.com/consta...
http://www.open-open.com/lib/...

相關文章
相關標籤/搜索