网络爬虫及分布式系统
一.抓取网页
1.URL
Web 上每种可用的资源, 如HTML 文档、 图像、 视频片段、 程序等都由一个通用资源标志符(Universal Resource Identifier,URI)进行定位。
URI 通常由三部分组成:①访问资源的命名机制;②存放资源的主机名;③资源自身的名称。
URL 是 URI 的一个子集。 它是 Uniform Resource Locator 的缩写, 译为 “统一资源定位符”。通俗地说,URL 是 Internet 上描述信息资源的字符串,主要用在各种 WWW 客户程序和服务器程序上, 特别是著名的 Mosaic。 采用 URL 可以用一种统一的格式来描述各种信息资源,包括文件、服务器的地址和目录等。URL 的格式由三部分组成:
*第一部分是协议(或称为服务方式)。
*第二部分是存有该资源的主机 IP 地址(有时也包括端口号)。
*第三部分是主机资源的具体地址,如目录和文件名等。
第一部分和第二部分用“://”符号隔开,第二部分和第三部分用“/”符号隔开。第一部分和第二部分是不可缺少的,第三部分有时可以省略。
2.Http报文及状态码
参考:http://www.cnblogs.com/jslee/p/3449915.html
3.宽度优先遍历
图的宽度优先遍历需要一个队列作为保存当前节点的子节点的数据结构。具体的算法
如下所示:
(1) 顶点 V 入队列。
(2) 当队列非空时继续执行,否则算法为空。
(3) 出队列,获得队头节点 V,访问顶点 V 并标记 V 已经被访问。
(4) 查找顶点 V 的第一个邻接顶点 col。
(5) 若 V 的邻接顶点 col 未被访问过,则 col 进队列。
(6) 继续查找 V 的其他邻接顶点 col,转到步骤(5),若 V 的所有邻接顶点都已经被访问过,则转到步骤(2)。
宽度优先遍历是爬虫中使用最广泛的一种爬虫策略,之所以使用宽度优先搜索策略,主要原因有三点:
*重要的网页往往离种子比较近,例如我们打开新闻网站的时候往往是最热门的新闻,随着不断的深入冲浪,所看到的网页的重要性越来越低。
*万维网的实际深度最多能达到 17 层, 但到达某个网页总存在一条很短的路径。而宽度优先遍历会以最快的速度到达这个网页。
*宽度优先有利于多爬虫的合作抓取,多爬虫合作通常先抓取站内链接,抓取的封闭性很强。
4.页面选择
在宽度优先遍历中,在 URL 队列中选择需要抓取的 URL 时,不一定按照队列“先进先出”的方式进行选择。 而把重要的 URL 先从队列中 “挑” 出来进行抓取。 这种策略也称作 “页面选择”
(Page Selection)。这可以使有限的网络资源照顾重要性高的网页。
判断网页的重要性的因素很多, 主要有链接的欢迎度(知道链接的重要性了吧)、 链接的重要度和平均链接深度、网站质量、历史权重等主要因素。
*链接的欢迎度主要是由反向链接(backlinks,即指向当前 URL 的链接)的数量和质量决定的,我们定义为 IB(P)。
*链接的重要度, 是一个关于 URL 字符串的函数, 仅仅考察字符串本身, 比如认为 “.com”和“home”的 URL 重要度比“.cc”和“map”高,我们定义为 IL(P)。
*平均链接深度,根据上面所分析的宽度优先的原则计算出全站的平均链接深度,然后认为距离种子站点越近的重要性越高。我们定义为 ID(P)。
如果我们定义网页的重要性为 I(P),那么,页面的重要度由下面的公式决定:
I(P)=X*IB(P)+Y*IL(P) (1.1)
其中,X 和 Y 两个参数,用来调整 IB(P)和 IL(P)所占比例的大小,ID(P)由宽度优先的遍历规则保证,因此不作为重要的指标函数。
如何实现最佳优先爬虫呢,最简单的方式可以使用优先级队列来实现 TODO 表,并且把每个 URL 的重要性作为队列元素的优先级。这样,每次选出来扩展的 URL 就是具有最高重要性的网页。
5.爬虫队列
数以十亿计的 URL 地址,使用内存的链表或者队列来存储显然不够,因此,需要找到一种数据结构,这种数据结构具有以下几个特点:
*能够存储海量数据,当数据超出内存限制的时候,能够把它固化在硬盘上。
*存取数据速度非常快。
*能够支持多线程访问(多线程技术能够大规模提升爬虫的性能)。
对存储速度的要求,使 Hash 成为存储结构的不二选择。通常,在进行 Hash 存储的时候,key 值都选取 URL 字符串,但是为了更节省空间, 通常会对 URL 进行压缩。常用的压缩算法是 MD5 压缩算法。
选择一个可以进行线程安全、使用 Hash 存储,并且能够应对海量数据的内存数据库是存储 URL 最合适的数据结构。 因此, 由 Oracle 公司开发的内存数据库产品 Berkeley DB 就进入了我们的视线。
关键字/数据(key/value)是 Berkeley DB 用来进行数据库管理的基础。每个 key/value 对构成一条记录。而整个数据库实际上就是由许多这样的结构单元所构成的。通过这种方式,开发人员在使用 Berkeley DB 提供的 API 访问数据库时, 只需提供关键字就能够访问到相应的数据。当然也可以提供 Key 和部分 Data 来查询符合条件的相近数据。Berkeley DB 底层实现采用 B 树。
6.布隆过滤器(Bloom Filter)
在网络爬虫里,如何判断一个网址是否被访问过等。最直接的方法就是将集合中全部的元素存在计算机中,遇到一个新元素时, 将它和集合中的元素直接比较即可。 一般来讲, 计算机中的集合是用哈希表(Hash Table)来存储的。它的好处是快速而准确,缺点是费存储空间。当集合比较小时,这个问题不显著,但是当集合巨大时,哈希表存储效率低的问题就显现出来了。
由于哈希表的存储效率一般只有 50%, 因此一个电子邮件地址需要占用十六个字节。一亿个地址大约要 1.6GB,即十六亿字节的内存。因此存储几十亿个邮件地址可能需要上百 GB 的内存。计算机内存根本不能存储。
一种称作布隆过滤器的数学工具, 它只需要哈希表 1/8 到 1/4 的大小就能解决同样的问题。
假定存储一亿个电子邮件地址,先建立一个 16 亿二进制常量,即两亿字节的向量,然后将这 16 亿个二进制位全部设置为零。对于每一个电子邮件地址 X,用 8 个不同的随机数产生器(F1,F2, …, F8)产生 8 个信息指纹(f1, f2, …, f8)。再用一个随机数产生器 G 把这 8 个信息指纹映射到 1 到 16 亿中的 8 个自然数 g1, g2, …, g8。 现在我们把这 8 个位置的二进制位全部设置为 1。 当我们对这 1 亿个 E-mail 地址都进行这样的处理后。 一个针对这些 E-mail地址的布隆过滤器就建成了,如图所示。
现在,来看看布隆过滤器是如何检测一个可疑的电子邮件地址 Y 是否在黑名单中的。我们用 8 个随机数产生器(F1, F2, …, F8)对这个地址产生 8 个信息指纹 S1, S2, …, S8。 然后将这 8 个指纹对应到布隆过滤器的 8 个二进制位,分别是 T1, T2, …, T8。如果 Y 在黑名单中,显然,T1, T2, …, T8 对应的 8 个二进制位一定是 1。
但是,它有一条不足之处。也就是它有极小的可能将一个不在黑名单中的电子邮件地址判定为在黑名单中,因为有可能某个好的邮件地址正巧对应 8 个都被设置成 1 的二进制位。好在这种可能性很小。我们把它称为误识概率。常见的补救办法是建立一个小的白名单,存储那些可能误判的邮件地址。
7.爬虫架构
一个设计良好的爬虫架构必须满足如下需求。
(1) 分布式:爬虫应该能够在多台机器上分布执行。
(2) 可伸缩性:爬虫结构应该能够通过增加额外的机器和带宽来提高抓取速度。
(3) 性能和有效性:爬虫系统必须有效地使用各种系统资源,例如,处理器、存储空间和网络带宽。
(4) 质量:鉴于互联网的发展速度,大部分网页都不可能及时出现在用户查询中,所以爬虫应该首先抓取有用的网页。
(5) 新鲜性:在许多应用中,爬虫应该持续运行而不是只遍历一次。
(6) 更新:因为网页会经常更新,例如论坛网站会经常有回帖。爬虫应该取得已经获取的页面的新的拷贝。例如一个搜索引擎爬虫要能够保证全文索引中包含每个索引页面的较新的状态。对于搜索引擎爬虫这样连续的抓取,爬虫访问一个页面的频率应该和这个网页的更新频率一致。
(7) 可扩展性:为了能够支持新的数据格式和新的抓取协议,爬虫架构应该设计成模块化的形式。
最主要的关注对象是爬虫和存储库。其中的爬虫部分阶段性地抓取互联网上的内容。存储库存储爬虫下载下来的网页,是分布式的和可扩展的存储系统。在往存储库中加载新的内容时仍然可以读取存储库。
下图是单线程架构:
(1) URL Frontier 包含爬虫当前待抓取的 URL(对于持续更新抓取的爬虫,以前已经抓取过的 URL 可能会回到 Frontier 重抓)。
(2) DNS 解析模块根据给定的 URL 决定从哪个 Web 服务器获取网页。
(3) 获取模块使用 HTTP 协议获取 URL 代表的页面。
(4) 解析模块提取文本和网页的链接集合。
(5) 重复消除模块决定一个解析出来的链接是否已经在 URL Frontier 或者最近下载过(检查Visited)。
通用的爬虫框架流程:
1)首先从互联网页面中精心选择一部分网页,以这些网页的链接地址放在URL集中;
2)将这些种子URL放入URL Frontier中;
3)爬虫从待抓取 URL队列依次读取,并将URL通过DNS解析,把链接地址转换为网站服务器对应的IP地址。
4)然后将IP地址和网页相对路径名称交给网页下载器,
5)网页下载器负责页面内容的下载。
6)对于下载到本地的网页,一方面将其解析页面,判断内容是否重复,放入文档数据库中;另一方面将下载网页的 URL放入Visited中,这个队列记载了爬虫系统己经下载过的网页URL,以避免网页的重复抓取。
7)对于刚下载的网页,从中抽取出所包含的所有链接信息,并在已抓取URL队列中检査,如果发现链接还没有被抓取过,则将这个URL放入URL Frontier!
8,9)末尾,在之后的 抓取调度中会下载这个URL对应的网页,如此这般,形成循环,直到待抓取URL队列为空.
DNS 解析是网络爬虫的瓶颈。 由于域名服务的分布式特点, DNS 可能需要多次请求转发,并在互联网上往返,需要几秒有时甚至更长的时间解析出 IP 地址。如果我们的目标是一秒钟抓取数百个文件,这样就达不到性能要求。一个标准的补救措施是引入缓存:最近完成 DNS 查询的网址可能会在 DNS 缓存中找到,避免了访问互联网上的 DNS 服务器。然而,由于抓取礼貌的限制,降低了 DNS 缓存的命中率。
用 DNS 解析还有一个难点:在标准库中实现的查找是同步的。这意味着一旦一个请求发送到 DNS 服务器上,在那个节点上的其他爬虫线程也被阻塞直到第一个请求完成。为了避免这种情况发生,许多爬虫自己来实现 DNS 解析。
对单线程并行抓取来说,异步 I/O 是很重要的基本功能。异步 I/O 模型大体上可以分为两种,反应式(Reactive)模型和前摄式(Proactive)模型。
传统的 select/epoll/kqueue 模型以及 Java NIO 模型,都是典型的反应式模型,即应用代码对 I/O 描述符进行注册,然后等待 I/O 事件。 当某个或某些 I/O 描述符所对应的 I/O 设备上产生 I/O 事件(可读、 可写、异常等)时,系统将发出通知,于是应用便有机会进行 I/O 操作并避免阻塞。由于在反应式模型中应用代码需要根据相应的事件类型采取不同的动作,因此最常见的结构便是嵌套的if {…} else {…} 或 switch,并常常需要结合状态机来完成复杂的逻辑。
前摄式模型则恰恰相反。 在前摄式模型中, 应用代码主动投递异步操作而不管 I/O 设备当前是否可读或可写。投递的异步 I/O 操作被系统接管,应用代码也并不阻塞在该操作上,而是指定一个回调函数并继续自己的应用逻辑。当该异步操作完成时,系统将发起通知并调用应用代码指定的回调函数。在前摄式模型中,程序逻辑由各个回调函数串联起来:异步操作 A 的回调发起异步操作 B,B 的回调再发起异步操作 C,以此往复。
8.MapReduce执行爬虫
MapReduce流程:
(1) MapReduce函数库首先把输入文件分成M块,每块大概16MB到64MB。接着在集群的机器上执行处理程序。MapReduce算法运行过程中有一个主控程序,称为master。主控程序会产生很多作业程序,称为worker。并且把M个map任务和R个reduce任务分配给这些worker,让它们去完成。
(2) 被分配了map任务的worker读取并处理相关的输入(这里的输入是指已经被切割的输入小块splite)。它处理输入的数据,并且将分析出的键/值(key/value)对传递给用户定义的reduce()函数。map()函数产生的中间结果键/值(key/value)对暂时缓冲到内存。
(3) map()函数缓冲到内存的中间结果将被定时刷写到本地硬盘,这些数据通过分区函数分成R个区。这些中间结果在本地硬盘的位置信息将被发送回master,然后这个master负责把这些位置信息传送给reduce()函数的worker。
(4) 当master通知了reduce()函数的worker关于中间键/值(key/value)对的位置时,worker调用远程方法从map()函数的worker机器的本地硬盘上读取缓冲的中间数据。当reduce()函数的worker读取到了所有的中间数据,它就使用这些中间数据的键(key)进行排序,这样可以使得相同键(key)的值都在一起。如果中间结果集太大了,那么就需要使用外排序。
(5) reduce()函数的worker根据每一个中间结果的键(key)来遍历排序后的数据,并且把键(key)和相关的中间结果值(value)集合传递给reduce()函数。reduce()函数的worker最终把输出结果存放在master机器的一个输出文件中。
(6) 当所有的map任务和reduce任务都已经完成后,master激活用户程序。在这时,MapReduce返回用户程序的调用点。
(7) 当以上步骤成功结束以后,MapReduce的执行数据存放在总计R个输出文件中(每个输出文件都是由reduce任务产生的,这些文件名是用户指定的)。通常,用户不需要将这R个输出文件合并到一个文件,他们通常把这些文件作为输入传递给另一个MapReduce调用,或者用另一个分布式应用来处理这些文件,并且这些分布式应用把这些文件看成为输入文件由于分区(partition)成为的多个块文件。
爬虫利用MapReduce流程:
1) 插入URL列表(inject)
MapReduce程序1:
目标:转换input输入为CrawlDatum格式
输入:URL文件
步骤:
(1) map(line) →<url, CrawlDatum>
(2) reduce()合并多重的URL
输出:临时的CrawlDatum文件
MapReduce程序2:
目标:合并上一步产生的临时文件到新的CrawlDB
输入:上次MapReduce输出的CrawlDatum
步骤:
(1) map()过滤重复的URL
(2) reduce:合并两个CrawlDatum到一个新的CrawlDB
输出:CrawlDatum
2) 生成抓取列表(Generate)
MapReduce程序:
目标:选择抓取列表
输入:CrawlDB文件
步骤:
(1) map() → 如果抓取当前时间大于现在时间,转换成<CrawlDatum,URL>格式
(2) reduce:取最顶部的N个链接
输出:< URL,CrawlDatum>文件
3) 抓取内容(Fetch)
MapReduce程序:
目标:抓取内容
输入:< URL,CrawlDatum>,按主机划分,按hash排序
步骤:
(1) map(URL,CrawlDatum) → 输出<URL,FetcherOutput>
(2) 多线程,调用Nutch的抓取协议插件,抓取输出<CrawlDatum, Content>
输出:<URL,CrawlDatum>和<URL,Content>两个文件
4) 分析处理内容(Parse)
MapReduce程序:
目标:处理抓取的内容
输入:抓取的< URL, Content>
步骤:
(1) map(URL,Content) →<URL,Parse>
(2) raduce()函数调用Nutch的解析插件,输出处理完的格式是<ParseText,ParseData>
输出:< URL,ParseText>、<URL,ParseData>、<URL,CrawlDatum>
5) 更新CrawlDB库(updateDB)
MapReduce程序:
目标:将fetch和parse整合到DB中
输入:<URL, CrawlDatum> 现有的DB加上fetch和parse的输出,合并上面3个DB为一个新的DB
输出:新的抓取DB
6) 建立索引(index)
MapReduce程序:
目标:生成Lucene索引
输入:多种文件格式
步骤:
(1) parse处理完的<URL,ParseData> 提取title、metadata信息等
(2) parse处理完的<URL,ParseText> 提取text内容
(3) 转换链接处理完的<URL,Inlinks> 提取anchors
(4) 抓取内容处理完的<URL,CrawlDatum> 提取抓取时间
(5) map()函数用ObjectWritable包裹上面的内容
(6) reduce()函数调用Nutch的索引插件,生成Lucene Document文档
输出:输出Lucene索引
二.分布式爬虫
1.分布式存储
使用集群的方法有很多种,但大致分为两类:一类仍然采用关系数据库管理系统(RDBMS),然后通过对数据库的垂直和水平切割将整个数据库部署到一个集群上,这种方法的优点在于可以采用RDBMS这种熟悉的技术,但缺点在于它是针对特定应用的。由于应用的不同,切割的方法是不一样的。关于数据库的垂直和水平切割的具体细节可以查看相关资料。
还有一类就是Google所采用的方法,抛弃RDBMS,采用key/value形式的存储,这样可以极大地增强系统的可扩展性(scalability),如果要处理的数据量持续增大,多加机器就可以了。比如在key/value中想要查找,只需输入key就行了,而关系数据库中,需要表,和其中一项,还有其它很多操作,关系数据库都需要维护(建表,表中外键等等),规则。
云存储简单点说就是构建一个大型的存储平台给别人用,这也就意味着在这上面运行的应用其实是不可控的。如果其中某个客户的应用随着用户的增长而不断增长时,云存储供应商是没有办法通过数据库的切割来达到扩展的,因为这个数据是客户的,供应商不了解这个数据自然就没法作出切割。在这种情况下,key/value的存储就是唯一的选择了,因为这种条件下的可扩展性必须是自动完成的,不能有人工干预。
key/value存储与RDBMS相比,一个很大的区别就是它没有模式的概念。在RDBMS 中,模式所代表的其实就是对数据的约束,包括数据之间的关系(relationship)和数据的完整性(integrity),比如RDBMS中对于某个数据属性会要求它的数据类型是确定的(整数或者字符串等),数据的范围也是确定的(0~255),而这些在key/value存储中都没有。在key/value存储中,对于某个key,value可以是任意的数据类型。
在所有的RDBMS中,都是采用SQL语言对数据进行访问。一方面,SQL对于数据的查询功能非常强大;另一方面,由于所有的RDBMS都支持SQL查询,所以可移植性很强。而在key/value 存储中,对于数据的操作使用的都是自定义的一些API,而且支持的查询也比较简单。
所谓的可扩展性,其实包括两方面内容。一方面,是指key/value存储可以支持极大的数据存储。它的分布式的架构决定了只要有更多的机器,就能够保证存储更多的数据。另一方面,是指它可以支持数量很多的并发的查询。对于RDBMS,一般几百个并发的查询就可以让它很吃力了,而一个key/value存储,可以很轻松地支持上千个并发查询。
key/value存储的缺陷主要有两点:
* 由于key/value存储中没有schema,所以它是不提供数据之间的关系和数据的完备性的,所有的这些东西都落到了应用程序一端,其实也就是开发人员的头上。这无疑加重了开发人员的负担。
* 在RDBMS中,需要设定各表之间的关系,这其实是一个数据建模的过程(data modeling process)。当数据建模完成后,这个数据库对于应用程序就是独立的了,这就意味着其他程序可以在不改变数据模型的前提下使用相同的数据集。但在key/value存储中,由于没有这样一个数据模型,不同的应用程序需要重复进行这个过程。
*key/value存储最大的一个缺点在于它的接口是不熟悉的。这阻碍了开发人员可以快速而顺利地用上它。当然,现在有种做法,就是在key/value存储上再加上一个类SQL语句的抽象接口层,从而使得开发人员可以用他们熟悉的方式(SQL)来操作key/value存储。但由于RDBMS和key/value存储的底层实现有着很大的不同,这种抽象接口层或多或少还是受到了限制。
Consistent Hash算法 参见
http://www.cnblogs.com/jslee/p/3444887.html
2.GFS(google file system)
在分布式存储环境中,常常会产生以下一些问题。
1. 在分布式存储中,经常会出现节点失效的情况
2. 分布式存储的文件都是非常巨大的
3. 对于搜索引擎的业务而言,大部分文件都只会在文件尾新增加数据,而少见修改已有数据的
4. 与应用一起设计的文件系统API对于增加整个系统的弹性和适用性有很大的好处
5. 系统必须非常有效地支持多个客户端并行添加同一个文件GFC文件经常使用生产者/消费者队列模式,或者以多路合并模式进行操作。好几百个运行在不同机器上的生产者,将会并行增加一个文件。
6. 高性能的稳定带宽的网络要比低延时更加重要GFS目标应用程序一般会大量操作处理比较大块的数据。
为了满足以上几点需要,Google遵照下面几条原则设计了它的分布式文件系统(GFS):
(1) 系统建立在大量廉价的普通计算机上,这些计算机经常出故障。则必须对这些计算机进行持续检测,并且在系统的基础上进行检查、容错,以及从故障中进行恢复。
(2) 系统存储了大量的超大文件。数GB的文件经常出现并且应当对大文件进行有效的管理。同时必须支持小型文件,但是不必为小型文件进行特别的优化。
(3) 一般的工作都是由两类读取组成:大的流式读取和小规模的随机读取。在大的流式读取中,每个读操作通常一次就要读取几百字节以上的数据,每次读取1MB或者以上的数据也很常见。因此,在大的流式读取中,对于同一个客户端来说,往往会发起连续的读取操作顺序读取一个文件。而小规模的随机读取通常在文件的不同位置,每次读取几字节数据。对于性能有过特别考虑的应用通常会做批处理并且对它们读取的内容进行排序,这样可以使得它们的读取始终是单向顺序读取,而不需要往回读取数据。
(4) 通常基于GFS的操作都有很多超大的、顺序写入的文件操作。通常写入操作的数据量和读入的数据量相当。一旦完成写入,文件就很少会更改。应支持文件的随机小规模写入,但是不需要为此做特别的优化。
在GFS下,每个文件都被拆成固定大小的块(chunk)。每一个块都由主服务器根据块创建的时间产生一个全局唯一的以后不会改变的64位的块处理(chunk handle)标志。块服务器在本地磁盘上用Linux文件系统保存这些块,并且根据块处理标志和字节区间,通过Linux文件系统读写这些块的数据。出于可靠性的考虑,每一个块都会在不同的块处理器上保存备份。
主服务器负责管理所有的文件系统的元数据,包括命名空间、访问控制信息、文件到块的映射关系、当前块的位置等信息。主服务器也同样控制系统级别的活动,比如块的分配管理,孤点块的垃圾回收机制、块服务器之间的块镜像管理。
连接到各个应用系统的GFS客户端代码包含了文件系统的API,并且会和主服务器及块服务器进行通信处理,代表应用程序进行读写数据的操作。客户端和主服务器进行元数据的操作,但是所有的数据相关的通信是直接和块服务器进行的。
由于在流式读取中,每次都要读取非常多的文件内容,并且读取动作是顺序读取,因此,在客户端没有设计缓存。没有设计缓存系统使得客户端以及整个系统都大大简化了(少了缓存的同步机制)。块服务器不需要缓存文件数据,因为块文件就像本地文件一样被保存,所以Linux的缓存已经把常用的数据缓存到了内存里。
在读取时,首先,客户端把应用要读取的文件名和偏移量,根据固定的块大小,转换为文件的块索引。然后向主服务器发送这个包含了文件名和块索引的请求。主服务器返回相关的块处理标志以及对应的位置。客户端缓存这些信息,把文件名和块索引作为缓存的关键索引字。
具体分布式存储可参见:
http://www.cnblogs.com/jslee/p/3457475.html
3.BigTable
是一种key/value型分布式数据库系统。应用程序通常都不会直接操作GFS文件系统,而直接操作它的上一级存储结构——BigTable。这正如一般文件系统和关系数据库的道理一样.
BT在很多地方和关系数据库类似:它采用了许多关系数据库的实现策略。但和它们不同的是,BT采用了不同的用户接口。BT不支持完全的关系数据模型,而是为客户提供了简单的数据模型,让客户来动态控制数据的分布和格式(就是只存储字符串,格式由客户来解释),这样能大幅度地提高访问速度。数据的下标是行和列的名字,数据本身可以是任意字符串。BT的数据是字符串,没有具体的类型。
BT的本质是一个稀疏的、分布式的、长期存储的、多维度的和排序的Map。Map的key是行关键字(Row)、列关键字(Column)和时间戳(Timestamp)。Value是一个普通的bytes数组。如下所示:
(row:string, column:string,time:int64)->string
BT通过行关键字在字典中的顺序来维护数据。一张表可以动态划分成多个连续“子表”(tablet)。这些“子表”由一些连续行组成,它是数据分布和负载均衡的单位。这使得读取较少的连续行比较有效率,通常只需要少量机器之间的通信即可。用户可以利用这个属性来选择行关键字,从而达到较好的数据访问“局部性”。举例来说,在webtable中,通过反转URL中主机名的方式,可以把同一个域名下的网页组织成连续行。具体而言,可以把站点maps.google.com/index.html中的数据存放在关键字com.google.maps/index.html所对应的数据中。这种存放方式可以让基于主机和基于域名的分析更加有效。
一组列关键字组成了“列族”(column famliy),这是访问控制的基本单位。同一列族下存放的所有数据通常都是同一类型的。“列族”必须先创建,然后才能在其中的“列关键字”下存放数据。“列族”创建后,其中任何一个“列关键字”都可使用。“列关键字”用如下语法命名:“列族”:限定词。“列族”名必须是看得懂的字符串,而限定词可以是任意字符串。比如,webtable可以有个“列族”叫language,存放撰写网页的语言。我们在language“列族”中只用一个“列关键字”,用来存放网页的语言标识符。该表的另一个有用的“列族”是anchor。“列族”的每一个“列关键字”代表一个锚链接,访问控制、磁盘使用统计和内存使用统计,均可在“列族”这个层面进行。例子,可以使用这些功能来管理不同应用:有的应用添加新的基本数据,有的读取基本数据并创建引申的“列族”,有的则只能浏览数据(甚至可能因为隐私权的原因不能浏览所有数据)。
BT表中的每一个表项都可以包含同一数据的多个版本,由时间戳来索引。BT的时间戳是64位整型,表示准确到毫秒的“实时”。需要避免冲突的应用程序必须自己产生具有唯一性的时间戳。不同版本的表项内容按时间戳倒序排列,即最新的排在前面。
BT使用Google分布式文件系统(GFS)来存储日志和数据文件。一个BT集群通常在一个共享的机器池中工作,池中的机器还运行着其他分布式应用,BT和其他程序共享机器(BT的瓶颈是I/O内存,可以和CPU要求高的程序并存)。BT依赖集群管理系统来安排工作,在共享的机器上管理资源,处理失效机器并监视机器状态。
BT还依赖一个高度可用的分布式数据锁服务(Chubby)。一个Chubby 由5个“活跃”的备份构成,其中一个被这些备份选成主备份,并且处理请求。这个服务只有在大多数备份都是“活跃”的并且互相通信的时候,才是“活跃”的。当有机器失效的时候,Chubby使用一定的算法来保证备份的一致性。Chubby提供了一个名字空间,里面包括目录和一系列文件。每个目录或者文件可以当成一个锁来用,读写文件操作都是原子操作。Chubby客户端的程序库提供了对Chubby文件的一致性缓存。每个Chubby客户维护一个和Chubby通信的会话。如果客户不能在一定时间内更新自己的会话,会话就失效了。当一个会话失效时,其拥有的锁和打开的文件句柄都失效。Chubby客户可以在文件和目录上登记回调函数,以获得改变或者会话过期的通知。BT使用锁服务来完成以下几个任务:
(1) 保证任何时间最多只有一个活跃的主备份。
(2) 存储BT数据的启动位置。
(3) 发现“子表”服务器,并处理tablet服务器失效的情况。
(4) 存储BT数据的“模式”信息(每张表的列信息)。
(5) 存储访问权限列表。在Chubby中,存储了BT的访问权限,如果Chubby不能访问,那么由于获取不到访问权限,因此BT就也不能访问了。
BT主要由三个构件组成:
(1) 一个客户端的链接库。
(2) 一个主服务器。
(3) 许多“子表”服务器。“子表”服务器可以动态地从群组中被添加和删除,以适应流量的改变。
主服务器的作用是给“子表”服务器分配“子表”、探测“子表”服务器的增加和缩减、平衡“子表”服务器负载,以及回收GFS系统中文件的碎片。此外,它还可以创建模式表。
一个“子表”服务器管理许多子表(一般每个“子表”服务器可以管理10到1000个子表)。“子表”服务器处理它所管理的“子表”的读写请求,还可以将那些变得很大的“子表”分割。
像许多单主机的分布式存储系统一样,客户端数据不是通过主服务器来传输的:客户端要读写时直接与“子表”服务器通信。因为BT客户端并不依赖主服务器来请求“子表”本地信息,大多数客户端从不与主服务器通信。因此,实际上主机的负载往往很小。一个BT群组可以存储大量的表。每一个表有许多“子表”,并且每个“子表”包含一行上所有相关的数据。最初,每个表只包含一个子表。随着表的增长,自动分成了许多的“子表”,每个子表的默认大小为100~200MB。
BT用三层体系的B+树来存储子表的地址信息。
第一层是一个存储在Chubby中的文件,它包含“根子表”(root tablet)的地址。“根子表”包含一些“元数据表”(MetaData tablets)的地址信息。这些“元数据表”包含用户“子表”的地址信息。“根子表”是“元数据表”中的第一个“子表”,但它从不会被分割。
客户端缓存“子表”地址。如果客户端发现缓存的地址信息是错误的,那么它会递归地提升“子表”地址等级。如果客户端缓存是空的,寻址算法需要三个网络往返过程,包括一次从Chubby的读取。如果客户端缓存是过期的,那么寻址算法可能要用6个往返过程。
尽管“子表”地址缓存在内存里,不需要GFS访问,但还是可以通过客户端预提取“子表地址”进一步降低性能损耗。“子表”一次被分配给一个子表服务器。主服务器跟踪“活跃”的“子表”服务器集合以及当前“子表”对“子表服务器”的分配状况。当一个“子表”还没有被分配,并且有一个“子表”服务器是可用的,主机就通过传输一个“子表”装载请求到“子表”服务器来分配“子表”。
BT使用Chubby来跟踪“子表”服务器。当“子表”服务器启动时,它在一个特别的Chubby目录中创建一个文件,并且获得一个互斥锁。主机通过监听这个目录(服务器目录)来发现“子表”服务器。如果“子表”服务器丢失了自己的互斥锁,就会停止为它的“子表”服务。
主机负责探测何时“子表”服务器不再为它的“子表”服务,以便可以尽快地分配那些“子表”。为了达到这个目的,主机会周期性地询问每个“子表”服务器的锁的状态。如果一个“子表”服务器报告它丢失了锁,主机会尝试在服务器的文件中获取一把互斥锁。如果主机能获得这把锁,则表示Chubby是可用的并且“子表”服务器已经失效。因此主机通过删除“子表”服务器的服务文件来确保它不会再工作。一旦服务文件被删除,主机可以把先前分配给这台服务器的所有“子表”移到未分配的“子表”集合中。“子表”的持久化状态存储在GFS文件里。
每次更新“子表”前都要更新“子表”的重做日志(redo log)。并且最近更新的内容(已经提交的但还没有写入到磁盘的内容)会存放在内存中,称为memtable。之前的更新(已经提交的并且固化在磁盘的内容)会被持久化到一系列的SSTable中。当一个写操作请求过来时,“子表”服务器会先写日志,当提交的时候,就把这些更新写入memtable中。之后等系统不繁忙的时候,就写入SSTable中(这个过程和Oracle数据库写操作基本一致)。
如果请求是读操作,则可以根据当前的memtable和SSTable中的内容进行合并,然后对请求返回结果。因为memtable和SSTable有相同的结构,因此,合并是一个非常快的操作。
4.MapReduce
参见:书。
参考:自己动手写网络爬虫