nodejs爬蟲內存泄露排查

引子

最近在學推薦系統,萌生一個從頭實現一個推薦系統的想法。說作就開始着手,第一步先寫一個視頻爬蟲。html

在網上找了一個有網頁的版的視頻聚合源,用nodejs+jsdom快速搭建了一個spider,爬取過程發現用併發的請求個數很差控制,太多容易把源網站爬掛了,就引入了async.parallelLimit和async.queue來作併發請求控制;另外看網上資料jsdom資源佔用比較多,cheerio更輕便,便切換到cheerio。node

但運行一段時間以後發現內存漲的很是快,像是存在內存泄露問題。git

遇到問題不要着急,先進行下邏輯分析,再經過工具去逐步確認本身的假設或找到更多可疑的地方,兩種方式不斷交叉最終確認問題。github

分析流程

問題:爬蟲啓動以後內存快速增加。segmentfault

  1. 根據以前分析內存泄露的經驗先仔細讀下代碼,看看是否有容易出現內存泄露的代碼。這種代碼排查過,沒有可疑的地方。
  2. 引入的cheerio是否有內存泄露?快速網上查閱,有人有說起。換回jsdom快速試驗下,一樣出現。有多是這2個庫原本就有內存問題或者是爬蟲邏輯上就存在內存的問題。
  3. 先經過工具判斷下爬蟲邏輯是否存在內存問題。js是內存自動管理,那看看主動gc有沒有效果。給node增長了--max_old_space_size=512 --gc_interval=100 --expose_gc,而後在代碼裏面定時主動調用global.gc(),但內存仍是飈的很快。
  4. 主動gc都無法解決,那確定是有內存泄露,使用heapdump,定時打印heapdump出來分析對比。

發現有大量的字符串(網站的html)沒有被釋放,獲取網上html的地方有好幾處,經過二分查找能定位到代碼

其中videoData就是存儲從網上獲取到的html。也就是說videoData沒有被正確釋放,根據以前作iOS的經驗,若是在一個大循環內產生不少臨時對象,但又沒有建立AutoReleasePool的話會直到這個Runloop結束才能被釋放,難道js也是這樣?查詢了下資料js的gc實際上是很頻繁的,沒有這些限制,並且這個for循環裏面有await,有足夠的時機能夠gc。那就想辦法看看能不能找到是哪一句致使的問題。 同時看到日誌裏面有請求的url,打開一看是抓取蠟筆小新的視頻,其中有1600+集。直覺告訴我這應該是集數太多因此才容易出現這個問題,這確定跟這個大循環有關。既然知道了一個復現的地址,改下單測的代碼直接抓取這個頁面,同時經過--trace_gc來跟蹤下內存gc狀況

node --trace_gc spider.js | grep Mark-sweep數組

而後從代碼405行開始增長continue來進行定位,這個時候內存變得很是穩定

發如今直到415行以後添加continue,內存又開始漲得很厲害了。因此能夠定位是415行這句代碼致使了內存泄露。415行就一個tvLink的賦值爲啥會致使內存泄露呢?處於好奇就這414行打印了一句閉包

console.log("tvLink=", tvLink) 併發

神奇的事情發生了,再次跑的時候內存又不暴漲了,內存泄露問題解決了。諮詢了下同事super大神,思路切換到既然知道videoData沒有被釋放掉,那就看看是誰retain着他?切換到Chrome的Profiler,能夠點擊字符串看到誰retain着這些字符串。dom

看到是一個數組retain着這些對象,而後在這個數組上Review in summary view

能夠看到href是一個sliced string,記得以前看一篇文章說過sliced string致使的內存不釋放的問題,頓時明白了,sliced string顧名思義就是他不實際存儲字符串,而是存儲他在父字符串的startOffset和len 因此href其實就是videoData的sliced string,這也是爲啥videoData不能在循環的時候雖然不用了但仍是不能被釋放。但只要console.log就能迫使sliced string提取出確切的值,既然提取出值後面也不必再存儲成sliced string,因此內存泄露的問題也就解決了。附錄還有一篇super大神寫的SliceString的文章。 能夠理解sliced string實際上是爲了優化字符串使用,但在我這個特定場景確會產生內存不能被快速釋放的問題。準確的講這不算是一個內存泄露的問題,而是一個內存堆積的問題。那有啥辦法能夠規避sliced string引入的問題呢?經同事建議,只要對這個字符串進行操做就能flatten sliced string,好比輕量的parseInt,而console.log其實也是一種,但不建議。async

總結

  1. 對js底層的字符串機制得了解清楚,這個道理對於其餘語言也同樣。好比不少語言都有sliced string機制
  2. 可測性,不必定都有時間寫單測,但儘可能保證關鍵步驟都是拆分紅能夠獨立測試的函數
  3. 若是有大循環,必定要注意哪些地方是sliced string,若是是的話執行必要的flatten操做,以便內存能及時釋放
  4. 不建議着急用工具調試,有bug的代碼都有規律,能夠先通讀代碼確保邏輯上沒有明顯的問題,這樣能提升效率;工具分析爲輔助,好的工具像利器,得熟練掌握。

參考

相關文章
相關標籤/搜索