引子
最近在學推薦系統,萌生一個從頭實現一個推薦系統的想法。說做就開始著手,第一步先寫一個視訊爬蟲。
在網上找了一個有網頁的版的視訊聚合源,用nodejs+jsdom快速搭建了一個spider,爬取過程發現用併發的請求個數不好控制,太多容易把源網站爬掛了,就引入了async.parallelLimit和async.queue來做併發請求控制;另外看網上資料jsdom資源佔用比較多,cheerio更輕便,便切換到cheerio。
但執行一段時間之後發現記憶體漲的非常快,像是存在記憶體洩露問題。
遇到問題不要著急,先進行下邏輯分析,再通過工具去逐步確認自己的假設或找到更多可疑的地方,兩種方式不斷交叉最終確認問題。
分析流程
問題:爬蟲啟動之後記憶體快速增長。
- 根據之前分析記憶體洩露的經驗先仔細讀下程式碼,看看是否有容易出現記憶體洩露的程式碼。這種程式碼排查過,沒有可疑的地方。
- 引入的cheerio是否有記憶體洩露?快速網上查閱,有人有提及。換回jsdom快速試驗下,同樣出現。有可能是這2個庫本來就有記憶體問題或者是爬蟲邏輯上就存在記憶體的問題。
- 先通過工具判斷下爬蟲邏輯是否存在記憶體問題。js是記憶體自動管理,那看看主動gc有沒有效果。給node增加了
--max_old_space_size=512 --gc_interval=100 --expose_gc
,然後在程式碼裡面定時主動呼叫global.gc()
,但記憶體還是飈的很快。 - 主動gc都沒法解決,那肯定是有記憶體洩露,使用heapdump,定時列印heapdump出來分析對比。
node --trace_gc spider.js | grep Mark-sweep
發現在直到415行之後新增continue,記憶體又開始漲得很厲害了。所以可以定位是415行這句程式碼導致了記憶體洩露。415行就一個tvLink的賦值為啥會導致記憶體洩露呢?處於好奇就這414行列印了一句
console.log("tvLink=", tvLink)
神奇的事情發生了,再次跑的時候記憶體又不暴漲了,記憶體洩露問題解決了。諮詢了下同事super大神,思路切換到既然知道videoData沒有被釋放掉,那就看看是誰retain著他?切換到Chrome的Profiler,可以點選字串看到誰retain著這些字串。
看到是一個陣列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其實也是一種,但不建議。
總結
- 對js底層的字串機制得了解清楚,這個道理對於其他語言也一樣。比如很多語言都有sliced string機制
- 可測性,不一定都有時間寫單測,但儘量保證關鍵步驟都是拆分成可以獨立測試的函式
- 如果有大迴圈,一定要注意哪些地方是sliced string,如果是的話執行必要的flatten操作,以便記憶體能及時釋放
- 不建議著急用工具除錯,有bug的程式碼都有規律,可以先通讀程式碼確保邏輯上沒有明顯的問題,這樣能提高效率;工具分析為輔助,好的工具像利器,得熟練掌握。