Google论文之一----Bigtable学习翻译

Bigtable研究

摘要

Bigtable是一个用于管理结构型数据的分布式存储系统,被设计为可扩展到很大规模:通过数以千台的机器存储PB级数据。很多Google的工程都将数据存储在Bigtable中,包括网页索引、Google Earth和Google金融。这些应用在数据量和延迟方面对Bigtable的需求很不相同。尽管这些不尽相同的需求,Bigtable能够成功的为这些Google的产品提供一个弹性的、高性能的解决方案。在这篇文章中,我们描述Bigtable提供的简单的数据模型,它能给客户端在数据布局和格式上提供动态控制,而且我们会描述Bigtable的设计和实现。

一、        介绍

经过过去两年半的时间,我们设计、实现并部署了一个分布式存储系统,用来管理Google的结构化数据,称为Bigtable。

Bigtable被设计为一个可靠扩展到PB数据和数千台机器。Bigtable实现了几个目标:广泛应用、可扩展、高性能和高可用。目前Bigtable已被用于超过60个的Google产品和工程,包括Google分析、Google金融、Orkut、个人搜索、Writely和Google Earth。这些系统针对于各种不同的需求使用了Bigtable,范围从面向吞吐量的批处理进程到时延敏感的面向终端用户的数据服务。这些产品使用的Bigtable集群跨越了很多的结构,从少数到数以千计的服务器,存储多至几百TB的数据。

在很多方面,Bigtable很像一个数据库:它实现了很多数据库的策略。并行数据库和内存数据库已经实现了可扩展和高性能,但是Bigtable与这些系统相比提供了不同的接口。Bigtable不支持全关系型的数据模型;作为代替,它提供了一种简单的数据模型,在数据布局和格式上提供了动态控制,并且允许客户端推出在底层存储中数据的位置属性。数据通过行和列名进行索引,这些名字可以是任意的字符串。Bigtable也将数据看成是无解释的字符串,尽管客户端经常将结构化和半结构化的数据序列化成不同的格式。客户端能够通过在它们的模式中精心的选择来控制他们的数据位置。最后,Bigtable模式参数可以使客户端动态的控制是从内存中获取数据还是从硬盘中。

第二章描述了数据模型的更多细节,并且在第三章提供了客户端API进行了概述。第四章简要的描述了Bigtable使用到的Google基础项目。第五章描述了Bigtable实现的基本原理。第六章描述了几个用于提高Bigtable性能的优化。第七章测试了Bigtable的性能。我们在第八章描述了几个如何使用Bigtable的例子,并在第九章讨论一些在设计和实现Bigtable中的教训。最后,在第十章描述相关工作,在第十一章进行了总结。

二、        数据模型

一个Bigtable是一个稀疏的、分布式的、持久的多维排序映射(MAP)。这个映射(MAP)由行key、列key和时间戳进行索引,每个映射值都是一个连续的byte数组。

(row:string, column:string,time:int64)  à  string

 

图1:存储Web页面的样例表中的一部分。行名是一个翻转了的URL。Contents列族包含了页面内容,anchor列族包含了涉及页面中的所有anchor的文本。CNN主页被Sports Illustrated和My-look主页引用,所以,本行包含了名为anchor:cnnsi.com和anchor:my.look.ca的列。每个anchor单元都有一个版本;contents列在根据时间戳t3,t5和t6有三个对应的版本。

在调查了一个类似Bigtable系统的各种潜在用途之后,我们选定了这个数据模型。作为驱动我们对设计做出一些决定的一个具体的例子,假设我们想要保留一个包含大量网页的集合和用于很多不同项目相关信息的拷贝,我们将这个特殊的表称为Webtable。在Webtable中,我们使用URL作为row key,页面的不同属性作为column names,并将页面的内容存储在contents:如图1所示,它们被获取时的时间戳将作为存放它们的列的列关键字。

2.1 行(Rows)

在表中,行的key是任意字符串(目前最大为64KB,尽管用户大多数只使用10-100字节)。每次在一行中读或写数据都是一个原子操作(尽管一行中不同列正在进行读或写),一个设计决定使客户端更加方便的推导出在并发更新相同行的系统行为。

Bigtable以row key的字典序保存数据。一个表的行范围是动态分配的。每个行范围被称为一个tablet,它是分布式和负载平衡的单位。因此,小范围的读取是高效的,只需要少量机器的通信。客户端可以通过选择合适的row keys来利用这个属性,这样可以为他们的数据访问提供良好的局域性。例如,在Webtable中,相同域名下的页面通过反转URL中的hostname,被集中存放到连续的行中。例如,我们将maps.google.com/index.html存放在关键字com.google.maps/index.html下。将相同域名的网页存储在一起可以更加高效对一些主机和域名进行分析。

2.2 列族(Column Families)

列关键字(Column keys)被聚合到一个名为列族的集合中,它形成了访问控制的基础单元。存储在一个列族中的所有数据通常有相同的类型(我们将在一个列族下的数据压缩到一起)。必须先创建列族,然后才能将数据存储到其下面的列;在列族创建好之后,列族中的任意一个列关键字(column key)都可用。我们的目的是每个表中不同的列族数量较小(最多不会多于几百个),并且列族很少在操作中变化。相反的,一个表可以有无限数量的列。

一个列关键字(column key)使用下面的语法进行命名:family:qualify。列族名必须是可显示的,但是qualify可以是任意字符。例如,Webtable的列族名是language,用于存储页面所用到的语言。我们在language列族中只使用一个列关键字(column key),它存储每个页面的languageID。另外一个有用的列族是anchor;这个族中的每个列关键字(column key)代表一个单独的anchor,如图1所示。qualify(限定词)是引用站点;该单元的内容是链接文本。

访问控制,以及磁盘和内存的统计信息都是在列族层面上进行的。在我们的Webtable例子中,这些控制允许我们管理几个不同的应用类型:一些添加新的基础数据,一些读取基础数据并创建派生列族,一些只允许查看已存在的数据(甚至出于隐私考虑也不能查看所有已存在的列族)。

