Bigtable:一个结构化数据的分布式存储系统

Bigtable:一个结构化数据的分布式存储系统

翻译人:HC

Bigtable是一个为管理大规模结构化数据而设计的分布式存储系统,这些大规模数据是分布在上千台普通服务器的PB级数据。Google的许多项目使用Bigtable存储数据,包括Web索引,Google Earth以及GoogleFinance。这些应用对Bigtable提出了不同的要求,比如数据大小(从URLs到网页到卫星图像)、延迟要求(从后端批处理到实时数据服务)。尽管这些要求各不相同,但是Bigtable为这些产品成功地提供了灵活、高性能的存储解决方案。在这篇文章中,我们描述Bigtable简单的数据模型,这个模型使用户可以动态的控制数据的分布和格式,我们还将描述Bigtable的设计和实现。

1.     介绍

从2003下半年开始,我们在Google设计、实现并部署了一个名为Bigtable的管理结构化数据的分布式存储系统。Bigtable的设计目的是能够可靠地处理PB级数据,并能够部署在上千台机器上。Bigtable已经完成了一些目标:应用广泛,可扩展性,高性能以及高可用性。Bigtable已经被Google的60多个产品和项目使用,包括Google Analytics,Google Finance,Orkut,Personalized Search,Writely以及Google Earth。这些产品对Bigtable要求各不相同,从高吞吐量的批处理到及时响应并将数据快速返回给终端客户。这些产品使用的Bigtable集群的配置有很大的不同,从几百台到上千台服务器,以及存储几百TB的数据。

在许多地方Bigtable和数据库相似:它有很多实现策略与数据库相同。并行数据库和内存数据库具有可扩展性和高性能,但是Bigtable提供了与这些系统不同的接口。Bigtable不支持完全的关系数据模型;相反,它为客户提供了一个简单的数据模型,这个数据模型支持动态控制数据的分布和格式,并允许客户推断底层存储数据的位置属性(or局部性or位置相关性)。使用行和列的名字索引数据,行和列的名字可以是任意的字符串。尽管客户常常把大量的结构化和半结构化的数据串行化到这些字符串中,但是Bigtable将数据视为无解释的字符串。客户通过仔细选择数据模式,可以控制数据的位置。最后,Bigtable模式参数使得客户可用动态的控制是把数据存在内存中还是硬盘中。

第二节更详细的描述数据模型,第三节概观客户端API。第四节简要介绍了Bigtable依赖的底层的Google的基础框架;第五节描述了Bigtable实现的关键部分,第6节描述了我们为了提高Bigtable的性能所做的改进。第7节提供Bigtable的性能数据。我们在第8节描述Google使用Bigtable的一些例子,并在第九节中讨论我们在设计和支撑Bigtable中学到的教训。最后,第10节描述相关工作,以及在第11节呈现我们的结论。

2.     数据模型

一个Bigtable集群是一个运行Bigtable软件的进程的集合。每个集群存储一些表。Bigtable的表是一个稀疏的、分布式的、持久化存储的多维度排序映射表。数据被组织成三维:行,列以及时间戳。


         我们把由一个特定行键,列键,以及时间戳定位的存储称为单元(cell)。行组合在一起形成了负载均衡的单位,列组合在一起形成了访问控制和资源统计的单位。

         我们在仔细分析一个与Bigtable相似的系统的大量潜在用途后决定使用这个数据模型。举一个促使我们做出许多设计决策的具体例子:存储大量网页及相关信息,以用于很多不同的项目。我们称这种特殊的表为Webtable。在Webtable里,我们将用URLs作为行键,用网页的许多属性作为列名,把网页内容存在contents:列中并用获取该网页的时间戳作为标识,如图一所示。


l 行。        

Bigtable按照行的键值的字典排序来维护数据。表中的行键都是任意的字符串(目前最大的是64KB,尽管大多数用户都是用10-100字节)。在一个行键下的每一个读或写操作都是原子操作(无论这一行的多少不同的列被读或写),这个设计决策使得用户更加容易理解程序在对同一个行进行并发更新操作时的行为。换句话说,在Bigtable中,行是事务一致性的单位,Bigtable目前不支持跨行的事务。

连续键值的行被组织成片(tablet),片是分配和负载均衡的单位。结果,读小范围的行是非常高效的,只需要少量的机器通信。用户可以通过选择他们的行键来利用这个属性,使得他们的数据访问获得好的局部性。例如,在Webtable中,在同一域里的网页被组织在邻近的行,这是通过把它们的URL反转来实现的。我们把com.google.maps/index.html作为网页maps.google.com/index.html的键值。将相同域的网页存储在相邻的位置使得有些主机和域名分析更加高效。

l 列。

列键被组织成称为列族的集合,列族是访问控制的基本单位。存放在同一列族下的所有数据通常是一个类型(我们把同一个列族下的数据压缩在一起)。必须先显式的创建列族,然后才能在它下面的列键中存储数据。在列族创建后,就能够使用族里的任何列键:数据能够被存储在这种列键下而不用影响表的模式。我们的意图是一张表中的列族数量尽量小(最多几百个),并且列族在运行期间很少改变;这种限制使得广泛分享的元数据不至于太大。相反,一个表可能有无数个列。

