高并发系统的海量数据处理架构
03 短 URL 生成器设计:百亿短 URL 怎样做到无冲突?
在社交媒体上,人们经常需要分享一些URL,但是有些URL可能会很长,比如:https://time.geekbang.org/hybrid/pvip?utm_source=geektime-pc-discover-banner&utm_term=geektime-pc-discover-banner
这样长的URL显然体验并不友好。我们期望分享的是一些更短、更易于阅读的短URL,比如像 http://1.cn/ScW4dt
这样的。当用户点击这个短URL的时候,可以重定向访问到原始的链接地址。为此我们将设计开发一个短URL生成器,产品名称是“Fuxi(伏羲)”。
我们预计Fuxi需要管理的短URL规模在百亿级别,并发吞吐量达到数万级别。这个量级的数据对应的存储方案是什么样的?用传统的关系数据库存储,还是有其他更简单的办法?此外,如何提升系统的并发处理能力呢?这些是我们今天要重点考虑的问题。
需求分析
短URL生成器,也称作短链接生成器,就是将一个比较长的URL生成一个比较短的URL,当浏览器通过短URL生成器访问这个短URL的时候,重定向访问到原始的长URL目标服务器,访问时序图如下。
对于需要展示短URL的应用程序,由该应用调用短URL生成器生成短URL,并将该短URL展示给用户,用户在浏览器中点击该短URL的时候,请求发送到短URL生成器(短URL生成器以HTTP服务器的方式对外提供服务,短URL域名指向短URL生成器),短URL生成器返回HTTP重定向响应,将用户请求重定向到最初的原始长URL,浏览器访问长URL服务器,完成请求服务。
短URL生成器的用例图
- 用户client程序可以使用短URL生成器Fuxi为每个长URL生成唯一的短URL,并存储起来。
- 用户可以访问这个短URL,Fuxi将请求重定向到原始长URL。
- 生成的短URL可以是Fuxi自动生成的,也可以是用户自定义的。用户可以指定一个长URL对应的短URL内容,只要这个短URL还没有被使用。
- 管理员可以通过web后台检索、查看Fuxi的使用情况。
- 短URL有有效期(2年),后台定时任务会清理超过有效期的URL,以节省存储资源,同时回收短URL地址链接资源。
性能指标估算
Fuxi的存储容量和并发量估算如下。
预计每月新生成短URL 5亿条,短URL有效期2年,那么总URL数量120亿。
(5亿×12月×2年=120亿5亿\times12月\times2年=120亿5亿×12月×2年=120亿)
- 存储空间- 每条短URL数据库记录大约1KB,那么需要总存储空间12TB(不含数据冗余备份)。
(120亿×1KB=12TB120亿\times1KB=12TB120亿×1KB=12TB)
- 吞吐量- 每条短URL平均读取次数100次,那么平均访问吞吐量(每秒访问次数)2万。
((5亿×100)÷(30×24×60×60)≈20000(5亿\times100)\div(30\times24\times60\times60)\approx20000(5亿×100)÷(30×24×60×60)≈20000)
一般系统高峰期访问量是平均访问量的2倍,因此系统架构需要支持的吞吐能力应为4万。
- 网络带宽- 短URL的重定向响应包含长URL地址内容,长URL地址大约500B,HTTP响应头其他内容大约500B,所以每个响应1KB,高峰期需要的响应网络带宽320Mb。
(4万(每秒)次请求×1KB=40MB×8bit=320Mb\times1KB=40MB\times8bit=320Mb×1KB=40MB×8bit=320Mb)
Fuxi的短URL长度估算如下。
短URL采用Base64编码,如果短URL长度是7个字符的话,大约可以编码4万亿个短URL。
( 647≈4万亿64^{7}\approx4万亿647≈4万亿)
如果短URL长度是6个字符的话,大约可以编码680亿个短URL。
(646≈680亿64^{6}\approx680亿646≈680亿)
按我们前面评估,总URL数120亿,6个字符的编码就可以满足需求。因此Fuxi的短URL编码长度6个字符,形如http://l.cn/ScW4dt 。
非功能需求
- 系统需要保持高可用,不因为服务器、数据库宕机而引起服务失效。
- 系统需要保持高性能,服务端80%请求响应时间应小于5ms,99%请求响应时间小于20ms,平均响应时间小于10ms。
- 短URL应该是不可猜测的,即不能猜测某个短URL是否存在,也不能猜测短URL可能对应的长URL地址内容。
概要设计
短URL生成器的设计核心就是短URL的生成,即长URL通过某种函数,计算得到一个6个字符的短URL。短URL有几种不同的生成算法。
单项散列函数生成短URL
通常的设计方案是,将长URL利用MD5或者SHA256等单项散列算法,进行Hash计算,得到128bit或者256bit的Hash值。然后对该Hash值进行Base64编码,得到22个或者43个Base64字符,再截取前面的6个字符,就得到短URL了,如图。
但是这样得到的短URL,可能会发生Hash冲突,即不同的长URL,计算得到的短URL是相同的(MD5或者SHA256计算得到的Hash值几乎不会冲突,但是Base64编码后再截断的6个字符有可能会冲突)。所以在生成的时候,需要先校验该短URL是否已经映射为其他的长URL,如果是,那么需要重新计算(换单向散列算法,或者换Base64编码截断位置)。重新计算得到的短URL依然可能冲突,需要再重新计算。
但是这样的冲突处理需要多次到存储中查找URL,无法保证Fuxi的性能要求。
自增长短URL
一种免冲突的算法是用自增长自然数来实现,即维持一个自增长的二进制自然数,然后将该自然数进行Base64编码即可得到一系列的短URL。这样生成的的短URL必然唯一,而且还可以生成小于6个字符的短URL,比如自然数0的Base64编码是字符“A”,就可以用http://1.cn/A作为短URL。
但是这种算法将导致短URL是可猜测的,如果某个应用在某个时间段内生成了一批短URL,那么这批短URL就会集中在一个自然数区间内。只要知道了其中一个短URL,就可以通过自增(以及自减)的方式请求访问其他URL。Fuxi的需求是不允许短URL可预测。
预生成短URL
因此,Fuxi采用预生成短URL的方案。即预先生成一批没有冲突的短URL字符串,当外部请求输入长URL需要生成短URL的时候,直接从预先生成好的短URL字符串池中获取一个即可。
预生成短URL的算法可以采用随机数来实现,6个字符,每个字符都用随机数产生(用0~63的随机数产生一个Base64编码字符)。为了避免随机数产生的短URL冲突,需要在预生成的时候检查该URL是否已经存在(用布隆过滤器检查)。因为预生成短URL是离线的,所以这时不会有性能方面的问题。事实上,Fuxi在上线之前就已经生成全部需要的144亿条短URL并存储在文件系统中(预估需要短URL120亿,Fuxi预生成的时候进行了20%的冗余,即144亿。)
Fuxi的整体部署模型
Fuxi的业务逻辑比较简单,相对比较有挑战的就是高并发的读请求如何处理、预生成的短URL如何存储以及访问。高并发访问主要通过负载均衡与分布式缓存解决,而海量数据存储则通过HDFS以及HBase来完成。具体架构图如下。
系统调用可以分成两种情况,一种是用户请求生成短URL的过程;另一种是用户访问短URL,通过Fuxi跳转到长URL的过程。
对于用户请求生成短URL的过程,在短URL系统Fuxi上线前,已经通过随机数算法预生成144亿条短URL并将其存储在HDFS文件系统中。系统上线运行后,应用程序请求生成短URL的时候(即输入长URL,请求返回短URL),请求通过负载均衡服务器被发送到短URL服务器集群,短URL服务器再通过负载均衡服务器调用短URL预加载服务器集群。
短URL预加载服务器此前已经从短URL预生成文件服务器(HDFS)中加载了一批短URL存放在自己的内存中,这时,只需要从内存中返回一个短URL即可,同时将短URL与长URL的映射关系存储在HBase数据库中,时序图如下。
对于用户通过客户端请求访问短URL的过程(即输入短URL,请求返回长URL),请求通过负载均衡服务器发送到短URL服务器集群,短URL服务器首先到缓存服务器中查找是否有该短URL,如果有,立即返回对应的长URL,短URL生成服务器构造重定向响应返回给客户端应用。
如果缓存没有用户请求访问的短URL,短URL服务器将访问HBase短URL数据库服务器集群。如果数据库中存在该短URL,短URL服务器会将该短URL写入缓存服务器集群,并构造重定向响应返回给客户端应用。如果HBase中没有该短URL,短URL服务器将构造404响应返回给客户端应用,时序图如下。
过期短URL清理服务器会每个月启动一次,将已经超过有效期(2年)的URL数据删除,并将这些短URL追加写入到短URL预生成文件中。
为了保证系统高可用,Fuxi的应用服务器、文件服务器、数据库服务器都采用集群部署方案,单个服务器故障不会影响Fuxi短URL的可用性。
对于Fuxi的高性能要求,80%以上的访问请求将被设计为通过缓存返回。Redis的缓存响应时间1ms左右,服务器端请求响应时间小于3ms,满足80%请求小于5ms的性能目标。对于缓存没有命中的数据,通过HBase获取,HBase平均响应时间10ms,也可以满足设计目标中的性能指标。
对于Redis缓存内存空间估算,业界一般认为,超过80%请求集中在最近6天生成的短URL上,Fuxi主要缓存最近六天生成的短URL即可。根据需求容量估计,最近6天生成的短URL数量约1亿条,因此需要Redis缓存服务器内存空间:(1亿×1KB=100GB1亿\times1KB=100GB1亿×1KB=100GB)。
详细设计
详细设计关注重定向响应码、短URL预生成文件及加载、用户自定义短URL等几个关键设计点。
重定向响应码
满足短URL重定向要求的HTTP重定向响应码有301和302两种,其中301表示永久重定向,即浏览器一旦访问过该短URL,就将重定向的原始长URL缓存在本地,此后不再请求短URL生成器,直接根据缓存在浏览器(HTTP客户端)的长URL路径进行访问。
302表示临时重定向,每次访问短URL都需要访问短URL生成器。
一般说来,使用301状态码可以降低Fuxi服务器的负载压力,但无法统计短URL的使用情况,而Fuxi的架构设计完全可以承受这些负载压力,因此Fuxi使用302状态码构造重定向响应。
短URL预生成文件及预加载
Fuxi的短URL是在系统上线前全部预生成的,并存储在HDFS文件中。共144亿个短URL,每个短URL 6个字符,文件大小(144亿×6B=86.4GB144亿\times6B=86.4GB144亿×6B=86.4GB)。
文件格式就是直接将144亿个短URL的ASC码无分割地存储在文件中,如下是存储了3个短URL的文件示例:
Wdj4FbOxTw9CHtvPM1
所以如果短URL预加载服务器第一次启动的时候加载1万个短URL,那么就从文件头读取60K数据,并标记当前文件偏移量60K。下次再加载1万个短URL的时候,再从文件60K偏移位置继续读取60K数据即可。
因此,Fuxi除了需要一个在HDFS记录预生成短URL的文件外,还需要一个记录偏移量的文件,记录偏移量的文件也存储在HDFS中。同时,由于预加载短URL服务器集群部署多台服务器,会出现多台服务器同时加载相同短URL的情况,所以还需要利用偏移量文件对多个服务器进行互斥操作,即利用文件系统写操作锁的互斥性实现多服务器访问互斥。
应用程序的文件访问流程应该是:写打开偏移量文件 -> 读偏移量 -> 读打开短URL文件 -> 从偏移量开始读取60K数据 -> 关闭短URL文件 -> 修改偏移量文件 -> 关闭偏移量文件。
由于写打开偏移量文件是一个互斥操作,所以第一个预加载短URL服务器写打开偏移量文件以后,其他预加载短URL服务器无法再写打开该文件,也就无法完成读60K短URL数据及修改偏移量的操作,这样就能保证这两个操作是并发安全的。
加载到预加载短URL服务器的1万个短URL会以链表的方式存储,每使用一个短URL,链表头指针就向后移动一位,并设置前一个链表元素的next对象为null。这样用过的短URL对象可以被垃圾回收。
当剩余链表长度不足2000的时候,触发一个异步线程,从文件中加载1万个新的短URL,并链接到链表的尾部。
与之对应的URL链表类图如下。
URLNode:URL链表元素类,成员变量uRL即短URL字符串,next指向下一个链表元素。
LinkedURL:URL链表主类,成员变量head指向链表头指针元素,uRLAmount表示当前链表剩余元素个数。acquireURL()方法从链表头指针指向的元素中取出短URL字符串,并执行urlAmount– 操作。当urlAmount < 2000的时候,调用私有方法loadURL(),该方法调用一个线程从文件中加载1万个短URL并构造成链表添加到当前链表的尾部,并重置uRLAmount。
用户自定义短URL
Fuxi允许用户自己定义短URL,即在生成短URL的时候,由用户指定短URL的内容。为了避免预生成的短URL和用户指定的短URL冲突,Fuxi限制用户自定义短URL的字符个数,不允许用户使用6个字符的自定义短URL,且URL长度不得超过20个字符。
但是用户自定义短URL依然可能和其他用户自定义短URL冲突,所以Fuxi生成自定义短URL的时候需要到数据库中检查冲突,是否指定的URL已经被使用,如果发生冲突,要求用户重新指定。
URL Base64编码
标准Base64编码表如下。
其中“+”和“/”在URL中会被编码为“%2B”以及“%2F”,而“%”在写入数据库的时候又和SQL编码规则冲突,需要进行再编码,因此直接使用标准Base64编码进行短URL编码并不合适。URL保留字符编码表如下。
所以,我们需要针对URL场景对Base64编码进行改造,使用URL保留字符表以外的字符对Base64编码表中的62,63进行编码:将“+”改为“-”,将“/”改为“_”,Fuxi最终采用的URL Base64编码表如下。
小结
我们开头提到,Fuxi是一个高并发(2万QPS)、海量存储(144亿条数据)、还需要10ms的高性能平均响应时间的系统。但是我们后面看到,Fuxi的架构并不复杂。
这一方面是源于Fuxi的业务逻辑非常简单,只需要完成短URL与长URL的映射关系生成与获取就可以了。另一方面则是源于开源技术体系的成熟,比如一个HDFS集群可支持百万TB规模的数据存储,而我们需要的存储空间只有区区不到100GB,都有点大材小用了。事实上,Fuxi选择HDFS更多的考量是利用HDFS的高可用,HDFS的自动备份策略为我们提供了高可用的数据存储解决方案。
同理,高并发也是如此,2万QPS看起来不小,但实际上,由于业务逻辑简单,单个数据都很小,加上大部分请求数据可以通过Redis缓存获取,所以实际响应时间是非常短的,10ms的平均响应时间使得Fuxi真正承受的并发压力只有200。对于这样简单的业务逻辑以及200这样的并发压力,我们使用配置高一点的服务器的话,只需要一台短URL服务器其实就可以满足了。所以,我们在短URL服务器之前使用负载均衡服务器,这也是更多地为高可用服务。
思考题
用户每次请求生成短URL的时候,Fuxi都会返回一个新生成的短URL,也就意味着,如果用户重复提交同一个长URL请求生成短URL,每次都会返回一个新的短URL。你认为这将导致什么问题?对此,你有什么解决方案?
另外,小结里提到,2万QPS,10ms平均响应时间,这种情况下,真正的并发量只有200,这个200是如何得到的?
04 网页爬虫设计:如何下载千亿级网页?
在互联网早期,网络爬虫仅仅应用在搜索引擎中。随着大数据时代的到来,数据存储和计算越来越廉价和高效,越来越多的企业开始利用网络爬虫来获取外部数据。例如:获取政府公开数据以进行统计分析;获取公开资讯以进行舆情和热点追踪;获取竞争对手数据以进行产品和营销优化等等。
网络爬虫有时候也被称为网络机器人,或者网络蜘蛛。我们准备开发一个全网爬虫,爬取全(中文)互联网的公开网页,以构建搜索引擎和进行数据分析。爬虫名称为“Bajie(八戒)”。
Bajie的技术挑战包括:如何不重复地获取并存储全网海量URL?如何保证爬虫可以快速爬取全网网页但又不会给目标网站带来巨大的并发压力?接下来我们就来看看Bajie的需求与技术架构。
需求分析
Bajie的功能比较简单,这里不再赘述。
性能指标估算
因为互联网网页会不断产生,所以全网爬虫Bajie也是一个持续运行的系统。根据设计目标,Bajie需要每个月从互联网爬取的网页数为20亿个,平均每个页面500KB,且网页需存储20年。
Bajie的存储量和TPS(系统吞吐量)估算如下。
- 每月新增存储量- 估计平均每个页面500KB,那么每个月需要新增存储1PB。
(20亿×500KB=1PB20亿\times500KB=1PB20亿×500KB=1PB)
-
总存储空间- 网页存储有效期20年,那么需要总存储空间240PB。
(1PB×12个月×20年=240PB1PB\times12个月\times20年=240PB1PB×12个月×20年=240PB) -
TPS- Bajie的TPS应为800。
(20亿÷(30×24×60×60≈80020亿\div(30\times24\times60\times60\approx80020亿÷(30×24×60×60≈800))
非功能需求
Bajie需要满足的非功能需求如下。
- 伸缩性:当未来需要增加每月爬取的网页数时,Bajie可以灵活部署,扩大集群规模,增强其爬取网页的速度。也就是说,Bajie必须是一个分布式爬虫。
- 健壮性:互联网是一个开放的世界,也是一个混乱的世界,服务器可能会宕机,网站可能失去响应,网页HTML可能是错误的,链接可能有陷阱……所以Bajie应该能够面对各种异常,正常运行。
- 去重:一方面需要对超链接URL去重,相同的URL不需要重复下载;另一方面还要对内容去重,不同URL但是相同内容的页面也不需要重复存储。
- 扩展性:当前只需要爬取HTML页面即可,将来可能会扩展到图片、视频、文档等内容页面。- 此外,Bajie必须是“礼貌的”。爬虫爬取页面,实际上就是对目标服务器的一次访问,如果高并发地进行访问,可能会对目标服务器造成比较大的负载压力,甚至会被目标服务器判定为DoS攻击。因此Bajie要避免对同一个域名进行并发爬取,还要根据目标服务器的承载能力增加访问延迟,即在两次爬取访问之间,增加等待时间。
并且,Bajie还需要遵循互联网爬虫协议,即目标网站的robots.txt协议,不爬取目标网站禁止爬取的内容。比如www.zhihu.com的robots.txt内容片段如下。
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如下。
User-agent: Baiduspider
Disallow: /
User-agent: baiduspider
Disallow: /
淘宝禁止百度爬虫访问根目录,也就是禁止百度爬取该网站所有页面。- robots.txt在域名根目录下,如www.taobao.com/robots.txt。Bajie应该首先获取目标网站的robots.txt,根据爬虫协议构建要爬取的URL超链接列表。
概要设计
Bajie的设计目标是爬取数千亿的互联网页,那么Bajie首先需要得到这千亿级网页的URL,该如何获得呢?
全世界的互联网页面事实上是一个通过超链接连接的巨大网络,其中每个页面都包含一些指向其他页面的URL链接,这些有指向的链接将全部网页构成一个有向(网络)图。如下图所示,每个节点是一个网页,每条有向的边就是一个超链接。
上图中,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的处理流程活动图如下。
首先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的部署图如下。
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,这样就天然实现了先进先出的广度优先算法,如下图。
但是,广度优先搜索算法可能会导致爬虫一段时间内总是访问同一个网站,因为一个HTML页面内的链接常常是指向同一个网站的,这样就会使爬虫“不礼貌”。
通常我们针对一个网站,一次只下载一个页面,所以URL调度器需要将待下载URL根据域名进行分类。此外,不同网站的信息质量也有高低之分,爬虫应该优先爬取那些高质量的网站。优先级和域名都可以使用不同队列来区分,如下图。
首先优先级分类器会根据网页内容质量将域名分类(后面专栏会讲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,我会在后面的《常见海量数据处理技术回顾》这一讲详细讨论它。
思考题
一个设计良好的爬虫需要面对的情况还有很多,你还能想到哪些文中没提及的情况?
05 网盘系统设计:万亿 GB 网盘如何实现秒传与限速?
网盘,又称云盘,是提供文件托管和文件上传、下载服务的网站(File hosting service)。人们通过网盘保管自己拍摄的照片、视频,通过网盘和他人共享文件,已经成为了一种习惯。我们准备开发一个自己的网盘应用系统,应用名称为“DBox”。
十几年前曾经有个段子,技术人员对老板说:您不能在公司电脑打开您家里电脑的文件,再贵的电脑也不能。事实上,随着网盘技术的成熟,段子中老板的需求已经成为现实:网盘可以自动将家里电脑的文件同步到公司电脑,老板可以在公司的电脑打开家里电脑的文件了。
网盘的主要技术挑战是海量数据的高并发读写访问。用户上传的海量数据如何存储?如何避免部分用户频繁读写文件,消耗太多资源,而导致其他的用户体验不佳?我们看下DBox的技术架构以及如何解决这些问题。
需求分析
DBox的核心功能是提供文件上传和下载服务。基于核心功能,DBox需要在服务器端保存这些文件,并在下载和上传过程中实现断点续传。也就是说,如果上传或下载过程被中断了,恢复之后,还能从中断的地方重新上传或者下载,而不是从头再来。
DBox还需要实现文件共享的需求。使用DBox的不同用户之间可以共享文件,一个用户上传的文件共享给其他用户后,其他用户也可以下载这个文件。
此外,网盘是一个存储和网络密集型的应用,用户文件占据大量硬盘资源,上传、下载需要占用大量网络带宽,并因此产生较高的运营成本。所以用户体验需要向付费用户倾斜,DBox需要对上传和下载进行流速控制,保证付费用户得到更多的网络资源。DBox用例图如下。
负载指标估算
DBox的设计目标是支持10亿用户注册使用,免费用户最大可拥有1TB存储空间。预计日活用户占总用户的20%,即2亿用户。每个活跃用户平均每天上传、下载4个文件。
DBox的存储量、吞吐量、带宽负载估算如下。
- 总存储量- 理论上,总存储空间估算为10亿TB,即1万亿GB。
(10亿×1TB=10亿TB10亿\times1TB=10亿TB10亿×1TB=10亿TB)
但考虑到大多数用户并不会完全用掉这个空间,还有很多用户存储的文件其实是和别人重复的(电影、电子书、软件安装包等),真正需要的存储空间大约是这个估算值的10%,即1亿TB。
- QPS- 系统需要满足的平均QPS约为10000。
(2亿×4÷(24×60×60)≈1万2亿\times4\div(24\times60\times60)\approx1万2亿×4÷(24×60×60)≈1万)
高峰期QPS约为平均QPS的两倍,即2万。
- 带宽负载- 每次上传下载文件平均大小1MB,所以需要网络带宽负载10GB/s,即80Gb/s。
(1万×1MB=10GB/s=80Gb/s1万\times1MB=10GB/s=80Gb/s1万×1MB=10GB/s=80Gb/s)
同样,高峰期带宽负载为160Gb/s。
非功能需求
- 大数据量存储:10亿注册用户,1000亿个文件,约1亿TB的存储空间。
- 高并发访问:平均1万QPS,高峰期2万QPS。
- 大流量负载:平均网络带宽负载80Gb/S,高峰期带宽负载160Gb/s。
- 高可靠存储:文件不丢失,持久存储可靠性达到99.9999% ,即100万个文件最多丢失(或损坏)1个文件。
- 高可用服务:用户正常上传、下载服务可用性在99.99%以上,即一年最多53分钟不可用。
- 数据安全性:文件需要加密存储,用户本人及共享文件外,其他人不能查看文件内容。
- 不重复上传:相同文件内容不重复上传,也就是说,如果用户上传的文件内容已经被其他用户上传过了,该用户不需要再上传一次文件内容,进而实现“秒传”功能。从用户视角来看,不到一秒就可以完成一个大文件的上传。
概要设计
网盘设计的关键是元数据与文件内容的分离存储与管理。所谓文件元数据就是文件所有者、文件属性、访问控制这些文件的基础信息,事实上,传统文件系统也是元数据与文件内容分离管理的,比如Linux的文件元数据记录在文件控制块FCB中,Windows的文件元数据记录在文件分配表FAB中,Hadoop分布式文件系统HDFS的元数据记录在NameNode中。
而DBox是将元信息存储在数据库中,文件内容则使用另外专门的存储体系。但是由于DBox是一个互联网应用,出于安全和访问管理的目的,并不适合由客户端直接访问存储元数据的数据库和存储文件内容的存储集群,而是通过API服务器集群和数据块服务器集群分别进行访问管理。整体架构如下图。
对于大文件,DBox不会上传、存储一整个的文件,而是将这个文件进行切分,变成一个个单独的Block,再将它们分别上传并存储起来。
这样做的核心原因是,DBox采用对象存储作为最终的文件存储方案,而对象存储不适合存储大文件,需要进行切分。而大文件进行切分还带来其他的好处:可以以Block为单位进行上传和下载,提高文件传输速度;客户端或者网络故障导致文件传输失败,也只需要重新传输失败的Block就可以,进而实现断点续传功能。
Block服务器就是负责Block上传和管理的。客户端应用程序根据API服务器的返回指令,将文件切分成一些Block,然后将这些Block分别发送给Block服务器,Block服务器再调用对象存储服务器集群,将Block存储在对象存储服务器中(DBox选择Ceph作为对象存储)。
用户上传文件的时序图如下。
用户上传文件时,客户端应用程序收集文件元数据,包括文件名、文件内容MD5、文件大小等等,并根据文件大小计算Block的数量(DBox设定每个block大小4MB),以及每个Block的MD5值。
然后客户端应用程序将全部元数据(包括所有Block的MD5值列表)发送给API服务器。API服务器收到文件元数据后,为每个Block分配全局唯一的BlockID(BlockID为严格递增的64位正整数,总可记录数据大小(264×4MB=180亿PB2^{64}\times4MB=180亿PB264×4MB=180亿PB),足以满足DBox的应用场景)。
下一步,API服务器将文件元数据与BlockID记录在数据库中,并将BlockID列表和应用程序可以连接的Block服务器列表返回客户端。客户端连接Block服务器请求上传Block,Block服务器连接API服务器进行权限和文件元数据验证。验证通过后,客户端上传Block数据,Block服务器再次验证Block数据的MD5值,确认数据完整后,将BlockID和Block数据保存到对象存储集群Ceph中。
类似的,用户下载文件的时序图如下。
客户端程序访问API服务器,请求下载文件。然后API服务器会查找数据库,获得文件的元数据信息,再将元数据信息中的文件BlockID列表及可以访问的Block服务器列表返回给客户端。
下一步,客户端访问Block服务器,请求下载Block。Block服务器验证用户权限后,从Ceph中读取Block数据,返回给客户端,客户端再将返回的Block组装为文件。
详细设计
为解决网盘的三个重要问题:元数据如何管理?网络资源如何向付费用户倾斜?如何做到不重复上传?DBox详细设计将关注元数据库、上传下载限速、秒传的设计实现。
元数据库设计
元数据库表结构设计如下。
从图中可以看出,元数据库表结构中主要包括三个表,分别是User用户表、File文件表和Block数据块表,表的用途和包含的主要字段如下:
- User用户表记录用户基本信息:用户名、创建时间、用户类型(免费、VIP)、用户已用空间、电话号码、头像等等。
- File文件表记录文件元信息:文件名、是否为文件夹、上级文件夹、文件MD5、创建时间、文件大小、文件所属用户、是否为共享文件等。
- Block数据块表记录Block数据,包括BlockID、Block MD5、对应文件等。
其中,User表和File表为一对多的关系,File表和Block表也是一对多的关系。
这3种表的记录数都是百亿级以上,所以元数据表采用分片的关系数据库存储。
因为查询的主要场景是根据用户ID查找用户信息和文件信息,以及根据文件ID查询block信息,所以User和File表都采用user_id作为分片键,Block表采用file_id作为分片键。
限速
DBox根据用户付费类型决定用户的上传、下载速度。而要控制上传、下载速度,可以通过限制并发Block服务器数目,以及限制Block服务器内的线程数来实现。
具体过程是,客户端程序访问API服务器,请求上传、下载文件的时候,API服务器可以根据用户类型,决定分配的Block服务器数目和Block服务器内的服务线程数,以及每个线程的上传、下载速率。
Block服务器会根据API服务器的返回值,来控制客户端能够同时上传、下载的Block数量以及传输速率,以此对不同用户进行限速。
秒传
秒传是用户快速上传文件的一种功能。
事实上,网盘保存的很多文件,内容其实是重复的,比如电影、电子书等等。一方面,重复上传这些文件会加大网盘的存储负载压力;另一方面,每次都要重新上传重复的内容,会导致用户网络带宽的浪费和用户等待时间过长的问题。
所以,在设计中,物理上相同的文件,DBox只会保存一份。用户每次上传文件时,DBox都会先在客户端计算文件的MD5值,再根据MD5值判断该文件是否已经存在。对于已经存在的文件,只需要建立用户文件和该物理文件的关联即可,并不需要用户真正上传该文件,这样就可以实现秒传的功能。
但是,计算MD5可能会发生Hash冲突,也就是不同文件算出来的MD5值是相同的,这样会导致DBox误判,将本不相同的文件关联到一个物理文件上。不但会使上传者丢失自己的文件,还会被黑客利用:上传一个和目标文件MD5相同的文件,然后就可以下载目标文件了。
所以,DBox需要通过更多信息判断文件是否相同:只有文件长度、文件开头256KB的MD5值、文件的MD5值,三个值都相同,才会认为文件相同。当文件长度小于256KB,则直接上传文件,不启用秒传功能。
为此,我们需要将上面的元数据库表结构进行一些改动,将原来的File表拆分成物理文件表Physics_File和逻辑文件表Logic_File。其中,Logic_File记录用户文件的元数据,并和物理文件表Physics_File建立多对1关联关系,而Block表关联的则是Physics_File表,如下。
Physics_File中字段md5和256kmd5字段分别记录了文件MD5和文件头256KB的MD5数据,而size记录了文件长度,只有这三个字段都相同才会启用秒传。
小结
我们在需求分析中讨论过,DBox需要支持大数据量存储、高并发访问、高可用服务、高可靠存储等非功能需求。事实上,对于网盘应用而言,元数据API服务其实和一般的高并发互联网系统网关没有太大差别。真正有挑战的是海量文件的高可用存储,而这一挑战,在DBox中,被委托给了分布式对象存储Ceph来完成。而Ceph本身设计就是支持大数据量存储、高并发访问、高可用服务、高可靠存储的。
架构师按照职责,可以分成两种,一种是应用系统架构师,负责设计、开发类似网盘、爬虫这样的应用系统;另一种是基础设施架构师,负责设计、开发类似Ceph、HDFS这样的基础设施系统。
应用架构师需要掌握的技术栈更加广泛,要能够掌握各种基础设施技术的特性,并能根据业务特点选择最合适的方案;而基础设施架构师需要的技术栈更加深入,需要掌握计算机软硬件更深入的知识,才能开发出一个稳定的基础技术产品。
当然,最好的架构师应该是技术栈既广泛又深入,既能灵活应用各种基础设施来开发应用系统,也能在需要的时候自己动手开发新的基础设施系统。
我们专栏大部分案例都是关于应用的,但是也不乏关于编程框架、限流器、安全防火墙、区块链等基础设施的案例。你也可以在学习的过程中,感受下这两种系统的设计方案和技术关键点的不同。
思考题
网盘元数据存储采用分片的关系数据库方案,查询目录和文件都比较简单,但是性能也比较差。而且文件表按用户ID分片,如果某个用户创建大量文件,还会导致分片不均衡,你有什么优化的手段和方法吗?
06 短视频系统设计:如何支持三千万用户同时在线看视频?
短视频(short video)通常时长在15分钟以内,主要是在移动智能终端上进行拍摄、美化编辑或加特效,并可以在网络社交平台上进行实时分享的一种新型视频形式。短视频具有时间短、信息承载量高等特点,更符合当下网民手机使用行为习惯,短视频的用户流量创造了巨大的商机。
我们准备开发一个面向全球用户的短视频应用,用户总量预计20亿,应用名称:QuickTok。
视频文件和其他媒体文件相比,会更大一点,这就意味着存储短视频文件需要更大的存储空间,播放短视频也需要更多的网络带宽。因此,QuickTok的主要技术挑战是:如何应对高并发用户访问时的网络带宽压力,以及如何存储海量的短视频文件。接下来我们就来看看QuickTok的需求与技术架构。
需求分析
QuickTok的核心功能需求非常简单:用户上传视频、搜索视频、观看视频。我们将主要分析非功能需求。
QuickTok预计用户总量为20亿,日活用户约10亿,每个用户平均每天浏览10个短视频,由此可以预估,短视频日播放量为100亿:
(10亿×10=100亿10亿\times10=100亿10亿×10=100亿)
平均播放QPS为11万/秒:
(100亿÷(24×60×60)≈11万/秒100亿\div(24\times60\times60)\approx11万/秒100亿÷(24×60×60)≈11万/秒)
每秒11万用户点击视频,假设用户平均观看5分钟,那么同时在观看的视频数就是:
(11万/秒×5×60秒=3千万11万/秒\times5\times60秒=3千万11万/秒×5×60秒=3千万)
假设每个短视频的平均播放次数200次,那么为了支撑这样体量的播放量,平均需要每秒上传视频数:
(11万/秒÷200=550/秒11万/秒\div200=550/秒11万/秒÷200=550/秒)
每个短视频平均大小100MB,每秒上传至服务器的文件大小为:
(100MB×550=55GB100MB\times550=55GB100MB×550=55GB)
(视频虽然不是一秒内上传至服务器的,但是这样计算依然没有问题。)
每年新增视频需要的存储空间:
(55GB×60×60×24×365=1700PB55GB\times60\times60\times24\times365=1700PB55GB×60×60×24×365=1700PB)
事实上,为了保证视频数据的高可用,不会因为硬盘损坏导致数据丢失,视频文件需要备份存储,QuickTok采用双副本的备份存储策略,也就是每个视频文件存储三份,需要的总存储空间:
(1700PB×3=5200PB1700PB\times3=5200PB1700PB×3=5200PB)
而播放视频需要的总带宽:
(11万×100MB×8bit=88Tb11万\times100MB\times8bit=88Tb11万×100MB×8bit=88Tb)
因此,我们需要设计的短视频应用是一个每秒上传550个视频文件、11万次播放、新增165GB存储以及88Tb总带宽的高并发应用系统。这个系统呢需要是高性能的,能迅速响应用户的上传和播放操作,也需要是高可用的,能面向全球用户提供7 * 24小时稳定的服务。
概要设计
QuickTok的核心部署模型如下图。
用户上传视频时,上传请求会通过负载均衡服务器和网关服务器,到达视频上传微服务。视频上传微服务需要做两件事:一是把上传文件数据流写入视频文件暂存服务器;二是把用户名、上传时间、视频时长、视频标题等视频元数据写入分布式MySQL数据库。
视频文件上传完成后,视频上传微服务会生成一个视频上传完成消息,并将其写入到消息队列服务器。视频内容处理器将消费这个上传完成消息,并根据消息内容,从视频文件暂存服务器获取视频文件数据,进行处理。
视频内容处理器是一个由责任链模式构建起来的管道。在这个管道中,视频将会被顺序进行内容合规性审查、内容重复性及质量审查、内容标签生成、视频缩略图生成、统一视频转码处理等操作,如下图。
合规且非重复的视频会经过统一转码,最终被写入分布式文件存储和CDN。这样视频上传处理就完成了,具体时序图如下。
以上就是对视频上传环节的设计,接下来我们将讨论对视频搜索及播放部分的设计,即核心部署模型图中标红的部分,如下。
视频搜索引擎会根据用户提交的视频标题、上传用户等元数据,以及视频内容处理器生成的内容标签构建倒排索引。当用户搜索视频时,系统会根据倒排索引来检索符合条件的视频,并返回结果列表。结果列表在App端向用户呈现时,会将此前视频内容处理器生成的缩略图展现给用户,使用户对视频内容有个初步而直观的感受。
当用户点击缩略图时,App开始播放视频。App并不需要下载完整个视频文件才开始播放,而是以流的方式一边下载视频数据,一边播放,使用户尽量减少等待,获得良好的观看体验。QuickTok使用MPEG–DASH流媒体传输协议进行视频流传输,因为这个协议具有自适应能力,而且支持HTTP,可以应对QuickTok的视频播放需求。
详细设计
为解决QuickTok的两个重要问题:如何存储海量视频文件?如何解决高并发视频播放导致的带宽压力?详细设计将关注视频存储系统、性能优化与CDN。
此外,“如何生成更吸引用户的缩略图”是短视频应用用户体验的一个关键问题,详细设计也会关注缩略图生成与推荐的设计实现。
视频存储系统设计
由需求分析可知,QuickTok每年新增5200PB的存储。因此,“如何存储海量视频文件”就是QuickTok设计的重要挑战之一。对此,我们可以尝试与[网盘]相同的存储技术方案,将视频文件拆分成若干block,使用对象存储服务进行存储。
但QuickTok最终采用了另一种存储方案,即使用Hadoop分布式文件系统HDFS进行存储。HDFS适合大文件存储的一次写入多次读取的场景,满足视频一次上传多次播放的需求;同时,它还可以自动进行数据备份(缺省配置下,每个文件存储三份),也满足我们关于数据存储高可用的需求。
HDFS适合存储大文件,大文件减少磁盘碎片,更有利于存储空间的利用,同时HDFS NameNode的访问压力也更小,所以我们需要把若干个视频文件合并成一个HDFS文件进行存储,并将存储相关的细节记录到HBase中。
举个例子,当用户上传一个视频文件,系统会自动生成一个视频ID,这里假设这个ID是123。视频内容处理器先对视频进行一系列处理,再调用视频文件存储服务来进行存储。
存储服务首先通过HDFS创建一个文件,比如/data/videos/clust0/p0/000000001,然后将视频文件数据顺序写入到HDFS中。写完后,存储服务就可以得到这个HDFS文件的全路径名(/data/videos/clust0/p0/000000001)、视频文件在HDFS中的偏移量0、文件大小99,000,000B。
然后,视频文件存储服务再将这些信息记录到HBase中,主键就是视频ID<123>,value就是。
假设另一个用户上传的视频ID为456,文件大小100,000,000B,紧随着上一个视频文件,也保存到同一个HDFS文件中。那么HBase中就可以记录主键<456>,value。
当其他用户播放视频456时,播放微服务根据主键ID在HBase中查找value值,得到HDFS文件路径/data/videos/clust0/p0/000000001,从该文件99,000,000偏移位置开始读取100,000,000Byte数据,就是视频ID 456完整的文件数据了。
性能优化与CDN设计
我们前面分析过,QuickTok需要的总带宽是88Tb,这是一个非常巨大的数字。如果单纯靠QuickTok自己的数据中心来承担这个带宽压力,技术挑战和成本都非常巨大。只有通过CDN将用户的网络通信请求就近返回,才能缓解数据中心的带宽压力。
App请求获取视频数据流的时候,会优先检查离自己比较近的CDN中是否有视频数据。如果有,直接从CDN加载数据,如果没有,才会从QuickTok数据中心获取视频数据流。
如果用户的大部分请求都可以通过CDN返回,那么一方面可以极大加快用户请求的响应速度,另一方面又可以较大缓解数据中心的网络和硬盘负载压力,进一步提升应用整体的性能。
通常的CDN设计,是在CDN中没有用户请求的数据时,进行回源,即由CDN请求数据中心返回需要的数据,然后缓存在CDN本地。
但QuickTok考虑到了短视频的特点:大V、网红们发布的短视频会被更快速、更广泛地播放。因此针对粉丝量超过10万的用户,系统将采用主动推送CDN的方法,以提高CDN的命中率,优化用户体验,如图:
从图中可以看出,视频内容处理器进行完视频处理后,一方面会将视频存储到前面说过的视频存储系统中,另一方面又会调用CDN推送服务。然后,CDN推送服务将调用大数据平台,获取视频上传者的活跃粉丝数、粉丝分布区域等数据。如果是10万粉丝以上的用户发布了短视频,CDN推送服务会根据其粉丝活跃的区域,将视频推送到对应区域的CDN服务器上。
短视频的完播率通常不足30%,所以QuickTok也不需要将完整视频推送到CDN,只需要根据视频发布者的历史播放记录,计算其完播率和播放期望进度,然后将短视频切分成若干chunk,将部分chunk推送到CDN即可。
业界一般共识,视频应用CDN处理的带宽大约占总带宽的95%以上,也就是说,通过合理使用CDN,QuickTok数据中心需要处理的带宽压力不到4Tb。
缩略图生成与推荐设计
用户可以通过App主页、搜索结果页、视频推荐页等页面看到视频列表,其中每个视频都需要有个缩略图。用户点击缩略图,就开始播放视频。
缩略图通常是由视频的某一帧画面缩略而生成的。事实上,缩略图的选择会极大地影响用户点击、播放视频的意愿。一个10分钟的视频大约包含3万帧画面,选择哪一帧画面,才能使用户点击视频的可能性最大?以及,针对不同的用户分类,是否选择不同的缩略图会产生更高的点击率?
我们需要通过大数据平台的机器学习引擎来完成缩略图的生成和推荐,如下图。
缩略图的生成和推荐可以分为两个具体过程:
- 实时在线的缩略图推荐过程a;
- 利用离线机器学习生成优质缩略图的过程b。
a过程中,用户通过搜索引擎搜索视频,搜索引擎产生搜索结果视频列表后,根据视频ID从缩略图存储中获取对应的缩略图。
但是,一个视频可能对应很多个缩略图,如果想要显示最吸引当前用户的那个,搜索引擎就需要调用QuickTok大数据平台的缩略图推荐引擎进行推荐。
推荐引擎可以获取当前用户的偏好特征标签以及视频对应的多个缩略图特征,使用XGboost算法训练好的模型,将用户特征标签和缩略图特征进行匹配,然后返回最有可能被当前用户点击的缩略图ID。搜索引擎再按照ID,将对应的缩略图构建到搜索结果页面,返回给用户。
用户浏览搜索结果列表,点击某些缩略图进行播放。App应用会将用户的浏览与点击数据发送给QuickTok大数据平台,这样就进入了利用机器学习来生成优质缩略图的过程b。
机器学习系统获取到了海量用户的浏览和点击数据,同时获取每个缩略图的特征。一方面,机器可以学习到,哪些特征的缩略图更容易获得用户点击,从而生成优质缩略图特征标签库;另一方面,机器还可以学习到每个用户自身更偏好的图像特征标签,供前面提到的推荐引擎使用。
有了机器学习系统的加持,视频内容处理器就可以使用优质特征标签库来处理上传的视频内容,抽取符合优质特征的帧,进而生成缩略图。
以上的a、b两个过程不断循环迭代,系统就可以不断优化优质特征标签库,不断使缩略图更符合用户喜好。
那最开始没有特征库的时候怎么办呢?视频内容处理器可以使用随机的办法,抽取一些帧作为缩略图,进行冷启动。机器学习再从这些随机抽取的缩略图上开始学习,从而进入循环优化过程。
小结
我们在缩略图生成部分,使用了大数据和机器学习的一些技术,如果你不熟悉,可能会觉得有点困难。但是现在人工智能和机器学习几乎是稍具规模的互联网系统的标配,架构师作为整个系统的设计者、技术负责人,可能对算法的细节无法做出具体的优化,但是对于算法在整个架构中的作用、相关数据的处理和流转必须非常熟悉,才能设计出满足业务需要的架构方案。
所以,大数据和机器学习的原理和应用方法应该是架构师技能栈的一部分,能够和算法工程师顺畅讨论技术细节是架构师必备的能力。如果你对这部分知识掌握不完整,可以阅读专栏《从0开始学大数据》。
思考题
不止是缩略图的选择需要用到推荐算法,视频内容本身也需要推荐算法:当用户播放完一个视频,QuickTok需要给用户自动播放下一个视频,以此增强用户粘性。那么下一个视频应该播放什么?你是否可以参考文中的缩略图生成与推荐架构图,自己画一个视频推荐的架构图?如果能说说你的设计思路就更好了。
07 海量数据处理技术回顾:为什么分布式会遇到 CAP 难题?
在这个模块的几个案例中,我们都需要处理海量的数据,需要用到海量的存储介质,其实海量数据本质上就是一种磁盘资源敏感的高并发场景。
我们说过,为了应对资源不足的问题,我们常采用水平伸缩,即分布式的方案。数据存储的分布式问题是所有分布式技术中最具挑战性的,因为相对于“无状态”(stateless)的计算逻辑(可执行程序),数据存储是“有状态”(stateful)的。无状态的计算逻辑可以在任何一台服务器执行而结果不会改变,但有状态的数据却意味着数据存储和计算资源的绑定:每一个数据都需要存储在特定的服务器上,如果再增加一台空的服务器,它没有数据,也就无法提供数据访问,无法实现伸缩。
数据存储的“有状态”特性还会带来其他问题:为了保证数据存储的可靠性,数据必须多备份存储,也就是说,同一个数据需要存储在多台服务器上。那么又如何保证多个备份的数据是一致的?
因此,海量数据存储的核心问题包括:如何利用分布式服务器集群实现海量数据的统一存储?如何正确选择服务器写入并读取数据?为了保证数据的高可用性,如何实现数据的多备份存储?数据多备份存储的时候,又如何保证数据的一致性?
为了解决这些问题,在这个模块的案例设计中,我们使用了多个典型的分布式存储技术方案:分布式文件系统HDFS、分布式NoSQL数据库HBase、分布式关系数据库。下面我们就来回顾这几个典型技术方案。你可以再重新审视一下,我们案例中的技术选型是否恰当,是否有改进的空间。
HDFS
这个模块中,我们用HDFS作为短URL、爬虫下载文件、短视频文件的存储方案。
HDFS,即Hadoop分布式文件系统,其架构如下。
HDFS的关键组件有两个,一个是NameNode,另一个是DataNode。
NameNode负责整个分布式文件系统的元数据管理,也就是文件路径名、访问权限、数据块ID、存储位置等信息。而DataNode负责文件数据的存储和读写操作,HDFS将文件数据分割成若干数据块(Block),每个DataNode存储一部分数据块,这样文件就分布存储在了整个HDFS服务器集群中。
HDFS集群会有很多台DataNode服务器(一般几百到几千不等),每台服务器配有数块硬盘,整个集群的存储容量大概在几PB到数百PB。通过这种方式,HDFS可以存储海量的文件数据。
HDFS为了保证数据的高可用,会将一个数据块复制为多份(缺省情况为3份),并将多份相同的数据块存储在不同的服务器上,甚至不同的机架上。这样当有硬盘损坏,或者某个DataNode服务器宕机,甚至某个交换机宕机,导致其存储的数据块不能访问的时候,客户端会查找其备份的数据块进行访问。
HDFS的典型应用场景是大数据计算,即使用MapReduce、Spark这样的计算框架来计算存储在HDFS上的数据。但是作为一个经典的分布式文件系统,我们也可以把HDFS用于海量文件数据的存储与访问,就像我们在这个模块的案例中那样。
分布式关系数据库
我们在[网盘案例]中,使用了分片的关系数据来存储元数据信息。这是因为关系数据存在存储结构的限制(使用B+树存储表数据),通常一张表的存储上限是几千万条记录。而在网盘的场景中,元数据在百亿以上,所以我们需要将数据分片存储。
分片的关系数据库,也被称为分布式关系数据库。也就是说,将一张表的数据分成若干片,其中每一片都包含了数据表中一部分的行记录,然后将每一片存储在不同的服务器上,这样一张表就存储在多台服务器上了。通过这种方式,每张表的记录数上限可以突破千万,保存百亿甚至更多的记录。
最简单的数据库分片存储可以采用硬编码的方式,我们在程序代码中直接指定把一条数据库记录存放在哪个服务器上。比如像下图这样,要将用户表分成两片,存储在两台服务器上,那么我们就可以在程序代码中根据用户ID进行分片计算,把ID为偶数(如94)的用户记录存储到服务器1,ID为奇数(如33)的用户记录存储到服务器2。
但是硬编码方式的缺点比较明显。如果要增加服务器,那么就必须修改分片逻辑代码,这样程序代码就会因为非业务需求产生不必要的变更;其次,分片逻辑会耦合在业务逻辑的程序代码中,修改分片逻辑或业务逻辑,都可能影响另一部分代码,从而出现Bug。
我们可以使用分布式关系数据库中间件来解决这个问题,在中间件中完成数据的分片逻辑,这样对应用程序是透明的。我们常用的分布式关系数据库中间件是MyCAT,原理如下图。
MyCAT是针对MySQL数据库设计的,应用程序可以像使用MySQL数据库一样连接MYCAT,提交SQL命令。MyCAT在收到SQL命令以后,查找配置的分片逻辑规则。
比如上图中,我根据地区进行数据分片,把不同地区的订单存储在不同的数据库服务器上。那么MyCAT就可以解析出SQL中的地区字段prov,根据这个字段连接相对应的数据库服务器。例子中SQL的地区字段是“wuhan”,而在MyCAT中配置“wuhan”对应的数据库服务器是dn1,所以用户提交的这条SQL最终会被发送给DB1@Mysql1数据库进行处理。
HBase
分布式关系数据库可以解决海量数据的存储与访问,但是关系数据库本身并不是分布式的,需要通过中间件或者硬编码的方式进行分片,这样对开发和运维并不友好,于是人们又设计出了一系列天然就是分布式的数据存储系统。因为这些数据存储系统通常不支持关系数据库的SQL语法,所以它们也被称为NoSQL数据库。
HBase就是NoSQL数据库中较为知名的一个产品。我们的短URL数据存储、短视频缩略图存储都使用了HBase作为存储方案。上面网盘元数据存储方案使用了分布式关系数据库,事实上,使用HBase这样的NoSQL数据库会是更好的方案。HBase架构如下。
HRegion是HBase中负责数据存储的主要进程,应用程序对数据的读写操作都是通过和HRetion通信完成的。也就是说,应用程序如果想要访问一个数据,必须先找到HRegion,然后将数据读写操作提交给HRegion,而HRegion最终将数据存储到HDFS文件系统中。由于HDFS是分布式、高可用的,所以HBase的数据存储天然是分布式、高可用的。
因此HBase的设计重点就是HRegion的分布式。HRegionServer是物理服务器,这些服务器构成一个分布式集群,每个HRegionServer上可以启动多个HRegion实例。当一个 HRegion中写入的数据太多,达到配置的阈值时,一个HRegion会分裂成两个HRegion,并将HRegion在整个集群中进行迁移,以使HRegionServer的负载均衡,进而实现HRegion的分布式。
应用程序如果想查找数据记录,需要使用数据的key。每个HRegion中存储一段Key值区间[key1, key2)的数据,而所有HRegion的信息,包括存储的Key值区间、所在HRegionServer地址、访问端口号等,都记录在HMaster服务器上。因此,应用程序要先访问HMaster服务器,得到数据key所在的HRegion信息,再访问对应的HRegion获取数据。为了保证HMaster的高可用,HBase会启动多个HMaster,并通过ZooKeeper选举出一个主服务器。
ZooKeeper
我们在上面提到,分布式数据存储为了保证高可用,需要对数据进行多备份存储,但是多份数据之间可能无法保证数据的一致性,这就是著名的CAP原理。
CAP原理认为,一个提供数据服务的分布式系统无法同时满足数据一致性(Consistency)、可用性(Availibility)、分区耐受性(Patition Tolerance)这三个条件,如下图所示。
其中,一致性的意思是,每次读取数据,要么读取到最近写入的数据,要么返回一个错误,而不是过期数据,这样就能保证数据一致。
可用性的意思是,每次请求都应该得到一个响应,而不是返回一个错误或者失去响应,不过这个响应不需要保证数据是最近写入的。也就是说,系统需要一直都能正常使用,不会引起调用者的异常,但是并不保证响应的数据是最新的。
分区耐受性的意思是,即使因为网络原因,部分服务器节点之间消息丢失或者延迟了,系统依然应该是可以操作的。
当网络分区失效发生时,要么我们取消操作,保证数据一致性,但是系统却不可用;要么我们继续写入数据,但是数据的一致性就得不到保证。
对于一个分布式系统而言,网络失效一定会发生,也就是说,分区耐受性是必须要保证的,那么可用性和一致性就只能二选一,这就是CAP原理。
由于互联网对高可用的追求,大多数分布式存储系统选择可用性,而放松对一致性的要求。而ZooKeeper则是一个保证数据一致性的分布式系统,它主要通过一个ZAB算法(Zookeeper Atomic Broadcast, Zookeeper原子广播)实现数据一致性,算法过程如下。
ZooKeeper集群由多台服务器组成,为了保证多台服务器上存储的数据是一致的,ZAB需要在这些服务器中选举一个Leader,所有的写请求都必须提交给Leader。Leader服务器会向其他服务器(Follower)发起Propose,通知所有服务器:“我们要完成一个写操作请求,请大家检查自己的数据状态是否有问题。”
如果所有Follower服务器都回复Leader服务器ACK,即没有问题,那么Leader服务器会向所有Follower发送Commit命令,要求所有服务器完成写操作。这样包括Leader服务器在内的所有ZooKeeper集群服务器的数据,就都更新并保持一致了。如果有两个客户端程序同时请求修改同一个数据,因为必须要经过Leader的审核,而Leader只接受其中一个请求,数据也会保持一致。
在实际应用中,客户端程序可以连接任意一个Follower,进行数据读写操作。如果是写操作,那么这个请求会被Follower发送给Leader,进行如上所述的处理;如果是读操作,因为所有服务器的数据都是一致的,那么这个Follower直接把自己本地的数据返回给客户端就可以了。
因为ZooKeeper具有这样的特性,所以很多分布式系统都使用ZooKeeper选择主服务器。为了保证系统高可用,像HDFS中的NameNode,或者HBase中的HMaste都需要主主热备,也就是多台服务器充当主服务器,这样任何一台主服务器宕机,都不会影响系统的可用性。
但是在运行期,只能有一台主服务器提供服务,否则系统就不知道该接受哪台服务器的指令,即出现所谓的系统脑裂,因此系统需要选举主服务器。而ZooKeeper的数据一致性特点可以保证只有一台服务器选举成功。在专栏后面的网约车架构案例中,我们也使用了ZooKeeper进行服务器管理。
布隆过滤器
我们在[短URL生成]以及[网络爬虫的案例]中,还使用了布隆过滤器检查内容是否重复,即检查短URL或者网页内容的MD5是否已经存在。如果用Hash表检查重复,千亿级的网页内容MD5就需要一个非常大的Hash表,内存资源消耗非常大。而用布隆过滤器,使用较小的内存就可以检查海量数据中一个数据是否存在。文件MD5重复性检查的布隆过滤器原理如下。
布隆过滤器首先开辟一块巨大的连续内存空间,比如开辟一个 1600G 比特的连续内存空间,也就是 200GB 大的一个内存空间,并将这个空间所有比特位都设置为 0。然后对每个MD5使用多种Hash算法,比如使用 8 种Hash算法,分别计算 8 个Hash值,并保证每个Hash值是落在这个 1600G 的空间里的,也就是,每个 Hash 值对应 1600G 空间里的一个地址下标。然后根据计算出来的Hash值将对应的地址空间里的比特值设为 1,这样一个MD5就可以将8个比特位设置为 1。
如果要检查一个MD5是否存在,只需要让MD5重复使用这 8 个哈希算法,计算出8个地址下标,然后检查它们里面的二进制数是否全是 1,如果是 ,那么表示这个MD5已经存在了。所以,在海量MD5中检查一个MD5是否存在,布隆过滤器会比哈希表更节约内存空间。
小结
因为数据存储是有状态的,所以海量数据存储的分布式架构要解决的核心问题就是:在一个有很多台服务器的分布式集群中,如何知道数据存储在哪台服务器上?
解决方案有两种,一种是有专门的服务器记录数据存储在哪里,即有一个元数据服务器。HDFS里的NameNode和HBase里的HMaster都是这样的角色。应用程序想访问数据,需要先和元数据服务器通信,获取数据存储的位置,再去具体的数据存储服务器上访问数据。
另一种解决方案是通过某种算法计算要访问的数据的位置,这种算法被称作数据路由算法。分片数据库的硬编码算法就是一种数据路由算法,根据分片键计算该记录在哪台服务器上。MyCAT其实也是采用路由算法,只不过将硬编码的分片逻辑记录在了配置文件中。
软件开发技术是一个快速发展的领域,各种新技术层出不穷,如果你只是被动地学习这些技术,很快就会迷失在各种技术细节里,疲惫不堪,最终放弃。事实上,每种技术的出现都因为要解决某个核心问题,最终诞生几种解决方案。同时,每种方案又会产生自己的新问题,比如分布式存储的数据的高可用,以及高可用带来的数据一致性,又需要产生相应的解决方案。
但是只要把握住核心问题和解决方案,就可以自己分析、推导各种衍生的问题和方案,思考各种优缺点和改进策略,最终理解、掌握一个新的技术门类。这不是通过辛苦学习,来掌握一个技术,而是从上帝视角,站在和这些技术的创造者一样的维度去思考,最终内化到自己的知识体系中。
思考题
分布式存储中有个非常著名的数据路由算法,叫一致性Hash算法,这个算法要解决的问题是什么?解决的思路是什么?