题干与适用场景
设计一个为搜索索引提供 HTML 的大规模网络爬虫。系统维护 100 亿个已知 URL,每天最多发起 10 亿次抓取;请设计 URL frontier、主机级礼貌调度、去重、重抓取、故障恢复与验证方案,并给出容量估算。
本题面向后端、基础设施、搜索和数据平台的高级工程师系统设计面试。抓取结果包含压缩后的原始 HTML、抓取元数据和新发现链接;全文索引、搜索排序、登录后页面、图片视频和默认的 JavaScript 渲染不在本轮范围内。系统采用尽力覆盖,不承诺遍历整个互联网。
以下数字都是便于推导的面试假设,不代表线上观测:平均成功响应体 200 KB;日抓取上限包含成功、304、失败和重试;平均负载按日上限计算,峰值按 25,000 次/秒规划。新发现 URL 在 60 秒内持久化,容量充足时 99% 已到期的高优先级 URL 在 10 分钟内获得租约。主机礼貌规则是硬约束,不能为了追赶吞吐而突破。
公开面经记录过“爬虫代码已经存在,重点设计可扩展架构”的 25 分钟设计题;2026 年公开系统设计材料也继续把分布式爬虫作为独立练习。单条记录不能证明公司固定题库或出现频率,因此这里按代表性系统设计题处理,不标注公司归属。
面试官考察点
第一项是范围和估算。强回答会先定义抓取对象、刷新目标、下游和失败语义,再计算吞吐与存储。直接画消息队列、爬虫节点和数据库,无法说明这些组件为什么需要存在。
第二项是 URL frontier 的调度不变量。一个全局 FIFO 可以并行取任务,却无法保证同一主机的并发、间隔和退避。强回答会把“选择哪个主机”和“选择该主机的哪个 URL”分成两层,并让同一 host_key 的令牌状态只有一个逻辑所有者。
第三项是区分三种重复:同一规范化 URL、不同 URL 返回完全相同内容、内容只有少量差异。它们分别需要精确 URL 状态、内容哈希和近重复指纹,不能用一个 Bloom filter 包办。
最后看故障模型。抓取外部网络必然遇到超时、429、5xx、DNS 错误、重定向环、超大响应和恶意页面。优秀回答会接受至少一次执行,用租约、版本条件写和幂等提交控制重复副作用,并给出能证伪设计的测试与指标。
回答前需要澄清的问题
- 下游用途是什么? 搜索索引需要保存 HTML、抓取时间、状态码和规范 URL;归档系统还会要求不可变版本;训练语料则会强化内容质量与许可证过滤。本题只把结果交给搜索索引。
- 抓取范围包含什么? 本题抓取公开 HTTP/HTTPS HTML。加入 PDF、媒体、登录态或 JavaScript 渲染会改变下载器、解析器、成本和安全隔离。
- 覆盖率与新鲜度如何取舍? 本题维护 100 亿个已知 URL:1 亿个高价值 URL 每天刷新,其余 99 亿个目标周期为 30 天。若要求所有页面每天刷新,10 亿次/日预算从数学上就不成立。
- 礼貌策略按什么边界执行? 这里以
scheme + authority形成host_key,集中执行 robots 策略、并发数、最小请求间隔和服务端退避。若业务与站点方约定专用额度,只调整该主机策略,不放松全局不变量。 - “不重复”要求多严格? URL 发现不能因 Bloom filter 假阳性而静默丢失,所以持久化唯一键是事实源;网络抓取允许重复执行,落库和下游事件必须幂等。
- 删除和失败页面保留多久?
404、410、连续失败和临时5xx的重抓取周期不同。这里保留墓碑与最近状态,避免每次重新发现都当成新 URL。
30 秒回答框架
“我先按 10 亿次/日算出平均约 11,600 次/秒、峰值 25,000 次/秒,并把新鲜度拆成 1 亿个 URL 每日刷新、99 亿个 URL 30 天刷新。架构上,发现服务做保守规范化并以唯一键去重;frontier 按主机分片,每个分片先选 nextallowedat 已到期的主机,再从该主机队列取最高优先级 URL,这样 robots、并发和退避只有一个所有者。抓取器用租约执行条件请求,HTML 进对象存储,解析器提取链接并回灌发现服务。系统采用至少一次执行,通过 URL 版本和幂等提交吸收重复。我会重点验证同主机限速、租约过期、429/503、robots 不可达、重定向环和爬虫陷阱。”
分步骤深入解答
第一步:用预算证明目标可同时满足
10 亿次/日除以 86,400 秒,平均约为 11,574 次/秒;按流量波动和补偿任务将峰值取整为 25,000 次/秒。若每次都返回 200 KB 响应体,网络入口上界约为 200 TB/日,平均约 2.31 GB/秒。304 Not Modified 没有响应内容,真实入口应低于这个保守上界,必须从压测和线上分布校准。
刷新计划每天需要:
100,000,000 + 9,900,000,000 / 30 = 430,000,000 次
剩余约 5.7 亿次预算用于新发现页面、失败重试和变化频繁页面。100 亿条 URL 状态若每条按 200 字节原始逻辑数据估算,约为 2 TB;副本、索引、LSM 放大和对象存储不包含在内。这个数量级说明元数据需要水平分片,HTML 应放对象存储,不能把正文塞进 frontier。
第二步:建立分阶段数据流
完整链路是:种子与 Sitemap → URL 发现/规范化 → 精确 seen 状态 → URL 元数据 → frontier 调度器 → robots 与主机礼貌检查 → DNS/HTTP 抓取器 → HTML 对象存储 → 解析器 → 新链接回灌。解析结果和抓取完成事件再交给搜索索引与重抓取计算器。
Sitemap 是补充种子的输入,不是覆盖承诺。一个 Sitemap 文件最多 50,000 个 URL、解压后最多 50 MB;大型站点用 Sitemap index 分片。链接发现、Sitemap 和人工种子最终都走同一个去重入口,避免三套状态互相打架。
拆开抓取与解析有两个直接收益:外部网络慢时不会占住解析 CPU;解析器崩溃时可以从已保存的 HTML 重放,无需再次访问站点。每个阶段都要有有界队列和背压,防止下载速度短时高于解析或存储能力时把内存打满。
第三步:让 URL frontier 围绕主机礼貌调度
frontier 采用两层队列。上层保存主机的 nextallowedat 和优先级,只选择已到可抓取时间且没有处于退避的主机;下层是每个主机自己的 URL 优先队列,排序键可包含业务价值、到期时间、链接深度和历史变化率。领取一个 URL 时,调度器原子更新该主机的 in_flight 与下一可用时间。
通过 hash(host_key) 将主机固定到一个调度分片。同一主机即使有百万 URL,也由一个逻辑所有者发放令牌;抓取工作可以分散到多台机器。热点主机可以拥有多条并发连接,额度仍由同一主机状态控制。增加 worker 只能提高跨主机并行度,无法合法突破单主机配额。
robots.txt 位于服务顶层的 /robots.txt。成功取得时遵守可解析规则;400–499 表示不可用时协议允许访问,网络错误或 500–599 导致不可达时按完全禁止处理。缓存通常不应使用超过 24 小时,除非文件不可达。默认“每主机一个并发、请求间隔一秒”只是本题的可配置初值,协议没有给出统一速率。收到 429 或 503 时读取 Retry-After,缺失时使用带抖动的指数退避并降低主机额度。
第四步:把 URL 去重与内容去重分开
规范化只做语义安全的变换:解析相对地址、移除 fragment、统一 scheme 与主机名大小写、处理默认端口和路径点段。不能全局删除查询参数或排序参数;有些站点把参数顺序和重复参数当作业务语义。页面声明的 canonical URL 可参与评分和聚类,不能直接覆盖事实 URL。
canonical_url 或其无碰撞歧义的唯一键写入分片元数据存储。Bloom filter 只作负向加速:判断“不存在”时直接尝试插入,判断“可能存在”时仍查持久化唯一键。这样假阳性只增加一次读取,不会漏抓页面。哈希碰撞时比较完整 URL 或第二指纹。
抓取后再计算内容哈希。完全相同内容可以复用对象并保留各自 URL 元数据;近重复页面使用 SimHash 一类指纹形成相似组。研究表明这类指纹适用于十亿级网页的近重复识别,但近重复信号更适合作为存储、索引或重抓取优先级输入。直接丢弃页面可能连同它独有的出链一起丢失。
第五步:用版本化状态和租约恢复故障
核心记录可以保持紧凑:
UrlState( urlid, canonicalurl, hostkey, stateversion, lastfetchat, nextfetchat, priority, etag, lastmodified, contenthash, failure_count )
HostState( hostkey, robotspolicy, robotsexpiresat, nextallowedat, inflight, backoffuntil, policy_version )
FetchLease(leaseid, urlid, urlversion, expiresat, attempt)
调度器发放带 urlversion 的有限期租约。抓取器可能在写完 HTML 后、确认任务前崩溃,租约到期会触发重复抓取。完成操作使用 (urlid, url_version) 条件写;旧租约或重复确认只返回已有结果,不再次发布索引事件。新一轮重抓取先递增版本,因此不会被上一轮幂等键误伤。
已保存 ETag 时发送 If-None-Match,否则可用 Last-Modified 发 If-Modified-Since。服务器返回 304 时更新抓取时间和下一次计划,不写空正文。DNS 超时、连接失败和 5xx 进入有上限的重试;永久 404/410 写墓碑并显著延长周期;重定向限制跳数并检测环。
第六步:让重抓取、陷阱防护和安全共享同一预算
重抓取优先级由页面价值、最近变化间隔、状态码和站点额度共同决定。每次内容变化缩短周期,连续未变化逐步拉长,并在 1 天到 30 天之间限幅。该规则能把预算从稳定页面移到变化页面,同时保留最低刷新保证。
只限制链接深度挡不住日历页、筛选组合和无限查询参数。还需要每主机日预算、URL 模板增长率、查询参数数量、重复路径段、最大响应体、解压后大小、解析时间和重定向跳数限制。触发预算时暂停对应模式并保留抽样,不影响其他主机。
抓取器面对不可信输入。DNS 解析结果要阻止回环、私网、链路本地和云元数据地址,并在连接前复核,降低 SSRF 与 DNS rebinding 风险;解析器设置内存/CPU 限额并隔离压缩炸弹和畸形 HTML。robots 规则控制抓取偏好,不承担访问授权。
第七步:用不变量和故障注入验证设计
首先做确定性调度模拟:给三个主机不同速率、robots 规则和 Retry-After,推进虚拟时钟,断言任意窗口内都不超配额,禁止路径永不发租约。再注入“HTTP 成功后进程崩溃”“租约确认丢失”“robots 缓存过期”“DNS 返回私网地址”“解析队列停摆”,确认任务可恢复、索引事件不重复、抓取阶段会背压。
容量测试至少覆盖 25,000 次/秒租约发放、100 亿 URL 键空间的分片倾斜和单个热点主机百万待办。关键指标包括:eligible lag、抓取次数与字节数、2xx/304/429/5xx 比例、每主机策略违规数、租约重试率、URL/内容重复率、robots 缓存年龄、解析积压和预算触发率。每主机策略违规数必须为零;平均吞吐达标却出现违规仍算失败。
替代方案及适用边界
几百万次/日、只抓自有站点时,可以用关系数据库的 nextfetchat 索引加 SKIP LOCKED 领取任务,主机令牌也放同一事务中,部署和排障更简单。规模升到十亿次/日后,全局索引扫描、热点更新和清理成本会成为瓶颈,两层分片 frontier 更合适。
Bloom filter 若直接代表 seen 集合,可以节省读取,但假阳性会永久牺牲覆盖率。本题把它降为缓存,把持久化唯一键保留为事实源。若业务明确接受少量漏抓,并能量化假阳性预算,才可以省掉二次确认。
高质量示范回答
“我会先锁定预算与礼貌两个不变量。10 亿次/日约等于平均 11,600 次/秒,25,000 次/秒峰值;1 亿个 URL 每日刷新加 99 亿个 URL 每 30 天刷新,一天计划量约 4.3 亿次,剩余预算容纳发现、重试和变化驱动刷新。按 200 KB 响应体算出的 200 TB/日是保守网络上界,304 会降低真实流量。
URL 进入系统时只做安全规范化,再用持久化唯一键确认 seen;Bloom filter 只减少明显未见 URL 的读取。frontier 按 scheme + authority 分片,每个分片维护主机就绪时间和主机内 URL 优先队列。领取时原子消耗主机令牌,因此多台抓取器不会共同压垮一个站点。robots 不可达时暂停该主机,429/503 按 Retry-After 或抖动退避。
抓取器拿有限期租约,做条件 GET,把 HTML 写对象存储并把解析任务交给下一阶段。解析器提取链接后回到同一发现入口。系统允许租约过期造成重复请求,完成写入由 URL 版本保护,旧租约不能覆盖新状态或重复发索引事件。内容哈希合并完全重复对象,SimHash 只影响近重复优先级,避免漏掉独有链接。
我会用虚拟时钟证明同主机请求间隔,用故障注入覆盖写完后崩溃、确认丢失、robots 过期、DNS rebinding 和解析背压。最终验收既看 25,000 次/秒峰值,也要求主机策略违规始终为零。”
常见错误
- 错误表现:只放一个全局消息队列。 失败原因:多个消费者无法共同判断同一主机的下一可用时间,吞吐越高越容易越过礼貌限制。修正方法:按
host_key固定调度所有权,使用主机就绪队列加主机内 URL 队列。 - 错误表现:用 Bloom filter 作为唯一的已见集合。 失败原因:假阳性会让从未抓取的 URL 永久消失,且无法保存状态码、版本与重抓时间。修正方法:Bloom filter 只作缓存,持久化唯一键保存事实状态。
- 错误表现:把 URL 去重等同于内容去重。 失败原因:不同 URL 可返回同一内容,同一 URL 也会随时间变化。修正方法:发现阶段做精确 URL 去重,抓取后另算内容哈希和近重复指纹。
- 错误表现:宣称 exactly-once 抓取。 失败原因:外部 HTTP 成功与内部确认无法组成原子事务,崩溃窗口仍会产生重复请求。修正方法:接受至少一次执行,用租约、版本条件写和幂等事件控制内部副作用。
- 错误表现:遇到所有 robots 错误都继续抓。 失败原因:协议区分不可用与不可达;网络错误或
5xx时需要按完全禁止处理。修正方法:实现明确状态机,并对缓存年龄、重定向和失败类别分别测试。 - 错误表现:只用最大深度防爬虫陷阱。 失败原因:同一深度就能生成无限筛选、日历和查询参数组合。修正方法:叠加主机预算、URL 模式增长、参数数、响应大小和解析时间限制。
追问及应对
如果 50% 的待抓 URL 都来自一个主机,怎样扩容?
先确认站点允许的并发与速率。主机额度固定时,增加 worker 不会增加该主机的合法吞吐,只能提高其他主机并行度。可以把该主机 URL 队列分片以降低存储热点,但所有分片仍向同一个逻辑令牌服务申请额度。若业务必须更快,需要与站点方约定专用 feed 或更高额度,并把新的政策版本化。
为什么不追求 exactly-once?
HTTP 响应已经送达而抓取器尚未确认租约时可能崩溃,外部站点不会参与内部事务。为消除这一个窗口引入分布式事务也无法回滚已发生的 GET。可执行目标是至少一次领取、可能重复抓取、内部幂等提交;监控重复率并用条件请求减小代价。
页面必须执行 JavaScript 才有正文怎么办?
普通 HTTP 抓取仍作为第一层。只有解析结果为空、站点策略允许且页面价值达到阈值时,才进入独立渲染队列。渲染器有更低并发、更严格的 CPU/内存/时间预算,并共享原主机令牌;否则昂贵渲染会绕过礼貌控制并吞掉总预算。
如何跨地域部署又不对同一主机重复施压?
按 host_key 指定 home region,只有该地域发放主机令牌;其他地域可以解析和存储。地域故障时通过带 fencing token 的租约转移所有权,旧地域恢复后必须持有新 epoch 才能继续发放。多地域同时主动调度同一主机会破坏礼貌不变量。
存储成本突然超过预算时先砍什么?
先提高条件请求命中、压缩与完全重复对象复用,再按内容价值缩短原始 HTML 保留期;URL 元数据和抓取审计不能一起删除,否则重抓、去重和合规排查会失去依据。近重复只用于降优先级或分层存储,不能未经验证直接批量删除。