网络爬虫及分布式系统

 一.抓取网页

  1.URL

  Web 上每种可用的资源, 如HTML 文档、 图像、 视频片断、 程序等都由一个通用资源标志符(Universal Resource Identifier,URI)进行定位。
  URI 一般由三部分组成:①访问资源的命名机制;②存放资源的主机名;③资源自身的名称。html

  URL 是 URI 的一个子集。 它是 Uniform Resource Locator 的缩写, 译为 “统一资源定位符”。通俗地说,URL 是 Internet 上描述信息资源的字符串,主要用在各类 WWW 客户程序和服务器程序上, 特别是著名的 Mosaic。 采用 URL 能够用一种统一的格式来描述各类信息资源,包括文件、服务器的地址和目录等。URL 的格式由三部分组成:
  *第一部分是协议(或称为服务方式)。
  *第二部分是存有该资源的主机 IP 地址(有时也包括端口号)。
  *第三部分是主机资源的具体地址,如目录和文件名等。
  第一部分和第二部分用“://”符号隔开,第二部分和第三部分用“/”符号隔开。第一部分和第二部分是不可缺乏的,第三部分有时能够省略。web

  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

  参见:书。

 

参考:本身动手写网络爬虫

相关文章
相关标签/搜索