首頁  >  文章  >  web前端  >  淺析Node.js中的記憶體洩漏問題_node.js

淺析Node.js中的記憶體洩漏問題_node.js

WBOY
WBOY原創
2016-05-16 15:53:191087瀏覽

 這篇文章是由Mozilla的Identity團隊帶來的 A Node.JS Holiday Season系列文章的首篇,該團隊上個月發布了 Persona的第一個測試版本。在開發Persona時我們建立了一系列的工具,包括了從調試,到本地化,到依賴管理以及更多的方面。在這一系列的文章中我們將與社群分享我們的經驗和這些工具,這對任何想用node.js建立一個高可用性服務的人都很有用。我們希望您能喜歡這些文章,並期待看到您的想法和貢獻。

我們將從一篇關於Node.js的實質問題:記憶體洩漏的主題文章開始。我們會介紹 node-memwatch — 一個幫助發現並隔離Node中的記憶體洩漏問題的函式庫。


為什麼自尋煩惱?

關於追蹤記憶體洩漏問得最多的問題就是,「為什麼要自尋煩惱?」。難道沒有更迫切的問題需要先解決嗎?為什麼不選擇不時地重啟服務,或為之分配更多的RAM?為了回答這些問題,我們提出了以下三點建議:

1.也許你不在乎不斷成長的記憶體佔用,但V8在乎(V8是Node運作時的引擎)。隨著記憶體洩漏的成長,V8對垃圾收集器越來越具有攻擊性,這會使你的應用程式運作速度變慢。所以,在Node上,記憶體洩漏會損害程式效能。

2.記憶體洩漏可能觸發其他類型的失敗。記憶體洩漏的程式碼可能會持續的引用有限的資源。你可能會耗盡檔案描述符;你還可能會突然無法建立新的資料庫連線。這類問題可能在你的應用程式耗盡記憶體前很早就會暴露出來,但它仍然會是你陷入困境。

3.最後,你的應用程式遲早會崩潰,並且在你的應用程式受到歡迎時肯定會發生。所有人都會在Hacker News上嘲笑你,諷刺你,這樣你就悲劇了。

潰千里之堤的蟻穴在哪裡?

在建構複雜應用的時候,很多地方都可能發生記憶體外洩。 閉包可能是最廣為人知也是最聲名狼藉的。因為閉包保留了對其作用域內的東西的引用,而這正是通常的記憶體外洩之源。

閉包洩漏往往只有在有人去尋找它們的時候才能發現。但在Node的非同步世界裡,我們隨時隨地的透過回呼函數不停的生成閉包。如果這些回呼函數沒有在創建後立刻使用,分配的記憶體就會持續成長,那些看起來沒有記憶體洩漏問題的程式碼也會產生洩漏。而這種問題更難發現。

你的應用程式也可能因為上游程式碼的問題導致記憶體外洩。也許你能定位到出現記憶體外洩的程式碼,但你可能只能眼巴巴地盯著你那完美無缺的程式碼然後困惑於這到底是怎麼洩漏的!


正是這些難以定位的記憶體洩漏促使我們想要一個node-memwatch這樣的工具。傳說幾個月以前,我們的Lloyd Hilaiel把他自己鎖在一個小房間裡兩天,試著追蹤一個在壓力測試下變得非常明顯的記憶體洩漏問題。 (順便說下,盡請期待Lloyd即將到來的關於負荷測試的文章)

經過兩天的努力,他終於發現了Node內核中的元兇:http.ClientRequest中的事件監聽器沒有被釋放。 (最終修復這個問題的補丁只有兩個但卻至關重要的字母)。正是這次痛苦的經歷促使Lloyd想要寫一個能夠幫助查找內存洩漏的工具。

記憶體外洩定位工具

現在已經有許多好用且不斷增強的工具用於定位Node.js應用的記憶體洩漏。以下是其中的一些:

上面的這些工具我們都很喜歡,但是沒有一個適用於我們的場景。 Web Inspector對於開發中的應用程式非常棒,但是很難用於熱部署的場景,尤其是在多伺服器和涉及子進程的時候。同樣的,在長時間高負載運行中出現的記憶體外洩也很難復現。像dtrace和libumem這樣的工具雖然讓人印象深刻,但不是所有的作業系統都能用。

