转载请注明:http://duanple.blog.163.com/blog/static/709717672010961173782/ 

作者 phylips@bmy

摘要

Bigtable是设计用来管理那些可能达到很大大小(比如可能是存储在数千台服务器上的数PB的数据)的结构化数据的分布式存储系统。Google的很多项目都将数据存储在Bigtable中,比如网页索引,google 地球,google金融。这些应用对Bigtable提出了很多不同的要求,无论是数据大小(从单纯的URL到包含图片附件的网页)还是延时需求。尽管存在这些各种不同的需求,Bigtable成功地为google的所有这些产品提供了一个灵活的,高性能的解决方案。在这篇论文中,我们将描述Bigtable所提供的允许客户端动态控制数据分布和格式的简单数据模型,此外还会描述Bigtable的设计和实现。 

1.导引

在过去的2年半时间里,我们设计,实现,部署了一个称为Bigtable的用来管理google的数据的分布式存储系统。Bigtable的设计使它可以可靠地扩展到成PB的数据以及数千台机器上。Bigtable成功的实现了这几个目标:广泛的适用性,可扩展性,高性能以及高可用性。目前,Bigtable已经被包括Google分析,google金融,Orkut,个性化搜索,Writely和google地球在内的60多个google产品和项目所使用。这些产品使用Bigtable用于处理各种不同的工作负载类型,从面向吞吐率的批处理任务到时延敏感的面向终端用户的数据服务。这些产品所使用的Bigtable集群也跨越了广泛的配置规模,从几台机器到存储了几百TB数据的上千台服务器。 

在很多方面,Bigtable都类似于数据库:它与数据库采用了很多相同的实现策略。目前的并行数据库和主存数据库已经成功实现了可扩展性和高性能,但是Bigtable提供了与这些系统不同的接口。Bigtable并不支持一个完整的关系数据模型,而是给用户提供了一个可以动态控制数据分布和格式的简单数据模型,允许用户将数据的局部性属性体现在底层的数据存储上。数据使用可以是任意字符串的行列名称进行索引。Bigtable将数据看做是未经解释的字符串,尽管用户经常将各种形式的结构化或半结构化的数据存储到这些字符串里。用户可以通过在schema中的细心选择来控制数据的locality。最后,Bigtable的schema参数还允许用户选择从磁盘还是内存获取数据。 

第2节更加详细的描述了该数据模型。第3节提供了关于用户API的概览。第4节简要描述了Bigtable所依赖的底层软件。第5节描述了Bigtable的基本实现。第6节描述了我们为提高Bigtable的性能使用的一些技巧。第7节提供了一些对于Bigtable的性能测量数据。第8节展示了几个Google内部的Bigtable的使用实例。第9节讨论了我们在设计支持Bigtable所学到的一些经验教训。最后第10节描述了相关工作,第11节进行了总结。

 2.数据模型

Bigtable是一个稀疏的,分布式的一致性多维有序map。这个map是通过行关键字,列关键字以及时间戳进行索引的;map中的每个值都是一个未经解释的字节数组。

(row:string,column:string,time:int64)  -> string

我们在对于这种类Bigtable系统的潜在使用场景进行了大量考察后,最终确定了这个数据模型。举一个影响到我们的某些设计决策的具体例子,比如我们想保存一份可以被很多工程使用的一大集网页及其相关信息的拷贝。我们把这个表称为webtable,在这个表中,我们可以使用URL作为行关键字,网页的各种信息作为列名称,将网页的内容作为表的内容存储:获取的时候还需要在列上加上时间戳,如图1所示。

【google论文四】Bigtable:结构化数据的分布式存储系统(上) - 星星 - 银河里的星星

 表中的行关键字是大字符串(目前最大可以到64KB,尽管对于大多数用户来说最常用的是10-100字节)。在一个行关键字下的数据读写是原子性的(无论这一行有多少个不同的列被读写),这个设计使得用户在对相同行的并发更新出现时,更容易理解系统的行为。

 Bigtable按照行关键字的字典序来维护数据。行组{row range,将它翻译为行组,一个row range可能由多个行组成}是可以动态划分的。每个行组叫做一个tablet,是数据存放以及负载平衡的单位。这样,对于一个短的行组的读就会很有效,而且只需要与少数的机器进行通信。客户端可以通过选择行关键字来利用这个属性,这样它们可以为数据访问得到好的局部性。比如,在webtable里,相同域名的网页可以通过将URL中的域名反转而使他们放在连续的行里来组织到一块。比如我们将网页maps.google.com/index.html的数据存放在关键字com.google.maps/index.html下。将相同域名的网页存储在邻近位置可以使对主机或域名的分析更加有效。

 列族

不同的列关键字可以被分组到一个集合,我们把这样的一个集合称为一个列族,它是基本的访问控制单元。存储在同一个列族的数据通常是相同类型的(我们将同一列族的数据压缩在一块)。在数据能够存储到某个列族的列关键字下之前,必须首先要创建该列族。我们假设在一个表中不同列族的数目应该比较小(最多数百个),而且在操作过程中这些列族应该很少变化。与之相比,一个表的列数目可以没有限制。

 一个列关键字是使用如下的字符来命名的:family:qualifier。列族名称必须是可打印的,但是qualifier可能是任意字符串。比如webtable有一个列族是language,它存储了网页所使用的语言。在language列族里,我们只使用了一个列关键字,里面存储了每个网页的language id。该表的另一个列族是anchor,在该列族的每个列关键字代表一个单独的anchor,如图1所示。Qualifier是站点的名称,里面的内容是链接文本。

 访问控制以及磁盘的内存分配都是在列族级别进行的。在webtable这个例子中,这些控制允许我们管理不同类型的应用:一些可能会添加新的基础数据,一些可能读取这些基础数据来创建新的列族,一些可能只需要查看现有数据(甚至可能因为隐私策略不需要查看所有现有数据)。

 时间戳

Bigtable里的每个cell可以包含相同数据的多个版本;这些不同的版本是通过时间戳索引的。Bigtable的时间戳是一个64位的整数。它们可以由Bigtable来赋值,在这种情况下它们以毫秒来代表时间。也可以由客户端应用程序显式分配。应用程序为了避免冲突必须能够自己生成唯一的时间戳。一个cell的不同版本是按照时间戳降序排列,这样最近的版本可以被首先读到。