所有的列族可能在改变表的模式时被删除,在这中情况下,存储在那个列族中的任何列键下的数据都会被删除。由于Bigtable不支持跨行的事务,无论怎样,如果存储在特定列键下的数据属于多行,就不能被自动删除。

列键使用以下语法命名:列族:限定词。列族的名字必须是可打印的,而限定词可以是任意的字符串。例如,Webtable有个列族language,它用来存放撰写网页的语言。我们在language列族中只使用一个限定词为空的列关键字,来存放每个网页的语言标识ID。在这个表中,另一个有用的列族是anchor;这个列族的每一个列关键字代表一个锚链接,如图一所示。限定词是引用该网页的站点名;这个单元里存放的是链接的文本。

访问控制以及磁盘和内存的使用统计都是在列族级别上进行的。在我们的Webtable的例子中,这些控制允许我们维护一些不同类型的应用:一些应用添加新的基本数据,一些应用可以读取基本数据并创建继承的列族,一些应用则只被允许查看已存在的数据(甚至可能因为隐私的原因不能浏览所有已存在的数据)。

l 时间戳。

表中不同的单元包括同一数据的不同版本,这些版本由时间戳来索引。Bigtable时间戳都是64位整型。时间戳可以默认的被Bigtable赋值,用来表示精确到毫秒的“实时”时间,或者他们也可以由用户应用程序明确的赋值。应用程序要想避免数据版本冲突就必须由自己生成唯一的时间戳。同一单元的数据的不同版本按照时间戳倒序排序,即最新的数据能被最先读取。

为了使管理不同版本数据不那么繁重,我们对每一个列族配有两个设置参数,Bigtable通过这两个参数可以对废弃版本的数据自动进行垃圾收集。用户既可以指定仅仅最近的n个版本被存储,也可以只存储足够新的版本(例如,只保存最近7天才写入的数据)。

在我们的Webtable的例子里,我们可以设定存储在contents:列中的网络爬虫网页的时间戳为网络爬虫抓取这个页面的实际时间。上述的垃圾收集机制可以让我们只保留最近三个版本的网页数据。

3.     API

Bigtable API提供创建删除表和列族的功能。它也提供改变集群,表,列族元数据的功能,例如访问控制权限。

客户应用程序能够写和删除Bigtable里的值,从独立的行中查询值,或者遍历表中数据的子集。图2 C++代码使用RowMutation抽象进行了一系列的更新操作。(为了使例子简短,不相干的代码被省去了)。调用Apply函数对Webtable进行了一个原子变化操作:它为www.cnn.com增加了一个anchor,同时删除了另外一个anchor。


         图3 C++代码使用Scanner抽象遍历一个特定行内的所有anchor。客户可以遍历多个列族,有一些机制可以限制扫描输出的行,列和时间戳。例如,我们可以限制上面的扫描,让它只输出那些匹配正则表达式*.cnn.com的anchor,或者只产生那些时间戳属于距当前时间10天内的anchor。

 

         Bigtable还支持一些其它的功能,这些功能允许用户使用更复杂的方式维护数据。首先,Bigtable支持单行上的事务,这可以用来对存储在一个行键下的数据进行原子性的读-修改-写序列的操作。尽管Bigtable提供了一个允许用户跨行批量写入数据的接口,但是Bigtable目前不支持通用的跨行事务。其次,Bigtable允许把单元用来作为整数计数器。最后,Bigtable允许用户在服务器的地址空间内执行脚本程序。脚本程序使用Google开发的用来数据处理的被称为Sawzall的语言。虽目前,我们基于的Sawzall语言的API还不允许客户的脚本程序写入数据到Bigtable,但是它允许多种形式的数据转换,基于任意表达式的过滤,以及通过许多操作符进行汇总。

Bigtable能够和MapReduce一起使用,MapReduce是Google开发的大规模并行计算框架。我们已经开发了一些Wrapper类,它使得Bigtable既可以作为MapReduce处理的输入来源也可以作为MapReduce处理的输出目标。

4.     组成单元

Bigtable是建立在一些其它Google基础构件之上的。一个Bigtable集群通常运行在一个共享的机器池中,这个共享机器池中还运行其它的分布式应用程序。Bigtable依赖于一个Google集群管理系统来调度任务,管理共享的机器上的资源,监视机器的状态,以及处理机器的故障,Bigtable的进程经常要和其它应用进程共享机器。例如,如图4所示,一个Bigtable服务器可能与一个mapreduce worker,应用服务器以及一个GFS服务器是同一台机器。

Bigtable使用GFS存储日志和数据文件。GFS是一个分布式文件系统,为了获得更好的可靠性和可用性它维护每个文件的多个副本。