2.3 时间戳(Timestamps)

Bigtable中的每个单元都能够包含相同数据的多个版本;这些版本由时间戳进行索引。Bigtable时间戳是一个64bit的整数。它们能够由Bigtable分配,在这种情况下,它们表现成以毫秒为单位的当前时间,或者显式的由客户端应用指定。应用必须保证时间戳的唯一性。不同版本的单元以时间戳的降序进行排列,这样可以使最近的版本最早被读取。

为了减少管理不同版本数据的工作量,我们支持两个列族设置,它们可以使Bigtable自动的垃圾回收单元中的版本。客户端可以指定保留最近的n个版本,也可以指定只保留new-enouge内的版本(如:只保留最近7天写入的数据)。

在我们的Webtable例子中,我们将抓取页面的时间戳存放在contents中:页面被抓取的时间就是所对应的时间戳。上面所提到的垃圾回收机制使我们只保留一个页面最近的三个版本。

三、        API

Bigtable API 提供了创建、删除表和族列的方法。它也提供了更改集群、表和列族元数据(如访问控制权限)的方法。

 

图2:向Bigtable写数据

客户端应用能够写或删除Bigtable中的值,从单独的行中查询值,或者循环访问一个表中的一部分数据。图2显示了使用一个RowMutation抽象执行的一系列更新操作的C++代码(为了使例子较短,去掉了不相干的代码)。Apply对Webtable执行了一个原子操作:它增加了另一个链接到www.cnn.com的anchor,并删除了一个不同的anchor。

 

图3:从Bigtable中读数据

图3显示了使用Scanner抽象方法遍历一个指定行的所有anchors的C++代码。客户端可以遍历多个列族,并且有一些对行、列和时间戳的浏览的限制机制。比如,我们能限制只浏览列可以正则匹配anchor:*.cnn.com列的anchors,或者是时间戳在距离当前时间10天之内的anchors。

Bigtable支持另外的一些功能,可以使用户通过更加复杂的方法操作数据。第一,Bigtable提供了单行事务,它能够用于在一个行关键字(row key)的数据上执行原子的读、改、写操作。Bigtable目前不支持一般的跨行关键字的事务处理,尽管在客户端提供了一个跨行关键字批写入的接口。第二,Bigtable允许单元用于整数计数。最后,Bigtable支持在服务器地址空间中执行客户端提供的脚本。这些脚本使用Google用于数据处理的语言Sawzall编写。同时,我们基于Sawzall的API不允许客户端脚本写回Bigtable,但是它允许不同形式的数据转换,基于任意表达式的过滤,以及使用各种操作的进行数据汇总。

Bigtable能够与MapReduce一起使用,后者是一个Google用于运行大规模并行计算的框架。我们已经写了一系列包装器,使Bigtable既能用于MapReduce的输入源,也能用于它的输出目标。

四、        构件

Bigtable建立在几个其它的Google基础构件之上。Bigtable使用Google文件系统GFS存储log和数据文件。一个Bigtable集群通常工作在一个共享的机器池中,运行各种的其它分布式应用,Bigtable进程与其它进程共享同一台机器。Bigtable依靠集群管理系统来分配任务、管理共享系统的资源、处理机器故障和监视机器状态。

Google的SSTable文件格式用于内部存储Bigtable数据。SSTable提供了一个持久的、排序不变的key-value映射,其中key和value都可以是任意字符串。提供了根据一个指定的key查找value的操作,以及遍历指定key的一个范围内所有的key-value对。内部地,每个SSTable包含了一系列的块(一般情况下,每个块的大小为64KB,但是这是可配置的)。一个块索引(存储在SSTable的末尾)用来定位块,这个索引会在SSTable打开时载入到内存中。一个查询可以执行一次独立的磁盘查询:我们首先通过在内存索引中的二叉查找找到适当的块,然后从磁盘中将适当的块读取出来。同样地,一个SSTable能够完整的被映射到内存中,这样可以让我们在不接触磁盘的情况下来执行查询和浏览。

Bigtable依赖一个高可用的、持久的称为Chubby的分布式锁服务。一个Chubby服务由5个活跃的副本组成,其中一个被选举为master,并处理请求。当大多数副本正常运行,并且能够相互通信时,这个服务是正常的。Chubby使用Paxos算法来保证它的副本在面对故障时的一致性。Chubby提供一个由目录和少量文件组成的命名空间。每个目录或者文件都能被用做一个锁,并且读和写文件都是原子的。Chubby客户端库提供了一致的Chubby文件缓存。每个Chubby客户端维护一个Chubby服务的会话。如果一个客户端会话在租约到期后不能更新它的会话租约时,则就会过期。当一个客户端会话过期时,它将释放所有的锁和打开的句柄。Chubby客户端能够在Chubby文件和目录下注册回调函数,用于处理变化通知或会话过期。

Bigtable将Chubby用于各种工作:确保在任何时刻最多存在一个活跃的master;存储Bigtable数据的引导位置;发现tablet服务器和处理失败的tablet服务器;存储Bigtable概要信息(每个表的列族信息);存储访问控制列表。如果Chubby超过一段时间不可用,则Bigtable会变为不可用。我们最近在一个有11个Chubby实例的14个Bigtable集群上测试了效果。由于Chubby不可用(由Chubby中断或者是网络问题造成)而造成存储在Bigtable中的数据不可用的时间占Bigtable服务器时间的平均百分比为0.0047%。在单独的集群中受到Chubby不可用影响的最大百分比为0.0326%。

五、        实现

Bigtable实现由三个主要的组成部分:一个连接到每个客户端的库、一个master服务器和许多tablet服务器。Tablet服务器能够从集群中动态的增加(或删除)以适应工作量的变化。

Master负责将tablet分配到tablet服务器上,探测增加和超时的tablet服务器,平衡tablet服务器的负载,以及在GFS上回收垃圾文件。此外,它会处理概要变化,如表和列族的创建。

