open atlas
↑ К треку
Разборы System Design SDC · 01 · 03

Спроектируйте веб-краулер

Проектируем распределённый веб-краулер: вежливый URL frontier, robots.txt и лимиты на хост, дедуп через bloom filter плюс хеш контента, DNS на масштабе, перекраул по свежести и ловушки, в которых наивный краулер зацикливается.

SDC Senior ◷ 30 min
Уровень
ОсновыJuniorMiddleSenior

Команда собрала краулер за выходные: очередь URL, воркер, который фетчит страницу, извлекает ссылки и кладёт их обратно в очередь. Он прекрасно проработал час, потом случились две вещи разом. Во-первых, небольшой интернет-магазин гневно написал — их краулер слал сорок запросов в секунду на один слабый сервер и фактически устроил ему DDoS. Во-вторых, собственная очередь краулера раздулась до пятидесяти миллионов URL и продолжала расти, потому что он забрёл в виджет-календарь со ссылкой «следующий месяц», порождающей свежий бесконечный URL на каждый клик — а краулер, не помня, где был, шёл по каждой. Выходной краулер открыл две истины, вокруг которых строится настоящий: веб враждебен вежливости и полон бесконечных циклов, и краулер — это в основном машина для не-перефетча, не-задалбливания хостов и не-падения в ловушки.

Требования

Каждое требование ниже напрямую связано с одним из двух провалов из хука. Читая список, сопоставь каждое ограничение с выходным краулером, который его нарушил — это сопоставление и есть дизайн.

Функциональные

  • Стартовать с набора сидов URL и открывать достижимый веб, идя по ссылкам.
  • Фетчить каждую страницу, извлекать ссылки и отдавать контент потребителям ниже (индексатор, архиватор).
  • Уважать robots.txt и директивы crawl-delay — не обсуждается.
  • Дедуплицировать: никогда не перефетчить URL без нужды и обнаруживать, когда два URL возвращают одинаковый контент.
  • Перекраулить ради свежести: возвращаться к страницам, чтобы индекс не устаревал.

Нефункциональные

  • Вежливость — жёсткое ограничение, а не любезность. Никогда не перегружать один хост; краулер, делающий DDoS сайтам, банится и плохой гражданин.
  • Огромный масштаб. Миллиарды страниц, поэтому дизайн распределён по многим машинам с первого дня.
  • Устойчивость. Веб враждебен — кривой HTML, циклы редиректов, бесконечные пространства URL, медленные серверы, ловушки-пауки. Краулер обязан пережить всё это, не зависая и не зацикливаясь.
  • Расширяемость. Новые типы контента и потребители подключаются без переписывания.

Два ограничения, доминирующих в дизайне, — вежливость (как быть быстрым в сумме, но мягким к хосту) и дедуп (как не утонуть в одном контенте дважды).

Оценка

  • Цель — 1 миллиард страниц/месяц. Это 10^9 / (30 × 10^5 с)~400 страниц/с устойчиво — но реальное число выше, ведь большая часть времени фетча — это ожидание сети, так что темп задаёт конкурентность, а не CPU.
  • Средняя страница ~100 КБ HTML → 10^9 × 100 КБ = ~100 ТБ/месяц сырого фетченного контента до дедупа. Доминируют хранение и пропускная способность, не вычисления.
  • Бухгалтерия URL: чтобы обойти миллиард страниц, нужно помнить десятки миллиардов виденных URL, чтобы не перефетчить. По ~100 байт/URL точное множество seen из 10 миллиардов URL — это ~1 ТБ RAM — слишком дорого держать точно в памяти, что и есть вся мотивация bloom filter (несколько бит на URL вместо 100 байт).
  • DNS: каждый фетч нуждается в разрешении имени хоста; при ~400 страниц/с без кеширования DNS становится синхронным узким местом — так что локальный резолвер/кеш обязателен, не опционален.

Число бухгалтерии и есть драйвер дизайна: нельзя позволить себе помнить каждый URL точно, поэтому дедуп вероятностный, и один этот факт формирует архитектуру.

Высокоуровневый дизайн

URL frontier держит URL для обхода, упорядоченные ради вежливости и приоритета. Воркеры тянут из frontier, разрешают DNS, фетчат страницу (после проверки robots.txt), проверяют контент на дублирование, хранят его, извлекают новые ссылки, фильтруют их против множества seen и подают выживших обратно в frontier.

Глубокое погружение

URL frontier и вежливость

