Lesson 0018 · 系统设计 · Phase 5 进阶案例 · 细节版

大案例 VI:分布式网页爬虫

⏱ 预计 40 分钟 🎯 吞吐型系统:礼貌性、去重、反陷阱三大主题 📖 前置:L6 · L7 · L5
与你的 Mission 的关系

爬虫是吞吐型批处理的原型:优化目标不是延迟而是「单位时间完成量 ÷ 不惹麻烦」。它教的三件事在工作中极常用:外部数据采集、按 key 限速的并发控制、以及和不可信外部世界打交道的防御性设计(超时、陷阱、封禁)。

件 ①需求表(Step 1)

类别内容
功能(MVP)给定种子 URL 集 → 抓取页面 → 解析出链接继续抓 → 存储页面内容与结构化元数据,供下游(索引/分析)使用。明确不做:JS 重度页面的完整渲染(v2 提)、登录后内容
非功能(本题灵魂)吞吐 1000 页/s礼貌性:同一域名 ≤ 1 req/s、遵守 robots.txt(RFC 9309[1]);页面去重;可断点续爬;对单站点故障有免疫(慢站不能拖垮全局)
关键洞察 爬虫的速度上限不是我们的带宽,而是「对每个域名的礼貌速率」——系统设计 = 全局吞吐 ÷ 逐域限速的调度问题

件 ②容量估算(Step 2,全程算式)

指标算式架构暗示
原始存储1000 页/s × 200KB ≈ 200MB/s → 日 ~17TB顺序写大文件/对象存储,别用事务库
去重后净增重复率 ~40%(转载/镜像)→ 净增 ~10TB/天去重省 40% 存储——去重是成本组件不是附属品
URL 队列待抓 URL 池 ~100 亿 × 150B ≈ 1.5 TB → 内存放不下frontier 必须落盘 + 分片(件 ⑥),内存只放热队列
域名规模1000 页/s 打散到 ~100 万活跃域名 → 每域均 ~0.001 req/s逐域 1 req/s 的礼貌限速几乎不构成瓶颈——瓶颈在大域名(件 ⑦)
DNS 查询每页一次解析 → 1000 QPS DNS;解析 20-100msDNS 缓存服务必须独立(缓存命中 > 95%)

件 ③API 与内部接口(Step 3)

# 对下游(索引/分析方)
GET  /pages?crawl_run={run_id}&after={cursor}   # 批量拉取本次抓取产出(流式)
POST /crawl_jobs {seeds: [...], max_depth, budget}   # 发起一次抓取任务

# 内部(worker → 调度器,消息驱动)
SCHED → WORKER: {url, domain, depth, priority, retry_count}
WORKER → SCHED: FETCHED {url, status, content_hash, extracted_links[]}
             或 FAILED {url, error_type}   # dns/timeout/robots_denied/http_4xx/5xx
             或 RETRY_LATER {url, backoff_until}

件 ④数据 Schema(Step 3)

存储结构与设计
URL frontier(调度队列)domain 分桶,桶内 FIFO + 桶级优先级(重要域先出);持久化于 KV/日志(断点续爬),内存只缓存活跃桶
seen_urls(已见 URL 集)url_hash(SHA-1 截断)→ first_seen, last_crawled, status。分片存于 KV(百亿级);运行中用 Bloom filter 前置挡重复(说「没见过」才入队)
robots 缓存(Redis)domain → {rules, fetched_at};TTL 24h;未命中回源拉 /robots.txt(也要礼貌限速!)
pages(产出)url, content_hash, raw_blob(对象存储), parsed{title, links[], text_len}, crawl_run_id。原始与解析分离,解析规则升级可重放

件 ⑤一个 URL 的完整旅程(Step 3,背下来)

种子/解析出的新链接
   │ ①规范化(去 tracking 参数、统一大小写/协议)
   │ ②Bloom filter 检查:可能已见 → 查 seen_urls 精确确认 → 重复则丢弃
   ▼
frontier 入队(按 domain 分桶 + 优先级)
   │ ③调度器出队:桶内令牌桶放行(逐域 ≤1 req/s)
   ▼
抓取 worker(worker 与域名按一致性哈希绑定 → 同域串行化,L6)
   │ ④robots.txt 检查(缓存)→ 禁抓则标记丢弃
   │ ⑤DNS 解析(独立缓存服务,95%+ 命中)
   │ ⑥HTTP 抓取(超时 10s 硬限,UA 标明身份)
   ▼
内容管道:content_hash 去重(内容级)→ 存对象存储 → 解析抽链
   │ ⑦新链接回到 ①(深度限制 max_depth=5,防环)
   ▼
FETCHED 上报调度器(失败按 error_type 决定:重试/降权/放弃)

件 ⑥关键组件选型对比

