接上篇。
下載使用
目前為止dhtcrawler2相對dhtcrawler而言,數(shù)據(jù)庫部分調(diào)整很大,DHT部分基本沿用之前。但單純作為一個爬資源的程序而言,DHT部分可以進行大幅削減,這個以后再說。這個版本更快、更穩(wěn)定。為了方便,我將編譯好的erlang二進制文件作為git的主分支,我還添加了一些Windows下的批處理腳本,總之基本上下載源碼以后即可運行。
項目地址:https://github.com/kevinlynx/dhtcrawler2
使用方法
爬蟲每次運行都會保存DHT節(jié)點狀態(tài),早期運行的時候收集速度會不夠。dhtcrawler2將程序分為3部分:
- crawler,即DHT爬蟲部分,僅負責(zé)收集hash
- hash,準(zhǔn)確來講叫
hash reader
,處理爬蟲收集的hash,處理過程主要涉及到下載種子文件
- http,使用hash處理出來的數(shù)據(jù)庫,以作為Web端接口
我沒有服務(wù)器,但程序有被部署在別人的服務(wù)器上:bt.cm,http://222.175.114.126:8000/。
其他工具
為了提高資源索引速度,我陸續(xù)寫了一些工具,包括:
- import_tors,用于導(dǎo)入本地種子文件到數(shù)據(jù)庫
- tor_cache,用于下載種子到本地,僅僅提供下載的功能,hash_reader在需要種子文件時,可以先從本地取
- cache_indexer,目前hash_reader取種子都是從torrage.com之類的種子緩存站點取,這些站點提供了種子列表,cache_indexer將這些列表導(dǎo)入數(shù)據(jù)庫,hash_reader在請求種子文件前可以通過該數(shù)據(jù)庫檢查torrage.com上有無此種子,從而減少多余的http請求
這些工具的代碼都被放在dhtcrawler2中,可以查看對應(yīng)的啟動腳本來查看具體如何啟動。
OS/Database
根據(jù)實際的測試效果來看,當(dāng)收集的資源量過百萬時(目前bt.cm錄入近160萬資源),4G內(nèi)存的Windows平臺,mongodb很容易就會掛掉。掛掉的原因全是1455,頁面文件太小。有人建議不要在Windows下使用mongodb,Linux下我自己沒做過測試。
mongodb可以部署為集群形式(replica-set),當(dāng)初我想把http部分的查詢放在一個只讀的mongodb實例上,但因為建立集群時,要同步已有的10G數(shù)據(jù)庫,而每次同步都以mongodb掛掉結(jié)束,遂放棄。在目前bt.cm的配置中,數(shù)據(jù)庫torrent的鎖比例(db lock)很容易上50%,這也讓http在搜索時,經(jīng)常出現(xiàn)搜索超時的情況。
技術(shù)信息
dhtcrawler最早的版本有很多問題,修復(fù)過的最大的一個問題是關(guān)于erlang定時器的,在DHT實現(xiàn)中,需要對每個節(jié)點每個peer做超時處理,在erlang中的做法直接是針對每個節(jié)點注冊了一個定時器。這不是問題,問題在于定時器資源就像沒有GC的內(nèi)存資源一樣,是會由于程序員的代碼問題而出現(xiàn)資源泄漏。所以,dhtcrawler第一個版本在節(jié)點數(shù)配置在100以上的情況下,用不了多久就會內(nèi)存耗盡,最終導(dǎo)致erlang虛擬機core dump。
除了這個問題以外,dhtcrawler的資源收錄速度也不是很快。這當(dāng)然跟數(shù)據(jù)庫和獲取種子的速度有直接關(guān)系。尤其是獲取種子,使用的是一些提供info-hash到種子映射的網(wǎng)站,通過HTTP請求來下載種子文件。我以為通過BT協(xié)議直接下載種子會快些,并且實時性也要高很多,因為這個種子可能未被這些緩存網(wǎng)站收錄,但卻可以直接向?qū)Ψ秸埱蟮玫健榇耍疫€特地翻閱了相關(guān)協(xié)議,并且用erlang實現(xiàn)了(以后的文章我會講到具體實現(xiàn)這個協(xié)議)。
后來我懷疑get_peers的數(shù)量會不會比announce_peer多,但是理論上一般的客戶端在get_peers之后都是announce_peer,但是如果get_peers查詢的peers恰好不在線呢?這意味著很多資源雖然已經(jīng)存在,只不過你恰好暫時請求不到。實際測試時,發(fā)現(xiàn)get_peers基本是announce_peer數(shù)量的10倍。
將hash的獲取方式做了調(diào)整后,dhtcrawler在幾分鐘以內(nèi)以幾乎每秒上百個新增種子的速度工作。然后,程序掛掉。
從dhtcrawler到今天為止的dhtcrawler2,中間間隔了剛好1個月。我的所有業(yè)余時間全部撲在這個項目上,面臨的問題一直都是程序的內(nèi)存泄漏、資源收錄的速度不夠快,到后來又變?yōu)閿?shù)據(jù)庫壓力過大。每一天我都以為我將會完成一個穩(wěn)定版本,然后終于可以去干點別的事情,但總是干不完,目前完沒完都還在觀察。我始終明白在做優(yōu)化前需要進行詳盡的數(shù)據(jù)收集和分析,從而真正地優(yōu)化到正確的點上,但也總是憑直覺和少量數(shù)據(jù)分析就開始嘗試。
這里談?wù)動龅降囊恍﹩栴}。
erlang call timeout
最開始遇到erlang中gen_server:call
出現(xiàn)timeout
錯誤時,我還一直以為是進程死鎖了。相關(guān)代碼讀來讀去,實在覺得不可能發(fā)生死鎖。后來發(fā)現(xiàn),當(dāng)erlang虛擬機壓力上去后,例如內(nèi)存太大,但沒大到耗盡系統(tǒng)所有內(nèi)存(耗進所有內(nèi)存基本就core dump了),進程間的調(diào)用就會出現(xiàn)timeout。
當(dāng)然,內(nèi)存占用過大可能只是表象。其進程過多,進程消息隊列太長,也許才是導(dǎo)致出現(xiàn)timeout的根本原因。消息隊列過長,也可能是由于發(fā)生了消息泄漏的緣故。消息泄漏我指的是這樣一種情況,進程自己給自己發(fā)消息(當(dāng)然是cast或info),這個消息被處理時又會發(fā)送相同的消息,正常情況下,gen_server處理了一個該消息,就會從消息隊列里移除它,然后再發(fā)送相同的消息,這不會出問題。但是當(dāng)程序邏輯出問題,每次處理該消息時,都會發(fā)生多余一個的同類消息,那消息隊列自然就會一直增長。
保持進程邏輯簡單,以避免這種邏輯錯誤。
erlang gb_trees
我在不少的地方使用了gb_trees,dht_crawler里就可能出現(xiàn)gb_trees:get(xxx, nil)
這種錯誤。乍一看,我以為我真的傳入了一個nil
值進去。然后我苦看代碼,以為在某個地方我會把這個gb_trees對象改成了nil。但事情不是這樣的,gb_tress使用一個tuple作為tree的節(jié)點,當(dāng)某個節(jié)點沒有子節(jié)點時,就會以nil表示。
gb_trees:get(xxx, nil)
類似的錯誤,實際指的是xxx
沒有在這個gb_trees中找到。
erlang httpc
dht_crawler通過http協(xié)議從torrage.com之類的緩存網(wǎng)站下載種子。最開始我為了盡量少依賴第三方庫,使用的是erlang自帶的httpc。后來發(fā)現(xiàn)程序有內(nèi)存泄漏,google發(fā)現(xiàn)erlang自帶的httpc早為人詬病,當(dāng)然也有大神說在某個版本之后這個httpc已經(jīng)很不錯。為了省事,我直接換了ibrowse,替換之后正常很多。但是由于沒有具體分析測試過,加之時間有點遠了,我也記不太清細節(jié)。因為早期的http請求部分,沒有做數(shù)量限制,也可能是由于我的使用導(dǎo)致的問題。
某個版本后,我才將http部分嚴(yán)格地與hash處理部分區(qū)分開來。相較數(shù)據(jù)庫操作而言,http請求部分慢了若干數(shù)量級。在hash_reader中將這兩塊分開,嚴(yán)格限制了提交給httpc的請求數(shù),以獲得穩(wěn)定性。
對于一個復(fù)雜的網(wǎng)絡(luò)系統(tǒng)而言,分清哪些是耗時的哪些是不大耗時的,才可能獲得性能的提升。對于hash_reader而言,處理一個hash的速度,雖然很大程度取決于數(shù)據(jù)庫,但相較http請求,已經(jīng)快很多。它在處理這些hash時,會將數(shù)據(jù)庫已收錄的資源和待下載的資源分離開,以盡快的速度處理已存在的,而將待下載的處理速度交給httpc的響應(yīng)速度。
erlang httpc ssl
ibrowse處理https請求時,默認和erlang自帶的httpc使用相同的SSL實現(xiàn)。這經(jīng)常導(dǎo)致出現(xiàn)tls_connection
進程掛掉的錯誤,具體原因不明。
erlang調(diào)試
首先合理的日志是任何系統(tǒng)調(diào)試的必備。
我面臨的大部分問題都是內(nèi)存泄漏相關(guān),所以依賴的erlang工具也是和內(nèi)存相關(guān)的:
-
使用etop
,可以檢查內(nèi)存占用多的進程、消息隊列大的進程、CPU消耗多的進程等等:
spawn(fun() -> etop:start([{output, text}, {interval, 10}, {lines, 20}, {sort, msg_q }]) end).
使用erlang:system_info(allocated_areas).
檢查內(nèi)存使用情況,其中會輸出系統(tǒng)timer
數(shù)量
- 使用
erlang:process_info
查看某個具體的進程,這個甚至?xí)敵鱿㈥犃欣锏南?/li>
hash_writer/crawler
crawler本身僅收集hash,然后寫入數(shù)據(jù)庫,所以可以稱crawler為hash_writer。這些hash里存在大量的重復(fù)。hash_reader從數(shù)據(jù)庫里取出這些hash然后做處理。處理過程會首先判定該hash對應(yīng)的資源是否被收錄,沒有收錄就先通過http獲取種子。
在某個版本之后,crawler會簡單地預(yù)先處理這些hash。它緩存一定數(shù)量的hash,接收到新hash時,就合并到hash緩存里,以保證緩存里沒有重復(fù)的hash。這個重復(fù)率經(jīng)過實際數(shù)據(jù)分析,大概是50%左右,即收到的100個請求里,有50個是重復(fù)的。這樣的優(yōu)化,不僅會降低hash數(shù)據(jù)庫的壓力,hash_reader處理的hash數(shù)量少了,也會對torrent數(shù)據(jù)庫有很大提升。
當(dāng)然進一步的方案可以將crawler和hash_reader之間交互的這些hash直接放在內(nèi)存中處理,省去中間數(shù)據(jù)庫。但是由于mongodb大量使用虛擬內(nèi)存的緣故(內(nèi)存映射文件),經(jīng)常導(dǎo)致服務(wù)器內(nèi)存不夠(4G),內(nèi)存也就成了珍稀資源。當(dāng)然這個方案還有個弊端是難以權(quán)衡hash緩存的管理。crawler收到hash是一個不穩(wěn)定的過程,在某些時間點這些hash可能爆多,而hash_reader處理hash的速度也會不太穩(wěn)定,受限于收到的hash類別(是新增資源還是已存在資源)、種子請求速度、是否有效等。
當(dāng)然,也可以限制緩存大小,以及對hash_reader/crawler處理速度建立關(guān)系來解決這些問題。但另一方面,這里的優(yōu)化是否對目前的系統(tǒng)有提升,是否是目前系統(tǒng)面臨的最大問題,卻是需要考究的事情。
cache indexer
dht_crawler是從torrage.com等網(wǎng)站獲取種子文件,這些網(wǎng)站看起來都是使用了相同的接口,其都有一個sync目錄,里面存放了每天每個月索引的種子hash,例如 http://torrage.com/sync/。這個網(wǎng)站上是否有某個hash對應(yīng)的種子,就可以從這些索引中檢查。
hash_reader在處理新資源時,請求種子的過程中發(fā)現(xiàn)大部分在這些服務(wù)器上都沒有找到,也就是發(fā)起的很多http請求都是404回應(yīng),這不但降低了系統(tǒng)的處理能力、帶寬,也降低了索引速度。所以我寫了一個工具,先手工將sync目錄下的所有文件下載到本地,然后通過這個工具 (cache indexer) 將這些索引文件里的hash全部導(dǎo)入數(shù)據(jù)庫。在以后的運行過程中,該工具僅下載當(dāng)天的索引文件,以更新數(shù)據(jù)庫。 hash_reader 根據(jù)配置,會首先檢查某個hash是否存在該數(shù)據(jù)庫中,存在的hash才可能在torrage.com上下載得到。
種子緩存
hash_reader可以通過配置,將下載得到的種子保存在本地文件系統(tǒng)或數(shù)據(jù)庫中。這可以建立自己的種子緩存,但保存在數(shù)據(jù)庫中會對數(shù)據(jù)庫造成壓力,尤其在當(dāng)前測試服務(wù)器硬件環(huán)境下;而保存為本地文件,又特別占用硬盤空間。
基于BT協(xié)議的種子下載
通過http從種子緩存里取種子文件,可能會沒有直接從P2P網(wǎng)絡(luò)里取更實時。目前還沒來得及查看這些種子緩存網(wǎng)站的實現(xiàn)原理。但是通過BT協(xié)議獲取種子會有點麻煩,因為dht_crawler是根據(jù)get_peer
請求索引資源的,所以如果要通過BT協(xié)議取種子,那么這里還得去DHT網(wǎng)絡(luò)里查詢該種子,這個查詢過程可能會較長,相比之下會沒有http下載快。而如果通過announce_peer
來索引新資源的話,其索引速度會大大降低,因為announce_peer
請求比get_peer
請求少很多,幾乎10倍。
所以,這里的方案可能會結(jié)合兩者,新開一個服務(wù),建立自己的種子緩存。
中文分詞
mongodb的全文索引是不支持中文的。我在之前提到,為了支持搜索中文,我將字符串拆成了若干子串。這樣的后果就是字符串索引會稍稍偏大,而且目前這一塊的代碼還特別簡單,會將很多非文字字符也算在內(nèi)。后來我加了個中文分詞庫,使用的是rmmseg-cpp。我將其C++部分抽離出來編譯成erlang nif,這可以在我的github上找到。
但是這個庫拆分中文句子依賴于詞庫,而這個詞庫不太新,dhtcrawler爬到的大部分資源類型你們也懂,那些詞匯拆出來的比率不太高,這會導(dǎo)致搜索出來的結(jié)果沒你想的那么直白。當(dāng)然更新詞庫應(yīng)該是可以解決這個問題的,目前還沒有時間顧這一塊。
總結(jié)
一個老外對我說過,”i have 2 children to feed, so i will not do this only for fun”。
你的大部分編程知識來源于網(wǎng)絡(luò),所以稍稍回饋一下不會讓你丟了飯碗。
我很窮,如果你能讓我收獲金錢和編程成就,還不會嫌我穿得太邋遢,that’s really kind of you。