這篇文章也一樣發表在個人我的博客中,歡迎訪問:Node 性能優化javascript
沒有 profile 談優化都是耍流氓,性能優化的大前提是 profile ,有數據才能找出程序慢在哪裏了。
本篇文章主要介紹 Node 後端的性能優化,前端的同窗能夠看看 Chrome 的 devtools https://github.com/CN-Chrome-...html
性能的瓶頸每每在 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...
Node 是基於 V8 這個 js 引擎的,這裏咱們瞭解下 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 的垃圾回收機制與內存限制
在啓動程序的時候添加 --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 | 查找不能優化的函數 |
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 能夠抓下當時內存的快照。
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目錄下。
heapdump 提供的內存快照是能夠用 Chrome 的開發者工具來查看的。把 .heapsnapshot 文件導入到 Chrome Developer Tools
怎麼使用內存分析工具呢?
Chrome開發者工具之JavaScript內存分析
這篇文件詳細介紹瞭如何使用開發者工具來分析內存的使用狀況。能夠參考,這裏就不細說了。
摘取個例子,使用對比視圖。
對比視圖 demo
這個例子展現了經過對比先後的內存變化來找出內存泄露的緣由,看起來仍是很簡單方便的。
可是,理想很美好,現實很殘酷。下面展現下平常開發中 dump 下的數據。
使用對比視圖:
能夠看出 array 是內存增加的主要元兇,但也只能獲得這個線索,那具體是那些 array 消耗了內存呢?
點開 array 查看詳細信息:
一大堆的匿名數組,沒法準確查到具體那些 array 消耗了內存。
主要緣由是後端使用了 sails 這個 web 框架,框架裏的代碼量比較多,干擾項太多,沒法準確地判斷是哪些 function 出現了問題。
一般,形成內存泄露的緣由有以下幾個。
慎用內存當緩存,非用的話控制好緩存的大小和過時時間,防止出現永遠沒法釋放的問題
隊列消費不及時,數組、回調,生產者的速度比消費者速度快,堆積了大量生產者致使沒法釋放做用域或變量
做用域未釋放,沒法當即回收的內存有全局變量和閉包,儘可能使用變量賦值爲 null|undefined 來觸發回收
這部分的詳細解釋請參考《深刻淺出 nodejs》5.4 內存泄露。
上面介紹了 IO 優化,內存優化,使用 Node 作後端的話還會常常碰到 CPU 瓶頸。總所周知,Node 是單線程的,因此對 CPU 密集的運算不是太勝任,因此應該避免使用 Node 來進行 CPU 密集的運算。
那麼若是出現了 CPU 類的問題要怎麼處理呢?
加入 --prof 參數能夠在應用結束是收集 log,執行命令以後,會在該目錄下產生一個 *-v8.log 的日誌文件,咱們能夠安裝一個日誌分析工具 tick
能夠分析每一個 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 用於分析。
alinode,基於 Node 運行時的應用性能管理解決方案,筆者沒有體驗過,不預評價。
文章主要介紹的仍是後端開發中如何作性能優化的幾種方式:
添加索引
接口緩存
靜態文件緩存
合併查詢
這幾種方法的目的其實都是爲了減小 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/...