系统设计实战 27:设计网络爬虫(Web Crawler)

张开发
2026/4/6 5:53:46 15 分钟阅读

分享文章

系统设计实战 27:设计网络爬虫(Web Crawler)
系统设计实战 27设计网络爬虫Web Crawler摘要网络爬虫是搜索引擎的基础需要高效抓取数十亿网页。本文深入剖析 URL 调度策略、分布式抓取架构、去重机制布隆过滤器、Robots 协议遵守、增量抓取优化并提供完整的 Java 实现。 场景引入Google 如何发现互联网上新出现的网页一个爬虫系统每天要抓取数十亿个 URL。核心挑战URL 去重已经抓过的 URL 不要重复抓如何用布隆过滤器高效判断礼貌策略不能把目标网站爬挂了如何控制抓取频率优先级调度新闻网站要频繁抓取个人博客可以低频如何设计优先级队列 场景引入你打开手机准备使用设计网络爬虫服务。看似简单的操作背后系统面临三大核心挑战挑战一高并发——如何在百万级 QPS 下保持低延迟挑战二高可用——如何在节点故障时保证服务不中断挑战三数据一致性——如何在分布式环境下保证数据正确一、问题背景1.1 核心需求需求说明广度优先抓取优先抓取重要页面URL 去重避免重复抓取同一页面Robots 遵守尊重网站的 robots.txt礼貌抓取控制对同一域名的请求频率增量更新只抓取有变化的页面分布式扩展支持数百台机器并行抓取1.2 容量估算指标数值目标网页总数100 亿每日抓取量10 亿页平均页面大小100KB每日下载量100TB抓取 QPS~12000二、整体架构┌─────────────────────────────────────────────────────┐ │ URL 调度器 (Frontier) │ │ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ │ │ 优先队列 │ │ 域名队列 │ │ 礼貌性调度器 │ │ │ └────┬─────┘ └────┬─────┘ └────────┬─────────┘ │ └───────┼──────────────┼─────────────────┼────────────┘ │ │ │ ▼ ▼ ▼ ┌─────────────────────────────────────────────────────┐ │ 分布式抓取工作器集群 │ │ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ │ │ DNS 解析 │→│ HTTP 下载 │→│ 内容解析/提取 │ │ │ └──────────┘ └──────────┘ └──────────────────┘ │ └───────────────────────┬─────────────────────────────┘ │ ┌───────────────┼───────────────┐ ▼ ▼ ▼ ┌──────────────┐ ┌───────────┐ ┌──────────────┐ │ URL 去重 │ │ 内容存储 │ │ 链接提取 │ │ (布隆过滤器) │ │ (HDFS/S3) │ │ (新 URL) │ └──────────────┘ └───────────┘ └──────┬───────┘ │ ▼ 回到 URL 调度器三、核心模块实现3.1 URL 调度器Frontier/** * URL 调度器 * * 两级队列优先级队列 → 域名队列 * 保证重要页面优先抓取同时控制对同一域名的请求频率 */ServicepublicclassUrlFrontier{// 优先级队列按页面重要性排序privatefinalPriorityBlockingQueueCrawlUrlpriorityQueuenewPriorityBlockingQueue(10000,Comparator.comparingInt(CrawlUrl::getPriority).reversed());// 域名队列每个域名一个队列控制抓取频率privatefinalConcurrentHashMapString,LinkedBlockingQueueCrawlUrldomainQueuesnewConcurrentHashMap();// 域名最后抓取时间礼貌性控制privatefinalConcurrentHashMapString,LongdomainLastFetchTimenewConcurrentHashMap();privatestaticfinallongPOLITENESS_DELAY_MS1000;// 同一域名间隔 1 秒AutowiredprivateBloomFilterServicebloomFilter;/** * 添加 URL 到调度器 */publicvoidaddUrl(Stringurl,intpriority){// 1. 布隆过滤器去重if(bloomFilter.mightContain(url)){return;// 可能已抓取过}bloomFilter.put(url);// 2. 加入优先级队列StringdomainextractDomain(url);CrawlUrlcrawlUrlnewCrawlUrl(url,domain,priority,System.currentTimeMillis());priorityQueue.offer(crawlUrl);}/** * 获取下一个要抓取的 URL考虑礼貌性 */publicCrawlUrlgetNextUrl()throwsInterruptedException{while(true){CrawlUrlurlpriorityQueue.take();Stringdomainurl.getDomain();// 检查礼貌性延迟LonglastFetchdomainLastFetchTime.get(domain);if(lastFetch!null){longelapsedSystem.currentTimeMillis()-lastFetch;if(elapsedPOLITENESS_DELAY_MS){// 还没到时间放回队列取下一个priorityQueue.offer(url);Thread.sleep(100);continue;}}domainLastFetchTime.put(domain,System.currentTimeMillis());returnurl;}}}3.2 抓取工作器/** * 抓取工作器 */ServicepublicclassCrawlWorker{AutowiredprivateUrlFrontierfrontier;AutowiredprivateRobotsServicerobotsService;AutowiredprivateContentStoragecontentStorage;/** * 抓取循环 */publicvoidcrawlLoop(){while(true){try{CrawlUrlcrawlUrlfrontier.getNextUrl();// 1. 检查 robots.txtif(!robotsService.isAllowed(crawlUrl.getUrl())){continue;}// 2. HTTP 下载CrawlResponseresponsehttpClient.fetch(crawlUrl.getUrl(),Duration.ofSeconds(10));if(response.getStatusCode()!200){handleError(crawlUrl,response);continue;}// 3. 内容解析Stringhtmlresponse.getBody();ParsedPagepagehtmlParser.parse(html,crawlUrl.getUrl());// 4. 存储内容contentStorage.store(crawlUrl.getUrl(),page);// 5. 提取新链接加入调度器for(Stringlink:page.getOutLinks()){StringnormalizedUrlnormalizeUrl(link);if(normalizedUrl!null){intprioritycalculatePriority(normalizedUrl);frontier.addUrl(normalizedUrl,priority);}}}catch(Exceptione){log.error(抓取异常,e);}}}/** * 计算 URL 优先级 */privateintcalculatePriority(Stringurl){intpriority5;// 默认优先级if(url.endsWith(/))priority2;// 首页优先if(url.contains(/news/))priority1;// 新闻页优先if(url.split(/).length6)priority-2;// 深层页面降低returnMath.max(1,Math.min(10,priority));}}3.3 布隆过滤器去重/** * 分布式布隆过滤器基于 Redis */ServicepublicclassBloomFilterService{AutowiredprivateStringRedisTemplateredisTemplate;privatestaticfinalStringBF_KEYcrawler:bloom;privatestaticfinalintEXPECTED_INSERTIONS10_000_000_000LInteger.MAX_VALUE?Integer.MAX_VALUE:1_000_000_000;// 10 亿privatestaticfinalintNUM_HASH_FUNCTIONS7;privatestaticfinallongBIT_SIZE10L*1_000_000_000L;// 100 亿位publicbooleanmightContain(Stringurl){long[]hashesgetHashes(url);// 使用 Lua 脚本原子检查多个位StringBuilderluanewStringBuilder(local key KEYS[1]\n);for(inti0;iNUM_HASH_FUNCTIONS;i){lua.append(String.format(if redis.call(getbit, key, %d) 0 then return 0 end\n,Math.abs(hashes[i]%BIT_SIZE)));}lua.append(return 1);DefaultRedisScriptLongscriptnewDefaultRedisScript(lua.toString(),Long.class);LongresultredisTemplate.execute(script,Collections.singletonList(BF_KEY));returnresult!nullresult1;}publicvoidput(Stringurl){long[]hashesgetHashes(url);for(longhash:hashes){redisTemplate.opsForValue().setBit(BF_KEY,Math.abs(hash%BIT_SIZE),true);}}privatelong[]getHashes(Stringurl){longhash1Hashing.murmur3_128().hashString(url,StandardCharsets.UTF_8).asLong();longhash2Hashing.sipHash24().hashString(url,StandardCharsets.UTF_8).asLong();long[]hashesnewlong[NUM_HASH_FUNCTIONS];for(inti0;iNUM_HASH_FUNCTIONS;i){hashes[i]hash1i*hash2;}returnhashes;}}四、性能优化优化点方案效果URL 去重布隆过滤器内存占用低O(1) 查询礼貌抓取域名级别频率控制不被封禁DNS 缓存本地 DNS 缓存减少 DNS 查询延迟增量抓取If-Modified-Since / ETag减少无效下载分布式按域名哈希分配到不同工作器线性扩展 方案对比维度BFS 爬取优先级爬取 (本方案)增量爬取覆盖率高中低时效性低高高资源消耗高中低适用场景全网索引搜索引擎监控/比价五、高频面试问题Q1如何避免重复抓取答布隆过滤器。将已抓取的 URL 加入布隆过滤器新 URL 先查询是否存在。有一定误判率约 1%但不会漏判。对于需要精确去重的场景可以用 Redis Set 或数据库辅助。Q2如何实现礼貌抓取答(1) 遵守 robots.txt(2) 同一域名的请求间隔至少 1 秒(3) 使用域名级别的队列每个域名独立调度(4) 设置合理的 User-Agent。Q3如何处理动态渲染的页面SPA答使用 Headless Browser如 Puppeteer/Playwright渲染 JavaScript 后再提取内容。但成本高只对重要页面使用。大部分页面仍用普通 HTTP 抓取。六、架构设计检查清单检查项状态URL 优先级调度✅布隆过滤器去重✅礼貌性抓取控制✅Robots.txt 遵守✅分布式工作器✅增量抓取✅监控告警✅预防监控主从延迟延迟 1 秒时告警 架构演进路径阶段一单机版 MVP用户量 10 万单体应用 单机数据库快速验证核心功能适用场景产品早期快速迭代阶段二基础版分布式用户量 10 万 → 100 万应用层水平扩展 数据库主从分离 Redis 缓存引入消息队列解耦异步任务适用场景业务增长期阶段三生产级高可用用户量 100 万微服务拆分独立部署和扩缩容数据库分库分表 多机房部署全链路监控 自动化运维 异地容灾⚖️ 关键 Trade-off 分析 Trade-off 1一致性 vs 可用性选择强一致CP适用于金融交易、库存扣减等不能出错的场景选择高可用AP适用于社交动态、推荐等允许短暂不一致的场景本系统选择核心路径强一致非核心路径最终一致 Trade-off 2实时性 vs 吞吐量同步处理延迟低但吞吐受限适用于核心交互路径异步处理吞吐高但增加延迟适用于后台计算和批处理本系统选择核心路径同步保证体验非核心路径异步提升吞吐 故障处理与容灾故障场景一服务节点宕机现象请求超时健康检查失败应对自动摘除故障节点流量切换到健康实例降级策略保证核心功能可用触发自动重启和恢复故障场景二数据不一致现象主从延迟导致读到旧数据缓存与数据库不一致应对关键读走主库强一致场景Cache-Aside TTL 保证最终一致异步补偿任务定时对账修复故障场景三下游服务超时现象依赖的下游服务响应变慢请求堆积应对熔断机制超时率 50% 自动熔断降级返回兜底数据重试策略指数退避 最大重试次数⚖️ 关键 Trade-off 分析 Trade-off 1一致性 vs 可用性强一致CP适用于金融交易等不能出错的场景高可用AP适用于社交动态等允许短暂不一致的场景本系统选择核心路径强一致非核心路径最终一致 Trade-off 2同步 vs 异步同步处理延迟低但吞吐受限适用于核心交互路径异步处理吞吐高但增加延迟适用于后台计算本系统选择核心路径同步非核心路径异步 监控与可观测性关键 Metrics指标告警阈值说明P99 延迟 500ms → Warning核心接口响应时间错误率 1% → Critical5xx 错误占比QPS超过容量 80% → Warning扩容预警可观测性三支柱MetricsPrometheus Grafana 采集系统和业务指标LoggingELK 集中日志结构化日志便于检索TracingJaeger 分布式链路追踪定位跨服务瓶颈核心权衡在一致性 vs 可用性的 Trade-off 中本系统选择核心路径强一致、非核心路径最终一致的混合策略。这是 CP vs AP 取舍的工程实践。Q4如何做容量规划基于业务增长预测提前规划。无状态服务水平扩展加机器。有状态服务通过分片扩展。弹性伸缩基于 CPU/QPS 指标自动扩缩容。定期压测验证系统容量上限。Q5安全设计有哪些考虑OAuth2 JWT 认证授权。敏感数据 AES-256 加密存储TLS 传输加密。RBAC 权限模型最小权限原则。审计日志记录关键操作。限流 WAF SQL 注入/XSS 防护。

更多文章