![]()
大家好,我是Ai学习的老章
由于某些原因,没有人写过关于如何在短时间内爬取大量网页所需的技术细节:我最后看到的参考点是迈克尔·尼尔森在 2012 年发布的帖子 [1]。
显然自那以后很多事情都发生了变化。更大、更好、更快:CPU 的核心数增加了不少,旋转硬盘已经被 NVMe 固态硬盘取代,其 I/O 带宽接近 RAM,网络带宽也大幅增加,EC2 的实例类型从尝鲜菜单变成了整个 Rolodex。但有些事情更难:网络上的动态内容更多,内容也更重。最先进的技术发生了哪些变化?瓶颈是否已经转移?建立自己的 Google 是否仍然需要大约 4.1 万美元?我想弄清楚,因此在相似的约束条件下构建并运行了自己的网络爬虫。
问题描述
24 小时时间限制。因为我认为一天内爬取十亿个页面是可行的,而且 40 小时听起来不够酷。在我的最终爬取中, 每台机器的平均活跃时间是25.5小时,略有波动。这还不包括一些机器需要重启的几个小时。
预算为几百美元。尼尔森的爬取成本略低于 580 美元。我很幸运有足够的闲置资金,希望我的最终爬取能够符合这个预算。 最终运行仅包括25.5小时的活跃时间大约花费了462美元。在优化单节点系统的过程中,我还运行了若干小型实验(这花费要少得多),并且进行了第二次 大规模实验以看看垂直扩展我能做到多远(我早期就停止了扩展,但大致相当)。
仅HTML . 一个显而易见的问题。即使在 2017 年,网络上的大部分内容也需要 JavaScript。但我想进行一个与旧版网络爬取的直接比较,而且无论如何,这是我作为副项目进行的,没有时间添加和优化大量的 playwright 工作进程。所以,我用老方法来做:请求所有链接,但不运行任何 JS - 只是直接解析 HTML,并将标签中的所有链接添加到待爬取列表中。 我还想知道还能用这种方式爬取多少网络内容;结果发现,相当多!
礼貌。这超级重要!我读过几篇关于礼貌的文章(example[1]),提到 大规模不尊重 robots.txt 的网页抓取给管理员带来了痛苦,它们还会冒充其他代理以逃避封锁,并不断猛烈攻击端点。我遵循了前人的做法:我遵循了 到 robots.txt,添加了一个包含联系信息的说明性用户代理,维护了一个排除域名列表,根据请求我可以添加更多排除的域名,并严格遵循我的初始种子列表中的前 100 万顶级域名 为了避免击中家庭小企业网站,并在访问同一域名之间强制执行70秒的最小延迟。
容错性。这在我不知何故需要暂停并重新启动爬虫时非常重要(我确实这样做了)。此外,这对于实验也非常有帮助,因为在我的一次性的爬虫运行中 在该过程中,性能特征依赖于状态:爬虫的开始阶段看起来与稳定状态非常不同。我没有追求完美的容错性;在某些情况下可能会丢失一些 在系统崩溃或故障后的恢复中访问的站点也没有问题,因为我的爬虫本质上是对网络的一个样本。
高级设计
我最终设计的方案与系统设计面试中典型的爬虫解决方案有很大的不同,后者通常会将解析、获取、数据存储和爬虫状态等功能分散到完全独立的机器池中。相反,我选择了一组十二个高度优化的独立节点集群,每个节点都包含了所有爬虫功能,并处理不同的域名分片。我这样做是因为:
我在实验和最终运行时都受到了预算限制,因此从一开始就从小规模开始,尽可能将所有功能压缩到单台机器上,然后再逐步扩展。
我最初的目标其实是最大化单机性能,而不是后来的在一整天内爬取一亿个网页的目标(我在中途添加了这个目标)。即使在添加了这个目标之后,我仍然对垂直扩展非常乐观,直到接近我自己设定的截止日期时才不得不转向集群设计。
每个节点包含以下内容:
一个 Redis 实例存储表示爬虫状态的数据结构:
每个域的前沿,即要爬取的 URL 列表
按下次可获取时间戳排序的域队列,根据其爬取延迟 2
所有已访问 URL 的条目,每个 URL 关联一些元数据和保存内容在磁盘上的路径 3
已访问 URL 布隆过滤器,以便我们可以快速确定一个 URL 是否已经被添加到前沿中。这与已访问条目分开,因为我们不希望如果一个 URL 已经存在于前沿中但尚未获取,就将其添加到前沿中。布隆过滤器给出假正例的小概率是可以接受的,因为再次强调,我决定我的爬取是互联网的一个样本,我优化的是速度。
领域元数据,包括该域名是否被手动排除、是否为原始种子列表的一部分,以及其完整的 robots.txt 内容(+ robots 过期时间戳)。
解析队列,包含已抓取的 HTML 页面供解析器处理。
抓取进程池:
抓取器在一个简单的循环中运行:从 Redis 中弹出下一个就绪的域名,然后从其前沿中弹出下一个 URL 并抓取它(同时将域名替换到就绪队列中),然后将结果推送到解析队列中。
每个进程通过 asyncio 将高并发压缩到单个核心上;我实测发现抓取器可以支持 6000-7000 个“工作者”(独立的异步抓取循环)。请注意,这并没有接近饱和网络带宽:瓶颈是 CPU,我稍后会详细说明。异步设计是一种用户空间的多任务处理方式,并且由于它完全避免了上下文切换,因此长期以来一直很受欢迎,适用于高并发系统(Python Tornado 早在 2009 年就问世了!)。
抓取器和解析器还维护了重要的域名数据(如 robots.txt 内容)的最近最少使用(LRU)缓存,以减少对 redis 的压力。
解析进程池:
解析器的操作与抓取器类似;每个解析器由 80 个异步工人组成,从解析队列中获取下一个项目,解析 HTML 内容,提取链接并写回到相应的域名边界中,然后将保存的内容写入持久化存储。并发度较低的原因是因为解析是 CPU 密集型操作而不是 I/O 密集型操作(尽管解析器仍然需要与 Redis 通信,并且偶尔需要抓取 robots.txt),80 个工人足以饱和 CPU。
其他:
对于持久化存储,我遵循了现有技术,使用了实例存储。教科书中的面试解决方案会告诉你使用 S3;我考虑过这个问题,但 S3 不仅按请求收费,还会按比例计费 GB 月。假设每页 250KB(总共 250TB),存储 10 亿页只需一天,标准层级的费用为
0.022*1000*250*(1/30)+0.005*1e6= $5183.33,而 Express 层级的费用为0.11*1000*250*(1/30)+0.00113*1e6= , 这 比 我 实 际 花 费 的 要 高 出 一 个 数 量 级 ! 即 使 不 考 虑 所 有 的 费 用 , 标 准 层 级 的 费 用 也 会 达 到 183.33,Express 层级的费用则为 $916.67。 即使我将页面批量处理,存储一天的数据也不会具有竞争力。我最终选择了 i7i 系列的存储优化实例,并截断了我的保存页面以确保它们能够适应。显然,在实际系统中截断页面不是一个好主意。 爬虫;我考虑在解析器中使用快速压缩方法,比如 snappy,或者使用一个较慢的后台压缩器,但没有时间尝试。
池中的第一个获取器进程也被指定为“领导者”,并会定期将指标写入本地的 prometheus 数据库。在实际系统中,最好为所有节点使用一个单一的指标数据库。
最终的集群由以下内容组成:
12节点
每台机器,配备 16 个 vCPU、128GB 内存、10Gbps 网络带宽和 3750GB 实例存储
每个节点围绕 1 个 Redis 进程+9 个抓取器进程+6 个解析器进程
节点集群中的节点之间没有跨节点通信,种子域名列表在节点之间进行了切片。由于我只爬取种子域名,这意味着每个节点爬取互联网的不重叠区域。这主要是因为我没有足够的时间让我的另一种设计(带有跨通信)运行起来。
为什么只有 12 个节点?我在一次实验中发现,将种子域名切分得太细会导致严重的热点切片问题,一些处理非常热门域名的节点工作量很大,而其他节点则很快就完成了。我还停止了每个 Redis 进程中的抓取器和解析器池的垂直扩展,总共不超过 15 个进程,因为 Redis 开始达到每秒 120 次操作,我读到超过这个数量会导致问题(如果有更多时间,我会进行实验以找到确切的饱和点)。
调查的替代方案
我在设计时尝试了几种不同的方案,最终确定了上面的那个。看来最近的爬虫 [1][2][3] 大多是这样的。 使用快速的内存数据存储,比如 Redis,是有充分理由的。我曾使用 SQLite 和 PostgreSQL 作为后端进行小规模原型开发,但要使前沿查询变得快速,尽管在概念上很简单,但实际上却非常复杂。 数据结构的简洁性。AI 编码工具在这方面帮助了很多;我将在另一篇博客中详细说明。
我也尽力让单节点垂直扩展工作;我对这一点持乐观态度,因为过去限制大规模爬取的许多硬件瓶颈似乎已经消失了。例如,AWS 提供了i7i.48xlarge实例,这实际上就是 12i7i.4xlarge机器堆叠在一起。它的网络带宽要少得多(100Gbps 对比 12x25Gbps),但在需要达到每天爬取 10 亿个页面的吞吐量时,即使每页是 1MB(实际情况并非如此),我使用的带宽也只有8*1e6*(1e9/86400)=92Gbps,还有余量用于出站流量(当然,每请求并非 1MB!)
我尝试的第一个大规模设计将所有内容打包到了一个 i7i.48xlarge 上,将进程组织成“pod”,这与我最终集群中的节点非常相似(每组 16 个进程, 单个 Redis 实例),但允许跨通信。第二种设计去掉了跨通信,只是独立运行容器;在这一设计下进行的大型运行结果令人失望。 (整个系统只管理了每秒1000页,这略高于最终集群中单个节点的吞吐量)。时间用完了,所以我放弃了,转而采用了水平扩展。 我怀疑限制因素可能是软件(操作系统资源)而不是硬件。
学习经验 解析是很大的瓶颈
我对解析环节成为瓶颈感到非常惊讶。在最终系统中,我只需要分配2:3的解析到获取比例的进程,但这并不是一开始就这样的,而是经过多次迭代才达到的。实际上,在我最初构建的专门用于解析/获取的系统中,为了跟上1个部分闲置的获取器(该获取器有1000个工人,每秒处理55页),需要2个解析器。这真的让我担心解析会让我无法在预算内达到一亿!
这真的让我感到惊讶,因为这意味着我的四核节点在吞吐量上还不如 2012 年一个性能较弱的四核盒子。性能分析显示解析是瓶颈,但我使用的是 Gemini 建议的在 2012 年流行的 lxml 解析库。最终我搞清楚了,这是因为平均网页的大小已经大幅增加:测试运行的指标显示,未压缩的页面 P50 大小现在是 138KB5,而平均值甚至更大,达到了 242KB,远远超过了尼尔森估计的平均值 2012 年!51KB!
最有助于的是两件事:
我从
lxml切换到了selectolax,这是一个更新的库,它封装了 Lexbor,这是一个专门为 HTML5 设计的现代 C++解析器。页面声称它可以 比 lxml 快 30 倍。虽然不是整体快 30 倍,但这是一个巨大的提升。我也在传递给解析器之前将页面内容截断为 250KB。由于截断阈值高于平均值且几乎为中位数的两倍,我认为尼尔森 [1] 的观点仍然适用:我们捕获了大多数网页的完整内容,这对大多数应用来说应该是足够的。
通过这样的设置,我能够实现单个解析进程每秒解析约160页,这使得我的最终设置能够使用9倍的抓取器和6倍的解析器,以每秒解析约950页。
获取: 更容易但也更难
许多爬虫相关的处理都将网络带宽和 DNS 视为重要的瓶颈。例如,在研究这个话题时,我给 CMU 的杰米·卡尔兰发邮件询问了 2009 年的 Sapphire 项目 [4];卡尔兰教授告诉我 DNS 解析吞吐量是一个瓶颈,而在 2012 年使用 CMU 校园网络进行的后续爬虫中,爬虫吞吐量必须被限制,以避免使用所有带宽。大约一年前的[2]埃文·金的这个采访分析 也建议了 DNS 解析的优化。[3]
对于我的爬虫,DNS 完全没有出现。我认为这是因为我将爬虫限制在了顶级约 100 万个域名的种子列表中。集群中的任何节点的网络带宽也没有接近饱和;大多数节点在稳定状态下平均约为 1 GB/s(8 Gbps),但的最大带宽是 25 Gbps。数据中心的带宽现在非常充足,尤其是对于 AI:AWS 提供了一个 P6e-GB200[4] 实例,带宽高达 28.8 太比特 !
话说回来,获取数据的一部分变得更加困难:现在使用 SSL 的网站比十年前多了很多。这在性能分析中表现得非常清楚,SSL 握手计算占据了最昂贵的函数调用,平均占用了所有 CPU 时间的 25%,这意味着,在网络带宽尚未饱和的情况下,获取数据的瓶颈已经变成了 CPU,而不是网络!
![]()
SSL growth
从 https://letsencrypt.org/stats/[5] - 2014 年 Firefox 的 SSL 加载量为 30%,到 2025 年超过 80%。
大规模爬取
![]()
![]()
爬虫早期某个节点的指标。 Grafana 仪表盘中的部分单位有误(例如,错误率和解析队列大小错误地使用了“字节”)。
在运行包含 12 个节点的大规模爬虫之前,我最大的实验是在一个上运行几个小时,所以从这次运行中有很多意想不到的惊喜, 运行过程中出现的规模跃升,我整个周日从日出到日落(甚至更晚)都在为自己的运行提供支持,监控指标并及时修复问题 问题。其中一些是愚蠢的操作失误,比如忘记设置日志轮转,导致根卷空间不足,但最大的问题是内存增长导致的。 到前沿。
这具体与我的设计有关,将所有前沿数据存放在内存中。在之前的、规模较小的爬取任务中我也遇到过内存问题,但问题出现在 HTTP 客户端或已访问条目等其他组件上。我已经计算出了这些组件在 10 亿个已访问页面时所需的内存余量,但没有预料到某些非常热门的领域前沿数据会增长到数十 GB(数亿或数十亿个 URL),在运行过程中,我的节点开始频繁宕机。我不得不手动干预,重启无响应的机器并截断前沿数据。好在容错机制使得修复后恢复变得容易。
问题域名是拖慢网站加载的吗?从我看来,大多数都是非常受欢迎的网站,有大量的链接。例如,yahoo.com和wikipedia.org就是其中一些。另一个是“cosplayfu”网站,初看像是一个奇怪的购物网站,但经过网络搜索后看起来是合法的。无论如何,最麻烦的域名 都被添加到了我的手动排除列表中。
讨论 理论 vs. 实践
我的爬虫与埃 van·金在 HelloInterview 分析中的教科书式解决方案相比有何不同? 这里感兴趣的指标可能是 King 的“粗略估计”,即 5 台机器可以在 5 天内抓取 100 亿个页面。在这个声明中,这些机器完全用于获取页面, 解析器和前沿数据存储位于其他地方。除了假设每台机器有 400Gbps 的带宽,并实现 30%的利用率外,没有详细说明每台机器的硬件配置。
利用率大致合适;我的节点提供的带宽只有 25Gbps,但在稳定状态下我确实得到了约 32%的利用率,带入 8Gbps 的输入输出。不过,我只在每台机器上启用了 9/16 个核心来抓取数据,根据简单的扩展计算,我本可以达到 53%的网络利用率。同样地,因为我用了 12 台机器在大约一天内爬取了 10 亿个页面,理论上使用 6.75 台仅用于抓取的机器也可以达到每天 10 亿个页面的吞吐量。如果我们假设从 i7i.4xlarge 到 i7i.8xlarge 的直接扩展,这意味着 6.75 台双倍大小的 仅用于抓取的机器可以在5天内爬取10亿个页面。所以国王的数字并不太远,但可能需要比我系统中做的更多优化!
现在怎么办?
老实说,我惊讶于如此多的网页在不运行 JS 的情况下仍然可以访问。这太棒了!我通过这次爬取发现了像 ancientfaces.com[6] 这样的有趣网站。 下载的页面中,即使是许多可爬取的网站如 GitHub,也没有真正有意义的标记文本内容;所有内容都嵌在巨大的 presumably 是打算在客户端由所谓的“轻量级”JS 脚本渲染的字符串。我认为有趣的未来工作将涉及解决这个问题: 当我们实际上需要动态渲染页面时,大规模爬虫会是什么样子?我怀疑同样的规模将会更加昂贵。
另一个问题是:我爬取的十亿个网页的形状和分布是什么样的?我保留了一些样本,但还没有时间进行任何分析。了解一些基本的元数据信息会很有趣,比如实际存活的 URL 数量与已失效的 URL 数量相比是多少,有多少是 HTML 内容类型而非多媒体等。
最后,本文讨论了过去十年间网络的一些重大变化,但网络的格局再次发生变化。大规模资源支持下的积极抓取/爬取并不是新鲜事(Facebook 曾因 OpenGraph 抓取[7] 而陷入困境),但随着人工智能的发展,这种情况变得更加严重。我非常 认真地遵循了 robots.txt 等规范,但许多爬虫并没有这样做,互联网开始发展出防御措施。Cloudflare 的实验性 按次付费抓取[8] 功能是市场上的一种新服务,可能会有很大帮助。
原文:https://andrewkchan.dev/posts/crawler.html
参考资料
example: https://drewdevault.com/2025/03/17/2025-03-17-Stop-externalizing-your-costs-on-me.html
杰米·卡尔兰发邮件询问了 2009 年的 Sapphire 项目 [4];卡尔兰教授告诉我 DNS 解析吞吐量是一个瓶颈,而在 2012 年使用 CMU 校园网络进行的后续爬虫中,爬虫吞吐量必须被限制,以避免使用所有带宽。大约一年前的: https://www.cs.cmu.edu/~callan/
埃文·金的这个采访分析 也建议了 DNS 解析的优化。: https://www.hellointerview.com/learn/system-design/problem-breakdowns/web-crawler
P6e-GB200: https://aws.amazon.com/ec2/instance-types/p6/
[5]
https://letsencrypt.org/stats/: https://letsencrypt.org/stats/
[6]
ancientfaces.com: http://ancientfaces.com/
[7]
OpenGraph 抓取: https://news.ycombinator.com/item?id=23490367
[8]
按次付费抓取: https://blog.cloudflare.com/introducing-pay-per-crawl/
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.