Bigtable内部存储数据的文件是GoogleSSTable格式的。SSTable是一个持久化的、排序的、不可更改的键-值的映射,这些键和v值都是任意的字符串。我们可以进行查询与一个指定键相关的值,以及在指定键范围内遍历所有的键-值对。从内部看,SSTable是一系列的数据块(通常每个块默认的大小是64KB,但是这个大小是可配置的)。块索引(通常存储在SSTable的最后)被用来定位数据块;当打开SSTable的时候,索引被加载进内存。每次查找可以通过一次磁盘搜索完成:我们首先通过在内存索引中使用二分查找法找到数据块,然后从硬盘读取这个数据块。也可以选择把整个SSTable都映射到内存中,这使得我们的查询和浏览操作不必访问硬盘了。

BigTable还依赖一个高可用持久化的分布式锁服务,称作Chubby。一个Chubby服务包括了5个活动的副本,其中的一个副本被选为Master,并且处理请求。只有在大多数副本都是正常运行的,并且彼此之间能够互相通信的情况下,Chubby服务才是可用的。当有副本失效的时候,Chubby使用Paxos算法来保证副本的一致性。Chubby提供了一个名字空间,里面包括了目录和小文件。每个目录或者文件可以当成一个锁来使用,读写一个文件都是原子的。Chubby客户程序库提供Chubby文件的一致性缓存。每个Chubby客户程序都维护一个与Chubby服务的会话。如果客户程序不能在租约到期的时间内重新签订会话的租约,这个客户会话就失效了当一个会话失效时,它拥有的锁和打开的文件句柄都失效了。Chubby客户程序可以在文件和目录上注册回调函数,当文件或目录改变、或者会话过期时,回调函数会通知客户程序。

Bigtable使用Chubby完成以下的几个任务:确保在任何时刻最多只有一个活动的Master;存储BigTable数据的引导程序的位置(参考5.1节);发现片服务器以及在确定片服务器的失效(参考5.2节);存储BigTable的模式信息(参考5.5节)。如果Chubby长时间无法访问,BigTable开始失效。在2006年八月,我们使用11个Chubby服务实例的14个BigTable集群上测量了这个影响。由于Chubby不可用而导致BigTable中的部分数据不能访问的平均比率是0.0047%(Chubby不能访问的原因可能是Chubby本身失效或者网络问题)。单个集群里,受Chubby失效影响最大的百分比是0.0326%。

5.     实现

Bigtable实现有三个主要的组件:链接到每个客户的库、一个Master服务器,以及多个片(tablet)服务器。片服务器能够从集群中被动态的添加(或移除)以适应负载的变化。 

Master服务器负责把片分配给片服务器,检测添加的或者过期失效的片服务器,对片服务器进行负载均衡,以及对GFS上的文件进行垃圾收集。除此之外,它还处理对模式的修改,例如表和列族的创建和删除。

每个片服务器都管理一个片的集合(通常每个片服务器大约有十个至千个片)。片服务器处理它所加载的片的读写操作,当片增长到够大时负责将片分割。

和许多Single-Master类型的分布式存储系统一样,客户数据都不经过Master服务器:客户程序直接和片服务器通信进行读写操作。因为Bigtable的客户不依靠Master服务器来获取片的位置信息,所以大多数客户从不和Master服务器通信。结果,实际上Master服务器的负载是很轻的。

一个Bigtable集群存储了很多表。每个表包含了一个片的集合,每个片包含了某个范围内的行的所有相关数据。初始状态下,一个表只有一个片。随着表的增长,它被自动分割成多个片,默认情况下每个片大约是1GB。

尽管我们的模型支持任意大小的数据,但是目前的Bigtable实现不支持特别大的值。因为一个片不能在一个行的中间被分割,我们建议用户,一个行包含不超过几百GB的数据。

在这节的剩余部分我们描述Bigtable实现的一些细节。

5.1片定位

我们使用一个三层的类似B+树的结构存储片位置的信息(如图5所示)。第一层是一个存储在Chubby中包含root tablet位置的文件。root tablet包含了一个特殊的METADATA表里所有的片的位置。METADATA表的每个片包含了一个用户片的集合的位置。root tablet很特殊-他从不被分割-以确保片的位置信息存储结构不会超过三层。


在METADATA表里面,每个片的位置信息都存放在一个行键里,而这个行键是由片所在的表的标识符和它的最后一行编码而成的。METADATA的每一行都存储了大约1KB的内存数据。在一个大小适中的、容量限制为128MB的METADATA 片中,采用这种三层结构的存储模式,可以足够标识234个片的地址(或者128MB片的261字节)。

客户程序库通过定位层结构来定位片,并缓存片的位置信息。如果客户程序没有缓存某个片的地址信息,或者如果发现它缓存的地址信息不正确,它就在树状的存储结构中递归的查询片位置信息。如果客户端缓存是空的,那么定位算法需要通过三次网络来回通信,包括了一次Chubby读操作。如果客户端缓存的地址信息过期了,那么定位算法可能需要最多6次网络来回才能更新数据,因为只有在缓存中没有查到数据的时候才能发现数据过期(假设METADATA的片没有被频繁的移动)。尽管片的地址信息是存放在内存里的,对它的操作不必访问GFS文件系统,但是,通常我们会通过预取片地址来进一步的减少访问的开销:每次需要从METADATA表中读取一个片的元数据的时候,它都会多读取几个片的元数据。