为了使不同版本的数据管理更简单,我们支持2个针对每个列族的设定来告诉Bigtable可以对cell中的数据版本进行自动的垃圾回收。用户可以指定最近的哪几个版本需要保存,或者保存那些足够新的版本(比如只保存那些最近7天写的数据)。

 在我们的webtable中,我们将爬取的网页的时间戳存储在内容里:这些时间说明了这些网页的不同版本分别是在何时被抓取的。前面描述的垃圾回收机制,使我们只保存每个网页最新的三个版本。

 3. API

Bigtable API提供了一些函数用于表及列族的创建和删除。它也提供了一些用于改变集群,表格及列族元数据的函数,比如访问控制权限。

 客户端应用程序可以写或者删除Bigtable里的值,从行里查找值或者在表中的一个数据子集中进行迭代。图2展示了使用RowMutation执行一系列更新的C++代码(为了保持简单省略了不相关的细节)。Apply调用对webtable执行了一个原子性的变更操作:给www.cnn.com增加一个anchor,然后删除另一个anchor。

【google论文四】Bigtable:结构化数据的分布式存储系统(上) - 星星 - 银河里的星星

图3展示的c++代码使用Scanner来在一个特殊行上的所有anchor进行迭代,用户可以在多个列族上进行迭代,存在几种机制来对扫描到的行,列,时间戳进行过滤。比如我们可以限制只扫描那些与正则表达式”anchor:*.cnn.com”匹配的列,或者那些时间戳距离当前时间10天以内的anchor。

【google论文四】Bigtable:结构化数据的分布式存储系统(上) - 星星 - 银河里的星星

 Bigtable提供了几种其他的feature允许用户使用更复杂的方式熟练控制数据。首先,Bigtable支持单行事务,能够支持对存储在一个行关键字上的执行原子性的读写修改序列。Bigtable当前并不支持跨行的事务,尽管它提供了一个多个用户的跨行写的接口。其次,Bigtable允许用户将cell作为一个整数计数器来使用。最后,Bigtable支持在服务器地址空间内执行一个客户端脚本。这些脚本是使用google内部开发的数据处理语言sawzall编写的。当前,我们基于Sawzall的API不允许客户端脚本向Bigtable中写回,但是它允许进行各种形式的数据转换,基于各种表达式的过滤以及大量的统计操作符。

 Bigtable可以与MapReduce(google内部开发的一个运行大规模并行程序的框架)一起使用。我们写了很多wrapper它允许将Bigtable作为输入源或者输出目标。

4. 基础构件

Bigtable是建立在google的其他几个设施之上。Bigtable使用GFS来存储日志和数据文件。Bigtable集群通常运行在一个运行着大量其他分布式应用的共享机器池上。Bigtable依赖于一个集群管理系统进行job调度,共享机器上的资源管理,处理机器失败以及监控机器状态。

 Bigtable内部采用Google SSTable文件格式来存储数据。一个SSTable提供了一个一致性的,有序的从key到value的不可变map,key和value都是任意的字节串。操作通常是通过一个给定的key来查找相应的value,或者在一个给定的key range上迭代所有的key/value对。每个SSTable内部包含一系列的块(通常每个块是64KB大小,但是该大小是可配置的)。一个块索引(保存在SSTable的尾部)是用来定位block的,当SSTable打开时该索引会被加载到内存。一次查找可以通过一次磁盘访问完成:首先通过在内存中的索引进行一次二分查找找到相应的块,然后从磁盘中读取该块。另外,一个SSTable可以被完全映射到内存,这样就不需要我们接触磁盘就可以执行所有的查找和扫描。{关于SSTable(StaticSearchTable)的具体格式可以参考YunTable开发日记(4)-BigTable的存储模型,中对HBASE的HFile的介绍}

 Bigtable依赖于一个高可用的一致性分布式锁服务Chubby。Chubby由5个活动副本组成,其中的一个选为master处理请求。当大部分的副本运行并且可以相互通信时,该服务就是活的。Chubby使用Paxos算法来在出现失败时,保持副本的一致性。Chubby提供了一个由目录和小文件组成的名字空间。每个文件或者目录可以当作一个锁来使用,对于一个文件的读写是原子性的。Chubby的客户端库为Chubby文件提供一致性缓存。每个Chubby客户端维护这一个与Chubby服务的会话。如果在租约有效时间内无法更新会话的租约,客户端的会话就会过期。当一个客户端会话过期后,它会丢失所有的锁和打开的文件句柄。Chubby客户端也会在Chubby文件和目录上注册回调函数来处理这些变更或者会话的过期。

 Bigtable使用Chubby来完成各种任务:保证任意时刻最多只有一个活动的master;存储Bigtable数据的bootstrap location(参见5.2节);发现tablet服务器以及finalize tablet服务器的死亡(参见5.2节);保存Bigtable schema信息(每个表的列族信息);存储访问控制列表。如果chubby在一段时间内不可用,Bigtable也会不可用。我们最近在使用了11个chubby实例的14个Bigtable集群进行了测量。由于Chubby不可用而造成的存储在Bigtable上的数据不可用的平均概率是0.0047%。对于单个集群来说,由于Chubby不可用造成的这个概率是0.0326%。

 5. 实现

Bigtable实现有3个主要的组件:每个客户端需要链接的库,一个master服务器,很多tablet服务器。tablet服务器可以从一个集群中动态添加(或者删除)来适应工作负载的动态变化。

 Master负责将tablet分配到tablet服务器,检测tablet服务器的添加和过期,平衡tablet服务器负载,GFS文件的垃圾回收。另外,它还会处理schema的变化,比如表和列族的创建。

