英文原文:Tracking Down Memory Leaks in 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 Geisendörfer的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 3 |
memwatch.on('leak', function(info) { // look at info to find out about what might be leaking }); |
- 一個‘狀態’事件發射器
1 2 3 4 |
var memwatch = require('memwatch'); memwatch.on('stats', function(stats) { // do something with post-gc memory usage stats }); |
- 一個堆記憶體區分類
1 2 3 |
var hd = new memwatch.HeapDiff(); // your code here ... var diff = hd.end(); |
- 並且還有一個在測試時很有用處的,可以觸發垃圾收集器的功能。好吧,一共四點。
1 |
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。左下側的盒子展示了附加資訊。
注意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 5 |
var 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方法。
在下圖中,我們加上了堆記憶體物件分配數量排行:
下一步怎麼做
node-memwatch提供了:
- 準確的記憶體使用情況跟蹤
- 疑似記憶體洩漏通知
- 堆差異比較
- 這是跨平臺的
- 並且不要求任何額外的裝置
我們想要它的功能更多。特別是,我們希望node-memwatch能夠提供一些導致記憶體洩漏的物件的使用案例(例如,變數名稱,陣列下標或閉包程式碼)。
我們希望您能在除錯Node應用程式洩漏問題時發現memwatch很好用,也希望您能複製一份程式碼並幫助我們做得更好。