在METADATA表中还存储了其它信息,包括每个片的所有事件日志(例如,什么时候一个服务器开始为该片提供服务)。这些信息对排查错误和性能分析很有帮助。

5.2片分配

在任何一个时刻,一个片最多分配给一个片服务器。Master服务器记录活跃的片服务器,以及分配给片服务器的片,包括还没有被分配的片。当一个片还没有被分配,有一个片服务器有足够的空间存储这个片,Master服务器通过向目标片服务器发送请求把这个片分配给这个片服务器。当且仅当这个片加载请求没有在下一个备用Master服务器启用前到达片服务器,这个分配才会失败:一个片服务器只接受当前Master服务器发送的片加载请求。因此,一旦Master服务器已经发送一个片加载请求,它可以认为片已经分配给了这台片服务器直到这台片服务器失效或者这台片服务器通知Master服务器他没有加载这个片。

Bigtable使用Chubby跟踪记录服务器的片状态。当一个片服务器启动时,它在Chubby的一个特定的Chubby目录下的一个有唯一性名字的文件建立并获取独占锁。Master服务器监控这个目录(服务器目录)去发现片服务器。如果片服务器丢失了它的独占锁那么片服务器就停止服务它的片:例如,网络中断可能导致服务器失去他的Chubby锁。(Chubby提供了一个高效的机制,允许片服务器在不增加网络负载的情况下检查是否他还持有它的锁。)只要文件还存在,一个片服务器会尝试再次获得它的文件独占锁。如果文件不存在了,那么片服务器就不再提供服务了,它会自动退出。无论何时一个片服务器终止时(比如,因为集群的管理系统将运行该片服务器的主机从集群中移除),它会尝试释放它的锁,使得Master服务器将会尽快把片重新分配到其它的片服务器。

Master服务器负责检查一个片服务器何时不再为它的片服务了,并且会尽快重新分配这些片。为了检测何时片服务器不再为它的片服务,Master服务器通过轮询片服务器文件锁的状态。如果一个片服务器报告它失去了文件锁,或者Master服务器在最近几次尝试和它通信都没有得到响应,Master服务器就会尝试获取该片服务器文件的独占锁。如果Master服务器成功获取了独占锁,那么Chubby是正常运行的,片服务器要么是宕机了要么是不能和Chubby通信了,因此,Master服务器通过删除这个片服务器的文件来确保这个片服务器不再服务。一旦片服务器的文件被删除了,Master服务器就把之前分配给它的所有的片放入到未分配的片集合中。为确保Bigtable集群在Master服务器和Chubby之间网络出现故障的时候仍然可以使用,Master服务器在它的Chubby会话过期后主动退出。Master服务器的故障不会改变片在片服务器上的分配。

当集群管理系统启动了一个Master服务器之后,Master服务器需要在修改片的分配之前了解当前片的分配状态。Master服务器在启动的时执行以下步骤:

(1)      Master服务器从Chubby获取一个唯一的Master锁,用来阻止创建其它的Master服务器实例;

(2)      Master服务器扫描Chubby的服务器目录,发现正在运行的服务器;

(3)      Master服务器和所有的正在运行的片服务器通信,来得知那些片已经分配给了那个片服务器,并更新片服务器对当前的Master服务器的认知(为了让来自前一个Master服务器的后续的接受片加载请求被拒绝)。

(4)      Master服务器扫描METADATA表获取所有的片的集合。在扫描的过程中,当Master服务器发现了一个还没有分配的片,Master服务器就将这个片加入到未分配的片集合等待合适的时机分配。

一个复杂的情况是,在METADATA表的片没有被分配之前是不能够扫描它的。因此,在开始扫描之前(步骤4),如果在第三步的扫描过程中发现root tablet还没有分配,Master服务器就把root tablet加入到未分配的片集合。这个附加操作确保了root tablet会被分配。因为root tablet包括了所有METADATA的片的名字,所以Master服务器扫描完root tablet后,就知道所有的METADATA表的片的名字了。

已存在的片的集合会发生改变仅当一个表被创建了或删除了,两个已存在的片合并成一个更大的片,或者一个片被分割成两个小的片。Master服务器可以跟踪记录所有这些事件,因为除了最后一个事件外的两个事件都是由它启动的。Tablet分割事件需要特殊处理,因为它是由片服务器启动。片服务器通过在METADATA表中记录新片的信息来提交这个分割。当这个分割提交后,片服务器通知Master服务器。如果分割通知丢失了(因为片服务器或者Master服务器宕机了),Master服务器在要求片服务器装载已经被分割的子表的时候会发现一个新的片。这个片服务器会通知Master服务器这个分割操作,因为它会发现在METADATA表中找到片入口所指的片只是Master服务器让它加载的片的一部分。

5.3片服务