每个tablet服务器管理一个tablets集合(通常每个tablet服务器有10到1000个tablet)。Tablet服务器负责它已经加载的那些tablet的读写请求,也会将那些过于大的tablet进行分割。{tablet服务器本身实际上也是GFS的用户,它们只是负载它加载的那些tablet的管理,这些tablet的物理存储并不一定存放在管理它的服务器上,底层的存储是由GFS完成的,tablet服务器可以只调用它的接口来完成相应任务。而METADATA表中的位置信息应该是指某个tablet由哪个tablet服务器管理,而不是物理上存储在哪个机器上。}

 正如很多单master的分布式存储系统,客户端数据的移动并不会经过master:客户端直接与tablet服务器进行通信来进行读写。因为Bigtable客户端并不依赖于master得到tablet的位置信息,大部分的客户端从来不会于master通信。所以,master实际中通常都是负载很轻的。

 Bigtable集群存储了大量的表。每个表由一系列的tablet组成,每个tablet包含一个行组的所有相关数据。一开始,每个表由一个tablet组成。随着表格的增长,它会自动分割成多个tablet,它们大小默认是100-200MB。

 5.1 Tablet存放位置

我们使用一个类似于B+树的三级结构来存储tablet的放置信息如图4。

【google论文四】Bigtable:结构化数据的分布式存储系统(上) - 星星 - 银河里的星星

第一级是一个存储在Chubby的包含了root tablet位置信息的文件。root tablet包含了在一个特殊的METADATA表里的所有tablet的位置信息{root tablet实际上是METADATA表的第一个tablet,它存储了该表其他的tablet的位置信息}。每个METADATA tablet包含了一集用户tablet的位置。Root tablet仅仅是METADATA表的第一个tablet,但是是特殊对待的-它永远不会被分割-为了保证tablet位置信息的层次结构不会超过3级。

 METADATA表的每一个行关键字(由tablet所属的表标识符和它的结束行组成)下存储了一个tablet的位置。每个METADATA行在内存中大概存储了1KB数据。我们限制METADATA的tablet的大小为128MB,我们的三级层次结构足以用来寻址2^34个tablet(如果tablet按照128MB算,就是2^61字节){root tablet大小为128M,每个行1KB,那么它就可以存储128*2^20/2^10=128*2^10个METADATA tablet,同样的,每个METADATA tablet可以存储128*2^10个普通tablet,这样总共可以存储128*2^10*128*2^10即2^34个普通tablet,每个tablet又将近1KB数据,这样算起来存储这些元信息就需要4TB的数据,所以该METADATA表也不可能全部放入内存,而是采用与普通的表一样的存储方式,放在GFS上。但是会把某些特殊信息放在内存中,比如第6节提到的:METADATA中的location列族会被放入内存 }。

 客户端库会缓存tablet的位置信息。如果客户端不知道某个tablet的信息,或者发现缓存的位置信息是错误的,那么它就会递归地在tablet位置存储结构中查找。如果客户端缓存是空的,定位算法需要三次网络往返,包括从Chubby的一次读操作。如果客户端缓存是陈旧的,定位算法将需要多达6次的往返,因为陈旧的缓存值只有在不命中时才会被发现(假设METADATA tablet并不会经常移动)。尽管Tablet位置信息是存储在内存中(如上所述),不需要访问GFS,但是我们还是通过在客户端库里进行预取来降低花费:当读取METADATA表时,它会读取不止一个tablet的信息。

 我们也会将一些额外信息存放在METADATA表里,包括对于每个tablet有关的事件日志(比如一个服务器何时开始提供服务)。这些信息对于调试和性能分析很有帮助。

 5.2 tablet分配

每个Tablet一次只会分配给一个tablet服务器。Master保存了现有的活着的tablet服务器集合的所有行踪,tablet服务器当前分配的tablet,哪些tablet未被分配。当一个tablet没有被分配并且有一个可以存储该tablet的tablet服务器存在时,master通过给那个tablet服务器发送一个tablet负载请求来分配该tablet。

 Bigtable使用Chubby来追踪tablet服务器的状态。当一个tablet服务器启动时,它创建并获取一个在给定的Chubby目录上的唯一命名的文件的独占锁。Master通过监控这个目录(服务器目录)来发现tablet服务器。Tablet服务器如果丢失了它的独占锁(比如由于网络问题)就停止它上面的tablet服务。一个tablet服务器会尝试重新获取在该文件上的独占锁,只要该文件还存在。如果该文件也不存在了,那么tablet服务器就永远无法提供该服务了。当一个tablet服务器停止(比如集群管理系统从集群中删除了该tablet 服务器机器),它就会尝试释放这个锁这样master就可以更快地重新分配它上面的tablets了。

 Master负责检测一个tablet服务器何时停止提供服务,以尽快重新安排它的tablets。为了进行检测,master周期性的向每个tablet服务器询问它的锁状态。如果一个tablet服务器报告它丢失了它的锁,或者master在它的几次尝试中不能到达一个服务器,master会尝试获取该服务器的锁。如果master可以获取该锁,那么Chubby就是活的,而tablet服务器要么是死的要么因为某些问题而无法到达Chubby,那么master就可以通过删除它的server文件来使得该tablet服务器永远都不能提供服务。一旦一个服务器的文件被删除了,master就可以将之前分配给该服务器的所有tablet移到那些为分配的tablet集合中。为了保证一个Bigtable集群不会因为与master和Chubby间的网络问题而变得脆弱,如果master的Chubby会话过期了,master会自杀。然而,如前面所述,master的失败并不会改变tablet服务器的tablet分配。

当master被集群管理系统启动后,在它可以改变tablets之前需要知道它们当前的分配状态。Master在启动时执行如下步骤:1.master在Chubby获得一个唯一的master锁,该锁可以防止出现同时生成多个master实例。2.master扫描Chubby的服务器目录来找到所有活着的服务器。3.master与活着的tablet服务器通信来发现每个服务器安排了哪些tablet。4.master扫描METADATA表来找到tablet集合。当扫描中碰到一个未被分配的tablet,master会将它添加到未分配的tablet集合,并对这个tablet进行分配。

