You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

210 lines
18 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 04 | 网页爬虫设计:如何下载千亿级网页?
你好,我是李智慧。
在互联网早期,网络爬虫仅仅应用在搜索引擎中。随着大数据时代的到来,数据存储和计算越来越廉价和高效,越来越多的企业开始利用网络爬虫来获取外部数据。例如:获取政府公开数据以进行统计分析;获取公开资讯以进行舆情和热点追踪;获取竞争对手数据以进行产品和营销优化等等。
网络爬虫有时候也被称为网络机器人或者网络蜘蛛。我们准备开发一个全网爬虫爬取全中文互联网的公开网页以构建搜索引擎和进行数据分析。爬虫名称为“Bajie八戒”。
Bajie的技术挑战包括如何不重复地获取并存储全网海量URL如何保证爬虫可以快速爬取全网网页但又不会给目标网站带来巨大的并发压力接下来我们就来看看Bajie的需求与技术架构。
## 需求分析
Bajie的功能比较简单这里不再赘述。
#### 性能指标估算
因为互联网网页会不断产生所以全网爬虫Bajie也是一个持续运行的系统。根据设计目标Bajie需要每个月从互联网爬取的网页数为20亿个平均每个页面500KB且网页需存储20年。
Bajie的存储量和TPS系统吞吐量估算如下。
* **每月新增存储量**
估计平均每个页面500KB那么每个月需要新增存储1PB。
$\\small 20亿\\times500KB=1PB$
* **总存储空间**
网页存储有效期20年那么需要总存储空间240PB。
$\\small 1PB\\times12个月\\times20年=240PB$
* **TPS**
Bajie的TPS应为800。
$\\small 20亿\\div30\\times24\\times60\\times60\\approx800$
#### 非功能需求
Bajie需要满足的非功能需求如下。
1. 伸缩性当未来需要增加每月爬取的网页数时Bajie可以灵活部署扩大集群规模增强其爬取网页的速度。也就是说Bajie必须是一个分布式爬虫。
2. 健壮性互联网是一个开放的世界也是一个混乱的世界服务器可能会宕机网站可能失去响应网页HTML可能是错误的链接可能有陷阱……所以Bajie应该能够面对各种异常正常运行。
3. 去重一方面需要对超链接URL去重相同的URL不需要重复下载另一方面还要对内容去重不同URL但是相同内容的页面也不需要重复存储。
4. 扩展性当前只需要爬取HTML页面即可将来可能会扩展到图片、视频、文档等内容页面。
此外Bajie必须是“礼貌的”。爬虫爬取页面实际上就是对目标服务器的一次访问如果高并发地进行访问可能会对目标服务器造成比较大的负载压力甚至会被目标服务器判定为DoS攻击。因此Bajie要避免对同一个域名进行并发爬取还要根据目标服务器的承载能力增加访问延迟即在两次爬取访问之间增加等待时间。
并且Bajie还需要遵循互联网爬虫协议即目标网站的robots.txt协议不爬取目标网站禁止爬取的内容。比如www.zhihu.com的robots.txt内容片段如下。
```plain
User-agent: bingbot
Disallow: /appview/
Disallow: /login
Disallow: /logout
Disallow: /resetpassword
Disallow: /terms
Disallow: /search
Allow: /search-special
Disallow: /notifications
Disallow: /settings
Disallow: /inbox
Disallow: /admin_inbox
Disallow: /*?guide*
```
Zhihu约定Bing爬虫可以访问和不可以访问的路径都列在robots.txt中其他的Google爬虫等也在robots.txt中列明。
robots.txt还可以直接禁止某个爬虫比如淘宝就禁止了百度爬虫淘宝的robots.txt如下。
```plain
User-agent: Baiduspider
Disallow: /
User-agent: baiduspider
Disallow: /
```
淘宝禁止百度爬虫访问根目录,也就是禁止百度爬取该网站所有页面。
robots.txt在域名根目录下如www.taobao.com/robots.txt。Bajie应该首先获取目标网站的robots.txt根据爬虫协议构建要爬取的URL超链接列表。
## 概要设计
Bajie的设计目标是爬取数千亿的互联网页那么Bajie首先需要得到这千亿级网页的URL该如何获得呢
全世界的互联网页面事实上是一个通过超链接连接的巨大网络其中每个页面都包含一些指向其他页面的URL链接这些有指向的链接将全部网页构成一个有向网络图。如下图所示每个节点是一个网页每条有向的边就是一个超链接。
![图片](https://static001.geekbang.org/resource/image/70/2f/70737d192e53fbfdbc42a4c5a6bcf12f.jpg?wh=1920x910)
上图中www.a.com包含两个超链接分别是www.b.com和www.c.com对应图中就是节点www.a.com指向节点www.b.com和节点www.c.com的边。同样地www.b.com节点也会指向www.d.com节点。
如果我们从这个图中的某个节点开始遍历,根据节点中包含的链接再遍历其指向的节点,再从这些新节点遍历其指向的节点,如此下去,理论上可以遍历互联网上的全部网页。而**将遍历到的网页下载保存起来**,就是爬虫的主要工作。
所以Bajie不需要事先知道数千亿的URL然后再去下载。Bajie只需要知道一小部分URL也就是所谓的种子URL然后从这些种子URL开始遍历就可以得到全世界的URL并下载全世界的网页。
Bajie的处理流程活动图如下。
![图片](https://static001.geekbang.org/resource/image/72/0f/7278302442830b4576bbc04d36171d0f.jpg?wh=1920x916)
首先Bajie需要构建种子URL它们就是遍历整个互联网页面有向图的起点。种子URL将影响遍历的范围和效率所以我们通常选择比较知名的网站的主要页面比如首页作为种子URL。
然后URL调度器从种子URL中选择一些URL进行处理。后面将在详细介绍中说明URL调度器的算法原理。
Bajie对选择出来的URL经过域名解析后下载得到HTML页面内容进而解析HTML页面分析该内容是否已经在爬虫系统中存在。因为在互联网世界中大约有三分之一的内容是重复的下载重复的内容就是在浪费计算和存储资源。如果内容已存在就丢弃该重复内容继续从URL调度器获取URL如果不存在就将该HTML页面写入HDFS存储系统。
然后Bajie进一步从已存储的HTML中提取其内部包含的超链接URL分析这些URL是否满足过滤条件即判断URL是否在黑名单中以及URL指向的目标文件类型是否是爬虫要爬取的类型。
如果HTML中的某些URL满足过滤条件那么就丢弃这些URL如果不满足过滤条件那么进一步判断这些URL是否已经存在如果已经存在就丢弃该URL如果不存在就记录到待下载URL集合。URL调度器从待下载URL集合中选择一批URL继续上面的处理过程。
这里需要注意想判断URL是否已经存在就要判断这个URL是否已经在待下载URL集合中。此外还需要判断这个URL是否已经下载得到HTML内容了。只有既不是待下载也没被下载过的URL才会被写入待下载URL集合。
可以看到,在爬虫的活动图里是没有结束点的,从开始启动,就不停地下载互联网的页面,永不停息。其中,**URL调度器是整个爬虫系统的中枢和核心也是整个爬虫的驱动器**。爬虫就是靠着URL调度器源源不断地选择URL然后有节奏、可控地下载了整个互联网所以**URL调度器也是爬虫的策略中心**。
据此Bajie的部署图如下。
![图片](https://static001.geekbang.org/resource/image/a6/25/a613bd705567d0ba9db7d50ff2830c25.jpg?wh=1920x1005)
Bajie系统中主要有两类服务器一类是URL调度器服务器一类是URL下载处理服务器集群它是一个分布式集群。
URL调度器从种子URL或待下载URL集合中载入URL再根据调度算法选择一批URL发送给URL下载处理服务器集群。这个下载处理服务器集群是由多台服务器组成的根据需要达到的TPS集群规模可以进行动态伸缩以实现需求中的伸缩性要求。
每台URL下载处理服务器先得到分配给自己的一组URL再启动多个线程其中每个线程处理一个URL按照前面的流程调用域名解析组件、HTML下载组件、HTML内容解析组件、内容去重组件、URL提取组件、URL过滤组件、URL去重组件最终将HTML内容写入HDFS并将待下载URL写入待下载URL集合文件。
#### **分布式爬虫**
需要注意的是URL下载处理服务器采用分布式集群部署主要是为了提高系统的吞吐能力使系统满足伸缩性需求。而URL调度器则只需要采用一台高性能的服务器单机部署即可。
事实上单机URL调度器也完全能够满足目前800TPS的负载压力以及将来的伸缩要求。因为800TPS对于URL调度器而言其实就是每秒产生800个URL而已计算压力并不大单台服务器完全能够满足。
同时URL调度器也不需要考虑单服务器宕机导致的可用性问题因为爬虫并不是一个实时在线系统如果URL调度器宕机只需要重新启动即可并不需要多机部署高可用集群。
相对应地每个URL在URL下载处理服务器上的计算负载压力要大得多需要分布式集群处理也因此大规模爬虫被称为分布式爬虫Bajie就是一个分布式爬虫。
## 详细设计
Bajie详细设计关注3个技术关键点URL调度器算法、去重算法、高可用设计。
#### **URL调度器算法**
URL调度器需要从待下载URL集合中选取一部分URL进行排序然后分发给URL下载服务器去下载。待下载URL集合中的URL是从下载的HTML页面里提取出来然后进行过滤、去重得到的。一个HTML页面通常包含多个URL每个URL又对应一个页面因此URL集合数量会随着页面不断下载而指数级增加。
待下载URL数量将远远大于系统的下载能力**URL调度器就需要决定当前先下载哪些URL**。
如果调度器一段时间内选择的都是同一个域名的URL那就意味着我们的爬虫将以800 TPS的高并发访问同一个网站。目标网站可能会把爬虫判定为DoS攻击从而拒绝请求更严重的是高并发的访问压力可能导致目标网站负载过高系统崩溃。这样的爬虫是“不礼貌”的也不是Bajie的设计目标。
前面说过网页之间的链接关系构成一个有向图因此我们可以按照图的遍历算法选择URL。图的遍历算法有深度优先和广度优先两种深度优先就是从一个URL开始访问网页后从里面提取第一个URL然后再访问该URL的页面再提取第一个URL如此不断深入。
深度优先需要维护较为复杂的数据结构,而且太深的下载深度导致下载的页面非常分散,不利于我们构建搜索引擎和数据分析。所以我们没有使用深度优先算法。
那广度优先算法如何呢广度优先就是从一个URL开始访问网页后从中得到N个URL然后顺序访问这个N个URL的页面然后再从这N个页面中提取URL如此不断深入。显然广度优先实现更加简单获取的页面也比较有关联性。
图的广度优先算法通常采用**队列**来实现。首先URL调度器从队列头出队列dequeue取一个URL交给URL下载服务器下载得到HTML再从HTML中提取得到若干个URL入队列enqueue到队列尾URL调度器再从队列头出队列dequeue取一个URL……如此往复持续不断地访问全部互联网页这就是互联网的广度优先遍历。
事实上由于待下载URL集合存储在文件中URL下载服务器只需要向待下载URL集合文件尾部追加URL记录而URL调度器只需要从文件头顺序读取URL这样就天然实现了先进先出的广度优先算法如下图。
![图片](https://static001.geekbang.org/resource/image/d6/e0/d67140e0d7yyf7011f9a3a9e515e91e0.jpg?wh=1920x916)
但是广度优先搜索算法可能会导致爬虫一段时间内总是访问同一个网站因为一个HTML页面内的链接常常是指向同一个网站的这样就会使爬虫“不礼貌”。
通常我们针对一个网站一次只下载一个页面所以URL调度器需要将待下载URL根据域名进行分类。此外不同网站的信息质量也有高低之分爬虫应该优先爬取那些高质量的网站。优先级和域名都可以使用不同队列来区分如下图。
![图片](https://static001.geekbang.org/resource/image/a5/3b/a5e8d1dd4ab4b622b79c46066004ca3b.jpg?wh=1920x1544)
首先优先级分类器会根据网页内容质量将域名分类后面专栏会讲PageRank质量排名算法并为不同质量等级的域名设置不同的优先级然后将不同优先级记录在“域名优先级表”中。
接下来按照广度优先算法URL列表会从待下载URL集合文件中装载进来。根据“域名优先级表”中的优先级顺序优先级分类器会将URL写入不同的队列中。
下一步优先级队列选择器会根据优先级使用不同的权重从这些优先级队列中随机获取URL这样使得高优先级的URL有更多机会被选中。而被选中的URL都会交由域名分类器进行分类处理。域名分类器的分类依据就是“域名队列映射表”这个表中记录了不同域名对应的队列。所以域名分类器可以顺利地将不同域名的URL写入不同的域名队列中。
最后域名队列选择器将轮询所有的域名队列从其中获得URL并分配给不同的URL下载服务器进而完成下载处理。
#### 去重算法
爬虫的去重包括两个方面一个是URL相同URL不再重复下载一个是内容相同页面内容不再重复存储。去重一方面是提高爬虫效率避免无效爬取另一方面提高搜索质量避免相同内容在搜索结果中重复出现。URL去重可以使用**布隆过滤器**以提高效率。
内容去重首先要判断内容是否重复,由于爬虫存储着海量的网页,如果按照字符内容对每一个下载的页面都去和现有的页面比较是否重复,显然是不可能的。
Bajie计算页面内容的MD5值通过判断下载页面的内容MD5值是否已经存在判断内容是否重复。
如果把整个HTML内容都计算MD5那么HTML中的微小改变就会导致MD5不同事实上不同网站即使相同内容的页面也总会改成自己的HTML模板导致HTML内容不同。
所以比较内容重复的时候需要将HTML里面的有效内容提取出来也就是提取出去除HTML标签的文本信息针对有效内容计算MD5。更加激进的做法是从有效内容中抽取一段话比如最长的一句话计算这段话的MD5进而判断重复。
而一个内容MD5是否存在需要在千亿级的数据上查找如果用Hash表处理计算和内存存储压力非常大我们将用布隆过滤器代替Hash表以优化性能。
#### 高可用设计
Bajie的可用性主要关注两个方面一是URL调度器或URL下载处理服务器宕机二是下载超时或内容解析错误。
由于Bajie是一个离线系统暂时停止爬取数据的话不会产生严重的后果所以Bajie并不需要像一般互联网系统那样进行高可用设计。但是当服务器宕机后重启时系统需要能够正确恢复保证既不会丢失数据也不会重复下载。
所以URL调度器和URL下载处理服务器都需要记录运行时状态即存储本服务器已经加载的URL和已经处理完成的URL这样宕机恢复的时候就可以立刻读取到这些状态数据进而使服务器恢复到宕机前的状态。对于URL下载处理服务器Bajie采用Redis记录运行时状态数据。
此外为了防止下载超时或内容解析错误URL下载处理服务器会采用多线程设计。每个线程独立完成一个URL的下载和处理线程也需要捕获各种异常不会使自己因为网络超时或者解析异常而退出。
## 小结
架构设计是一个权衡的艺术,**不存在最好的架构,只存在最合适的架构**。架构设计的目的是解决各种业务和技术问题,而解决问题的方法有很多种,每一种方法都需要付出各自的代价,同时又会带来各种新的问题。架构师就需要在这些方法中权衡选择,寻找成本最低的、代价最小的、自己和团队最能驾驭得住的解决方案。
因此,架构师也许不是团队中技术最好的那个人,但一定是**对问题和解决方案优缺点理解最透彻**的那个人。很多架构师把高可用挂在嘴上。可是,你了解你的系统的高可用的目的是什么吗?你的用户能接受的不可用下限在哪里?你为高可用付出的代价是什么?这些代价换来的回报是否值得?
我们在Bajie的设计中核心就是URL调度器。通常在这样的大规模分布式系统中核心组件是不允许单点的也就是不允许单机部署因为单机宕机就意味着核心功能的故障也就意味着整个系统无法正常运行。
但是如果URL调度器采用分布式集群架构提高可用性多服务器共同进行URL调度就需要解决数据一致性和数据同步问题反而会导致系统整体处理能力下降。而Bajie采用单机部署的的方式虽然宕机时系统无法正常运行但是只要在运维上保证能快速重新启动长期看系统整体处理能力反而更高。
此外对于一个千亿级网页的爬虫系统而言最主要的技术挑战应该是海量文件的存储与计算这也确实是早期搜索引擎公司们的核心技术。但是自从Google公开自己的大数据技术论文而Hadoop开源实现了相关技术后这些问题就变得容易很多了。Bajie的海量文件存储就使用了Hadoop分布式文件系统HDFS我会在后面的《常见海量数据处理技术回顾》这一讲详细讨论它。
## 思考题
一个设计良好的爬虫需要面对的情况还有很多,你还能想到哪些文中没提及的情况?最好也能和我聊聊对应的设计方案。
欢迎在评论区分享你的思考,或者提出对这个设计文档的评审意见,我们共同进步。