片的持久化状态信息保存在GFS上,如图6所示。更新操作提交到redo日志中。在这些更新操作中,最近提交的那些被存放在一个排序的缓存中,称这个缓存为memtable。较早的更新存放在一系列SSTable中。一个memtable维护一行一行的更新,每一行都是copy-on-write的方式来维护行层次上的一致性。旧的更新存储在按序排好的SSTables中(不可改变的)。

为了恢复一个片,片服务器首先从METADATA表中读取它的元数据。片的元数据包含了组成这个片的SSTable的列表,以及一系列的Redo Point,这些Redo Point指向可能含有该片数据的已提交的日志记录。片服务器把SSTable的索引读进内存,之后通过重复Redo Point之后提交的更新来重建memtable。


当一个写操作到达片服务器时,片服务器检查这个操作格式是否正确(例如不是由程序出错的或废弃的用户发送),以及操作发送者是否有权限执行这个操作。权限验证的方法是通过从一个Chubby文件里读取出来的具有写权限的操作者列表来进行验证(这个文件几乎一定会存放在Chubby客户缓存里)。一种有效的变化是修改操作会记录在提交日志里。把大量小的修改操作聚在一起提交可以提高吞吐量。当一个写操作提交后,它的内容插入到memtable里面。

当一个读操作到达片服务器时,Tablet服务器会作类似的完整性和权限检查。一个有效的读操作在一个由一系列SSTable和memtable合并的视图里执行。由于SSTable和memtable是按字典排序的数据结构,因此可以高效生成合并视图。

当进行Tablet的合并和分割时,正在进行的读写操作能够继续进行。读写操作可以在片进行压缩时继续执行;将在下一小节中描述压缩。

5.4压缩

随着写操作的执行,memtable的大小不断增大。当memtable的尺寸到达一个阈时,这个memtable就会被冻结,一个新的memtable被创建,被冻结住memtable会被转换成SSTable写入GFS。MinorCompaction过程有两个目的:压缩片服务器使用的内存,以及如果这台片服务器宕机了,在恢复期间,可以减少必须从提交日志里读取的数据量。

每一次Minor Compaction都会创建一个新的SSTable。如果Minor Compaction过程不停滞的持续进行下去,读操作可能需要合并任意多个SSTable的更新。相反,我们通过定期在后台执行Merging Compaction过程合并文件来限制这些文件的数量。Merging Compaction过程读取一些SSTable和memtable的内容,合并成一个新的SSTable。只要Merging Compaction过程完成了,输入的这些SSTable和memtable就可以被丢弃了。

合并所有的SSTable并生成一个新的SSTable的MergingCompaction过程叫作Major Compaction。由非Major Compaction产生的SSTable可能含有特殊的删除条目,这个条目会隐藏就得但依然有用的SSTable中的删除的数据。另一方面,Major Conpaction产生不包含已删除的信息和数据的SSTable。Bigtable循环扫描它所有的片,并且定期对它们执行Major Compaction。Major Compaction机制允许Bigtable回收已经删除的数据占有的资源,并且确保BigTable能及时清除已经删除的数据,这对存放敏感数据的服务是非常重要。

Bigtable的读性能受益于GFS的位置相关性的优化。当文件被写时,GFS试图在写的那台机器上放置数据的副本。当GFS文件被读时,读操作从最近的可用副本获取数据。因此,在通常情况下,片服务器与GFS服务器共享机器,片服务器将会把数据压缩进SSTable,SSTable在本地磁盘上有副本,这就允许当处理随后的读请求时能够快速的访问这些SSTable。

5.5模式管理

Bigtable模式都存在Chubby中。Chubby对Bigtable架构来说是有效的通信底层,因为Chubby提供了整个文件的原子写操作和小文件的一致性缓存。例如,假设一个客户想要从表中删除一些列族。Master服务器实现访问控制检查,验证结果模式是正确的,然后通过重写Chubby相应的模式文件来安装新的模式。无论何时片服务器需要了解哪些列族存在,它们只需要简单的在Chubby中读取合适的模式文件,而这些文件在服务器的Chubby客户缓存中总能被找到。因为Chubby缓存是一致的,保证片服务器能知道这个文件的所有修改。

 

 

6.     优化

上一节我们描述了Bigtable的实现,我们还需要很多优化工作才能使Bigtable到达用户要求的高性能、高可用性和高可靠性。为了更好的强调这些优化工作,本节更加细节的描述了Bigtable实现的其它部分。

l  局部性群组

         客户程序可以将多个列族组合成一个局部性群族,这是一个允许客户程序控制它们数据分布的抽象。对片中的每个局部性群组都会生成一个单独的SSTable。将通常不会一起访问的列族分割成不同的局部性群组可以提高读取操作的效率。例如,在Webtable表中,网页的元数据(比如语言和Checksum)可以在一个局部性群组中,网页的内容可以在另外一个群组:当一个应用程序要读取网页的元数据的时候,它没有必要去读取所有的页面内容。

         此外,可以以局部性群组为单位设定一些有用的调试参数。例如,可以把一个局部性群组设定为全部存储在内存中。片服务器依照惰性加载的策略将设定为放入内存的局部性群组的SSTable装载进内存。因为SSTable是不可修改的,所以没有一致性的问题。加载完成之后,访问属于该局部性群组的列族的时候就不必读取硬盘了。这个特性对于需要频繁访问的小块数据特别有用;在Bigtable内部,我们利用这个特性提高METADATA表中具有位置相关性的列族的访问速度。