在METADATA 的tablets未被分配之前,对于METADATA的扫描不能进行。因此在开始扫描之前(步骤4),如果在步骤3没有发现对于root tablet的分配,master会将root tablet添加到未分配tablets集合中。这个添加将会使root tablet变得可以被分配。因为root tablet包含所有METADATA tablets的名称,master当扫描完root tablet后就能知道METADATA的所有的tablets。

 只有当一个表被创建,现有的两个tablets合并为一个,或者一个tablet被分割为一个时,现有的tablet集合才会发生变化。Master能够追踪所有的这些变化,因为它负责维护它们。Tablet分割需要特殊对待因为它们是由一个tablet服务器启动的。Tablet服务器通过将新的tablet的信息记录到METADATA表中来提交这个分割。当分割提交后,它会通知master。为了防止分割通知丢失(因为tablet服务器或者master死了),当master向tablet服务器请求加载刚刚发生分割的那个tablet时,它会检测到这个新的tablet。Tablet服务器会将这个分割通知master,因为它在METADATA表中的tablet键值仅包含了master让它加载的那个tablet的一部分。{假设master没有收到这个分割通知,那么它所记录的tablet与METADATA表中的就是不一致的,这样在它让tablet服务器加载该tablet时就会发现该不一致}。

 5.3 tablet服务

Tablet的持久性状态是通过GFS进行存储的,如图5所示。

【google论文四】Bigtable:结构化数据的分布式存储系统(上) - 星星 - 银河里的星星

更新是提交到一个保存了redo记录的提交日志里。在这些更新里,最近提交的那些被保存到内存中一个叫做memtable的有序缓存里,老的更新则被保存在一系列的SSTable中。为了恢复一个tablet,tablet服务器从METADATA表中读取该tablet的元数据。元数据中包含组成该tablet的SSTable列表,以及一系列的redo点(指向那些包含该tablet数据的commit日志条目)的集合。Tablet服务器将所有SSTable的索引读入内存,然后通过应用那些从redo点开始以及提交的更新操作来重新构建memtable。

{更新操作肯定会被保存到commit log里,但是当某个服务器挂掉时,它那些保存在memtable的最新的更新就不存在了,而redo点应该就是记录已保存到SSTable的与还在memtable中的操作的分界点,这样通过重新执行它之后的那些操作就可以将memtable重建}{redo点何时被更新?有多少个commit log?参见第6节}

当一个写操作到达一个tablet服务器时,服务器首先检查它的格式是否合法,发送者是否有权限进行该操作。权限检查是通过从Chubby读取一个允许的写操作者列表(通常它直接存在于Chubby的客户端缓存中)完成的。一个合法的变更会被写入到已提交日志里。操作的提交可以通过分组执行来提高大量小变更操作出现时的吞吐率,当该写操作提交后,它的内容会被插入到memtable里。

当一个读操作到达一个tablet服务器时,类似的首先要进行格式和权限检查。一个合法的读操作将会在一系列SSTable和memtable的一个合并视图上执行{因为SSTable一旦写入就不可变,这样就使得更新操作必须写到新的SSTable中,这样就导致同一个key值可能在多个SSTable中出现,这样读取时就必须读取多个SSTable才能得到它真实的最终状态}。因为SSTable和memtable都是字典有序的数据结构,因此可以很快生成这个视图。{为了读取一个key时,要读入所有的SSTable,所以第6节有一个针对该问题的优化Bloom Filter。此外伴随着SSTable的增多,这种视图合并也会变得低效,所以也引出了下面的Compation}

当tablet发生分割或者合并时,也可以继续接受读写操作。

 5.4  compaction

伴随着写操作的执行,memtable的大小会逐渐变大。当memtable大小增长到一个阈值,这个memtable就会被冻结,一个新的memtable被创建,被冻结的旧的memtable会被转化为一个SSTable写入GFS。这个minor compaction过程有两个目的:降低tablet server的内存使用,降低该tablet服务器挂掉时需要从已提交日志中读取的数据大小。当compaction发生时,也可以继续接受读写操作。

 每次minor Compation会创建一个新的SSTable。如果这个行为持续的进行而不检查,那么读操作就可能会需要从大量的SSTable中合并它们的更新。我们通过周期性的执行一个merging compaction来将这样的文件数目限制在一定范围内。一个merging compaction读取多个SStable和memtable,然后写入到一个新的SSTable(形成一个最终的归并视图)。一旦这个compaction结束,这些SSTable和memtable就可以丢弃了。

将多个SSTable重新写入到一个SSTable的merging compaction称为主compaction。由非主compaction产生的SSTable里可以包含一些在旧的SSTable中仍然存活但是目前已经被删除的数据。另一方面,一个主compaction产生的SSTable不会包含删除操作信息或者已删除数据。Bigtable循环扫描它所有的tablet,周期的对它们执行主compaction。这些主compaction使得Bigtable可以回收那些被已删除的数据使用的资源。这也保证那些已经删除的数据在一定时间内会从系统中消失,这对于那些存储敏感数据的服务来说很重要。

 6 技巧

前面一节描述的实现需要大量的技巧来到达用户所需要的高性能,可用性,可靠性。这一节更细节地描述下实现的各个部分,着重讲述下使用的这些技巧。

 局部性群组(对应一个SSTable)

用户可以将多个列族组织为一个局部性群组。对于每个tablet里的每个局部性群组都会生成一个单独的SSTable。将那些通常不会被一起访问的列族分离到独立的局部性群组可以增加访问的效率。比如,webtable的关于网页的元数据(比如语言,校验和)可放到一个局部性群组,网页内容可以放到另一个群组里:这样一个访问元数据的应用程序就不需要读取所有网页的内容。

 另外,一些有用的tuning参数也可以以局部性群组为单位进行设置。比如一个局部性群组可以声明为放入内存的。对于声明为放入内存的局部性群组的SSTable在需要时才会加载到tablet服务器的内存中。一旦加载之后,对于该局部性群组的访问就不需要访问磁盘。这个特点对于那些需要经常访问的小片数据很有用:比如在Bigtable内部我们将它应用在METADATA表的location列族上。

 压缩