每个tablet服务器管理一个tablet集合(一般情况下在一个tablet服务器上我们有10到1000个tablets)。Tablet服务器处理它负载的tablet相关的读和写请求,并在tablets过大后进行分片。

像许多单master分布式存储系统一样,客户端数据不能通过master进行传输:客户端直接与tablet服务器进行读和写的通信。由于Bigtable客户端不依赖master获取tablet位置信息,大多数客户端不需要跟master进行通信。因此,在实际中master的负载很轻。

一个Bigtable集群存储了大量的表。每个表有一系列tablet组成,每个tablet包含了一个行范围内的所有数据。最初,每个表都仅仅由一个tablet组成。随着表的增长,它会自动的分片成多个tablets,默认情况下,每个tablet的大小大约在100-200MB。

5.1 Tablet定位

我们使用三层的类似于B+树的结构存储tablet位置信息(图4)。

 

图4:Tablet位置等级

第一层是存储在Chubby的一个文件,它包含了root tablet的位置。Root tablet将所有tablet的位置包含在一个特殊的METADATA表中。每个METADATA tablet包含一系列的用户tablet位置。Root tablet只是METADATA表的第一个tablet,但是特殊之处在于其永远不会分裂,以此确保tablet位置层级不会超过3层。

在METADATA表中,每个tablet的位置信息都存放在一个行关键字下面,而这个关键字是由tablet所在的表的标示符和它的最后一行编码形成的。每个METADATA行在内存中存储了大约1KB的数据。通过限制tablets的大小为128MB,三层定位方案可以满足定位2^34个tablets(或者是2^61字节,按着每个tablet有128MB数据)。

客户端库会缓存tablet位置。如果客户端不知道一个tablet的位置,或者它发现缓存的位置信息不正确,则它会递归查询tablet的位置信息。如果客户端的缓存是空的,定位算法需要3次网络交互更新数据,包括一次Chubby文件的读取。如果客户端缓存过时,则定位算法需要6次网络交互才能更新数据,因为过时的客户端缓存条目只有在没有查到数据的时候才能发现数据过期(假设METADATA tablets移动的不频繁)。尽管tablet的位置信息存在内存中,不需要访问GFS,但是我们会通过客户端库预取tablet位置的方式来减少这种消耗:无论何时读取METADATA表都读取不止一个的METADATA tablets。

我们在METADATA表中还存储了次要的信息,包含与每个tablet有关的所有事件(如:什么时候一个服务器开始为该tablet提供服务)。这些信息对debugging和性能分析很有帮助。

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服务:例如,由于网络终端,造成了服务器丢失了它的Chubby会话。(Chubby提供了一个高效的机制使tablet服务器在不引入网络流量的情况下,能够检测它是否仍然持有它的锁。)只要文件依然存在,一个tablet服务器试图再次在这个文件上获取一个互斥锁。如果文件不存在了,则tablet服务器将不能再次提供服务,进而会kill掉自己。一个tablet服务器无论何时结束,它都会试图释放它的锁,以使master能够更加快速的重新分配它上面的tablets。

Master负责检测一个tablet服务器何时不能继续为它的tablets服务,并尽快将这些tablets重新分配。Master通过周期性的询问每个tablet服务器的状态来检测一个tablet服务器何时不能继续工作。如果一个tablet服务器报告它失去了它的锁,或者如果master在最近的几次尝试都不能到达一个服务器,则master会尝试获取这个服务器文件的互斥锁。如果master能够获取这个锁,则Chubby运行正常, tablet要么是宕机了,要么就是不能与Chubby正常通信了,因此master通过删除这个tablet服务器的服务器文件来确保这个服务器不能再次进行服务。一旦一个服务器文件被删除,master将之前分配到这个tablet服务器上的所有tablets移动到未被分配的tablets集合里面。为了确保Bigtable集群不易受到master和Chubby之间的网络问题的影响,master将会在它的Chubby会话超时后kill掉自己。然而,如上所说,master失败不会改变tablet服务器上的tablet分布。

当一个master被集群管理系统启动时,它需要在改变tablet分布之前先发现当前的分布。Master在启动时执行下面的步骤。

(1)            master从Chubby中抢占一个唯一的master锁,用来阻止其它的master实例化。

(2)            master扫描Chubby中的服务器目录,来查找哪些服务器正在运行。

(3)            master与每个正常运行的tablet服务器通信,获取每个tablet服务器上tablet的分配信息。

(4)            master扫描METADATA表获取所有的tablets的集合。在扫描的过程中,如果遇到一个tablet没有被分配,则将其放入到未被分配的tablets集合中,并可以进行分配。

一种复杂的情况是,在METADATA tablets被分配之前,不能扫描METADATA表。因此在开始扫描之前(第4步),如果在第三步发现root tablet没有分配,则master将root tablet加入到未被分配的tablet集合中。这个附加的操作确保了root tablet将会被分配。因为root tablet包含了所有的METADATA tablets的名字,所以在扫描完root tablet之后,master会得到所有METADATA tablet的名字。

已存在的tablet集合只有在创建或删除表、两个已存在的tablet合并成一个更大的tablet,或者一个已存在的tablet分裂成两个较小的tablet时才会改变。Master会记录所有的这些变化,因为上面几种情况除了最后一个都是它发起的。tablet分裂是比较特殊的,因为它是由tablet服务器发起的。Tablet服务器为METADATA表中记录新tablet的信息提交这次分裂操作。当分裂操作提交后,它会通知master。如果分裂通知丢失(因为tablet服务器或者master宕机),master在询问一个tablet器载入那个分裂的tablet时会检测到新的tablet。

5.3 Tablet服务

 

图5:Tablet表述

