Webクローラーの設計
数十億ページを礼儀正しく集める設計を、URLフロンティア・重複排除・分散協調まで一気につかめます。面接で問われる規模見積もりとトレードオフの答え方も身につきます。
- コアはURLフロンティアを中心にしたBFS。取得→パース→リンク抽出→重複排除→再投入のループを、ダウンローダ・パーサ・フロンティアに分けて分散させる。
- 重複はブルームフィルタでURL訪問済みを省メモリ判定し、コンテンツはSimHashで近似重複を検出。礼儀正しさはドメイン別キューとレート制御でrobots.txtとサーバー負荷を守る。
- 10億ページ/月なら約400 QPS・数十TB級ストレージ。ボトルネックはDNSとI/O待ちで、非同期取得・クロール遅延・優先度と鮮度の両立が設計の勘所。
要件と規模の見積もり
まず何を作るかを固定します。設計面接では要件を自分で狭めるのが最初の得点源です。ここでは「検索エンジンのインデックス用に、公開Webページを大規模に収集するクローラー」を対象とし、以下を仮定します。
機能要件: シードURLから出発しHTMLを取得、リンクを抽出して再帰的に巡回する。robots.txt を尊重する。同一コンテンツの重複取得を避ける。ページの再クロールで鮮度を保つ。非機能要件: スケーラブル(数十億ページ)、礼儀正しい(サーバーを過負荷にしない)、頑健(不正HTML・無限空間で落ちない)、拡張可能(画像やPDFなど新種を後から足せる)。
規模をフェルミ推定で桁まで出します。目標を「10億ページ/月」とします。
| 項目 | 見積もりの根拠 | 概算値 |
|---|---|---|
| 取得QPS(平均) | 10億 ÷ (30日×86400秒) ≈ 386 | 約400 QPS |
| 取得QPS(ピーク) | 平均の2倍を想定 | 約800 QPS |
| ページ平均サイズ | 圧縮前HTMLで想定 | 約500 KB |
| 生データ帯域 | 400 QPS × 500 KB | 約200 MB/s(1.6 Gbps) |
| 月間ストレージ(生) | 10億 × 500 KB | 約500 TB |
| 圧縮後(gzip 約1/5) | 500 TB ÷ 5 | 約100 TB/月 |
| URLメタ情報 | 10億 × 数百B(URL+状態) | 数百 GB〜TB級 |
この桁感が後の判断を支配します。400〜800 QPS は1台では到底さばけず、ストレージも単一ノードに載りません。ゆえに最初から分散・水平スケール前提で設計します。逆に、レイテンシ要件は緩い(リアルタイム応答不要のバッチ処理)ので、スループットと堅牢性に全振りできる、という判断が立ちます。
大枠の設計
全体はキュー(URLフロンティア)を心臓に据えたパイプラインです。BFS(幅優先探索)を採る理由は、DFSだと1サイトの深部へ潜り込んで礼儀正しさを壊しやすいのに対し、キューによるBFSは自然に浅く広く辿り、優先度制御も差し込みやすいからです。
[シードURL] → URLフロンティア(優先度つきキュー群)
│ dequeue
▼
DNSリゾルバ(キャッシュ付き)
▼
ダウンローダ(非同期HTTP取得・robots遵守)
▼
コンテンツ保存(生HTML → オブジェクトストレージ)
▼
パーサ(HTML解析・リンク抽出)
▼
┌── コンテンツ重複検出(SimHash / ハッシュ)
└── URL重複検出(ブルームフィルタ「URL Seen?」)
│ 新規URLのみ
▼
フロンティアへ再投入(ループ)
データモデルの中心は3つです。URLフロンティア(未取得URLを優先度・ドメイン別に保持)、訪問済みインデックス(URLと簡易フィンガープリント)、ページストア(生HTML+メタ)。フロンティアの取り出しは「どのドメインを・いつ・どの優先度で」引くかを内包した擬似コードで表せます。
def crawl_loop(worker):
while True:
url = frontier.dequeue() # 礼儀正しさを満たすURLだけ返る
if not robots.allowed(url): # robots.txt(キャッシュ済み)
continue
ip = dns.resolve(url.host) # DNSキャッシュ参照
html = downloader.fetch(url, ip, timeout=10)
page_store.save(url, html)
if content_seen.is_duplicate(html): # SimHash 近似重複
continue
for link in parser.extract_links(html):
norm = normalize(link) # 正規化(後述)
if not url_seen.contains(norm): # ブルームフィルタ
url_seen.add(norm)
frontier.enqueue(norm, priority_of(norm))
各段を独立プロセスにし、フロンティアを共有キュー(分散メッセージキュー)にすれば、ダウンローダとパーサを別々に水平スケールできます。処理特性が違う(ダウンローダはI/O待ち主体、パーサはCPU主体)ため、段を分けて別々に増やせることがこの構成の勝ち筋です。
主要コンポーネントの深掘り
URLフロンティアと礼儀正しさ。単一のFIFOでは礼儀正しさを守れません。同一ドメインへの同時多発アクセスを避けるため、Mercatorが示した二段キュー構成が定番です。前段(フロントキュー群)を優先度別に並べ、後段(バックキュー群)を1キュー=1ホストに固定します。各バックキューにはヒープで「次に叩いてよい時刻」を持たせ、直前の応答時間や robots.txt の Crawl-delay からドメイン別レートを算出します。これにより「重要ページを優先しつつ、どのサーバーにも一定間隔でしか触れない」を同時に満たせます。ワーカ数がホスト数を超えても、同一ホストは1本のバックキューに直列化されるため過負荷が起きません。
礼儀正しさの本質は、多数のワーカがいても各ホストへのアクセスを直列化することです。ホストをキーにしたキュー割り当て(ハッシュでホスト→バックqueue番号)にすれば、ワーカ台数と無関係にホスト別間隔を保証できます。IPを共有する仮想ホストでは、IP単位のレート制御も併用します。
重複URL検出。数十億URLを訪問済み判定するのに、全URLをハッシュセットで持つとメモリが破綻します。ここでブルームフィルタが効きます。偽陰性ゼロ(「未訪問」と言えば本当に未訪問)で偽陽性のみという非対称性が、クローラーに理想的です。偽陽性は「実は新規なのに訪問済みと誤判定」してそのURLを捨てるだけで、致命傷になりません。要素あたり約10ビットで偽陽性1%弱に抑えられるので、10億URLでも約1.2GB程度に収まります。判定前にURLを正規化(スキーム小文字化、既定ポート除去、フラグメント削除、クエリ順序整列、末尾スラッシュ統一)することが、無駄な重複を根本から減らす前処理として重要です。
コンテンツ重複と近似重複。別URLが同一・ほぼ同一の中身を返すことは頻繁にあります(ミラー、印刷用ページ、パラメータ違い)。完全一致はコンテンツのSHA-256などで弾けますが、広告やタイムスタンプだけ違う近似重複はハッシュでは捕まりません。そこでSimHashを使います。文書を特徴語のハッシュから1本の指紋(例 64ビット)に落とし、指紋間のハミング距離が小さいほど内容が近いという性質を利用します。距離が閾値(例 3ビット)以下なら重複とみなし、インデックスから外します。
| 手法 | 捕まえる重複 | コスト | 適所 |
|---|---|---|---|
| URL正規化 | 表記ゆれの同一URL | 極小 | 取得前の入口 |
| ブルームフィルタ | 訪問済みURL | 約10bit/URL | フロンティア投入前 |
| コンテンツハッシュ | 完全一致本文 | 1ハッシュ/ページ | 保存前の完全重複 |
| SimHash | 近似重複(広告差など) | 指紋+近傍探索 | インデックス品質向上 |
優先度と鮮度。すべてを同頻度で再クロールするのは無駄です。ページの重要度(被リンク数・PageRank的スコア)と変更頻度は相関しないため、両者を分けて扱います。優先度はフロントキューの選択確率に反映し、鮮度は「変更が観測された頻度」からポアソン過程的に再訪間隔を推定して、よく変わる重要ページほど短周期で再取得します。HTTPの Last-Modified / ETag と条件付きGET(If-Modified-Since)を使えば、変わっていないページは304応答で本文取得を省け、帯域を大きく節約できます。
カレンダーのように「次月」リンクが無限に続くページや、セッションIDで無限にURLが増えるサイトは、素朴なBFSを永久に走らせます。防御は多層です。ドメインごとの取得上限、URL長・パスの深さ上限、同一パターンURLの頻度制限、そして robots.txt と nofollow の尊重。加えて、極端に多くのURLを生むホストを検知して減速するヒューリスティックを入れます。
ボトルネックとトレードオフ
横にスクロール
DNSが最初の隠れた壁。ホスト名解決は同期的だと1件あたり数十msかかり、400 QPSでは解決だけで詰まります。対策は、TTLを見つつ積極的にキャッシュするローカルDNSキャッシュと、複数リゾルバへの並行問い合わせです。DNSを軽視した設計は、コードが正しくてもスループットが出ません。
I/O待ちとスループット。ダウンローダの時間の大半はネットワーク待ちです。スレッドを1接続1本で増やすとメモリとコンテキストスイッチで頭打ちになるため、非同期I/O(イベントループ)で1ワーカが数千接続を多重化するのが定石です。ここでCPU律速のパーサと分離しておくと、それぞれを最適な台数・機種で増やせます。
分散クローラーの協調。数百ノードで走らせるとき、URL空間をどう分割するかが核心です。ホスト名のハッシュでノードを決めるコンシステントハッシュ法を使えば、ノード追加・故障時の再配置を最小化しつつ、同一ホストは常に同一ノードへ集約できます。この集約が礼儀正しさを分散環境でも成立させます(ホスト別レート制御が1ノード内で完結する)。フロンティアの永続化と各ノードの担当範囲はシャーディングで分け、データベースのシャーディング戦略と同じ原理でホットスポットを避けます。
| 論点 | 選択肢A | 選択肢B | 設計上の判断 |
|---|---|---|---|
| 探索順 | BFS(キュー) | DFS(スタック) | BFSを採る。浅く広く辿り礼儀と優先度制御に馴染む |
| フロンティア | インメモリのみ | 永続キュー | 永続キュー。障害復帰と数十億件の規模に必須 |
| URL重複判定 | 完全なハッシュセット | ブルームフィルタ | ブルームフィルタ。偽陽性は許容、メモリを桁で節約 |
| ノード間協調 | 中央コーディネータ | コンシステントハッシュ | ハッシュで分散。単一障害点と調整コストを回避 |
一貫性と可用性のトレードオフ。分散フロンティアは厳密な整合より可用性を優先します。同一URLが稀に2ノードで取得されても、コンテンツ重複検出が後段で吸収するため実害は小さい——これはCAP定理でいうAP寄りの割り切りです。逆に「訪問済み集合」を全ノードで強整合にしようとすると調整コストで失速するので、各ノードにローカルなブルームフィルタを持たせ、緩やかに突き合わせる設計が現実的です。フロンティアへの投入を確実に一度届けたい部分では、メッセージキューの配信保証の考え方(at-least-once+冪等な重複排除)がそのまま効きます。
URLフロンティアが心臓で、BFS+ドメイン別バックキューにより優先度と礼儀正しさを両立すること。訪問済み判定はブルームフィルタ(偽陰性ゼロ・偽陽性許容で省メモリ)、近似重複はSimHashのハミング距離で捕まえること。DNSとI/O待ちがボトルネックで非同期化とキャッシュが鍵、分散協調はホスト名のコンシステントハッシュで同一ホストを同一ノードへ集約——この4点を桁つきの規模見積もりとセットで答えられるようにしておきます。
まとめ
Webクローラーの設計は、URLフロンティアを中心にしたBFSパイプラインに集約されます。ダウンローダ・パーサ・フロンティアを分離して水平スケールし、重複はブルームフィルタ(URL)とSimHash(コンテンツ)で省メモリに排除、礼儀正しさはドメイン別バックキューとレート制御で守ります。規模は10億ページ/月なら約400 QPS・圧縮後100TB/月級。ボトルネックはDNSとI/O待ちで、非同期取得とキャッシュ、そしてホスト名のコンシステントハッシュ法による分散協調が実運用の勘所です。優先度と鮮度、トラップ回避まで含めて、スケール時のトレードオフを言語化できることが設計面接の合否を分けます。
システム設計の記事ガイド
Webクローラーの設計を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
システム設計
比較で見る軸
難易度: advanced / カテゴリ: システム設計 / タグ数: 5
導入後に効く点
重複はブルームフィルタでURL訪問済みを省メモリ判定し、コンテンツはSimHashで近似重複を検出。礼儀正しさはドメイン別キューとレート制御でrobots.txtとサーバー負荷を守る。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- システム設計
- タグ数
- 5
判断チェックリスト
- 自社の用途が「システム設計 / Webクローラー」に近いか確認する。
- 強みである「コアはURLフロンティアを中心にしたBFS。取得→パース→リンク抽出→重複排除→再投入のループを、ダウンローダ・パーサ・フロンティアに分けて分散させる。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。