用户可以控制对于一个局部性群组的SSTables是否进行压缩,以及使用哪种压缩格式。用户指定的压缩格式会应用在SSTable的每个块上(块大小可以通过一个局部性群组的参数进行控制)。对于每个块单独进行压缩,尽管这使我们丢失了一些空间,但是这使得我们不需要解压整个文件就可以读取SSTable的部分内容。很多用户使用一个两遍压缩模式,第一遍压缩使用Bentley and McIlroy模式,该模式在一个很大的窗口大小里压缩普通的长字符串。第二遍压缩了一个快速压缩算法,该算法在一个小的16KB窗口大小内查找重复。这两遍压缩都很快速,压缩速率在100-200MB/s,解压速率在400-1000MB/s。

 尽管在选择压缩算法时,我们更重视速率而不是空间的减少,但是这个两遍压缩模式或做的出奇地好。比如,在webtable里我们使用这种压缩模式存储网页内容。实验中,我们在一个压缩的局部性群组里存储了大量文档。为了实验目的,我们将文档的版本数限制为1。该压缩模式达到了10 :1的压缩率。这比通常的Gzip 对于HTML网页的3:1或4:1的压缩率要好多了。这是由我们的webtable的行分布方式造成的:来自相同主机的网页被存储在相邻的位置。这使Bentley and McIlroy算法可以识别出来自相同站点的大量固有模式。很多应用程序,不仅仅是webtable,选择的行名称使得类似数据会聚集在一起,因此达到了很好的压缩率。当我们在Bigtable中存储相同值的多个版本时压缩率会更好。

 为了读性能进行缓存

为了提高读性能,tablet服务器使用一个二级缓存。扫描缓存是一个用来缓存由SSTable返回给tablet服务器的key-value对的高级缓存。块缓存是用来缓存从GFS读取的SSTable块的低级缓存。扫描缓存主要用于那些倾向于重复读取相同数据的应用,块缓存则用于那些倾向于从最近读取的数据的邻近位置读取数据的应用(比如顺序读,或者读取一个热点行内的相同局部性群组里的不同列值)。

 Bloom Filters

正如5.3节所描述的,一个读操作需要从组成该tablet的所有SSTable里读取。如果这些SSTable不在内存,就需要很多磁盘操作。通过让用户为某个局部性群组的SSTables指定对应的Bloom filters,可以降低磁盘访问次数。一个Bloom Filter允许我们查询对应的SSTable是否包含某个给定的row/column对的数据。对于特定的应用程序来说,只需要很少的tablet服务器内存来保存Bloom Filter,但可以大大减少读操作所需要的磁盘操作。同时Bloom Filter可以避免那些对于不存在的行列的查找访问磁盘。

 提交日志实现

如果我们为每个tablet的提交日志建立一个独立的日志文件,就会使得大量的文件需要并发写入GFS。由于每个GFS服务器的底层文件系统实现,这些写操作会引起在不同物理日志文件上的大量的磁盘寻道。另外,每个tablet一个日志文件会降低分组提交优化的效率。为了解决这些问题,每个tablet服务器将更新操作append一个日志文件里,将对于不同的tablet的变更放到同一个物理日志文件里。

 使用一个日志为正常操作提供了很明显的性能好处,但是使恢复变复杂了。当一个tablet服务器挂掉后,它负责的那些tablet需要移动到大量其他的tablet服务器上:每个服务器通常都会加载一些该服务器的tablet。为了恢复一个tablet的状态,新的tablet服务器需要通过原来那个tablet服务器的提交日志重新应用这个tablet的变更操作。然而对于这些tablet的变更是混在同一个日志文件里的。一种方法是,每个新的tablet服务器全部读取这个日志文件,然后仅应用那些它需要恢复的tablet的变更操作。然而,在这种模式下,如果失败的那台tablet服务器的tablet被分配到了100个机器,那么这个日志文件就需要读取100次。

 我们通过对提交日志里的entry根据<table,row name,log sequence number>进行排序避免了重复的日志读取。在已排序的输出中,对于一个tablet的所有变更都是连续的,因此可以通过一次的磁盘寻道和顺序读操作就可以完成读取。为了并行化排序,我们将该日志文件划分为64MB大小的段,在不同的tablet服务器上对它们进行排序。排序过程是由master协调进行的,当一个tablet服务器指出它需要从一个日志文件中恢复变更时开始启动。

 将提交日志写入GFS有时候可能因为各种原因导致性能抖动(比如GFS服务器处在繁重的写操作中,或者网络处于拥塞或者重载)。为了避免变更操作受到GFS延时的影响,每个tablet服务器实际上有2个写日志线程,每个写它们各自的日志文件,在同一时间只有一个处在活跃期。如果对于活动日志文件的写性能急剧下降,它就会切换到另一个线程,在提交日志队列中的那些变更操作将由这个新的活动线程负责写入。日志条目里包含序列号,这就使得恢复过程中可以删除那些由于日志切换过程造成的重复条目。

 加速tablet恢复

如果master将tablet从一个tablet服务器移动到另一个,源tablet服务器会首先在该tablet上进行一个minor compaction。这个compaction将会减少在tablet服务器的日志里的uncompacted状态数。当compaction结束后,tablet服务器停止针对该tablet的服务。在彻底卸载该tablet之前,tablet服务器再进行一次minor compaction(通常是很快速的)来消除那些上次minor compaction之后该tablet服务上剩余的uncompacted状态。当第二次的minor compaction结束后,该tablet就可以直接由另一个tablet服务器加载而不需要从日志条目中进行恢复。{通过这个过程也可以看出,tablet服务器只负责管理memtable和SSTable,对于底层的存储它并不负责,当tablet迁移到另一个服务器时,它在GFS的存储并没有变,变的只是管理它的tablet服务器,而新的tablet服务器也不需要进行数据移动之类的操作,因为它同样可以看到原来的GFS文件。}

 利用不可变性

除了SSTable缓存,Bigtable系统的各部分通过利用”SSTable生成之后就是不可变的”这个事实也得到了大大的简化。比如我们从SSTable中读取时,不需要对文件系统的访问进行任何同步。这样,在行上的并发控制就可以有效的实现。唯一的可以读写的可变数据结构就是memtable。为了减少在memtable读取时的竞争,我们对每个memtable行进行写时复制,这就允许读写并行处理。

 因为SSTable是不可变的,已删除数据的清除就转换成了对于过时的SSTable的垃圾回收。每个tablet的SSTables会注册在METADATA表中。Master服务器采用“标记-删除”的垃圾回收方式删除SSTable集合中废弃的SSTable。

 最后,SSTable的不可变性使得我们可以快速分割tablet。我们让子tablets共享父tablet的SSTables,而不是为每个子tablet生成新的SSTables。{如果是这样的话,如前所述,一开始只有一个tablet,这样会不会导致SSTable的数目一直未变,只是它的大小一直在上升,但这样会导致它很难一次加载入内存,那么SSTable的分割又是何时发生的呢?}