Frontier — не простая FIFO-очередь, это движок вежливости, и ошибка в нём — то, что делает DDoS сайту. Настоящий frontier (дизайн Mercator) двухступенчатый:

  • Передние очереди — приоритет. Входящие URL сортируются по очередям по приоритету (важность, нужда свежести), так что ценные страницы обходятся раньше. Приоритизатор выбирает, из какой передней очереди тянуть.
  • Задние очереди — вежливость. Каждая задняя очередь привязана к ровно одному хосту, а роутер гарантирует, что две задние очереди не обслуживают один хост. Воркеру назначены задняя очередь и временная метка, когда он может снова стукнуться к этому хосту; он спит, пока не истёк crawl-delay хоста (из robots.txt или дефолт). Это и обеспечивает «не более одного запроса к хосту X каждые N секунд» независимо от того, сколько URL этого хоста ждут.

Так frontier одновременно говорит обходи важное первым (передние очереди) и никогда не задалбливай один хост (задние очереди) — две цели, которым одна очередь служить не может. Вежливость — на хост, а не на IP или на краулер, потому что один медленный сервер за одним именем хоста и есть то, что надо защитить.

Почему это работает

Зачем разделять передние (приоритет) и задние (вежливость) очереди вместо одной приоритетной? Потому что две цели дерутся. Тянешь строго по приоритету — с радостью выпустишь десять высокоприоритетных URL, все случайно на одном хосте, подряд — перегружая его. Тянешь строго round-robin по хостам ради честности — теряешь способность обходить важные страницы первыми. Двухступенчатый frontier снимает напряжение: передние очереди решают, что важно, задние (по хосту каждая, с меткой следующего разрешённого) решают, когда вежливо фетчить. Путь URL — «приоритет спереди, лимит хоста сзади», и лишь URL, прошедший оба, фетчится — поэтому краулер на миллиард страниц может быть агрессивным в сумме и мягким к каждому отдельному серверу.

Дедуп: bloom filter для URL, хеш контента для страниц

Есть две разные задачи дедупа, и им нужны два разных инструмента.

Видел ли я этот URL? При десятках миллиардов URL точное множество в памяти — это ~1 ТБ — неподъёмно. Bloom filter отвечает «видел ли я этот URL?» за несколько бит на URL без ложно-отрицательных (если говорит не видел — точно новый) и с настраиваемой долей ложно-положительных (изредка может сказать видел, когда нет, безвредно пропустив реально новый URL). Для краулера эта асимметрия идеальна: цена ложно-положительного — одна необойдённая страница из миллиардов; цена точного множества — терабайт RAM. (Пререквизит про bloom filter разбирает механику битового массива; здесь это трюк памяти, делающий дедуп веб-масштаба возможным.)

Видел ли я этот контент? Два разных URL часто возвращают одинаковые байты — http против https, трекинг-параметры ?utm=..., зеркала, версии для печати. Дедуп URL это не ловит, ведь URL различаются. Поэтому ты также хешируешь контент (например MD5/SHA нормализованного тела или SimHash для почти-дублей) и пропускаешь хранение страницы, чей хеш контента уже видел. Дедуп URL не даёт пере-фетчить; дедуп контента не даёт пере-хранить и пере-индексировать один текст, достижимый многими путями.

Ловушки, свежесть и перекраул

Ловушки — это календарь из хука: страницы, порождающие бесконечные постоянно меняющиеся URL (session ID в пути, ?page=1,2,3… до бесконечности, глубокие динамические фасеты). Наивный краулер считает каждую новой и зацикливается. Защиты: ограничить глубину обхода на хост, ограничить URL на хост, обнаруживать паттерны URL, что взрываются, и опираться на хеш контента (страницы ловушки часто почти идентичны, так что проверка хеша контента отсеивает их, даже когда URL различаются). Правила robots.txt Disallow тоже отгораживают многие склонные к ловушкам пути.

Свежесть — обратная задача: веб меняется, поэтому обход никогда не «завершён». Страницы надо перекраулить, но не равномерно — главная новостей меняется ежечасно, архивный PDF — никогда. Поэтому приоритет перекраула задаётся оценкой темпа изменений на страницу: отслеживай, как часто хеш контента страницы реально менялся в прошлых обходах, и планируй волатильные часто, а статичные редко. Это делает frontier непрерывной системой (URL добавляются обратно по графику свежести), а не одноразовым сливом.

Граничные случаи