l  压缩

         客户程序可以控制一个局部性群组的SSTable是否需要压缩,如果需要压缩,那么以什么格式来压缩。每个SSTable的块(块的大小由局部性群组的优化参数指定)都使用用户指定的压缩格式来压缩。虽然分块压缩浪费了少量空间,但是,我们在只读取SSTable的一小部分数据的时候就不必解压整个文件了。很多客户程序使用了两阶段的可定制的压缩方式。第一阶段采用Bentley and McIlroy’s方式,这种方式在一个很大的扫描窗口里对常见的长字符串进行压缩。第二阶段是采用快速压缩算法,在一个16KB的小扫描窗口中寻找重复数据。两个压缩的算法都很快,在现在的机器上,压缩的速率达到100-200MB/s,解压的速率达到400-1000MB/s。

         虽然我们在选择压缩算法的时候重点考虑的是速度而不是压缩的空间,但是这种两阶段的压缩方式在空间压缩率上的表现也是令人惊叹。例如,在Webtable的例子里,我们使用这种压缩方式来存储网页内容。在一次实验中,我们在一个压缩的局部性群组中存储了大量的文档。针对实验的目的,我们没有存储每个文档所有版本的数据,我们仅仅存储了一个版本的数据。该模式的空间压缩比达到了10:1。这比传统的Gzip在压缩HTML页面时3:1或者4:1的空间压缩比好的多,这是因为Webtable的行的存放方式:从同一个主机获取的页面都存在临近的地方。这使得Bentley-McIlroy算法可以从来自同一个主机的页面里找到大量的重复内容。不仅仅是Webtable,其它的很多应用程序也通过选择合适的行名来将相似的数据聚簇在一起,以获取较高的压缩率。当我们在Bigtable中存储同一份数据的多个版本的时候,压缩效率会更高。

l  通过缓存提高读操作的性能

         为了提高读操作的性能,Tablet服务器使用二级缓存的策略。扫描缓存是第一级缓存,主要缓存片服务器通过SSTable接口获取的键-值对。Block缓存是二级缓存,缓存的是从GFS读取的SSTable的Block。对于经常要重复读取相同数据的应用程序来说,扫描缓存非常有效;对于经常要读取刚刚读过的数据附近的数据的应用程序来说,Block缓存更有用(例如,顺序读,或者在一个热点的行的局部性群组中随机读取不同的列)。

 

l  Bloom过滤器

         如5.3节所述,一个读操作必须读取构成片状态的所有SSTable的数据。如果这些SSTable不在内存中,那么就需要多次访问硬盘。我们通过允许客户程序对特定局部性群组的SSTable指定Bloom过滤器来减少硬盘访问的次数。我们可以使用Bloom过滤器查询一个SSTable是否包含了特定行和列的数据。对于某些特定应用程序,我们只付出了少量的用于存储Bloom过滤器的内存的代价,就换来了读操作显著减少的磁盘访问的次数。使用Bloom过滤器也隐式的达到了当应用程序访问不存在的行或列时,大多数时候我们都不需要访问硬盘的目的。

l  Commit日志的实现

         如果我们把对每个片的操作的Commit日志都存在一个单独的文件的话,那么就会产生大量的文件,并且这些文件会并行的写入GFS。根据GFS服务器底层文件系统实现的方案,要把这些文件写入不同的磁盘日志文件时会有大量的磁盘Seek操作。除此之外,由于批量提交中操作的数目一般比较少,因此,对每个片设置单独的日志文件也会给批量提交本应具有的优化效果带来很大的负面影响。为了避免这些问题,我们设置每个片服务器一个Commit日志文件,把修改操作的日志以追加方式写入同一个日志文件,因此一个实际的日志文件中混合了对多个片修改的日志记录。

         使用单个日志显著提高了普通操作的性能,但是将恢复的工作复杂化了。当一个片服务器宕机时,它加载的片将会被移到很多其它的片服务器上;每个片服务器都装载很少的几个原来的服务器的片。为了恢复一个片的状态,新的片服务器要从原来的片服务器写的日志中提取修改操作的信息,并重新执行。然而,这些片修改操作的日志记录都混合在同一个日志文件中的。一种方法新的片服务器读取完整的Commit日志文件,然后只重复执行它需要恢复的片的相关修改操作。使用这种方法,假如有100台片服务器,每台都加载了失效的片服务器上的一个片,那么,这个日志文件就要被读取100次(每个服务器读取一次)。

         为了避免多次读取日志文件,我们首先把日志按照关键字(table,row name,log sequence number)排序。排序之后,对同一个片的修改操作的日志记录就连续存放在了一起,因此,我们只要一次磁盘Seek操作,之后顺序读取就可以了。为了并行排序,我们先将日志分割成64MB的段,之后在不同的片服务器对段进行并行排序。这个排序工作由Master服务器来协同处理,并且在一个片服务器表明自己需要从Commit日志文件恢复Tablet时开始执行。

         在向GFS中写Commit日志的时候可能会引起系统颠簸,原因是多种多样的(比如,写操作正在进行的时候,一个GFS服务器宕机了;或者连接三个GFS副本所在的服务器的网络拥塞或者过载了)。为了确保在GFS负载高峰时修改操作还能顺利进行,每个片服务器实际上有两个日志写入线程,每个线程都写自己的日志文件,并且在任何时刻,只有一个线程是工作的。如果一个线程的在写入的时候效率很低,片服务器就切换到另外一个线程,修改操作的日志记录就写入到这个线程对应的日志文件中。每个日志记录都有一个序列号,因此,在恢复的时候,Tablet服务器能够检测出并忽略掉那些由于线程切换而导致的重复的记录。