7.性能评价

我们建立了一个N个tablet服务器的Bigtable集群来测量Bigtable伴随着N的变化的性能和可扩展性。Tablet服务器配置成由含有1G内存 400G IDE硬盘的1786个机器组成的GFS cell写入。N个客户端为这些测试生成工作负载。(我们使用与tablet服务器相同数目的客户端来保证客户端不会成为瓶颈)。每个机器有一个双核Opteron 2GHz 芯片,供运行的进程使用的足够的物理内存,一个gigabit 以太网链路。机器通过一个两级树状交换机网络连接,根节点总体带宽接近100-200Gbps。所有机器具有相同的主机配置,因此任意两个机器间的往返时间小于1ms。

 Tablet服务器和master,测试客户端,GFS服务器都运行在相同的机器集合上。每个机器运行一个GFS服务器。另外这些机器要么运行一个tablet服务器要么运行一个客户端进程,或者一些其他同时使用这些机器的job的进程。

 R是测试集中Bigtable行关键字的个数,通过对它的取值进行选择使得每个tablet服务器每个基准测试读写接近1G的数据。

 顺序写基准测试使用0-R-1作为行关键字。这个行关键字空间又被划分为相同大小的10N个相同大小的区间。这些区间通过一个中央调度器分配给N个客户端,当客户端处理完分配给它的前一个区间后就继续分配给它下一个区间处理{分配是动态的,中央调度器维护一个未分配集合,当发现某个客户端完成后,就给它下一个区间,而不是每个客户端一开始就分配了10个固定区间}。这种动态分配有助于减轻客户端机器上的其他进程造成的性能影响。在每个行关键字下我们写一个字符串,这个字符串是随机生成的,因此也是未压缩的。另外,不同行关键字下的字符串是不同的,因此跨行的压缩也是不可能的。随机写基准测试与之类似,除了在写之前行关键字是经过hash然后模R得到的,这样对于整个测试过程来说,写负载就可以在整个行空间上随机分布。

 顺序读基准测试与顺序写采用了完全相同的行关键字生成方式。但是它不是在一个行关键字下写,而是读该行关键字下存储的字符串(由前面调用的顺序写基准测试写的)。类似的,随机读基准测试的操作对应着随机写基准测试操作。

 扫描基准测试类似于顺序读基准测试,但是它使用了Bigtable API对于扫描一个行组的所有值的操作支持。通过使用扫描操作,可以降低基准测试程序所执行的RPC调用次数,因为此时的一次RPC会从一个tablet服务器上获取一大串的值。

 随机读(mem)基准测试类似于随机读基准测试,但是包含基准测试数据的locality group是被标记为in-memory的。因此读操作只需要与tablet服务器内存交互而不需要读GFS。对于这个基准测试,我们将每个tablet服务器的数据从1GB降低到了100MB,这样它就可以很容易地放到tablet服务器的内存里。

 图6展示了当我们从Bigtable读写1000字节的value值时的测试程序性能。其中表格展示了每个tablet服务器的每秒操作数;图形展示了每秒的操作总数。

【google论文四】Bigtable:结构化数据的分布式存储系统(下) - 星星 - 银河里的星星

  单tablet服务器性能

我们首先来考虑单tablet服务器性能。随机读要比所有其他操作慢。每个随机读操作需要将64KB大小的SSTable块从GFS传输到tablet服务器,在这些数据里只有1000字节会被使用。Tablet服务器每秒大概执行1200次读操作,从GFS传输的传输速率大概75MB/s。在这个带宽级别下,会由于网络协议栈,SSTable解析,Bigtable代码的耗费占掉大量的服务器的CPU,同时也几乎占满了我们系统的带宽。大部分具有这种访问模式的Bigtable应用需要将块大小设成一个更小的值,通常是8KB。

 从内存中的随机读要更快些,因为这1000个字节的读是从tablet的本地内存中直接读取的,不需要从GFS中获取一个大的64KB块。

 随机和顺序写比随机读的执行效率更高是因为每个tablet服务器将所有的写入请求写到一个commit log里,然后使用按组提交来有效的将这些写入交给GFS。在随机写和顺序写之间并没有明显的不同;在这两种情况下,对于tablet服务器的所有写入都是记录在同一个commit log里。

 顺序写比随机写执行的更好,因为从GFS获取的64KB SSTable块会被存储到我们的块缓存里,用来为下面的64个请求服务。

 扫描甚至更快,因为tablet服务器可以在客户端的一次RPC请求中返回更大量的value,因此RPC的耗费可以平摊到大量的value上。

 扩展性

当我们将tablet服务器的数目从1增长到500时,整体的吞吐率有上百倍的增长。比如从内存中随机读的性能当tablet服务器数目增长了500倍时增长了300倍。发生这种情况是因为对于这个基准测试的性能瓶颈在tablet服务器CPU数。

 然而,性能也不是线性增长的。对于大多数基准测试来说,当从1增加到50个tablet服务器时,单服务器的吞吐率有一个明显的下降。这个下降是由于多个服务器时的负载不均衡导致的,通常是由于需要网络和CPU的进程引起的。我们的负载平衡算法会尝试解决这种问题,但是无法完美的完成。主要有两个原因:为了减少tablet的移动重新的平衡会被抑制(当tablet移动时,它会在短时间内不可用,通常小于1秒),随着测试程序的执行它生成的负载也在变化。

 随机读基准测试表现出最糟糕的可扩展性(当服务器数目增长了500倍后,它只增长了100倍)。发生这种情况的原因(正如前面所述)是对于每个1000字节的读操作我们都需要在网络上传输64KB大小的一个块。这个传输会消耗掉我们网络的1 GB共享带宽,这样当我们增加机器数目的时候吞吐率就会明显降低。

 8.实际应用

截至2006年8月,有388个非测试Bigtable集群运行在google机器群上,总共大概有24500个tablet服务器。表1显示了每个集群上的tablet服务器个数的粗略分布情况。这些集群大部分是用于开发目的,因此很多时候可能都是空闲的。在14个比较忙的集群中,总共有8069个tablet服务器,每秒有超过120万个请求,741MB/s的RPC输入流量,以及16GB/s的RPC输出流量。