А как насчёт страницы, что законно меняет байты на каждом фетче, но не ловушка — главная с ротирующей «цитатой дня», рендеренная сервером метка времени, рекламный слот? Чистый хеш контента помечает каждый фетч как «новый контент», так что ты хранишь и переиндексируешь почти идентичные страницы вечно, а твоя оценка темпа изменений вопит «эта страница гипер-волатильна, перекраулить постоянно». Починка — хешировать нормализованную версию: вырезать волатильные области (метки времени, рекламные блоки, CSRF-токены) или использовать near-duplicate хеш вроде SimHash, игнорирующий мелкие диффы, чтобы две страницы, различающиеся лишь ротирующей цитатой, легли на тот же (или близкий) хеш. Без нормализации сам механизм, призванный снизить работу (дедуп контента + перекраул по темпу), инвертируется в беговую дорожку, что её максимизирует.

Викторина

Множество виденных URL твоего краулера потребовало бы ~1 ТБ RAM, чтобы хранить точно, поэтому ты заменяешь его на bloom filter. Какое новое поведение надо принять и почему оно приемлемо для краулера?

Закончи аналогию

_______ очереди frontier каждая привязаны к ровно одному хосту с меткой следующего разрешённого времени, что и обеспечивает вежливость — не более одного запроса к данному серверу за crawl-delay — пока отдельные приоритетные очереди решают, какие страницы важнее всего.

Узкие места и компромиссы

  • Масштаб и баланс frontier. При миллиардах URL frontier не влезает на одну машину; он партиционируется (часто по хешу хоста) по узлам — но тогда один гигантский хост (огромный сайт) может перегрузить свою партицию, пока другие простаивают. Балансировка назначения хост-к-партиции — реальная операционная проблема.
  • DNS как скрытое узкое место. Синхронный DNS при сотнях запросов/с тормозит фетчеры; локальный кеширующий резолвер обязателен, и даже тогда наплыв новых хостов может его насытить.
  • Вежливость против пропускной способности. Строгие лимиты на хост ограничивают, как быстро можно обходить один большой сайт — так что суммарная пропускная способность идёт от обхода многих хостов параллельно, а не одного быстро. Краулер, застрявший за несколькими мега-хостами, недорабатывает даже при запасе мощности.
  • Ложно-положительные дедупа и нормализация контента. Bloom filter изредка отбрасывает реальный новый URL (настрой размер под приемлемую долю); хеш контента без нормализации считает страницы с ротирующим контентом бесконечно новыми, инвертируя дедуп в пустую работу — обе ручки корректность-против-цены, а не бесплатные победы.
Вспомните перед уходом
  1. 01
    Почему URL frontier двухступенчатый и что обеспечивает каждая ступень?
  2. 02
    Зачем bloom filter для дедупа URL и зачем ещё хешировать контент?
  3. 03
    Что такое ловушки обхода и как дизайн справляется со свежестью, не зацикливаясь вечно?
Итог

Веб-краулер — не очередь с фетчером, это движок вежливости и дедупа, построенный вокруг двух фактов: веб враждебен вежливости и полон бесконечных циклов. Оценка называет драйвер дизайна — помнить десятки миллиардов виденных URL точно это ~1 ТБ RAM, неподъёмно, поэтому дедуп обязан быть вероятностным. Двухступенчатый URL frontier снимает конфликт приоритет-против-вежливости: передние очереди обходят важные страницы первыми, задние привязаны по одному хосту каждая с меткой следующего разрешённого, так что ни один сервер не задалбливается. Дедуп идёт в два слоя: bloom filter отвечает «видел этот URL?» за несколько бит каждый (без ложно-отрицательных, с настраиваемой долей ложно-положительных — пропущенная страница из миллиардов лучше терабайта RAM), чтобы избежать перефетча, а хеш контента отбрасывает одни байты, достигнутые через http/https, трекинг-параметры или зеркала, чтобы избежать переиндексации. Ловушки (календари, бесконечная пагинация) отгораживаются лимитами глубины/URL, обнаружением паттернов, правилами robots и хешем контента; свежесть превращает краулер в непрерывную систему, перекраулящую каждую страницу по её оценке темпа изменений. Ловушки в самом дизайне — DNS (Domain Name System, служба разрешения доменных имён) как скрытое узкое место, лимиты на хост, ограничивающие пропускную способность одного сайта, и хеш контента, буксующий на ненормализованном ротирующем контенте — и есть то, почему настоящий краулер — машина прежде всего устойчивости, а не выходной скрипт. Теперь, когда проектируешь любую систему, обращающуюся к внешним хостам на масштабе, первый вопрос тот же, что задаёт краулер: что не даёт задолбить один таргет — и что не даёт ходить по одному пути вечно?

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 7 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.