一个tablet的持久状态存储在GFS中,如图5中的描述。更新操作提交到REDO日志中。在这些更新中,最近提交的那些操作存储在一块名为memtable的有序缓存中;较老的更新存放在一系列的SSTable中。为了恢复这个tablet,一个tablet服务器会从METADATA表中读取它的元数据(metadata),这个元数据包含了组成这个tablet的SSTable的列表,以及一系列redo点,这些点指向可能含有该Tablet数据已提交的日志记录。服务器将SSTable的索引读入到内存,并通过执行从redo点开始的所有已提交的更新操作重构memtable。

当一个写操作到达一个tablet服务器时,服务器检查其是否符合语法要求,并验证发送者是否有权限执行这个操作。验证是通过读取一个Chubby文件(这个文件几乎会存在客户端的缓存中)中的可写用户的列表完成的。一个有效的写操作会写入到操作日志中。批处理方式可以提高大量细小操作的吞吐量。在写操作提交后,它的内容被写入到memtable中。

当一个读操作到达一个tablet服务器时,同样会检查是否符合语法要求和本身的权限。一个有效的读操作会在一系列SSTable和memtable合并视图上执行。由于SSTable和memtable是按字典序排序的数据,所以能够高效的生成合并视图。

当tables进行分裂和合并时,进来的读和写操作能够继续执行。

5.4 合并压缩

随着写操作的执行,memtable的大小会增加。当memtable的大小达到一个门限值时,这个memtable会被冻结,创建一个新的memtable,并将冻结的memtable转换成一个SSTable写入到GFS中。这里的次压缩(minor compaction)过程有两个目标:减少tablet服务器的内存使用,减少操作日志中在恢复tablet服务器时需要读取的数据总量。当压缩发生时,进来的读和写操作能够继续执行。

每次次压缩(minor compaction)都会创建一个新的SSTable。如果这种行为不停的进行下去,则读操作可能需要合并来自任意数量的SSTable的更新。否则,我们通过在后台周期性的执行合并压缩来限制这些文件的数量。一个合并压缩读取一些SSTable和memtable中的内容,并写入到一个新的SSTable中。输入SSTable和memtable可以在压缩完成后立即丢弃。

一个将所有SSTables写入到一个SSTable中的合并压缩称为主压缩(major compaction)。非主压缩产生的SSTable能够包含特定的删除条目,它阻止在仍然活着的旧SSTable中删除数据。另一方面,主压缩产生的SSTable不会包含删除信息或已删除的数据。Bigtable循环扫描所有的tablets,并定期的对它们执行主压缩。这些主压缩可以回收删除数据所使用的资源,并尽快的确保删除的数据在系统内彻底消失,对于存储的敏感数据,这是十分重要的。

六、        优化

在前面章节所描述的实现需要一些优化来满足我们用户的高性能、高可用和高可靠性需求。这一章通过对这些实现更加细节的描述来强调这些优化。

局域性群组

客户端能将多个列族聚集成一个局域性群组。对Tablet中每个局域性群组都会产生一个单独的SSTable。将通常不会被一起访问的列族分隔成不同的局域性群组能够提高读取效率。例如,Webtable中的页面元数据(如语言和校验和)作为一个局域性群组,而页面的内容作为另外一个局域性群组:一个想要读取元数据的应用不需要读取所有的页面内容。

此外,可以针对于每个局域性群组设定一些有用的调整参数。例如,一个局域性群组可以声明为存储在内存中。Tablet服务器采用惰性加载的策略对设定为内存中存储的局域性群组的SSTable进行内存加载。一旦加载过,则属于这些局域性群组的列族能够直接被读取,而不需要访问硬盘。这个功能对一些经常访问的小片数据很有用:在内部,我们使用它存放METADATA表中的位置信息列族。

压缩

客户端能够控制作为局域性群组的SSTable是否被压缩,如果压缩,选定什么样的压缩格式。每个SSTable块(它的大小能够通过局域性群组特定的调整参数来控制)都会按着用户指定的格式进行压缩。尽管由于对每个块分开压缩而浪费了一些空间,但是我们能够受益于在不需要解压整个文件的情况下能够访问部分SSTable。许多客户端使用两遍定制的压缩方式。第一遍使用了Bentley和Mcllroy方式,它通过在一个很大的窗口中对常见的长字符串进行压缩;第二遍使用了一种快速压缩算法,它在16KB大小的窗口内查找重复的数据。两种压缩都很快,在当前的机器上,它们压缩速度为100MB-200MB/s,解压速度为400-1000MB/s。

虽然在选择压缩算法时我们更看重速度而不是减少的空间,但是这种两遍的压缩方式对空间上的压缩也出奇的好。例如,在Webtable中,我们使用了这种压缩方式存储web页面内容。在一个实验中,我们在一个局域性群组中存储大量的文档。针对这个实验的目的,我们没有存储文档的所有版本,而是只存了一份,这种方式实现了10:1的空间压缩率。这比对HTML页面的压缩率为3:1或4:1的常用的Gzip压缩更好,原因在于Webtable存放行的方式:一个域名下的所有页面会存储在临近的地方。这使Bentley-Mcllroy算法能够将相同域名下的大量页面的共同部分识别出来。很多应用,不只是Webtable,选择他们行名以至于所有相似的数据聚集到一起,因此实现了很好的压缩率。当我们在Bigtable中存储相同值的多个版本时,压缩率会更好。

读操作的缓存

为了读操作的性能,tablet服务器使用双层缓存。扫描缓存是高层缓存,它缓存了tablet服务器代码使用SSTable接口获取的key-value对;块缓存是底层缓存,它缓存了从GFS上读取的SSTables块。扫描缓存主要用于倾向重复访问相同数据的应用。块缓存主要用于倾向读取近期数据附近数据的应用(如:顺序读取或随机读取同一个局域性群组的一个频繁访问行的不同列)。

Bloom过滤器

如5.3中描述的,一个读操作必须从所有的组成tablet的SSTable中读取数据。如果这些SSTable没有在内存中,则我们最终会多次访问硬盘。我们通过允许客户端对特定局域性群组的SSTable指定Bloom过滤器来降低访问次数。一个Bloom过滤器允许我们查询一个SSTable是否含有特定的行/列对的数据。对于某些特定应用,虽然存储Bloom过滤器占用了tablet服务器少量的内存,但能够彻底的减少读操作对磁盘的查询次数。我们使用Bloom过滤器也可以隐式的达到了当查询的行和列不存在时,不需要访问磁盘。

