題目與適用情境
設計一個為搜尋索引提供 HTML 的大規模網頁爬蟲。系統維護 100 億個已知 URL,每天最多發起 10 億次抓取;請設計 URL frontier、主機層級禮貌排程、去重、重新爬取、故障復原與驗證方案,並提出容量估算。
本題適用於後端、基礎設施、搜尋和資料平台的資深工程師系統設計面試。抓取結果包含壓縮後的原始 HTML、抓取中繼資料和新發現連結;全文索引、搜尋排序、登入後頁面、圖片影片和預設的 JavaScript 轉譯不在本輪範圍內。系統採盡力覆蓋,不承諾走訪整個網際網路。
以下數字都是方便推導的面試假設,不代表線上觀測:平均成功回應內容為 200 KB;每日抓取上限包含成功、304、失敗和重試;平均負載依每日上限計算,尖峰按每秒 25,000 次規劃。新發現 URL 在 60 秒內持久化,容量充足時,99% 已到期的高優先 URL 在 10 分鐘內取得租約。主機禮貌規則是硬性約束,不能為了追趕吞吐量而突破。
公開面試經驗記錄過「爬蟲程式已經存在,重點在設計可擴充架構」的 25 分鐘設計題;2026 年公開系統設計教材也持續把分散式爬蟲列為獨立練習。單一記錄無法證明公司固定題庫或出現頻率,因此本文把它視為代表性系統設計題,不標示公司歸屬。
面試官評估重點
第一項是範圍和估算。強回答會先定義抓取物件、更新目標、下游和失敗語意,再計算吞吐量與儲存量。直接畫訊息佇列、爬蟲節點和資料庫,無法說明這些元件為何需要存在。
第二項是 URL frontier 的排程不變量。單一全域 FIFO 可以平行領取工作,卻無法保證同一主機的併發數、間隔和退避。強回答會把「選哪個主機」和「選該主機的哪個 URL」拆成兩層,並讓相同 host_key 的權杖狀態只有一個邏輯擁有者。
第三項是區分三種重複:相同正規化 URL、不同 URL 回傳完全相同內容、內容只有少量差異。它們分別需要精確 URL 狀態、內容雜湊和近重複指紋,不能用一個 Bloom filter 全部處理。
最後看故障模型。抓取外部網路必然遇到逾時、429、5xx、DNS 錯誤、重新導向迴圈、超大回應和惡意頁面。優秀回答會接受至少一次執行,用租約、版本條件寫入和冪等提交控制重複副作用,並提出能證偽設計的測試與指標。
回答前需要釐清的問題
- 下游用途是什麼? 搜尋索引需要保存 HTML、抓取時間、狀態碼和正規 URL;封存系統還會要求不可變版本;訓練語料則會強化內容品質與授權過濾。本題只把結果交給搜尋索引。
- 抓取範圍包含什麼? 本題抓取公開 HTTP/HTTPS HTML。加入 PDF、媒體、登入狀態或 JavaScript 轉譯會改變下載器、解析器、成本和安全隔離。
- 覆蓋率與新鮮度如何取捨? 本題維護 100 億個已知 URL:1 億個高價值 URL 每天更新,其餘 99 億個目標週期為 30 天。若要求所有頁面每天更新,10 億次/日的預算在數學上無法成立。
- 禮貌策略以什麼邊界執行? 這裡以
scheme + authority形成host_key,集中執行 robots 策略、併發數、最小請求間隔和伺服器端退避。若業務與網站方約定專用額度,只調整該主機策略,不放寬全域不變量。 - 「不重複」要求多嚴格? URL 發現不能因 Bloom filter 假陽性而無聲遺失,所以持久化唯一鍵是事實來源;網路抓取允許重複執行,寫入和下游事件必須冪等。
- 刪除和失敗頁面保留多久?
404、410、連續失敗和暫時性5xx的重新抓取週期不同。這裡保留墓碑與最近狀態,避免每次重新發現都當成新 URL。
30 秒回答架構
「我先依 10 億次/日算出平均約每秒 11,600 次、尖峰每秒 25,000 次,並把新鮮度拆成 1 億個 URL 每日更新、99 億個 URL 30 天更新。架構上,發現服務做保守正規化並以唯一鍵去重;frontier 依主機分片,每個分片先選 nextallowedat 已到期的主機,再從該主機佇列取最高優先 URL,讓 robots、併發和退避只有一個擁有者。抓取器用租約執行條件式請求,HTML 進物件儲存,解析器擷取連結並回灌發現服務。系統採至少一次執行,以 URL 版本和冪等提交吸收重複。我會重點驗證同主機限速、租約到期、429/503、robots 無法連線、重新導向迴圈和爬蟲陷阱。」
分步深入解答
第一步:用預算證明目標可以同時滿足
10 億次/日除以 86,400 秒,平均約為每秒 11,574 次;考量流量波動和補償工作,將尖峰取整為每秒 25,000 次。若每次都回傳 200 KB 回應內容,網路入口上限約為 200 TB/日,平均約 2.31 GB/秒。304 Not Modified 沒有回應內容,真實入口應低於這個保守上限,仍須用壓力測試和線上分布校準。
更新計畫每天需要:
100,000,000 + 9,900,000,000 / 30 = 430,000,000 次
剩餘約 5.7 億次預算用於新發現頁面、失敗重試和變化頻繁頁面。100 億筆 URL 狀態若每筆依 200 位元組原始邏輯資料估算,約為 2 TB;副本、索引、LSM 放大和物件儲存不包含在內。這個數量級表示中繼資料需要水平分片,HTML 應放物件儲存,不能把 HTML 內容塞進 frontier。
第二步:建立分階段資料流
完整流程是:種子與 Sitemap → URL 發現/正規化 → 精確 seen 狀態 → URL 中繼資料 → frontier 排程器 → robots 與主機禮貌檢查 → DNS/HTTP 抓取器 → HTML 物件儲存 → 解析器 → 新連結回灌。解析結果和抓取完成事件再交給搜尋索引與重新抓取計算器。
Sitemap 是補充種子的輸入,不是覆蓋承諾。一個 Sitemap 檔案最多 50,000 個 URL、解壓縮後最多 50 MB;大型網站使用 Sitemap index 分片。連結發現、Sitemap 和人工種子最後都走同一個去重入口,避免三套狀態互相衝突。
拆開抓取與解析有兩個直接收益:外部網路緩慢時不會占住解析 CPU;解析器當機時可以從已保存的 HTML 重播,不必再次造訪網站。每個階段都要有有界佇列和背壓,避免下載速度短時間高於解析或儲存能力時耗盡記憶體。
第三步:讓 URL frontier 以主機禮貌為排程核心
frontier 採兩層佇列。上層保存主機的 nextallowedat 和優先序,只選擇已到可抓取時間且未退避的主機;下層是每個主機自己的 URL 優先佇列,排序鍵可以包含業務價值、到期時間、連結深度和歷史變化率。領取一個 URL 時,排程器以原子操作更新該主機的 in_flight 與下次可用時間。
透過 hash(host_key) 將主機固定到一個排程分片。相同主機即使有百萬個 URL,也由一個邏輯擁有者發放權杖;抓取工作可以分散到多台機器。熱門主機可以有多條併發連線,額度仍由相同主機狀態控制。增加 worker 只能提高跨主機平行度,無法合法突破單一主機配額。
robots.txt 位於服務頂層的 /robots.txt。成功取得時遵守可解析規則;400–499 表示不可用時,協定允許存取;網路錯誤或 500–599 導致無法連線時,依完全禁止處理。快取通常不應使用超過 24 小時,除非檔案無法連線。預設「每主機一個併發、請求間隔一秒」只是本題的可設定初值,協定沒有給出統一速率。收到 429 或 503 時讀取 Retry-After,缺少時使用帶抖動的指數退避並降低主機額度。
第四步:分開處理 URL 去重和內容去重
正規化只做語意安全的轉換:解析相對位址、移除 fragment、統一 scheme 與主機名稱大小寫、處理預設連接埠和路徑點段。不能全域刪除查詢參數或將參數排序;有些網站把參數順序和重複參數視為業務語意。頁面宣告的 canonical URL 可以參與評分和分群,不能直接覆蓋事實 URL。
canonical_url 或其無碰撞歧義的唯一鍵寫入分片中繼資料儲存。Bloom filter 只做負向加速:判斷「不存在」時直接嘗試插入,判斷「可能存在」時仍查持久化唯一鍵。假陽性因此只增加一次讀取,不會漏抓頁面。雜湊碰撞時比較完整 URL 或第二指紋。
抓取後再計算內容雜湊。完全相同內容可以共用物件並保留各自 URL 中繼資料;近重複頁面使用 SimHash 一類指紋形成相似群組。研究顯示這類指紋適合十億級網頁的近重複辨識,但近重複訊號較適合作為儲存、索引或重新抓取優先序輸入。直接丟棄頁面可能連同它獨有的外連一起遺失。
第五步:用版本化狀態和租約復原故障
核心記錄可以維持精簡:
UrlState( urlid, canonicalurl, hostkey, stateversion, lastfetchat, nextfetchat, priority, etag, lastmodified, contenthash, failure_count )
HostState( hostkey, robotspolicy, robotsexpiresat, nextallowedat, inflight, backoffuntil, policy_version )
FetchLease(leaseid, urlid, urlversion, expiresat, attempt)
排程器發放帶 urlversion 的有限期租約。抓取器可能在寫完 HTML 後、確認工作前當機,租約到期會觸發重複抓取。完成操作使用 (urlid, url_version) 條件寫入;舊租約或重複確認只回傳既有結果,不再次發布索引事件。新一輪重新抓取先遞增版本,因此不會被上一輪冪等鍵誤判。
已保存 ETag 時傳送 If-None-Match,否則可以用 Last-Modified 傳送 If-Modified-Since。伺服器回傳 304 時更新抓取時間和下次計畫,不寫入空內容。DNS 逾時、連線失敗和 5xx 進入有上限的重試;永久 404/410 寫入墓碑並顯著延長週期;重新導向限制跳數並偵測迴圈。
第六步:讓重新抓取、陷阱防護和安全共用同一預算
重新抓取優先序由頁面價值、最近變化間隔、狀態碼和網站額度共同決定。每次內容變化縮短週期,連續未變化則逐步拉長,並在 1 天到 30 天之間限幅。這項規則能把預算從穩定頁面移到變動頁面,同時保留最低更新保證。
只限制連結深度擋不住日曆頁、篩選組合和無限查詢參數。還需要每主機每日預算、URL 樣板成長率、查詢參數數量、重複路徑段、最大回應內容、解壓縮後大小、解析時間和重新導向跳數限制。觸發預算時暫停對應模式並保留抽樣,不影響其他主機。
抓取器面對不可信輸入。DNS 解析結果要阻擋回環、私有網路、鏈路本地和雲端中繼資料位址,並在連線前複核,以降低 SSRF 與 DNS rebinding 風險;解析器設定記憶體/CPU 限額並隔離壓縮炸彈和畸形 HTML。robots 規則控制抓取偏好,不負責存取授權。
第七步:用不變量和故障注入驗證設計
先做確定性排程模擬:為三個主機設定不同速率、robots 規則和 Retry-After,推進虛擬時鐘,斷言任意時間窗都不超過配額,禁止路徑永遠不發租約。接著注入「HTTP 成功後行程當機」「租約確認遺失」「robots 快取過期」「DNS 回傳私網位址」「解析佇列停擺」,確認工作可復原、索引事件不重複、抓取階段會產生背壓。
容量測試至少覆蓋每秒 25,000 次租約發放、100 億 URL 鍵空間的分片傾斜和單一熱門主機百萬個待辦。關鍵指標包括:eligible lag、抓取次數與位元組數、2xx/304/429/5xx 比例、每主機策略違規數、租約重試率、URL/內容重複率、robots 快取年齡、解析積壓和預算觸發率。每主機策略違規數必須為零;平均吞吐量達標但出現違規仍算失敗。
替代方案與適用邊界
每天幾百萬次、只抓自有網站時,可以用關聯式資料庫的 nextfetchat 索引搭配 SKIP LOCKED 領取工作,主機權杖也放在同一交易中,部署和除錯更簡單。規模升到每天十億次後,全域索引掃描、熱門更新和清理成本會成為瓶頸,兩層分片 frontier 更合適。
Bloom filter 若直接代表 seen 集合,可以省下讀取,但假陽性會永久犧牲覆蓋率。本題把它降為快取,把持久化唯一鍵保留為事實來源。只有在業務明確接受少量漏抓,並能量化假陽性預算時,才考慮省略二次確認。
高品質示範回答
「我會先鎖定預算與禮貌兩個不變量。10 億次/日約等於平均每秒 11,600 次、尖峰每秒 25,000 次;1 億個 URL 每日更新加 99 億個 URL 每 30 天更新,一天計畫量約 4.3 億次,剩餘預算容納發現、重試和變化驅動更新。依 200 KB 回應內容算出的 200 TB/日是保守網路上限,304 會降低真實流量。
URL 進入系統時只做安全正規化,再用持久化唯一鍵確認 seen;Bloom filter 只減少明顯未見 URL 的讀取。frontier 依 scheme + authority 分片,每個分片維護主機就緒時間和主機內 URL 優先佇列。領取時原子消耗主機權杖,因此多台抓取器不會共同壓垮一個網站。robots 無法連線時暫停該主機,429/503 依 Retry-After 或抖動退避。
抓取器取得有限期租約,執行條件式 GET,把 HTML 寫入物件儲存並把解析工作交給下一階段。解析器擷取連結後回到同一發現入口。系統允許租約到期造成重複請求,完成寫入由 URL 版本保護,舊租約不能覆蓋新狀態或重複發出索引事件。內容雜湊合併完全重複物件,SimHash 只影響近重複優先序,避免漏掉獨有連結。
我會用虛擬時鐘證明同主機請求間隔,用故障注入覆蓋寫完後當機、確認遺失、robots 過期、DNS rebinding 和解析背壓。最終驗收既看每秒 25,000 次尖峰,也要求主機策略違規始終為零。」
常見錯誤
- 錯誤表現:只放一個全域訊息佇列。 失敗原因:多個消費者無法共同判斷同一主機的下次可用時間,吞吐量越高越容易越過禮貌限制。修正方法:依
host_key固定排程所有權,使用主機就緒佇列加主機內 URL 佇列。 - 錯誤表現:用 Bloom filter 作為唯一的已見集合。 失敗原因:假陽性會讓從未抓取的 URL 永久消失,且無法保存狀態碼、版本與重新抓取時間。修正方法:Bloom filter 只作快取,持久化唯一鍵保存事實狀態。
- 錯誤表現:把 URL 去重等同於內容去重。 失敗原因:不同 URL 可以回傳相同內容,同一 URL 也會隨時間變化。修正方法:發現階段做精確 URL 去重,抓取後另算內容雜湊和近重複指紋。
- 錯誤表現:宣稱 exactly-once 抓取。 失敗原因:外部 HTTP 成功與內部確認無法組成原子交易,當機窗口仍會產生重複請求。修正方法:接受至少一次執行,用租約、版本條件寫入和冪等事件控制內部副作用。
- 錯誤表現:遇到所有 robots 錯誤都繼續抓。 失敗原因:協定區分不可用與無法連線;網路錯誤或
5xx時需要依完全禁止處理。修正方法:實作明確狀態機,並分別測試快取年齡、重新導向和失敗類別。 - 錯誤表現:只用最大深度防範爬蟲陷阱。 失敗原因:同一深度就能產生無限篩選、日曆和查詢參數組合。修正方法:疊加主機預算、URL 模式成長、參數數量、回應大小和解析時間限制。
追問與應對
如果 50% 的待抓 URL 都來自一個主機,怎麼擴充?
先確認網站允許的併發與速率。主機額度固定時,增加 worker 不會增加該主機的合法吞吐量,只能提高其他主機平行度。可以把該主機 URL 佇列分片以降低儲存熱點,但所有分片仍向同一個邏輯權杖服務申請額度。若業務必須更快,需要與網站方約定專用 feed 或更高額度,並把新政策版本化。
為什麼不追求 exactly-once?
HTTP 回應已經送達而抓取器尚未確認租約時可能當機,外部網站不會參與內部交易。為消除這個窗口引入分散式交易,也無法回復已發生的 GET。可執行目標是至少一次領取、可能重複抓取、內部冪等提交;監控重複率並用條件式請求降低代價。
頁面必須執行 JavaScript 才有內容怎麼辦?
一般 HTTP 抓取仍作為第一層。只有解析結果為空、網站策略允許且頁面價值達到門檻時,才進入獨立轉譯佇列。轉譯器有更低併發、更嚴格的 CPU/記憶體/時間預算,並共用原主機權杖;否則昂貴轉譯會繞過禮貌控制並耗盡總預算。
如何跨地域部署又不對同一主機重複施壓?
依 host_key 指定 home region,只有該地域發放主機權杖;其他地域可以解析和儲存。地域故障時透過帶 fencing token 的租約轉移所有權,舊地域恢復後必須持有新 epoch 才能繼續發放。多地域同時主動排程同一主機會破壞禮貌不變量。
儲存成本突然超過預算時先刪減什麼?
先提高條件式請求命中率、壓縮與完全重複物件共用,再依內容價值縮短原始 HTML 保留期;URL 中繼資料和抓取稽核不能一起刪除,否則重新抓取、去重和合規調查會失去依據。近重複只用於降低優先序或分層儲存,不能未經驗證直接批次刪除。