淺析Node.js中的記憶體流失問題

來源:互聯網
上載者:User

淺析Node.js中的記憶體流失問題

   這篇文章是由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應用的記憶體泄露。下面是其中的一些:

  Jimb Esser的node-mtrace,它使用了GCC的mtrace工具來分析堆的使用。

  Dave Pacheco的node-heap-dump對V8的堆抓取了一張快照並把所有的東西序列化進一個巨大的JSON檔案。它還包含了一些分析研究快照結果的JavaScript工具。

  Danny Coates的v8-profiler和node-inspector提供了綁定在Node中的V8分析器和一個基於WebKit Web Inspector的debug介面。

  Felix Gnass的未禁用保持器圖表分支。

  Felix Geisendorfer的Node記憶體泄露指導(Node Memory Leak Tutorial)是一個又短又酷的v8-profiler和node-debugger使用教程。同時也是目前最先進的Node.js記憶體泄露調試技術指南。

  Joyent的SmartOS平台,它提供了大量用於調試Node.js記憶體泄露的工具。

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

  Enternode-memwatch

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

  它給我們提供三件東西:

  一個‘泄漏'事件發射器

  ?

  1

  2

  3memwatch.on('leak', function(info) {

  // look at info to find out about what might be leaking

  });

  一個‘狀態事件發射器

  ?

  1

  2

  3

  4var memwatch = require('memwatch');

  memwatch.on('stats', function(stats) {

  // do something with post-gc memory usage stats

  });

  一個堆記憶體區分類

  ?

  1

  2

  3var hd = new memwatch.HeapDiff();

  // your code here ...

  var diff = hd.end();

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

  ?

  1var 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。左下側的盒子展示了附加資訊。

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

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

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

  ?

  1

  2

  3

  4{ 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能比較堆上對象的名稱和分配數量的快照,其對比前後的差異可以協助找出導致記憶體流失的元兇。

  ?

  1

  2

  3

  4

  5var hd = new memwatch.HeapDiff();

  // Your code here ...

  var diff = hd.end();

  對比產生的內容就像這樣:

  ?

  1

  2

  3

  4

  5

  6

  7

  8

  9

  10

  11

  12

  13

  14

  15

  16

  17

  18

  19

  20

  21

  22

  23

  24

  25

  26

  27

  28

  29

  30

  31

  32

  33

  34

  35

  36

  37

  38

  39

  40

  41

  42

  43

  44

  45

  46

  47

  48{

  "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方法。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.