【譯】系統設計入門之面試題解答 —— 設計一個網頁爬蟲

吃土小2叉發表於2017-08-11

設計一個網頁爬蟲

注意:這個文件中的連結會直接指向系統設計主題索引中的有關部分,以避免重複的內容。你可以參考連結的相關內容,來了解其總的要點、方案的權衡取捨以及可選的替代方案。

第一步:簡述用例與約束條件

把所有需要的東西聚集在一起,審視問題。不停的提問,以至於我們可以明確使用場景和約束。討論假設。

我們將在沒有面試官明確說明問題的情況下,自己定義一些用例以及限制條件。

用例

我們把問題限定在僅處理以下用例的範圍中

  • 服務 抓取一系列連結:
    • 生成包含搜尋詞的網頁倒排索引
    • 生成頁面的標題和摘要資訊
      • 頁面標題和摘要都是靜態的,它們不會根據搜尋詞改變
  • 使用者 輸入搜尋詞後,可以看到相關的搜尋結果列表,列表每一項都包含由網頁爬蟲生成的頁面標題及摘要
    • 只給該用例繪製出概要元件和互動說明,無需討論細節
  • 服務 具有高可用性

無需考慮

  • 搜尋分析
  • 個性化搜尋結果
  • 頁面排名

限制條件與假設

提出假設

  • 搜尋流量分佈不均
    • 有些搜尋詞非常熱門,有些則非常冷門
  • 只支援匿名使用者
  • 使用者很快就能看到搜尋結果
  • 網頁爬蟲不應該陷入死迴圈
    • 當爬蟲路徑包含環的時候,將會陷入死迴圈
  • 抓取 10 億個連結
    • 要定期重新抓取頁面以確保新鮮度
    • 平均每週重新抓取一次,網站越熱門,那麼重新抓取的頻率越高
      • 每月抓取 40 億個連結
    • 每個頁面的平均儲存大小:500 KB
      • 簡單起見,重新抓取的頁面算作新頁面
  • 每月搜尋量 1000 億次

用更傳統的系統來練習 —— 不要使用 solrnutch 之類的現成系統。

計算用量

如果你需要進行粗略的用量計算,請向你的面試官說明。

  • 每月儲存 2 PB 頁面
    • 每月抓取 40 億個頁面,每個頁面 500 KB
    • 三年儲存 72 PB 頁面
  • 每秒 1600 次寫請求
  • 每秒 40000 次搜尋請求

簡便換算指南:

  • 一個月有 250 萬秒
  • 每秒 1 個請求,即每月 250 萬個請求
  • 每秒 40 個請求,即每月 1 億個請求
  • 每秒 400 個請求,即每月 10 億個請求

第二步: 概要設計

列出所有重要元件以規劃概要設計。

Imgur
Imgur

第三步:設計核心元件

對每一個核心元件進行詳細深入的分析。

用例:爬蟲服務抓取一系列網頁

假設我們有一個初始列表 links_to_crawl(待抓取連結),它最初基於網站整體的知名度來排序。當然如果這個假設不合理,我們可以使用 YahooDMOZ 等知名入口網站作為種子連結來進行擴散 。

我們將用表 crawled_links (已抓取連結 )來記錄已經處理過的連結以及相應的頁面簽名。

我們可以將 links_to_crawlcrawled_links 記錄在鍵-值型 NoSQL 資料庫中。對於 crawled_links 中已排序的連結,我們可以使用 Redis 的有序集合來維護網頁連結的排名。我們應當在 選擇 SQL 還是 NoSQL 的問題上,討論有關使用場景以及利弊

  • 爬蟲服務按照以下流程迴圈處理每一個頁面連結:
    • 選取排名最靠前的待抓取連結
      • NoSQL 資料庫crawled_links 中,檢查待抓取頁面的簽名是否與某個已抓取頁面的簽名相似
        • 若存在,則降低該頁面連結的優先順序
          • 這樣做可以避免陷入死迴圈
          • 繼續(進入下一次迴圈)
        • 若不存在,則抓取該連結
          • 倒排索引服務任務佇列中,新增一個生成倒排索引任務。
          • 文件服務任務佇列中,新增一個生成靜態標題和摘要的任務。
          • 生成頁面簽名
          • NoSQL 資料庫links_to_crawl 中刪除該連結
          • NoSQL 資料庫crawled_links 中插入該連結以及頁面簽名