【google论文四】Bigtable:结构化数据的分布式存储系统(下) - 星星 - 银河里的星星

 表2提供了一些关于现在正在使用的表的数据。一些表为用户存储数据,一些为批处理程序存储数据;这些表在总大小,平均cell大小,保存在内存中的数据比例,以及表的schema上跨度很大。在该节的剩余部分,我们将简要描述google的三个产品如何使用Bigtable。

【google论文四】Bigtable:结构化数据的分布式存储系统(下) - 星星 - 银河里的星星

 8.1google分析

Google分析是一个帮助站长分析它们站点的流量模式的服务。它提供整体的统计,比如每天内的不同的访问者数目,每个URL每天的访问数,以及一些站点反馈报告,比如那些打开了某特定页面后的访问者进行了交易的比例。

 为了使用这项服务,站长需要在他们的页面里嵌入一个小javascript程序。它会将关于请求的各项信息记录在google分析里,比如用户标识以及该网页被获取的信息。Google分析对这些数据进行分析并给站长使用。

 我们简要描述Google分析使用的两个表。原始的点击表(大概200TB)为每个终端用户会话维护一条记录。行的名称是一个由站点名称,以及会话创建时间组成的元祖。这种schema使得那些访问相同站点的会话是相邻的,而且是按照时间排序的,这个表格可以压缩到原始大小的14%。

 摘要表格(大概20TB)包含对每个站点各种预定义的摘要。这个表格是通过周期性调度的MapReduce jobs从原始点击表生成出来的。每个MapReduce job从原始点击表抽取出最近的会话数据。系统整体的吞吐率由GFS的吞吐率决定,这个表格可以压缩为原始大小的29%。

 8.2google地球

Google提供一组服务,使得用户即可以通过网页也可以通过google地球客户端软件访问地球表面的高分辨率卫星图像。这些产品允许用户浏览地球表面:他们可以在不同的分辨率上拍摄,观看,注释卫星图像。系统使用一个表来预处理数据,用另外一些表来为用户提供数据。

 预处理流程使用一个表来存储原始图像。在预处理期间,图像清理合并为最终的服务数据。这个表大概有70TB数据,因此是从硬盘上提供的。图像本来已进行了压缩,因此Bigtable的压缩选项被关掉了。

 图像表里的每一行对应一个地理区域,行的命名方式保证相邻的地理位置会被存储到邻近的位置。表格包含一个列族来保存每个区域的数据源。这个列族有大量的列:每个列保存一个原始数据图片。因为每个区域仅仅从是几个图片构建出来的,因此这个列族是很稀疏的。

 这个预处理流程依赖于MapReduce在Bigtable上进行数据转换。在Master jobs运行期间,整个系统的每个tablet服务器处理速度超过1MB/s。

 服务系统使用一个表来索引存储在GFS中的数据。这个表相对小一些(大概500GB),但是每个数据中心每秒必须要为数千个查询提供低延时服务。因此,这个表实际上保存在几百个tablet服务器中,而且是作为in-memory列族进行存储。

 8.3个性化搜索

个性化搜索是一个用来记录用户在google很多产品比如网页搜索,图像新闻搜索的查询和点击记录的可选服务。用户可以通过浏览搜索历史来查看过去的查询和点击,可以通过他们在google历史记录得到个性化的搜索结果。

 个性化搜索将每个用户的数据保存在Bigtable里。每个用户具有唯一的用户id,并分配给他一个以用户id命名的行。所有的用户动作保存在表中。每个类型的动作用一个独立的列族保存(比如有一个列族保存所有的网页查询)。每个数据元素使用对应的动作发生的时间作为它在Bigtable中的时间戳。个性化搜索通过使用一个在Bigtable上的MapReduce产生用户特征。这些特征被用来实时个性化搜索结果。

 为了提高可用性和降低用户延时,个性化搜索数据备份在多个Bigtable集群上。个性化搜索团队起初自己在Bigtable之上建立了一个用户端备份机制来保证所有备份的一致性。现在的系统使用的备份子系统已经内建到服务端了。

 个性化搜索存储系统的设计允许其他团队在他们的列中添加新的用户信息。现在这个系统已经被很多其他需要存储用户配置和设置信息的google产品使用。多个团队间共享一个表使得表具有大量的列族。为了支持共享,我们给Bigtable添加了一个简单的quota机制来限制共享表的用户的存储消耗。这个机制为不同产品团队使用该系统进行用户信息存储提供了独立性。

 9.经验教训

在Bigtable的设计,实现,维护和支持过程中,我们获得了很多经验以及一些教训。

 我们学到的第一个经验是:大型分布式系统在很多类型的失败面前是很脆弱的,这些失败不仅仅是标准的网络分划,各种分布式协议中的失败。比如我们看到了由下面的各种原因引起的失败:内存和网络损坏,大的时钟误差,机器挂起,扩展的非对称的网络分划,我们使用的其他系统的bug(比如chubby),GFS quota溢出,计划的以及临时的硬件维护。通过改变各种协议解决这些问题,我们得到了更多经验。比如,我们为我们的RPC机制增加了校验和。我们也通过消除系统中的一部分对另一部分的依赖来处理一些问题。比如我们不再假设Chubby只会返回一组固定错误集合中的错误{可能返回任意错误}。

我们学到的另一个经验是:将新feature的添加延迟到它会被怎样使用清楚了时是很重要的。比如,一开始我们计划在API中提供一个通用目的事务支持。因为当时我们没有一个现实的使用需求,所以也没有实现它。现在我们有很多运行在Bigtable上的实际应用,我们检查它们的实际需求,发现大多数的应用只需要一个单行事务。在已经提出的分布式事务需求中,最重要的使用是用来维护secondary索引,我们计划通过添加一个特殊的机制来满足这个需求。新的机制不如分布式事务通用,但是会更有效(尤其是在数百行或者更多数据上进行更新)也会能够更好地与我们的跨数据中心的备份机制进行交互。

 一个我们在进行Bigtable支持中学到的比较特殊的经验是:合适的系统级监控的重要性(比如即监控Bigtable自身,同时还监控使用Bigtable的用户进程)。比如我们扩展RPC系统使得可以追踪一次RPC中的重要动作。这个特点帮助我们检测并解决了很多问题比如在tablet数据结构上的锁竞争,在提交Bigtable变更时的GFS上的低速写,当METADATA tablet不可用时造成的METADATA表不可访问。另一个有用监控的是每个Bigtable集群会注册在Chubby。这就使得我们可以追踪所有的集群,发现它们的大小,检查它们使用的软件版本,它们收到的流量,是否存在一些问题比如未预料到的大延时。