l  片恢复加速

         在卸载一个片之前,片服务器会对这个片做一次Minor Compaction。这个Compaction操作减少了Tablet服务器的日志文件中没有归并的记录,从而减少了恢复的时间。Compaction完成之后,该服务器就停止为该片提供服务。在完全卸载片之前,片服务器还会再做一次(通常会很快)Minor Compaction,以消除前面在一次压缩过程中又产生的未归并的记录。第二次Minor Compaction完成以后,片就可以被装载到新的片服务器上了,并且不需要从日志中进行恢复。

l  利用不变性

         除了SSTable缓存之外的其它部分产生的SSTable都是不变的,们可以利用这一点对Bigtable系统进行简化。例如,当从SSTable读取数据的时候,我们不必对文件系统访问操作进行同步。这样一来,就可以非常高效的实现对行的并行操作。memtable是唯一一个能被读和写操作同时访问的可变数据结构。为了减少在读操作时的竞争,我们对内存表采用COW(Copy-on-write)机制,这样就允许读写操作并行执行。

         因为SSTable是不变的,因此,我们可以把永久删除被标记为“删除”的数据的问题,转换成对废弃的SSTable进行垃圾收集的问题了。每个片的SSTable都在METADATA表中注册了。Master服务器采用“标记-删除”的垃圾回收方式删除SSTable集合中废弃的SSTable,METADATA表则保存了root SSTable的集合。

         最后,SSTable的不变性使得分割片的操作非常快捷。我们不必为每个分割出来的片建立新的SSTable集合,而是共享原来的片的SSTable集合。

7.     性能评估

         为了测试Bigtable的性能和可扩展性,我们建立了一个包括N台片服务器的Bigtable集群,这里N是可变的。每台片服务器配置了1GB的内存,数据写入到一个包括1786台机器、每台机器有2个400G的IDE硬盘的GFS集群上。我们使用N台客户机生成工作负载测试Bigtable。(我们使用和片服务器相同数目的客户机以确保客户机不会成为瓶颈。)每台客户机配置2GZ双核Opteron处理器,配置了足以容纳所有进程工作数据集的物理内存,以及一张Gigabit的以太网卡。这些机器都连入一个两层的、树状的交换网络里,在根节点上的带宽加起来有大约100-200Gbps。所有的机器采用相同的设备,因此,任何两台机器间网络来回一次的时间都小于1ms。

         片服务器、Master服务器、测试机、以及GFS服务器都运行在同一组机器上。每台机器都运行一个GFS的服务器。其它的机器要么运行片服务器,要么运行客户程序,要么运行在测试过程中,使用这组机器的其它的任务启动的进程。

         R是测试过程中,Bigtable包含的不同的列关键字的数量。我们精心选择R的值,保证每次基准测试对每台片服务器读写的数据量都在1GB左右。

         在序列写的基准测试中,我们使用的列键的范围是0到R-1。这个范围又被划分为10N个大小相同的区间。核心调度程序把这些区间分配给N个客户端,分配方式是:只要客户程序处理完上一个区间的数据,调度程序就把后续的、尚未处理的区间分配给它。这种动态分配的方式有助于减少客户机上同时运行的其它进程对性能的影响。我们在每个列键下写入一个单独的字符串。每个字符串都是随机生成的、因此也没有被压缩。另外,不同列键下的字符串也是不同的,因此也就不存在跨行的压缩。随机写入基准测试采用类似的方法,除了行键在写入前先做Hash,Hash采用按R取模的方式,这样就保证了在整个基准测试持续的时间内,写入的工作负载均匀的分布在列存储空间内。 

         序列读的基准测试生成列键的方式与序列写相同,不同于序列写在列键下写入字符串的是,序列读是读取列键下的字符串(这些字符串由之前序列写基准测试程序写入)。同样的,随机读的基准测试和随机写是类似的。

         扫描基准测试和序列读类似,但是使用的是Bigtable提供的从一个列范围内扫描所有的值的API。由于一次RPC调用就从一个片服务器取回了大量的值,因此,使用扫描方式的基准测试程序可以减少RPC调用的次数。

         随机读(内存)基准测试和随机读类似,除了包含基准测试数据的局部性群组被设置为“in-memory”,因此,读操作直接从Tablet服务器的内存中读取数据,不需要从GFS读取数据。针对这个测试,我们把每台Tablet服务器存储的数据从1GB减少到100MB,这样就可以把数据全部加载到片服务器的内存中了。

 

         表1和图7中两个视图显示了当我们读和写1000字节值到Bigtable中时,我们的基准测试的性能。表格显示了每个片服务器每秒钟进行的操作的次数;图中的曲线显示了每秒种所有的片服务器上操作次数的总和。