操作日志实现

如果我们为每个tablet保存一份单独的日志,那么我将会在GFS中并发的写大量的文件。取决于每个GFS服务器的底层系统实现,这些写操作会引起大量的磁盘查找,用来将数据写入到不同的物理日志文件中。此外,由于批操作通常较小,每个tablet分开保存日志文件会影响批操作所带来的优化。针对这些问题,对于每个tablet服务器我们将修改追加到一份操作日志中,不同的tablet修改混合存储在一个物理日志文件中。

在进行一般的操作时,使用一个日志可以提供很好的性能,但是会使恢复复杂化。当一个tablet服务器宕机,其上的tablets会被移动到大量其它的tablet服务器上:每个tablet服务器通常只负载原始tablet服务器上的一小部分tablets。为了恢复一个tablet的状态,新的tablet服务器需要重新执行原始tablet服务器上操作日志中针对这个tablet的修改。然而,这些tablets的修改混合存在同一个物理日志文件中。一种方式是每个新tablet的服务器会读取整个操作日志文件,然后只执行对于需要恢复的tablet的修改。然而,在这种方式下,如果100台机器每个都从失败的tablet服务器上分配了一个tablet,那么日志文件将被读取100次。

我们通过对日志文件条目以key<table, row name, log sequence number>进行排序,来避免这种重复的文件读取。在排序输出中,对于一个指定的tablet的所有修改将会是连续的,并因此能够通过一次硬盘查询和顺序读取进行高效的操作。为了并行排序,我们先将日志文件分成64MB大小的片段,在不同的tablet服务器上对每个片段并行的进行排序。这个排序过程由master协同处理,并在当一个tablet服务器声明它需要从一些操作日志中恢复修改时启动。

向GFS中写日志文件有时会由于各种原因引起性能波动(如:写操作进行时,一个GFS服务器宕机了,或者连接三个GFS副本服务器所在的网络发生拥塞或过载)。为了确保在GFS高负载时修改能够正常进行,每个tablet服务器实际有两个写日志线程,每个线程写自己的日志文件,同一时刻,两个线程中只有一个是工作的。如果一个写日志的线程效率很差,则会切换到另一个线程,修改操作的日志就会写在这个线程下的日志文件中。每个日志记录都有一个序列号,以此使tablet服务器在恢复时忽略掉线程切换所产生的重复的条目。

Tablet恢复提速

如果master将一个tablet从一个tablet服务器移动到另一个服务器,源tablet服务器会在本地先进行一个次压缩。这个压缩通过减少了tablet服务器日志中没有归并的记录的数量来缩短恢复时间。压缩完成后,tablet服务器停止对该tablet的服务。在卸载tablet之前,源服务器还会再做一次次压缩(通常很快),以消除第一次次压缩过程中新进入的未归并到SSTable中的修改。在这次次压缩完成后,tablet能够被载入到另一个tablet服务器上,而无需通过任何的日志条目恢复。

利用不变性

除了SSTable缓存,实际中我们产生的其它部分的SSTable都是不变的,我们可以通过这个事实来简化Bigtable系统。例如,当从SSTable读取数据时,我们不需要对文件系统访问操作进行任何同步。这样,就可以非常高效的实现对行的并行操作。Memtable是唯一一个能被读和写操作同时访问的可变数据结构。为了减少在读操作时的竞争,我们对内存采用了copy-on-write机制,这样就允许读写操作同时进行了。

因为SSTable是不可修改的,所以我们将永久移除要删除的数据问题转换为对废弃的SSTable进行垃圾回收的问题。Tablet的每个SSTable都在METADATA表中注册过。Master通过标记-删除的方式移除SSTable集合中废弃的SSTable,METADATA表中包含了ROOT集合。

最后,SSTable的不可变性使我们能够快速的分裂tablet。我们让子tablet(分裂后的tablet)共享父tablet(分裂前的tablet)的SSTables,而不是为每个子tablet创建新的SSTable集合。

七、        性能评估

我们建立一个有N个tablet服务器的Bigtable集群,用于测试Bigtable的性能和可扩展性,其中N是可以变化的。每个Tablet服务器配置为1G的RAM,并向一个由1786台有两块400GB IDE硬盘的机器组成的一个GFS单元上写数据。N个客户端机器为这个测试生成Bigtable负载。(我们使用与tablet服务器相同数量的客户端,以保证客户端不会造成瓶颈。)每个服务器配置双核Opteron 2GHz处理器,足够容纳所有进程工作数据集所需的内存,一个Gb以太网卡。这些机器排列在两层树结构的交互网络中,在根节点出的带宽大约有100-200Gbps。所有的机器都在同一个虚拟环境中,这样可以使任意两台机器间的往返时间都在1ms以内。

Tablet服务器和master服务器、测试客户端,以及GFS服务器都运行在同一个机器集合中。所有的机器都运行一个GFS服务,其中一些机器同时还可以再运行一个GFS服务,或者是客户端进程,或者是其它的工作进程。

R是测试中Bigtable不同行关键字的具体数量。R的选取保证了每次对每个tablet服务器的基准读或写数据量都大概在1GB左右。

在序列写的基准测试中,使用了0 ~ R-1作为行关键字。这些行关键字可以被分成10N个大小相同的区间。这些区间通过一个中心调度者被分配到N个客户端,这个中心调度者能够快速的将下一个可用的区间分配到一个处理完之前分配过来的区间的客户端上。动态分配有助于缓解由于其他进程引起的性能变化所造成的影响。我们在每个行关键字下写一个单独的字符串。每个字符串都是随机生成的,因此不可被压缩。此外,不同行关键字下的字符串是不同的,所以也就没有可能跨越行进行压缩。随机写基准测试是类似的,除了行关键字是在写入前通过模R散列出来的。这样保证了在整个基准测试过程中,写操作的负载均匀的分布在整个行空间中。

