Капстоун: конкурентный краулер
Перед началом: модули 1–17. Проект собирается из всех паттернов курса, поэтому пропущенный модуль будет заметен. Инструменты: Go 1.22+,
go test -race, docker для лимита памяти. Трудоёмкость: ~8–10 ч (проект 6–8 ч · защита 1 ч).
🔥 Инцидент: обход «в лоб»: бан по IP, OOM и 40 тысяч URL без отчёта
Команде нужен был краулер для аудита собственного сайта. Написали «в лоб»: скользящая очередь ссылок, и на каждую — горутина. Через две минуты сайт отдал 429 по всему диапазону: краулер выжимал сотни запросов в секунду, не ограничивая скорость. Параллельно процесс упёрся в память: 40 тысяч горутин одновременно держали ответы и дублировали обход — одна и та же страница встречалась в графе десятки раз.
Переписали: пул воркеров с лимитом, token bucket на хоста, map посещённых под мьютексом, отмена по сигналу и отчёт в конце. Результат: те же 40 тысяч страниц за 12 минут, ровно по лимиту запросов, ноль дублей, память — десятки мегабайт.
Этот модуль — не новая теория, а сборка: каждый блок проекта уже отработан в предыдущих модулях. Ваша задача — соединить их так, чтобы работало под нагрузкой и не падало.
📖 Теория
18.1 Требования: что значит «краулер готов»
Функциональные:
- обойти связный граф страниц, начиная с корня, не выходя за домен;
- посетить каждую страницу ровно один раз (дедупликация);
- отдать отчёт: сколько страниц обойдено, сколько ошибок, где остановились по лимиту.
Нефункциональные — те, из-за которых «в лоб» и упало:
- лимит скорости на хост (иначе бан по IP);
- ограничение параллелизма (пул воркеров, а не горутина на страницу);
- отмена и graceful stop по сигналу (обход не должен терять отчёт);
- детерминизм в тестах: граф-заглушка вместо сети.
18.2 Архитектура: четыре уже знакомых кирпича
Краулер — это конвейер из модуля 6, внутри которого:
- Frontier — очередь URL, которые надо обойти. Bounded-канал (модуль 13): переполнение — сигнал, что лимит скорости не успевает, а не повод съесть память.
- Пул воркеров (модуль 7) — 3–8 воркеров, каждый забирает URL, делает fetch, отдаёт найденные ссылки.
- Visited —
map[string]boolподsync.Mutex(модуль 5): проверка и вставка — одна критическая секция, иначе два воркера поставят в очередь один и тот же URL. - Rate limiter (модуль 8) — token bucket на хост: 2 страницы
за раунд (в бою — по времени,
x/time/rate).
Сверху — отмена: ctx/done, который воркеры проверяют перед
fetch (модуль 3), и метрики: страниц обойдено, дублей
отброшено, ошибок.
Таблица сборки — какой модуль за что отвечает:
| Блок | Модуль | Что берём |
|---|---|---|
| Пул воркеров | 7 | jobs/results, лимит, отмена |
| Очередь frontier | 13 | bounded-канал, политика переполнения |
| Дедупликация | 5 | map + mutex, проверка и вставка за один захват |
| Лимит скорости | 8 | token bucket на хост |
| Ретраи/таймауты | 9 | повтор только идемпотентного GET |
| Отмена и останов | 3, 11 | done-канал, drain, отчёт после остановки |
| Метрики | 17 | счётчики вместо гаданий по логам |
| Race-тесты | 2 | go test -race, leak-тест |
18.3 Ловушки, на которых спотыкаются все
- Дедупликация «не в той точке». Проверять visited при извлечении из очереди поздно: дубли уже занимают место в frontier. Проверяйте при вставке — под тем же мьютексом, что и вставка.
- Отчёт после отмены. Если main вышел, не дождавшись воркеров,
часть страниц «обойдена» без учёта. WaitGroup и
deferна закрытие отчёта — обязательны. - Лимит на весь краулер вместо хоста. Общий bucket защищает вас, но не партнёрские сайты; per-host — правило вежливости и защиты от бана.
- Ретраи без лимита. Страница, отдающая 500, при ретраях в цикле даёт вечный обход; повторы — только с бюджетом (модуль 9) и только для идемпотентных GET.
18.4 Тесты: сеть заменяем графом
Тестируемость краулера — свойство архитектуры: fetch — это
функция func(url string) ([]string, error), поэтому в тестах
она подменяется графом-заглушкой (как в сниппете). Проверяем:
- Полноту: обойдены все достижимые узлы, ни одного лишнего.
- Отсутствие дублей: каждая страница в отчёте один раз.
- Отмену: после
close(done)новые fetch не начинаются, отчёт содержит то, что успели. - Гонки и утечки:
go test -race; после завершенияruntime.NumGoroutine()возвращается к базовому уровню (модуль 17).
18.5 Компромиссы
- Frontier в памяти vs внешняя очередь: в памяти — просто и быстро, но не переживает рестарт; внешняя (Redis/Kafka) — переживает, но добавляет зависимость и задержку.
- BFS vs произвольный порядок: BFS даёт предсказуемый отчёт («глубина N»), но держит широкий фронт; обход «как получится» дешевле по памяти, но отчёт хуже.
- Глобальный лимит vs per-host: глобальный проще, но не защищает конкретный сайт; per-host вежливее, но требует состояния на каждый хост.
- Точность vs скорость: больше воркеров — быстрее, но выше риск бана и больше нагрузки на чужой сервер.
💻 Практика
Проект собирается этапами; каждый этап — проверяемый.
- Минимум. Frontier (канал), пул из 3 воркеров, visited под
мьютексом,
fetchпо графу-заглушке. Критерий: все узлы обойдены ровно один раз. - Лимит скорости. Token bucket на хост: не больше 2 страниц за раунд. Критерий: в логе раундов видно темп; при лимите 1 обход занимает вдвое больше раундов, но всё ещё завершается.
- Надёжность. Таймаут на fetch (context), retry с бюджетом для временных ошибок, счётчик ошибок в отчёте. Критерий: страница, «падающая» первые два раза, обходится с третьей попытки, а «вечно падающая» — попадает в ошибки и не зацикливает обход.
- Останов и отчёт. По сигналу (в тесте —
close(done)) воркеры завершают текущие fetch, frontier закрывается, отчёт печатается. Критерий: ни одной горутины после завершения (leak-тест), отчёт содержит visited на момент остановки. - Защита. Покажите:
go test -raceчист; лимит памяти контейнера не превышен на 40k-графе; отчёт воспроизводим (детерминированный граф → одинаковые цифры). - Запустите сниппет модуля: мини-краулер на 8 страницах — 5 раундов по лимиту 2; проверьте, что в отчёте ровно 8 страниц и ни одной дважды.
🧩 Quiz
✅ Итоги модуля
Студент должен:
- Собрать краулер из пула, frontier, visited и лимитера
- Сделать дедупликацию на вставке, а не на извлечении
- Ограничить скорость на хост и объяснить выбор лимита
- Реализовать останов по сигналу с честным отчётом
- Писать тесты с графом-заглушкой без реальной сети
- Прогонять
-raceи leak-тест как часть приёмки - Объяснить, какой модуль курса за какой блок отвечает
- Ответить на все вопросы Quiz
🔗 Мостик: дальше по платформе
Курс закончился, навык — нет. Куда идти дальше:
- mq-track — брокеры всерьёз: гарантии доставки, Outbox, Saga, consumer groups. Конкурентные консьюмеры отсюда станут распределёнными, а идемпотентность — обязательной.
- pg-clickhouse — конкурентность встречается с данными: пулы, параллельные запросы, OLTP → OLAP.
- go-advanced m3 — распределённые паттерны: там ваши circuit breaker и retry поднимутся с уровня вызова на уровень системы.
И привычка, которую стоит забрать из курса: любой конкурентный
код проверяется -race, любой «фоновый» — на утечки, любой
внешний вызов — таймаутом, лимитом и отменой.