决策点选项 A选项 B结论与代价
worker 与域名绑定 任何 worker 抓任何域(全局队列) 一致性哈希:worker ↔ 域名固定子集 绑定后同域自然串行(礼貌性免费获得)、DNS/robots 连接复用、慢域只影响自己;代价:worker 宕机要迁移其域名桶(一致性哈希迁移量 ~K/N,L6)
URL 去重 只查 KV 精确集合(100% 准) Bloom filter 前置 + KV 精确兜底(两层) Bloom 挡掉 ~90% 重复查询(内存 ~10GB@百亿 URL/1% 误判);代价:误判率让极少数新 URL 被误杀——可接受
内容去重 全文哈希(完全相同才算重) SimHash 近似(排版微调也算重) 转载页常改排版/广告位——SimHash 汉明距离判重;代价:需要调阈值、边界情况误杀,先用全文哈希 + 热点页 SimHash 混合
调度队列 Kafka 按 domain 分区 自建 frontier(优先级 + 桶级令牌桶) Kafka 做不了「桶级限速 + 优先级插队」;frontier 是爬虫的灵魂组件,值得自建(代价:断点恢复与运维复杂度)

件 ⑦失败模式与应对(Step 4)

故障应对
蜘蛛陷阱(calendar?d=1,2,3… 无限 URL 空间) URL 规范化 + 参数黑名单;同域 URL 数上限(如 100 万)熔断;深度截断 max_depth;同站点路径模式检测(/d= 递增模式)标记陷阱域
URL 环(A→B→A 死循环) seen_urls 全局去重天然解决——环会在第二次到达时被丢弃;配合深度限制双保险
慢站点拖垮 worker 抓取超时 10s 硬限;同域连续超时 → 该域降权(半天内不调度);worker 池按域名隔离(件 ⑥ 绑定)保证隔离性
被目标站封禁 退避 + 尊重(429/503 按 Retry-After 退避);UA 诚实标明身份与联系邮箱;控制并发——长期可持续的爬虫靠被欢迎,不靠伪装
调度器/frontier 宕机 frontier 与 seen_urls 全部持久化 + 操作日志;重启后从日志重放恢复;worker 无状态可重连
JS 动态页面抓不到内容 v2:headless 渲染池(贵 10 倍),只对「解析后文本过短」的高价值域开启——渲染预算也是预算

件 ⑧演进路线(v1 → v2)

件 ⑨面试官追问预测

追问答案要点
「怎么保证不搞挂小网站?」 逐域令牌桶(1 req/s)+ 域名并发上限(如 2)+ 429/503 退避 + 小站点响应慢自动降权 + 全局并发预算监控告警——礼貌性是需求,不是美德
「和搜索引擎的爬虫有什么本质差异?」 搜索引擎重全量+及时性(全网 PB 级、增量重爬调度);业务爬虫重针对性(垂直域、结构化抽取、合规边界)——架构同源,调度目标不同
「重爬策略怎么设计?」 按历史更新频率建模:新闻页 5 分钟、首页 1 小时、关于页一年——调度优先级 = 重要度 × 更新概率,这就是 Google 的 freshness 思路
「Bloom filter 误判杀掉新 URL,能接受吗?」 能:误判率 1% 只影响极小部分新页,且下轮爬取周期会再遇到;换取的是 90%+ 的去重查询不落盘——用可量化的错误率换吞吐(L9 权衡句式)
「robots.txt 全网遵守的成本?」 缓存 + 24h TTL + 惰性加载(只查要抓的域);未命中也限速回源。成本可控且是法律/伦理底线(RFC 9309)

§·随堂检测

§·检索练习

盖住全部内容:① 默写一个 URL 的完整旅程(7 步);② 三层去重分别在哪一层、各防什么;③ worker 与域名一致性哈希绑定带来哪三个好处。

核对要点

① 规范化 → Bloom+seen 去重入队 → 桶级令牌桶出队 → robots 检查 → DNS 缓存 → 超时受控抓取 → 内容去重/存储/抽链(回①)。② URL 级(Bloom 前置 + seen_urls 精确)防重复抓取;内容级(全文哈希/SimHash)防重复存储;结构级:robots 与深度限制防无效抓取。③ 同域串行化=礼貌性免费、连接/DNS 复用、慢域故障隔离。

§·本周行动

行动任务(约 20 分钟)

① 读两个真实 robots.txt(如 taobao.com/robots.txtzh.wikipedia.org/robots.txt),比较谁宽松谁严格、猜原因;② 想一个你工作中需要采集外部数据的场景,用礼貌性四件套(逐域限速/robots/退避/身份声明)写三行设计约束。

§·延伸资源

💬 你公司有采集外部数据的场景?把约束贴来,一起设计「礼貌且高效」的调度策略。