l  单个片服务器性能

         我们首先分析下单个片服务器的性能。随机读的性能比其它操作慢一个数量级或以上。每个随机读操作都要通过网络从GFS传输64KB的SSTable到片服务器,而我们只使用其中大小是1000 字节的一个值。一个片服务器每秒大约执行1200次读操作,也就是每秒大约从GFS读取75MB的数据。这个传输带宽足以占满片服务器的CPU时间,因为其中包括了网络协议栈的消耗、SSTable解析、以及Bigtable代码执行;这个带宽也足以占满我们系统中网络的链接带宽。大多数采用这种访问模式Bigtable应用程序会减小Block的大小,通常会减到8KB。

         内存中的随机读操作速度快很多,原因是,所有1000 字节的读操作都是从Tablet服务器的本地内存中读取数据,不需要从GFS读取64KB的Block。

         序列读的性能好于随机读,因为每取出64KB的SSTable的Block后,这些数据会缓存到Block缓存中,后续的64次读操作直接从缓存读取数据。

         扫描的性能更高,这是由于客户程序每一次RPC调用都会返回大量的value的数据,所以,RPC调用的消耗基本抵消了。

         写性能比读性能好,原因是每个片服务器直接把写入操作的内容追加到一个Commit日志文件的尾部,并且采用批量提交的方式,通过把数据以流的方式写入到GFS来提高性能。随机写和序列写在性能上没有太大的差异,这两种方式的写操作实际上都是把操作内容记录到同一个片服务器的Commit日志文件中。

l  扩大规模

         随着我们将系统中的片服务器从1台增加到500台,系统的整体吞吐量有了梦幻般的增长,增长的倍率超过了100。比如,随着片服务器的数量增加了500倍,内存中的随机读操作的性能增加了300倍。之所以会有这样的性能提升,主要是因为这个基准测试的瓶颈是单台片服务器的CPU。

         尽管如此,性能的提升还不是线性的。在大多数的基准测试中我们看到,当片服务器的数量从1台增加到50台时,每台服务器的吞吐量会有一个明显的下降。这是由于多台服务器间的负载不均衡造成的,大多数情况下是由于其它的程序抢占了CPU。我们负载均衡的算法会尽量避免这种不均衡,但是基于两个主要原因,这个算法并不能完美的工作:一个是尽量减少Tablet的移动导致重新负载均衡能力受限(如果片被移动了,那么在短时间内 — 一般是1秒内,这个Tablet是不可用的),另一个是我们的基准测试程序产生的负载会有波动。

         随机读基准测试的测试结果显示,随机读的性能随片服务器数量增加的提升幅度最小(整体吞吐量只提升了100倍,而服务器的数量却增加了500倍)。这是因为每个1000字节的读操作都会导致一个64KB大的Block在网络上传输。这样的网络传输量消耗了我们网络中各种共享的1GB的链路,结果导致随着我们增加服务器的数量,每台服务器上的吞吐量急剧下降。

8.     结论

         我们已经描述了Bigtable这个Google用来存储结构化数据的分布式系统。Bigtable集群从2005年就已经开始投入了实际使用,在此之前,我们花费了大约7年时间用于设计和实现。在2006年8月,超过60个项目在使用Bigtable。我们的用户喜欢Bigtable为他们带来的高性能和高度可扩展性以及他们可以在需求随着时间变化时通过简单的增加更多的机器来伸缩集群的容纳能力。

         给定Bigtable非同寻常的接口,一个有趣的问题是我们的用户如何困难地适应了去使用它。新的用户往往不确定如何最优的去使用Bigtable接口,尤其是当他们已经适应了使用支持一般通用事务的关系型数据库。然而,许多Google产品成功运用了Bigtable的事实证明了我们的设计在实际中能够工作得很好。

         我们正在设计一些额外的Bigtable特性,例如对二级索引的支持,以及用于构建跨数据中心Bigtable副本的基础架构设计。我们也已经开始讲Bigtable部署为产品组的一种服务,因而单个小组不需要维护他们自己的集群。随着服务集群伸缩,我们需要在Bigtable内部处理更多的资源共享问题。

         最后,我们发现在Google构建我们自己的存储解决方案具有重大的优势。我们从自己为Bigtable设计的数据模型中获取了实质性的灵活性。除此之外,我们对Bigtable实现的控制,以及Bigtable所依赖的其它其它Google基础架构,意味着我们可以在瓶颈和低效出现时有效移除它们。

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值