9月14日
我們為這一天等了好幾個禮拜。我們的住宅代理伺服器供應商早在7月就因為帳單問題而失聯,接下來兩個月裡,我們1,375個資料來源中有愈來愈多的比例是以降級方式在爬取,甚至完全爬不到——抓取逾時、連線中途被封鎖IP,就是那種不會表現成明顯當機、而是慢慢流失到一個逐漸縮水的數字、直到難堪到讓人注意為止的狀況。我們把帳續上了。新的池子,20,000條輪替式住宅連線,每一條出口在進正式環境前都先測過。100%的探測全部通過。阿聯、德國、日本——全部上線,全部乾淨。
於是我們進行了一次有人監看的爬取——也就是真的有人盯著儀表板,而不是只信任排程任務。我原本預期的是跟災難相反的結果。
結果我們得到的是一個資料庫,在一次爬取中就少了84,401筆資料,而且有102個資料來源比前一次快照掉了超過25%。這比我們剛修好的那兩個月代理伺服器斷炊問題還嚴重。
錯誤的理論,簡述
我的第一個直覺——這個我要老實承認——是新的代理伺服器池其實比探測結果顯示的更差。也許連線用的IP在爬取過程中被漸進式限速,而一次性的探測根本測不出來。這是個合理的理論。但它也是錯的,而光是相信它半天,就讓我們付出了負擔不起的時間成本,因為在你還在為這種理論除錯的時候,爬取排程任務必須全程暫停——你不會想讓一次失敗的爬取覆蓋掉一份好的快照,而你還在瞎猜。
打破這個理論的關鍵線索是dubizzle。這是我們在阿聯酋量最大的資料來源之一,在那次爬取中它其實抓到了32,595筆刊登——這是個相當健康的數字,代理伺服器顯然運作正常——然後幾乎把全部都丟掉了。不是抓取失敗。是抓到了,然後被扔掉。
9月16日:找到真正的錯誤
就是在這時候,它不再是個代理伺服器的故事,而變成了一個「資料被覆蓋取代」的故事。我們的持久化層每一輪都會把每個資料來源的快照完整覆寫一次<site>_cars_for_sale.csv——只保留目前的刊登資訊,整張表全部取代。當一次爬取順利完成時,這是正確的行為。但當爬取進行到一半逾時、而爬蟲把它抓到的那一小批部分資料「當作完整快照」寫進去時,這就是場災難。一個正常應該有9,000筆活躍刊登的資料來源,會被它逾時前抓到的那1,200筆覆蓋掉,剩下的7,800筆就這樣消失了——不是因為那些車賣掉了,而是因為我們自己的程式碼判定了一次不完整的抓取是完整的。
代理伺服器沒有騙我們。問題出在重試邏輯,它早已悄悄地出問題很久了,只是被一個比較健康的代理伺服器池掩蓋住了——那個池子過去總能在重試機制真正發揮作用之前就完成抓取。
等我們把算術算出來後,才發現這套重試機制實際造成的代價有多大:
| 舊版重試機制 | 修正後的機制 | |
|---|---|---|
| 每個代理伺服器重試次數 | 3次 | 1次 |
| 每次嘗試間的延遲 | 1秒 | — |
| 50個代理伺服器池的有效上限 | 每個網址約56分鐘 | 總預算120秒 |
| 逾時時的行為 | 把部分批次當作完整快照寫入 | 跳過該資料來源,保留先前快照 |
為了放棄抓取單一網址,居然要耗上56分鐘。把這個數字乘上一次要處理超過一千個資料來源的爬取規模,你得到的正好就是我們看到的那種無聲、分布不均的失敗——有些資料來源完全沒事,102個被打得七零八落,但日誌裡卻沒有任何東西在喊「代理伺服器斷線」——因為嚴格來說,根本沒有斷線。
9月25日:修正方案,以及對你真正重要的部分
九天後上線的修補做了兩件不起眼的事。它為重試的運算設了上限,讓一次卡住的抓取能快速失敗,而不是耗掉將近一個小時;另外——這一點才是真正防止資料損失的關鍵——部分抓取的結果不再取代完整的資料集。如果一個資料來源只抓到平常9,000筆刊登裡的1,200筆,我們會保留先前那份完整的快照,而不是用一小部分去覆蓋它。同一天還上線了第二項變更:寫入一個可見的CRAWL_PAUSED旗標,讓我們的監控系統在對任何人發出資料下滑警報前先檢查這個旗標——這樣一次刻意的暫停讀起來就是暫停,而不是警報。
為什麼一個正在找中古現代車的讀者該在乎這些事?因為我們公布的低於市場行情標籤——截至本週已有12,823個在網站上即時顯示——只有在我們拿來比較的那個群組是真實、完整的區域市場、而不是被逾時截斷的片段時,才算誠實。一個建立在丟失了四分之三比較樣本的快照上的優惠標籤,根本不是優惠標籤,只是一個貼著綠色標籤的噪音。我們寧願讓一次爬取暫停九天,也不願發布那樣的東西。
今天的資料集涵蓋73個地區、1,375個資料來源,共計1,108,535筆刊登——比這一切開始之前還要大,代理伺服器已經更新,資料覆蓋的錯誤也已經修復。但我一直反覆想起的那個數字,不是總數。是84,401——那是一次在代理伺服器層面看起來健康的爬取,和一次真正健康的爬取之間的差距。