序列读基准测试使用的生成行关键字的方法与序列写的基准测试的基本相同,但它是从行关键字下读取字符串(之前在序列写基准测试中已经写入),而不是写入。类似的,随机读基准测试与随机写基准测试相似。

扫描基准测试与序列读基准测试类似,但是使用了Bigtable API所提供的接口来扫描一个行区间的所有数据。由于一次RPC就能够从tablet获得大量的数据,所以使用扫描操作减少了基准测试RPCs的执行次数。

随机读(内存)基准测试与随机读基准测试类似,但包含基准测试数据的局域性群组被标识为in-memory,因此,这些读操作能够直接从内存中进行读取,而不需要访问GFS。对于这个基准测试,我们将每个tablet服务器的数据量从1GB减少到100MB,以保证这些tablet服务器能够将所有数据加载到内存中。

 

图6:每秒读/写1000字节的数据。上表显示了每个tablet服务器的速率;上图显示了所有tablet服务器的总速率。

图6显示了当我们向Bigtable读写1000字节的基准测试性能的两个视图。表中显示了每个tablet服务器的操作数量,图中显示了每秒中操作的数量总和。

Tablet服务器性能

让我们先考虑单个tablet服务器的性能。随机读比其它的操作要慢一个数量级以上。每个随机读包含了从GFS到一个tablet服务器的一个64KB大小的SSTable块的网络传输,其中只有1000字节是被用到的。一个tablet服务器每秒执行大约1200次的读操作,也就是从GFS上每秒读取75MB的数据。由于网络栈的开销、SSTable的解析和Bigtable代码的执行,这个带宽使tablet服务器的CPUs接近饱和,并且我们系统中使用的网络链路也几乎被占满。大多数采用这种访问方式的Bigtable应用会把块大小设置为一个较小的值,通常情况下为8KB。

从内存中随机读会快很多,因为每个1000字节的数据都是从tablet服务器本地内存中获取的,而不需要去从GFS上获取64KB的大数据块。

随机写和序列写比随机读表现的好一些,因为每个tablet服务器将所有进来的写操作追加到一个单独的操作日志中,并且使用批提交的方式,将数据以流的形式高效的写入到GFS。随机写和序列写在性能上没有明显的区别,所有的tablet服务器写操作都是将记录追加到同一个操作日志中。

序列读比随机读的性能要好,因为每个从GFS读取到的64KB的SSTable块都放入了块缓存中,我们可以使用它完成接下来的64个读请求。

扫描的速度更快,因为tablet服务器对于一个单独的客户端RPC可以返回大量的数据,因此RPC的消耗基本可以抵消了。

缩放规模

随着我们将系统中的tablet服务器的数量从1增加到500,总吞吐量有着戏剧般的增长,有超过100倍的增长。举个例子,随着机器数量增加了500倍,从内存中随机读的性能同时提高了300倍。之所以是这种现象,是因为这个基准测试中,单个服务器的CPU达到了瓶颈。

然而,性能不会线性增长。对于大多数基准测试,随着tablet服务器数量从1增加到50,每台tablet服务器的吞吐量有着明显的下降。这是由于多台服务器配置负载不均衡造成的,主要是因为其他进程抢占CPU和网络。我们使用均衡算法来尽量解决这个问题,但是由于两个原因,这个算法不能完美的工作:一个是为了减少tablet的移动会压制重新均衡(一个tablet在移动时会短暂的不可用,通常会小于1秒);另一个是基准测试进程产生的负载会有波动。

随机读基准测试显示了最坏的比例(随着服务器数量增加了500倍,总吞吐量只增加了100倍)。造成这种情况的原因(如上说解释的)就是为了1000字节的数据我们需要在网络上传输64KB的大块数据,这种传输会使1G的链路饱和,因此每个服务器吞吐量随着机器的增加下降的很明显。

八、        真实应用

 

表1:Bigtable机群中的tablet服务器分布

截止到2006年8月,已经有388个未经测试的Bigtable集群运行在各种Google的机群上了,总共大概有24500个tablet服务器。表1中显示了每个集群中tablet服务器的粗略分布。这些集群中的很多都用于开发产品,因此会有一段时间比较空闲。观察一组由14个繁忙的集群、8069个tablet服务器集群组,我们看到整体的流量是大约每秒1200万个请求,其中包括接收到的大约741MB/s的RPC流量,以及发送出去的大约1GB/s的RPC流量。

 

表2:一些产品使用的表的特点。表大小(压缩前)和#Cell指出近似的大小。在表中禁用压缩的产品对应的没有给出压缩率。

表2提供了当期正在使用的一些表的数据。一些表存储了与用户相关的数据,而另一些存储了用于批处理的数据;表的大小、平均的单元大小、从内存中读取数据的比例和表概要的复杂度都有很大的区别。在本章下面的步骤中,我们简要的描述三个产品如何使用Bigtable。

8.1 Google分析

Google分析是用来帮助Web站点管理者分析他们网站的流量模式的服务。它提供了汇总统计,如每天独立访问的用户量和每个URL每天的访问量,它还提供了用户使用网站行为的报告,如根据之前访问的特殊页面,统计出有几成用户购买了东西。

为了开启这个服务,web站点管理员需要在他们的web页面中嵌入一小段Javascript程序。这个程序在页面被访问时调用。它记录了Google分析需要的各种信息,如一个用户的标识和获取的页面的信息。Google分析总结这些数据,提供给站点管理员有用的信息。

我们简单描述Google分析使用的两个表。Raw click表(大约200TB)每一行保存着一个终端用户的会话信息。行名是一个由网站的名字和会话信息建立的时间组成的元祖。这种模式保证了访问同一个网站的会话信息是相邻的,并且是按着时间顺序排列的。这个表压缩成原始大小的14%。

