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

設計一個網頁爬蟲

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

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

把全部須要的東西彙集在一塊兒,審視問題。不停的提問,以致於咱們能夠明確使用場景和約束。討論假設。react

咱們將在沒有面試官明確說明問題的狀況下,本身定義一些用例以及限制條件。android

用例

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

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

無需考慮

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

限制條件與假設

提出假設

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

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

計算用量

若是你須要進行粗略的用量計算,請向你的面試官說明。git

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

簡便換算指南:程序員

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

第二步: 概要設計

列出全部重要組件以規劃概要設計。 github

Imgur
Imgur

第三步:設計核心組件

對每個核心組件進行詳細深刻的分析。web

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

假設咱們有一個初始列表 links_to_crawl(待抓取連接),它最初基於網站總體的知名度來排序。固然若是這個假設不合理,咱們可使用 YahooDMOZ 等知名門戶網站做爲種子連接來進行擴散 。 面試

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

咱們能夠將 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前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃

相關文章
相關標籤/搜索