向面試官瞭解你需要寫多少程式碼

PagesDataStore爬蟲服務中的一個抽象類,它使用 NoSQL 資料庫進行儲存。

class PagesDataStore(object):

    def __init__(self, db);
        self.db = db
        ...

    def add_link_to_crawl(self, url):
        """將指定連結加入 `links_to_crawl`。"""
        ...

    def remove_link_to_crawl(self, url):
        """從 `links_to_crawl` 中刪除指定連結。"""
        ...

    def reduce_priority_link_to_crawl(self, url)
        """在 `links_to_crawl` 中降低一個連結的優先順序以避免死迴圈。"""
        ...

    def extract_max_priority_page(self):
        """返回 `links_to_crawl` 中優先順序最高的連結。"""
        ...

    def insert_crawled_link(self, url, signature):
        """將指定連結加入 `crawled_links`。"""
        ...

    def crawled_similar(self, signature):
        """判斷待抓取頁面的簽名是否與某個已抓取頁面的簽名相似。"""
        ...複製程式碼

Page爬蟲服務的一個抽象類,它封裝了網頁物件,由頁面連結、頁面內容、子連結和頁面簽名構成。

class Page(object):

    def __init__(self, url, contents, child_urls, signature):
        self.url = url
        self.contents = contents
        self.child_urls = child_urls
        self.signature = signature複製程式碼

Crawler爬蟲服務的主類,由PagePagesDataStore 組成。

class Crawler(object):

    def __init__(self, data_store, reverse_index_queue, doc_index_queue):
        self.data_store = data_store
        self.reverse_index_queue = reverse_index_queue
        self.doc_index_queue = doc_index_queue

    def create_signature(self, page):
        """基於頁面連結與內容生成簽名。"""
        ...

    def crawl_page(self, page):
        for url in page.child_urls:
            self.data_store.add_link_to_crawl(url)
        page.signature = self.create_signature(page)
        self.data_store.remove_link_to_crawl(page.url)
        self.data_store.insert_crawled_link(page.url, page.signature)

    def crawl(self):
        while True:
            page = self.data_store.extract_max_priority_page()
            if page is None:
                break
            if self.data_store.crawled_similar(page.signature):
                self.data_store.reduce_priority_link_to_crawl(page.url)
            else:
                self.crawl_page(page)複製程式碼

處理重複內容

我們要謹防網頁爬蟲陷入死迴圈,這通常會發生在爬蟲路徑中存在環的情況。

向面試官瞭解你需要寫多少程式碼.

刪除重複連結:

  • 假設資料量較小,我們可以用類似於 sort | unique 的方法。(譯註: 先排序,後去重)
  • 假設有 10 億條資料,我們應該使用 MapReduce 來輸出只出現 1 次的記錄。
class RemoveDuplicateUrls(MRJob):

    def mapper(self, _, line):
        yield line, 1

    def reducer(self, key, values):
        total = sum(values)
        if total == 1:
            yield key, total複製程式碼

比起處理重複內容,檢測重複內容更為複雜。我們可以基於網頁內容生成簽名,然後對比兩者簽名的相似度。可能會用到的演算法有 Jaccard index 以及 cosine similarity

抓取結果更新策略

要定期重新抓取頁面以確保新鮮度。抓取結果應該有個 timestamp 欄位記錄上一次頁面抓取時間。每隔一段時間,比如說 1 周,所有頁面都需要更新一次。對於熱門網站或是內容頻繁更新的網站,爬蟲抓取間隔可以縮短。

儘管我們不會深入網頁資料分析的細節,我們仍然要做一些資料探勘工作來確定一個頁面的平均更新時間,並且根據相關的統計資料來決定爬蟲的重新抓取頻率。