Summary表(大约20TB)保存着每个站点的预定义的各种汇总信息。这个表的数据是由周期性的调用MapReduce进程处理raw click表中的数据产生的,每个MapReduce进程从raw click表中获取最新的数据。整个系统的吞吐量是受限于GFS的吞吐量的。这个表能压缩到原始大小的29%。

8.2 Google Earth

Google提供了一组的服务,用于为用户提供高分辨率的地球表面卫星图像,可以通过网页版的Google地图接口进行访问,也可以通过Google Earth的客户端进行访问。这些产品允许用户浏览地球表面:他们能在不同的分辨率下平移、查看和标注卫星图像。这个系统使用一个表存储预处理的数据,用另外的一些表存储用户的数据。

预处理管道使用了一个表存储原始图像。在预处理过程中,图像被清除并被固化到最终的服务数据中。这个表包含了大约70TB的数据,因此存储在磁盘中。图像数据已经是高效压缩过的了,所以禁用了Bigtable的压缩功能。

图像表中的每一行都与一个地理分片相关。行名需要命名为可以使相邻地理分片能够存放在相连的位置。表包含了一个列族用于记录每个分片的原始数据。这个列族有很多列:基本上每列对应一个原始图片的数据。由于每个分片只是由一些图片组成的,所以这个列族是十分稀疏的。

预处理严重依赖于运行在Bigtable上MapReduce的数据传输。在这些MapReduce工作期间,整个系统中每个tablet服务器每秒处理1MB的数据。

这个服务系统使用了一个表来索引存在GFS中的数据。这个表相对较小(大约500GB)但是对于每个数据中心,它必须低时延的每秒处理数以万计的请求。因此这个表保存在数百个tablet服务器上,并且存储在in-memory的列族中。

8.3 个性化搜索

个性化搜索是一个双向的服务,它记录用户的请求和点击,涉及到各种Google的服务,如网页搜索、图片和新闻。用户能够浏览他们的搜索记录,来查看他们之前的请求和点击,并且他们能够定制基于Google历史使用习惯模式的个性化搜索。

个性化搜索将每个用户的数据存储在Bigtable中。每个用户有一个唯一的用户ID,并基于用户ID分配一个行名。所有用户行为都会存储在这个表中。一个单独的列族保存每个类型的行为(如,这有一个列族专门用来保存所有的web查询)。每个数据元素使用用户行为发生时的时间作为Bigtable的时间戳。个性化搜索通过运行在Bigtable上的MapReduce产生用户的行为概要。这些用户的行为概要用于个性化搜索结果。

个性化搜索数据会复制到几个Bigtable集群中,用于提高可用性和降低由于到客户端的距离所造成的延迟。个性化搜索组最初在Bigtable之上建立了一个客户端的副本机制,以确保所有副本数据的一致性。目前的系统使用了一个建立在服务器中的副本子系统。

个性化搜索存储系统的设计允许其他团队向他们自己的列中增加新的用户信息,并且目前则个系统被很多其他的Google服务使用,需要存放用户配置选项和设置。在很多团队中共享一个表的结果就是产生大量的列族。为了支持共享,我们为Bigtable增加一个简单的配额机制,来限制用户在共享表中的存储空间。这个机制也为使用这个系统进行用户信息存储的各种产品提供了隔离机制。

九、        经验教训

在设计、实现、维护和支持Bigtable过程中,我们得到了有用的经验和一些有趣的教训。

我们得到的一个教训就是,大的分布式系统很容易受到各种故障的影响,不仅仅是标准的网络中断和许多分布式协议中定义的“失败-停止”故障。举个例子,我们遇到了下列原因引起的问题:内存和网络问题、时钟偏差过大、机器挂起、扩展的和不对称的网络分区、我们使用的其它系统的bug(比如Chubby)、GFS配额溢出、计划内和计划外的硬件维护。我们通过这些问题收获了很多经验,我们通过修改各种协议解决了它们。举个例子,我们为我们的RPC机制增加了校验和。我们还通过删除对系统其它部分的假设解决了一些问题。比如,我们不再假设一个给定的Chubby操作只返回一个指定集合内的错误。

另一个教训就是推迟增加一个新功能,直到我们弄清楚这个新功能如何使用。比如,我们起初计划提供支持多用途的API。因为我们不是马上需要使用它们,所以我们就先不实现它们。现在,我们有很多现实的应用运行在Bigtable上,我们能调查它们的真正需求,并且发现大多数应用只需要单行上的事务功能。人们需要分布式事务功能之处,大多数都是用来存储二级索引的,我们计划增加一个特定的机制来实现这个需求。这个新机制的通用性比分布式事务要差,但是效率更高(特别是对于几百行以上的更新操作),并且更符合我们的跨数据中心复制的优化方案。

我们从支持Bigtable中还学到了一个有实践意义的教训,适当的系统级监控相当重要(如:监控Bigtable本身和监控使用Bigtable的客户端)。比如,我们扩展了我们的PRC系统,因此对于一个RPC的例子,它可以为这个RPC详细记录重要的动作。这个功能允许我们检测和修正很多问题,如在tablet上数据结构的锁竞争。当提交Bigtable修改时GFS写入慢,当METADATA tablet不可用时访问METADATA操作被挂起。关于监控的另外一个例子是,每个Bigtable集群都在Chubby中进行注册。这允许我们追踪多有的集群,发现他们有多大,查看它们运行的软件的版本,它们收到了多少流量以及是否存在任何问题。

我们学到的最重要的一课就是简单设计的价值。考虑到我们的系统的大小(大约100000行的非测试代码),随着时间的推移,新的代码以各种意想不到的方式加入进来,我们发现简洁的设计和编码给维护带来了巨大的好处。这方面的一个例子就是我们的tablet服务器的成员协议。我们第一版的协议很简单:master周期性的和tablet服务器签订租约,它们的租约过期后会kill掉自己。不幸的是,这个协议的可用性在网络问题出现时明显的降低了,并且对于master的恢复时间十分敏感。我们又重新几次设计了这个协议,直到这个协议表现良好。然而,最终的协议过于复杂,而且依赖了Chubby很少被其它应用使用的功能特性。我们发现浪费了大量的时间去调试各种古怪的用例,不仅有Bigtable的问题,也有Chubby本身的问题。最后,我们只好放弃这个协议,重新设计了一个新的更为简单的协议,只使用了Chubby广为使用的功能特性。

