Scrapy的記憶體洩露問題總結

pythontab發表於2016-11-22

  這幾天跟著小夥伴一起做專案,碰到不少平時碰不到的技術問題,真是很好玩的一件事。比如Scrapy這個爬蟲框架的的記憶體洩露問題就是一個很讓人頭疼的問題。


  歷來OOM(OOM - Out of Memory,記憶體溢位)問題都是專案裡最棘手的問題,這種問題debug的難度很大,原因在於問題不太好定位。因為OOM的成因往往比較複雜,不合理的物件建立,資料結構使用的不合理性,分散式架構中各系統的配合不好等情況,都有可能出現這個問題。

  而爬蟲這個任務,涉及網站各個頁面的遍歷,通常會在網站抓去期間產生大量的HTTP Request,而Request的處理往往是透過任務佇列來處理的。由於不是網站的所有的頁面都要抓去,但我們幾乎需要遍歷大部分的頁面,再考慮併發的情況,爬蟲任務開始後,往往會有大量的pending request進入佇列。而這些佇列,框架對其最常見的處理方式是放在記憶體中,因此,當要抓去的頁面層次位於網站的較深層時,這個佇列的記憶體佔用到了任務的中後期會變得非常可觀。以我這次碰到的情況看,在用完我所有想到的能用到的方法最佳化記憶體使用以後,在抓到將近20w條資料的時候,佇列裡pending request數量已經佔到了差不多40w個。此時消耗了差不多1.7G記憶體,而我整個虛擬機器也只有2G記憶體。

  需要說明的一點是,這個數值,根據不同的網站,爬蟲寫法的不同,會有不同的值。這裡只是我的專案中的資料。在沒有做任何最佳化之前,基本上跑到幾千甚至1w條資料的時候,爬蟲就吃光記憶體被ubuntu強制殺掉了。Scrapy的官方文件也提到過memory leak的debug方法,大家可以參考Scrapy文件中的Debugging memory leaks一節。官方文件給了比較詳細的說明, 詳情參考 Scrapy文件 - 除錯記憶體溢位


  那麼,Scrapy會造成OOM的原因是否只有request?答案是不是。這個專案裡面使用了django的orm作為資料操作的框架。可以說,之前吃記憶體太快的原因,主要不在於Scrapy的pending request膨脹得太快,而是django帶來的坑。

  因為我們要抓取的資料量也是很大的,因此也會有大量的資料會往資料庫裡寫,同時,也會有很多資料被讀取。有時候為了避免過多的資料庫io,我們會將常用的資料留在記憶體中,需要的時候直接取用。但很不幸的是,django的orm也會考慮同樣的問題。當我們呼叫django orm的QuerySet介面查詢的時候,django會把資料快取。這樣,同樣的東西會保持2份(一份是我們自己維護的記憶體快取,一份是orm自己的session快取)。只有使用QuerySet的iterator方法來迭代輸出的時候,django orm才不會有快取行為,這是django官方文件中的解釋。

  但是,事情並不會如此簡單,在開發階段,我們往往會把框架的debug引數設為True以便於debug的時候做profile和異常定位。但是在開發階段用debug=True的配置來跑程式的時候,orm永遠都會快取,因此在這種配置下,依然無法避免記憶體洩露的問題。另一個坑就是,像MySQL-Python(MySQLdb)和Psycopg2這樣的資料庫驅動,都有客戶端快取,因此,資料庫驅動也會儲存一份快取,這樣記憶體中就是3份快取,如果資料量大了,記憶體開銷的增長速度會相當可觀。因此,在有需要快取的資料的查詢的時候,要避免把所有的東西查出來,只取出自己需要的欄位就好。

  django的QuerySet有values和value_list方法來保證只取到需要的欄位,另一個辦法就是自己運算元據庫連線,寫SQL來獲取自己需要的欄位。前一種方法是較好的方式。其實這個問題對於其他的orm也會存在,因此也是相當的具有參考性。


  所以總結一下,當你在scrapy遇到記憶體洩露的問題的時候應該檢查以下情況:

  1.Scrapy的任務佇列中的請求數是否過多?

  如果是,那麼你應該看看自己的url抓取的rule是否應該最佳化,將完全沒有必要訪問的頁面都剔除掉,不抓取,不訪問。檢視pending request數量的方法請參看Scrapy的telnet console說明和文件中debugging memory leaks這一節。

  2.orm的寫法是否使用了快取,如果是,請尋找不快取的查詢方法,或者定時手動清理orm快取。

  3.看看資料庫驅動的特性裡是否存在快取結果的特性,是的話,可以看看能否關閉並在查詢的時候只查出自己需要的欄位,不要查出所有欄位。


相關文章