當然我們也應該根據站長提供的 Robots.txt 來控制爬蟲的抓取頻率。

用例:使用者輸入搜尋詞後,可以看到相關的搜尋結果列表,列表每一項都包含由網頁爬蟲生成的頁面標題及摘要

  • 客戶端向執行反向代理Web 伺服器傳送一個請求
  • Web 伺服器 傳送請求到 Query API 伺服器
  • 查詢 API 服務將會做這些事情:
    • 解析查詢引數
      • 刪除 HTML 標記
      • 將文字分割成片語 (譯註: 分詞處理)
      • 修正錯別字
      • 規範化大小寫
      • 將搜尋詞轉換為布林運算
    • 使用倒排索引服務來查詢匹配查詢的文件
      • 倒排索引服務對匹配到的結果進行排名,然後返回最符合的結果
    • 使用文件服務返回文章標題與摘要

我們使用 REST API 與客戶端通訊:

$ curl https://search.com/api/v1/search?query=hello+world複製程式碼

響應內容:

{
    "title": "foo's title",
    "snippet": "foo's snippet",
    "link": "https://foo.com",
},
{
    "title": "bar's title",
    "snippet": "bar's snippet",
    "link": "https://bar.com",
},
{
    "title": "baz's title",
    "snippet": "baz's snippet",
    "link": "https://baz.com",
},複製程式碼

對於伺服器內部通訊,我們可以使用 遠端過程呼叫協議(RPC)

第四步:架構擴充套件

根據限制條件,找到並解決瓶頸。

Imgur
Imgur

重要提示:不要直接從最初設計跳到最終設計!

現在你要 1) 基準測試、負載測試。2) 分析、描述效能瓶頸。3) 在解決瓶頸問題的同時,評估替代方案、權衡利弊。4) 重複以上步驟。請閱讀設計一個系統,並將其擴大到為數以百萬計的 AWS 使用者服務 來了解如何逐步擴大初始設計。

討論初始設計可能遇到的瓶頸及相關解決方案是很重要的。例如加上一套配備多臺 Web 伺服器負載均衡器是否能夠解決問題?CDN呢?主從複製呢?它們各自的替代方案和需要權衡的利弊又有哪些呢?

我們將會介紹一些元件來完成設計,並解決架構規模擴張問題。內建的負載均衡器將不做討論以節省篇幅。

為了避免重複討論,請參考系統設計主題索引相關部分來了解其要點、方案的權衡取捨以及替代方案。

有些搜尋詞非常熱門,有些則非常冷門。熱門的搜尋詞可以通過諸如 Redis 或者 Memcached 之類的記憶體快取來縮短響應時間,避免倒排索引服務以及文件服務過載。記憶體快取同樣適用於流量分佈不均勻以及流量短時高峰問題。從記憶體中讀取 1 MB 連續資料大約需要 250 微秒,而從 SSD 讀取同樣大小的資料要花費 4 倍的時間,從機械硬碟讀取需要花費 80 倍以上的時間。1

以下是優化爬蟲服務的其他建議:

  • 為了處理資料大小問題以及網路請求負載,倒排索引服務文件服務可能需要大量應用資料分片和資料複製。
  • DNS 查詢可能會成為瓶頸,爬蟲服務最好專門維護一套定期更新的 DNS 查詢服務。
  • 藉助於連線池,即同時維持多個開放網路連線,可以提升爬蟲服務的效能並減少記憶體使用量。
    • 改用 UDP 協議同樣可以提升效能
  • 網路爬蟲受頻寬影響較大,請確保頻寬足夠維持高吞吐量。

其它要點

是否深入這些額外的主題,取決於你的問題範圍和剩下的時間。

SQL 擴充套件模式

NoSQL

快取

非同步與微服務

通訊

安全性

請參閱安全

延遲數值

請參閱每個程式設計師都應該知道的延遲數

持續探討

  • 持續進行基準測試並監控你的系統,以解決他們提出的瓶頸問題。
  • 架構擴充套件是一個迭代的過程。

掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃

相關文章