十、        相关工作

Boxwood项目的组件在一些方面与Chubby、GFS和Bigtable相重叠,因为它也提供了分布式协议、锁、分布式块存储和分布式B树存储。在这些方面,Boxwood组件相对于其它Google服务来说,更倾向于提供更为底层的服务。Boxwood项目的目标是给类似于文件系统或数据库的高层服务提供基础构件,而Bigtable的目的则是为了给客户端应用直接提供数据存储服务的。

许多现有的项目已经攻克了很多问题,实现了广域网上的分布式存储或高级服务。这包括了分布式的Hash表,这个项目最初是由CAN、Chord、Tapestry和Pastry项目发起的。这些系统的主要关注点与Bigtable不同,比如各种不同的带宽、不可信的协作者或者频繁的配置更改等。此外,去中心化和Byzantine错误容忍也不是Bigtable的目的。

依照应用开发者提供的分布式数据存储的模型,我们相信,分布式B树或分布式Hash表提供的key-value对模型有很大的局限性。Key-velue是很有用的组件,但是它们不应该是唯一的提供给开发者的组件。我们选择的模型要不限于简单的key-value对,支持稀疏的半结构化的数据。另外,它依旧足够简单,以能高效的处理平面文件;它也是透明的(通过局域性群组),以允许用户对系统重要的行为进行调整。

有些数据库厂商已经开发出并行数据库,它能够存储海量的数据。Oracle的实时应用集群数据库使用了共享磁盘存储数据(Bigtable使用了GFS),并且有一个分布式锁管理系统(Bigtable使用了Chubby)。IBM的DB2平行版本基于一种类似于Bigtable的无共享架构。每个DB2服务器都复制处理存储在一个关系型数据库中的表的一个子表。两个产品都提供了带有事务功能的完整的关系模型。

Bigtable局域性群组基于列存储的方式实现了类似压缩和磁盘读性能的提升,而不是基于行进行存储,这样的系统主要有C-Stroe和一些商业产品如Sybase IQSenSage、KDB+和MonetDB/X100的ColumnDM存储层。另外的一些系统对平面文件使用了垂直和水平分区,可以实现很好的数据压缩率,如AT&T的Daytona数据库。局域性群组不支持CPU级缓存优化,如Ailamaki中所描述的。

Bigtable使用了memtables和SSTables存储tablet更新的方法,与Log-Structured Merge Tree存储更新索引数据的方法类似。这两个系统中,排序好的数据在写入磁盘前会存在内存中,读操作必须对来自内存和磁盘的数据进行融合。

C-Store和Bigtable有很多共同的特点:两个系统都是用了无共享架构,并且都有两种不同的数据结构,一个用于近期的写操作,另一个用于长时间存储数据,并有一个将数据从一种形式转换到另一种形式的机制。他们的系统API有很大的不同:C-Store操作像一个关系型数据库,而Bigtable则提供了一个更底层的读和写接口,用于支持每台服务器每秒中的数千次操作。C-Store也是一个读优先的关系型DBMS,而Bigtable则在密集读和密集写的应用上都有很好的性能。

Bigtable也必须解决一些所有无共享数据库锁需要面对的、相同类型的负载和内存均衡方面的问题。我们的问题再某种程度上简单一些:(1)我们不需要考虑同一份数据有多个拷贝的问题,同一份数据可能由于视图或索引的原因以不同的形式表现出来;(2)我们让用户决定哪些数据应该放在内存中,哪些应该放在磁盘中,而不是由系统动态的判断;(3)我们的系统中没有复杂的查询操作或优化工作。

十一、            总结

我们已经描述了Bigtable,一个分布式存储系统,用来存储Google的结构化数据。Bigtable集群已经从2005年4月开始用于生产环境,在这之前,我们在设计和实现上大概花费了7人年的时间。截止到2006年8月,已经有超过60个项目使用了Bigtable。我们的用户喜欢Bigtable实现提供的性能和高可用性,并且他们能简单的通过向系统中增加更多的机器来进行扩容,因为他们的资源需求一直在变。

由于Bigtable提供的接口并不常见,一个有趣的问题时我们的用户要去适应它有多困难。新的用户有时不确定如何最好的使用Bigtable的接口,特别是他们已经习惯使用支持通用事务功能的关系型数据库。但是,事实是,Google的很多产品都成功的使用了Bigtable,这证明了我们的设计在实际中工作的很好。

我们在实现Bigtable的几个其它的功能,如支持二级索引,以及支持多Master节点的跨数据中心的复制的Bigtable基础构件。我们已经开始部署Bigtable作为其它产品的服务,这样就可以使不同的团队不需要维护他们自己的Bigtable集群了。随着我们的服务集群的扩容,我们需要通过Bigtable自己处理更多的资源分配问题。

最后,我们发现建设Google自己的存储解决方案带来了明显的优势。通过为Bigtable设计自己的数据模型,使我们的系统有很好的灵活性。此外,由于我们能够控制Bigtable的实现,以及Bigtable使用到的其它的Google的基础构件,这就意味着我们在系统出现瓶颈或效率低下时能够及时解决问题。

十二、            一些个人的理解

12.1 关于table、tablet和SSTable的关系

 

图7:table、tablet和SSTable的关系

12.2 METADATA表中的数据

这是本人根据这篇论文推测出来的,不一定正确,只是个人的一个理解。

 

图8:METADATA表中一行的内容

12.3 次压缩和主压缩对删除数据的处理

 

图9:次压缩和主压缩对删除数据的处理

posted @ 2013-05-30 15:57  Geek_Ma  阅读(2352)  评论(0编辑  收藏  举报