我们学到的最重要的经验就是简单设计的价值。考虑到我们系统的规模(大概10万行非测试代码),以及代码会随着时间的推移以不可预料的方式演变,我们发现代码和设计的清晰对于代码的维护和调试有巨大的帮助。一个例子是我们的tablet服务器成员协议。我们最初的协议很简单:master周期性的给tablet服务签订租约,当租约过期后tablet服务器就会杀掉自己。不幸的是,这个协议很明显地降低了在网络问题出现时的可用性,而且对于master的恢复时间也很敏感。我进行了多次设计才得到一个执行的不错的协议。然而最终的协议变得太复杂而且依赖于Chubby系统中那些很少被其他应用所使用的feature。我们发现我们花费了大量时间调试令人费解的边边角角的问题,不仅仅是Bigtable代码,有时还是在Chubby代码里。最后,我们放弃了这个协议,使用了一个新的简化的协议,它只依赖于Chubby那些被广泛使用的feature。

 10.相关工作

Boxwood项目有些组件与Chubby,GFS,Bigtable在某些方面重叠,因为它提供了分布式协商,锁,分布式chunk存储,分布式B树存储。尽管存在这些方面的重叠,但是很明显的是与对应的Google服务相比Boxwood组件的定位是在更底层上。Boxwood项目的目标是为建立上层服务比如文件系统或者数据库提供基本的设施,而Bigtable的目标则是直接支持那些需要存储数据的用户应用程序。

 最近很多项目都是旨在解决提供分布式存储和在广域网上的上层服务的问题,通常是在整个互联网范围内。这包括在分布式hash表上的工作,对应的项目有CAN,Chord,Tapestry和Pstry。这些项目关注点与Bigtable不同,比如高可用带宽,不可信任成员,频繁重配置,非中央式控制,拜占庭式容错,这些都不是Bigtable的目标。

 在提供给应用程序开发者的分布式数据存储模型上,我们相信由分布式B树和分布式Hash表提供的key-value对模型太有限了。Key-value对是一种很有用的构建模式,但是它们并不是可以提供给开发者的唯一模式。我们所选择的模型要比简单的Key-value模式丰富,支持半结构化数据。然而,它仍然是足够简单地,使得在处理flat-file格式也很有效,同时它也允许用户透明地调整(通过locality group)系统的重要行为。

 一些数据库厂商已经开发出了可以用来存储大量数据的并行数据库。Oracle的RAC(Real Application Cluster)数据库使用共享磁盘来存储数据(Bigtable使用GFS)和一个分布式锁管理器(Bigtable使用Chubby)。IBM的DB2并行版基于类似于Bigtable的非共享架构。每个DB2服务器负责表中行的一个子集,存储在本地的关系数据库中。这两个产品都提供了事务支持的完全的关系模型。

 Bigtable的 locality group实现了与其他的基于列而不是基于行的磁盘数据组织系统(包括C-Store,以及一些商业性产品比如Sybase IQ,SenSage,KDB+,MonetDB/X100中的ColumnBM存储层)类似的压缩率和磁盘读性能提高。另一种将垂直和水平数据划分到Flat文件,并且达到了很高压缩率的系统是AT&T的Daytona数据库。locality group并不支持Ailamaki描述的那些CPU缓存级的优化。

 Bigtable使用memtable和SSTables存储对于tablet更新的方式类似于Log-Structured Merge Tree存储索引数据更新的方式。在这两个系统中,已排序的数据在写到磁盘之前都是要缓存在内存中,读的时候必须从内存和磁盘里merger数据。

 C-Store和Bigtable有很多共同点:这两个系统都使用了非共享的架构,都有两个数据结构,一个用来保存最近的写,一个用来保存长期存在的数据,存在一个把数据从一个搬到另一个的机制。这两个系统在它们的API上存在显著的不同:C-Store类似于一个关系数据库,而Bigtable提供了底层的读写操作接口,而且设计得可以支持每秒每个服务器数千次这样的操作。C-Store也可以说是一个”读优化的关系型DBMS”,而Bigtable则为读敏感和写敏感的应用都提供了好的性能。

 Bigtable的负载平衡器需要解决与非共享数据库碰到的一些相同的负载和内存均衡问题。我们的问题要简单一些:(1)我们不需要考虑相同数据由于索引或者视图产生的多个拷贝的可能性;(2)我们让用户决定数据是放在内存还是磁盘,而不是动态确定;(3)我们没有复杂的查询执行和优化。

 11.总结

我们已经描述了Bigtable,google的一个存储结构化数据的分布式系统。Bigtable集群自从2005年4月开始在产品中使用,之后我们又花费了大概7个人年在设计和实现上。截至2006年8月,有超过60个项目在使用Bigtable。我们的用户很喜欢我们的Bigtable实现提供的性能和高可用性,当随着时间的推移他们的系统资源需求发生变化时,他们可以简单地添加机器来扩展集群的容量。

 由于Bigtable提供的不一样的接口,有一个有趣的问题是对于用户来说适应它有多困难。新用户有时候不确定如何最好的使用Bigtable接口,尤其是如果他们已经习惯于使用支持通用事务的关系数据库。然而,事实是google的很多产品在实践中成功地使用Bigtable完成了他们的设计工作。

 我们目前正在实现Bigtable的几个额外的feature,比如支持二级索引,支持多master备份的跨数据中心的Bigtable备份。我们已经开始将Bigtable为产品团队部署为一个服务,这样不同的团队就不需要维护他们自己的集群。伴随这服务集群的扩展,我们将需要在Bigtable内部处理更多的资源共享问题。

 最后,我们发现在google内部部署我们自己的存储系统具有明显的优点。我们从我们为Bigtable设计的数据模型中得到了大量的灵活性。另外,我们控制了Bigtable的实现,以及Bigtable所依赖的其他基础构建的实现,意味着当瓶颈出现或者效率降低时,我们可以消除它们。

 致谢