Enternode-memwatch

我們需要一個跨平台的調試庫,當我們的程式可能存在記憶體洩漏時,它不需要設備告訴我們,並且會幫我們找到哪裡存在洩漏。所以我們實作了node-memwatch。

它提供我們三件東西:

    一個‘洩漏'事件發射器
   

   memwatch.on('leak', function(info) {
  // look at info to find out about what might be leaking
  });

    一個‘狀態事件發射器
   
     

  var memwatch = require('memwatch');
  memwatch.on('stats', function(stats) {
  // do something with post-gc memory usage stats
  });

    一個堆記憶體區分類
   

  var hd = new memwatch.HeapDiff();
  // your code here ...
  var diff = hd.end();

    並且還有一個在測試時很有用處的,可以觸發垃圾收集器的功能。好吧,一共四點。
   
  

 var stats = memwatch.gc();

memwatch.on('stats', ...): Post-GC堆統計

node-memwatch能夠在任何一個JS物件分配之前,緊隨著一次完整的垃圾回收和記憶體壓縮發出一個記憶體使用樣本。 (它使用了V8的post-gc鉤子,V8::AddGCEpilogueCallback,來在每次垃圾回收觸發時收集堆使用資訊)

統計資料包括:

  •     usage_trend(使用趨勢)
  •     current_base(目前基數)
  •     estimated_base(預期基數)
  •     num_full_gc (完整的垃圾回收次數)
  •     num_inc_gc (增加的垃圾回收次數)
  •     heap_compactions (記憶體壓縮次數)
  •     min (最小)
  •     max (最大)

這裡有一個展示存在記憶體外洩的應用程式的資料看起來是什麼樣的例子。下面的圖表隨著時間追蹤記憶體的使用。瘋狂的綠線展示了process.memoryUsage()報告的內容。紅線展示了node_memwatch報告的current_base。左下側的盒子展示了附加資訊。

2015623152204606.png (572×441)

 注意Incr GCs非常高。那說明V8在拼命的嘗試清理​​記憶體。

memwatch.on('leak', ...): 堆分配趨勢

我們定義了一個簡單的偵測演算法來提醒你應用程式可能存在記憶體洩漏。即如果經過連續五次GC,記憶體仍被持續分配而沒有釋放,node-memwatch就會發出一個leak事件。事件的具體資訊格式是明了易讀的,就像這樣:
 

{ start: Fri, 29 Jun 2012 14:12:13 GMT,
 end: Fri, 29 Jun 2012 14:12:33 GMT,
 growth: 67984,
 reason: 'heap growth over 5 consecutive GCs (20s) - 11.67 mb/hr' }

memwatch.HeapDiff(): 查找泄漏元凶

最后,node-memwatch能比较堆上对象的名称和分配数量的快照,其对比前后的差异可以帮助找出导致内存泄漏的元凶。
 

var hd = new memwatch.HeapDiff();
 
// Your code here ...
 
var diff = hd.end();

对比产生的内容就像这样:
 

{
 "before": {
  "nodes": 11625,
  "size_bytes": 1869904,
  "size": "1.78 mb"
 },
 "after": {
  "nodes": 21435,
  "size_bytes": 2119136,
  "size": "2.02 mb"
 },
 "change": {
  "size_bytes": 249232,
  "size": "243.39 kb",
  "freed_nodes": 197,
  "allocated_nodes": 10007,
  "details": [
   {
    "what": "Array",
    "size_bytes": 66688,
    "size": "65.13 kb",
    "+": 4,
    "-": 78
   },
   {
    "what": "Code",
    "size_bytes": -55296,
    "size": "-54 kb",
    "+": 1,
    "-": 57
   },
   {
    "what": "LeakingClass",
    "size_bytes": 239952,
    "size": "234.33 kb",
    "+": 9998,
    "-": 0
   },
   {
    "what": "String",
    "size_bytes": -2120,
    "size": "-2.07 kb",
    "+": 3,
    "-": 62
   }
  ]
 }
}

HeapDiff方法在进行数据采样前会先进行一次完整的垃圾回收,以使得到的数据不会充满太多无用的信息。memwatch的事件处理会忽略掉由HeapDiff触发的垃圾回收事件,所以在stats事件的监听回调函数中你可以安全地调用HeapDiff方法。

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn