论文:http://www.read.seas.harvard.edu/~kohler/class/cs239-w08/chang06bigtable.pdf
翻译版:https://blog.csdn.net/skiwnc/article/details/83926444
提纲版:https://www.jianshu.com/p/68d4a489a5f4
摘要
Bigtable是一个分布式的结构化数据存储系统,它被设计用来处理海量数据:通常是分布在数千台普通服务器上的PB级的数据。Google的很多项目使用Bigtable存储数据,包括Web索引、Google Earth、Google Finance。这些应用对Bigtable提出的要求差异非常大,无论是在数据量上(从URL到网页到卫星图像)还是在响应速度上(从后端的批量处理到实时数据服务)。尽管应用需求差异很大,但是,针对Google的这些产品,Bigtable还是成功的提供了一个灵活的、高性能的解决方案。本论文描述了Bigtable提供的简单的数据模型,利用这个模型,用户可以动态的控制数据的分布和格式;我们还将描述Bigtable的设计和实现。
1 介绍
在过去两年半时间里,我们设计、实现并部署了一个分布式的结构化数据存储系统:Bigtable。Bigtable的设计目的是可靠的处理PB级别的数据,并且能够部署到上千台机器上。Bigtable已经实现了下面的几个目标:适用性广泛、可扩展、高性能和高可用性。Bigtable已经在超过60个Google的产品和项目上得到了应用,包括Google Analytics、Google Finance、Orkut、PersonalizedSearch、Writely和Google Earth。这些产品对Bigtable提出了迥异的需求,有的需要高吞吐量的批处理,有的则需要及时响应,快速返回数据给最终用户。它们使用的Bigtable集群的配置也有很大的差异,有的集群只有几台服务器,而有的则需要上千台服务器、存储几百TB的数据。
在很多方面,Bigtable和数据库很类似:它使用了很多数据库的实现策略。并行数据库【14】和内存数据库【13】已经具备可扩展性和高性能,但是Bigtable提供了一个和这些系统完全不同的接口。Bigtable不支持完整的关系数据模型;与之相反,Bigtable为客户提供了简单的数据模型,利用这个模型,客户可以动态控制数据的分布和格式(alex注:也就是对BigTable而言,数据是没有格式的,用数据库领域的术语说,就是数据没有Schema,用户自己去定义Schema),用户也可以自己推测(alex注:reason about)底层存储数据的位置相关性。数据的下标是行和列的名字,名字可以是任意的字符串。Bigtable将存储的数据都视为字符串,但是Bigtable本身不去解析这些字符串,客户程序通常会在把各种结构化或者半结构化的数据串行化到这些字符串里。通过仔细选择数据的模式,客户可以控制数据的位置相关性。最后,可以通过BigTable的模式参数来控制数据是存放在内存中、还是硬盘上。
第二节描述关于数据模型更多细节方面的东西;第三节概要介绍了客户端API;第四节简要介绍了BigTable底层使用的Google的基础框架;第五节描述了BigTable实现的关键部分;第6节描述了我们为了提高BigTable的性能采用的一些精细的调优方法;第7节提供了BigTable的性能数据;第8节讲述了几个Google内部使用BigTable的例子;第9节是我们在设计和后期支持过程中得到一些经验和教训;最后,在第10节列出我们的相关研究工作,第11节是我们的结论。
2 数据模型
Bigtable是一个稀疏的、分布式的、持久化存储的多维度排序Map。Map的索引是行关键字、列关键字以及时间戳;Map中的每个value都是一个未经解析的byte数组。(row:string,column:string,time:int64)->string
我们在仔细分析了一个类似Bigtable的系统的种种潜在用途之后,决定使用这个数据模型。我们先举个具体的例子,这个例子促使我们做了很多设计决策;假设我们想要存储海量的网页及相关信息,这些数据可以用于很多不同的项目,我们姑且称这个特殊的表为Webtable。在Webtable里,我们使用URL作为行关键字,使用网页的某些属性作为列名,网页的内容存在“contents:”列中,并用获取该网页的时间戳作为标识(alex注:即按照获取时间不同,存储了多个版本的网页数据),如下图所示。
一个存储Web网页的例子的表的片断。行名是一个反向URL。contents列族存放的是网页的内容,anchor列族存放引用该网页的anchor。CNN的主页被Sports Illustrater和MY-look的主页引用,因此该行包含了名为“anchor:cnnsi.com”和 “anchhor:my.look.ca”的列。每个锚链接只有一个版本;而contents列则有三个版本,分别由时间戳t3,t5,和t6标识。
Rows
表中的row keys可以是任意的字符串(目前支持最大64KB的字符串,但是对大多数用户,10-100个字节就足够了)。对同一个row key的读或者写操作都是原子的(不管读或者写这一行里多少个不同列),这个设计决策能够使用户很容易的理解程序在对同一个行进行并发更新操作时的行为。
Bigtable通过row key的字典顺序来组织数据。表中的每个行都可以动态分区。每个分区叫做一个”Tablet”,Tablet是数据分布和负载均衡调整的最小单位。当操作只读取表中很少几行的数据时效率很高,通常只需要很少几次机器间的通信即可完成。用户可以通过选择合适的row key,在数据访问时有效利用数据的位置相关性,从而更好的利用这个特性。举例来说,在Webtable里,通过反转URL中主机名的方式,可以把同一个域名下的网页聚集起来组织成连续的行。比如,我们可以把maps.google.com/index.html的数据存放在key:com.google.maps/index.html下。把相同的域中的网页存储在连续的区域可以让基于主机和域名的分析更高效。
Column Families
Column keys组成的集合叫做“Column Familes“,Column Familes是访问控制的基本单位。存放在同一Column Family(CF)下的所有数据通常都属于同一个类型(可以把同一个CF下的数据压缩在一起)。Column Family在使用之前必须先创建,然后才能在CF中任何的Column key下存放数据;CF创建后,其中的任何一个Column key下都可以存放数据。一张表中的CF不能太多(最多几百个),并且CF在运行期间很少改变。但是一张表可以有无限多个Columns。
Column key的命名语法如下:family:qualifier。family的名字必须是可打印的字符串,而qualifier的名字可以是任意的字符串。比如,Webtable有个CF:language,language CF用来存放撰写网页的语言。我们在language CF中只使用一个column key,用来存放每个网页的语言标识ID。Webtable中另一个有用的CF是anchor;这个CF的每一个column key代表一个anchor,如图一所示。Anchor CF的qualifier是引用该网页的站点名;Anchor CF每列的数据项存放的是链接文本。
BigTable以CF为粒度来控制访问以及磁盘和内存的使用统计。在我们的Webtable的例子中,上述的控制权限能帮助我们管理不同类型的应用:一些应用可以写入数据、一些应用可以读数据并创建继承的CF、而一些应用则只允许浏览数据(甚至可能因为隐私的原因不能浏览所有数据)。
Timestamps
在Bigtable中,表的每一个cell都可以包含同一份数据的不同版本;不同版本的数据通过时间戳来索引。Bigtable时间戳的类型是64位整型。Bigtable可以给时间戳赋值,用来表示精确到毫秒的“实时”时间;用户程序也可以给时间戳赋值。如果应用程序需要避免数据版本冲突,那么它必须自己生成具有唯一性的时间戳。数据项中,不同版本的数据按照时间戳倒序排序,即最新的数据排在最前面。
为了减轻多个版本数据的管理负担,每一个CF有两个参数,Bigtable通过这两个参数可以对废弃版本的数据自动进行垃圾收集。用户可以指定只保存最后n个版本的数据,或者只保存“足够新”的版本的数据(如只保存最近7天写入的数据)。
在Webtable的举例里,contents:列存储的时间戳信息是网络爬虫抓取一个页面的时间。上面提及的垃圾收集机制可以让我们只保留最近三个版本的网页数据。
3 API
Bigtable提供了建立和删除表以及CF的API函数。Bigtable还提供了修改集群、表和CF的元数据的API,比如修改访问权限。
客户程序可以对Bigtable进行如下的操作:写入或者删除Bigtable中的值、从每个行中查找值、或者遍历表中的一个数据子集。图2中的C++代码使用RowMutation进行了一系列的更新操作。(为了保持示例代码的简洁,我们忽略了一些细节相关代码)。调用Apply函数对Webtable进行了一个原子修改操作:它为www.cnn.com增加了一个anchor,同时删除了另外一个anchor。
图3中的C++代码使用Scanner抽象遍历一个行内的所有anchor。Client可以遍历多个CF,也可以对扫描输出的行、列和时间戳进行限制。例如,我们可以限制上面的扫描,让它只输出那些匹配正则表达式*.cnn.com的anchor,或者那些时间戳在当前时间前10天的anchor。
Bigtable还支持一些其它的特性,利用这些特性,用户可以对数据进行更复杂的处理。
首先,Bigtable支持单行上的事务处理,利用这个功能,用户可以对存储在一个row key下的数据进行原子性的read-modify-write操作。Bigtable提供了一个允许用户跨行(cross row keys)批量写入数据的接口,但是,Bigtable目前还不支持通用的跨行事务处理。
其次,Bigtable允许把cells用做整数计数器。
最后,Bigtable允许用户在服务器的地址空间内执行脚本程序。脚本程序使用Google开发的Sawzall【28】数据处理语言。虽然目前我们基于的Sawzall语言的API函数还不允许客户的脚本程序写入数据到Bigtable,但是它允许多种形式的数据转换、基于任意表达式的数据过滤、以及使用多种操作符的进行数据汇总。
Bigtable可以和MapReduce【12】一起使用,MapReduce是Google开发的大规模并行计算框架。我们已经开发了一些Wrapper类,通过使用这些Wrapper类,Bigtable可以作为MapReduce框架的输入和输出。
4 Building Blocks
Bigtable是建立在其它的几个Google基础构件上的。BigTable使用GFS存储日志文件和数据文件。BigTable集群通常运行在一个共享的机器池中,池中的机器还会运行其它的各种各样的分布式应用程序,BigTable的进程经常要和其它应用的进程共享机器。BigTable依赖集群管理系统来调度任务、管理共享的机器上的资源、处理机器的故障、以及监视机器的状态。
BigTable内部存储数据的文件是Google SSTable格式的。SSTable是一个持久化的、排序的、不可更改的Map结构,而Map是一个key-value映射的数据结构,key和value的值都是任意的Byte串。可以对SSTable进行如下的操作:查询与一个key值相关的value,或者遍历某个key值范围内的所有的key-value对。从内部看,SSTable是一系列的blocks(通常每个块的大小是64KB,这个大小是可以配置的)。SSTable使用block index(通常存储在SSTable的最后)来定位blocks;在打开SSTable的时候,block index被加载到内存。每次查找都可以通过一次磁盘搜索完成:首先使用二分查找法在内存中的block index里找到block的位置,然后再从硬盘读取相应的block。也可以选择把整个SSTable都放在内存中,这样就不必访问硬盘了。
BigTable还依赖一个高可用的、序列化的分布式锁服务组件,叫做Chubby【8】。一个Chubby服务包括了5个活动的副本,其中的一个副本被选为Master,并且处理请求。只有在大多数副本都是正常运行的,并且彼此之间能够互相通信的情况下,Chubby服务才是可用的。当有副本失效的时候,Chubby使用Paxos算法【9,23】来保证副本的一致性。Chubby提供了一个名字空间,里面包括了目录和小文件。每个目录或者文件可以当成一个锁,读写文件的操作都是原子的。Chubby客户程序库提供对Chubby文件的一致性缓存。每个Chubby客户程序都维护一个与Chubby服务的会话。如果客户程序不能在租约到期的时间内重新签订会话的租约,这个会话就过期失效了。当一个会话失效时,它拥有的锁和打开的文件句柄都失效了。Chubby客户程序可以在文件和目录上注册回调函数,当文件或目录改变、或者会话过期时,回调函数会通知客户程序。
Bigtable使用Chubby完成以下的几个任务:
确保在任何给定的时间内最多只有一个活动的Master副本;
存储BigTable数据的bootstrap location(5.1节);
查找Tablet服务器,以及在Tablet服务器失效时进行善后(5.2节);
存储BigTable的schema信息(每张表的CF信息);以及存储访问控制列表。
如果Chubby长时间无法访问,BigTable就会无法工作。最近我们在使用11个Chubby服务实例的14个BigTable集群上测量了这个影响。由于Chubby不可用而导致BigTable中的部分数据不能访问的平均比率是0.0047%(Chubby不能访问的原因可能是Chubby本身失效或者网络问题)。单个集群里,受Chubby失效影响最大的百分比是0.0326%。
5 Implementation
Bigtable包括了三个主要的组件:链接到Client中的library、一个Master服务器和多个Tablet服务器。当系统工作负载变化时,BigTable可以动态的向集群中添加或者删除Tablet服务器。
Master服务器主要负责以下工作:为Tablet服务器分配Tablets、检测新加入的或者过期失效的Table服务器、对Tablet服务器进行负载均衡、以及对保存在GFS上的文件进行垃圾收集。除此之外,它还处理schema的相关修改操作,例如建表和CF。
每个Tablet服务器都管理一组Tablet(通常每个服务器有大约数十个至上千个Tablet)。每个Tablet服务器负责处理它所加载的Tablet的读写操作,以及在Tablets过大时,对其进行分割。
和很多Single-Master类型的分布式存储系统类似,Client在读写数据时都不经过Master服务器:Client直接和Tablet服务器通信进行读写操作。由于BigTable的Client不必通过Master服务器来获取Tablet的位置信息,因此,大多数Client甚至完全不需要和Master服务器通信。在实际应用中,Master服务器的负载是很轻的。
一个BigTable集群存储了很多表,每个表包含了很多个Tablet,而每个Tablet包含了某个范围内的行的所有相关数据。初始状态下,一个表只有一个Tablet。随着表中数据的增长,它被自动分割成多个Tablet,缺省情况下,每个Tablet的尺寸大约是100MB到200MB。
5.1 Tablet Location
我们使用一个三层的、类似B+树的结构来存储Tablet的位置信息(如图4)。
第一层是一个存储在Chubby中的文件,它包含了Root Tablet的位置信息。Root Tablet(是METADATA表的第一个Tablet)存放了METADATA表其他所有的Tablet的位置信息,而METADATA表的其他每个Tablet都存放了User Tables的Tablets位置信息。Root Tablet永远不会被分割,这就保证了Tablet的位置信息存储结构不会超过三层。
在METADATA表里面,每个Tablet的位置信息都存放在一个row中,而这个row key是由Tablet所在的表的标识符和Tablet的最后一行编码而成的(value应该就是Location)。METADATA的每一行都存储了大约1KB的内存数据。在一个大小适中的、容量限制为128MB的METADATA Tablet中,采用这种三层结构的存储模式,可以标识2^34个Tablet的地址(如果每个Tablet存储128MB数据,那么一共可以存储2^61字节数据)。
Client Library会缓存Tablet的位置信息。如果Client没有缓存某个Tablet的地址信息,或者发现它缓存的地址信息不正确,Client就在树状的存储结构中递归的查询Tablet位置信息;
1.如果客户端缓存是空的,那么寻址算法需要通过三次网络来回通信寻址,这其中包括了一次Chubby读操作;
2.如果Client缓存的地址信息过期了,那么寻址算法可能需要最多6次网络来回通信才能更新数据,因为只有在向缓存的地址中发送请求,但没有查到数据的时候,才能发现数据过期了(alex注:其中的三次通信发现缓存过期,另外三次更新缓存数据)(假设METADATA的Tablet没有被频繁的移动)。尽管Tablet的地址信息是存放在内存里的,对它的操作不必访问GFS文件系统,但是,通常会预取Tablet Location来进一步减少访问Chubby:每次需要从METADATA表中读取一个Tablet的元数据的时候,都会多读取几个其他Tablet的元数据。
在METADATA表中还存储了次级信息(secondary information),包括每个Tablet的事件日志(例如,什么时候一个服务器开始为该Tablet提供服务)。这些信息有助于排查错误和性能分析。
5.2 Tablet的分布
在任何一个时刻,一个Tablet只能分配给一个Tablet服务器。Master服务器记录了当前有哪些活跃的Tablet服务器、哪些Tablet分配给了哪些Tablet服务器、哪些Tablet还没有被分配。当一个Tablet还没有被分配、并且刚好有一个Tablet服务器有足够的空闲空间装载该Tablet时,Master服务器会给这个Tablet服务器发送一个load请求,把Tablet分配给这个服务器。
BigTable使用Chubby跟踪记录Tablet服务器的状态。当一个Tablet服务器启动时,它在Chubby的一个指定目录下建立一个有唯一性名字的文件,并且获取该文件的独占锁。Master服务器实时监控着这个目录(服务器目录),因此Master服务器能够知道有新的Tablet服务器加入了。如果Tablet服务器丢失了Chubby上的独占锁 (比如由于网络断开导致Tablet服务器和Chubby的会话丢失),它就停止对它上面的Tablets提供服务。(Chubby提供了一种高效的机制,利用这种机制,Tablet服务器能够在不增加网络负担的情况下知道它是否还持有锁)。只要文件还存在,Tablet服务器就会试图重新获得对该文件的独占锁;如果文件不存在了,那么Tablet服务器就不能再提供服务了,它会自行退出(alex注:so it kills itself)。当Tablet服务器terminates时(比如,集群管理系统将运行该TabletServer的主机从集群中移除),它会尝试释放它持有的文件锁,这样一来,Master服务器就能尽快把此Server上的Tablets分配到其它的Tablet服务器。
Master服务器负责检查一个Tablet服务器是否已经不再为它的Tablets提供服务了,并且要尽快重新分配它加载的Tablets。Master通过轮询各个TabletServer对其文件独占锁的持有状态,来检测TabletServer是否正在提供服务。如果一个Tablet服务器报告它丢失了文件锁,或者Master服务器最近几次尝试和它通信都没有得到响应,Master服务器就会尝试获取该Tablet服务器文件的独占锁;如果Master服务器成功获取了独占锁,那么就说明Chubby是正常运行的,而Tablet服务器要么是宕机了、要么是不能和Chubby通信了,因此,Master服务器就删除该Tablet服务器在Chubby上的服务器文件以确保它不再为Tablets提供服务。一旦Tablet服务器在Chubby上的服务器文件被删除了,Master服务器就把之前分配给它的所有的Tablets放入未分配的Tablet集合中。为了确保Bigtable集群在Master服务器和Chubby之间网络出现故障的时候仍然可以使用,Master服务器在它的Chubby会话过期后主动退出(kills itself)。但Master服务器挂了,并不会改变现有Tablets在Tablet服务器上的分布,所以没事。
当集群管理系统启动了一个Master服务器之后,Master服务器首先要了解当前Tablet的分布情况(Assignment),之后才能够修改它。因此,Master服务器在启动的时候执行以下步骤:
(1)Master服务器从Chubby获取一个唯一的Master锁,用来阻止其它Master实例的启动;
(2)Master服务器扫描Chubby的服务器文件锁目录,获取当前正在运行的服务器列表;
(3)Master服务器和所有正在运行的TabletServer通信,获取每个TabletServer上的Tablets分布信息;
(4)Master服务器扫描METADATA表获取所有的Tablets的集合。在扫描的过程中,当Master服务器发现了一个还没有分配的Tablet(METADATA表中存放的是所有用户Tablets的location,row key是表名+Tablet名,value是location,那就是说这个Tablet的value是空的,代表它没有被分配?),Master服务器就将这个Tablet加入未分配的Tablet集合等待合适的时机分配。
可能会遇到一种复杂的情况:在METADATA表的所有Tablets还没有被分配之前,是不能够扫描它的。因此,在开始扫描之前(步骤4),如果在第三步的扫描过程中发现Root Tablet还没有分配,Master服务器就把Root Tablet加入到未分配的Tablet集合。这个附加操作确保了Root Tablet会被分配。由于Root Tablet存放了METADATA所有的Tablet的名字(以及Location),因此Master服务器扫描完Root Tablet以后,就得到了METADATA表所有的Tablet的名字了。
现有的Tablet集合只有在以下情况才会被改变:create table,delete table,merge,split。除了split外的其他3个事件都是由Master触发的,因此Master服务器可以跟踪记录其他3个事件。Tablet Split事件需要特殊处理,因为它是由Tablet服务器触发。在split操作完成之后,Tablet服务器通过在METADATA表中记录新的Tablet的信息(同时修改原Tablet的key range)来提交这个操作。当split操作提交之后,Tablet服务器会通知Master服务器。如果没有通知到(可能两个服务器中有一个宕机了)也没关系,因为当Master服务器在要求Tablet服务器load parent Tablet的时候会发现这个新的Tablet的:Tablet服务器对比METADATA表中Tablet的信息(row key范围更小,因为split后只剩一部分)和Master服务器要求其装载的Tablet的信息(row key范围更大)不一致,Tablet服务器会重新向Master服务器发送Split commit信息。
5.3 Tablet服务
如图5所示,Tablet数据的持久化信息保存在GFS上。Update操作提交在commit log中。在这些Update操作中,最近提交的那些存放在内存中的一个sorted buffer中,称为memtable;较老的的Update存放在一系列SSTable中。为了恢复一个Tablet,Tablet服务器首先从METADATA表中读取它的元数据。Tablet的元数据包含了组成这个Tablet的SSTable的列表,以及一系列的Redo Points(a set of redo points),这些Redo Points指向commit logs。Tablet服务器把SSTable的索引读进内存,之后通过apply commit logs在Redo Point这个点之后提交的Update操作来重建memtable。(就是说redolog一直在写,过一段时间就把memtable dump到SSTable中,并且切一个新的memtable出来,此时就会给redolog打一个Redo Point,继续写入的过程中,写入到新的memtable;在load Tablet的过程中,只需要从Redo Point这个点开始replay commit log,用Update操作填充memtable即可,SSTable加载到内存,Redo Point前面的commit log record就不需要replay了,因为已经dump到SSTable了)
当对Tablet服务器进行写操作时,Tablet服务器首先要检查这个操作格式是否正确、操作发起者是否有执行这个操作的权限。权限验证的方法是通过从一个Chubby文件里读取出来的具有写权限的操作者列表来进行验证(这个文件几乎一定会命中Chubby Client cache)。成功的修改操作会记录在commit log里。如果有许多小value的修改操作,采用批量提交(group commit)可以提高吞吐量。当一个写操作提交后,写的内容插入到memtable里面。
当对Tablet服务器进行读操作时,Tablet服务器会作类似的完整性和权限检查。一个有效的读操作在一个由许多SSTable和memtable合并的视图里执行。由于SSTable和memtable是按字典排序的数据结构,因此可以高效生成merged view。
Tablet的merge和split操作不影响Table的读写操作。
5.4 Compactions
随着写操作的执行,memtable的大小不断增加。当memtable的尺寸到达一个门限值时,这个memtable就会被冻结,然后创建一个新的memtable来写;而这个被冻结住的memtable会被转换成SSTable,然后写入GFS,这个操作被称为Minor Compaction。Minor Compaction过程有两个目的:减少Tablet服务器使用的内存,以及在服务器宕机后重启的过程中,减少必须从commit log里回放的数据量。在Compaction过程中,Tablet的读写操作不受影响。
每一次Minor Compaction都会创建一个新的SSTable。如果一直进行Minor Compaction,那么一个读操作可能需要merge来自多个SSTable的Updates;为了解决这个问题,我们定期在后台执行Merging Compaction来合并SSTable。Merging Compaction过程读取一些SSTable和memtable的内容,合并成一个新的SSTable。只要Merging Compaction过程完成了,输入的这些SSTable和memtable就可以删除了。
合并所有的SSTable并生成一个新的SSTable的Merging Compaction称为Major Compaction。由非Major Compaction产生的SSTable可能含有已经被删除的条目(删除都是以append的方式实现),而这些删除条目在旧的SSTable中是存在的。而Major Compaction过程生成的SSTable不包含已经删除的信息或数据(也就是说Major Compaction会把已被删除的数据给compact掉)。Bigtable循环扫描所有的Tablet,并且定期对它们执行Major Compaction。Major Compaction机制允许Bigtable回收已经删除的数据占有的资源,并且确保BigTable能及时清除已经被删除的数据,这对存放敏感数据的服务是非常重要的。
6 优化
上一章我们描述了Bigtable的实现(Implementation),我们还需要很多优化工作才能使Bigtable到达用户要求的高性能、高可用性和高可靠性。
Locality Groups
Clients可以将多个CF组合成一个Locality Group。对Tablet中的每个LG都会生成一个单独的SSTable(每个LG对应一个或多个SSTable!)。将通常不会一起访问的CF分割成不同的LG可以提高读性能。例如,在Webtable表中,网页的元数据(比如Language和Checksum)可以在一个LG中,网页的content可以在另外一个LG:当一个apps要读取网页的元数据的时候,它没有必要去读取content。
此外,可以以LG为单位设定一些有用的调试参数。比如,可以把一个LG设定为全部存储在内存中。Tablet服务器依照惰性加载的策略将设定为放入内存的LG的SSTable装载进内存。加载完成之后,访问属于该LG的CF的时候就不必读取硬盘了。这个特性对于需要频繁访问的小块数据特别有用:在Bigtable内部,我们利用这个特性提高METADATA表中Location CF的访问速度。(METADATA表中有很多的内容,5.1描述的Tablet位置其实只是其中一个CF,因为这个CF经常被访问,因此可以把这个LG设定为in-memory,这样访问起来就快)
压缩
Clients可以控制一个LG的SSTable是否需要压缩;如果需要压缩,那么以什么格式来压缩。每个SSTable的Block(块的大小由LG的参数指定)都使用用户指定的压缩格式来压缩。虽然分块压缩浪费了少量空间(alex注:相比于对整个SSTable进行压缩,分块压缩压缩率较低),但是,我们在只读取SSTable的一小部分数据的时候就不必解压整个文件了。很多客户程序使用了“two-pass”可定制的压缩方式。第一遍采用Bentleyand McIlroy’s方式[6],这种方式在一个很大的扫描窗口里对常见的长字符串进行压缩;第二遍是采用快速压缩算法,即在一个16KB的小扫描窗口中寻找重复数据。两个压缩的算法都很快,在现在的机器上,压缩的速率达到100-200MB/s,解压的速率达到400-1000MB/s。
虽然我们在选择压缩算法的时候重点考虑的是速度而不是压缩的空间,但是这种两遍的压缩方式在空间压缩率上的表现也是令人惊叹。比如,在Webtable的例子里,我们使用这种压缩方式来存储网页内容。在一次测试中,我们在一个压缩的CF中存储了大量的网页。针对实验的目的,我们没有存储每个文档所有版本的数据,我们仅仅存储了一个版本的数据。该模式的空间压缩比达到了10:1。这比传统的Gzip在压缩HTML页面时3:1或者4:1的空间压缩比好的多;“两遍”的压缩模式如此高效的原因是由于Webtable的行的存放方式:从同一个主机获取的页面都存在临近的地方。利用这个特性,Bentley-McIlroy算法可以从来自同一个主机的页面里找到大量的重复内容。不仅仅是Webtable,其它的很多应用程序也通过选择合适的行名来将相似的数据聚簇在一起,以获取较高的压缩率。当我们在Bigtable中存储同一份数据的多个版本的时候,压缩效率会更高。
Caching for read performance
为了提高读操性能,Tablet服务器使用二级缓存的策略。Scan Cache是第一级缓存,主要缓存Tablet服务器通过SSTable接口获取的Key-Value对;Block Cache是二级缓存,缓存的是从GFS读取的SSTable的Blocks。对于经常要重复读取相同数据的apps来说,Scan Cache非常有效;对于经常要读取刚刚读过的数据附近的数据的apps来说,Block Cache更有用(例如,顺序读,或者在一个热点row的一个LG中随机读取不同的列)。
Bloom Filter
如5.3节所述,一个读操作必须merge该Tablet所有的SSTable。如果这些SSTable不在内存中,那么就需要多次访问硬盘。为了减少硬盘访问次数,Client可以对特定LG的SSTable指定Bloom filter。我们可以使用Bloom filter查询一个SSTable中是否包含了特定行和列的数据。因此,对于某些特定apps,我们只用少量的内存存储Bloom filter,就可以使读操作对磁盘访问次数显著减少。当apps访问不存在的行或列时,大多数时候我们都不需要访问硬盘。
Commit log implementation
如果每个Tablet的Commit log都存在一个单独的文件的话,那么就会产生大量的文件,并且这些文件会并行的写入GFS。根据GFS服务器底层文件系统实现的方案,要把这些文件写入不同的磁盘日志文件(physical log files)时,会有大量的磁盘Seek操作。另外,由于group commit中操作的数目一般比较少,因此,如果每个Tablet使用一个单独的日志文件也会影响group commit的效果。为了避免这些问题,我们使每个Tablet服务器使用一个Commit log文件,因此一个磁盘日志文件(physical log file)中混合了对多个Tablet的commit log record。
使用单个日志显著提高了普通操作的性能,但是使Tablet的recovery更加复杂了。当一个Tablet服务器宕机时,它serve的Tablets将会被很多其它的Tablet服务器load:每个Tablet服务器都load原来的服务器上的几个Tablets。当在recover一个Tablet的时候,新的Tablet服务器需要从原来的Tablet服务器写的commit log中提取该Tablet的mutations,并replay。然而,这些Tablets的mutations都混合在同一个commit log中。有一种方法:新的Tablet服务器读取完整的Commit日志文件,然后只apply它需要恢复的Tablet的mutations。但假如有100台Tablet服务器,每台都加载了宕机的Tablet服务器上的一个Tablet,那么,这个日志文件就要被读取100次。
为了避免多次读取commit log,我们首先把log按照关键字(table,row name,log sequence number)排序。排序之后,对同一个Tablet的mutations的log records就连续存放在了一起,因此,我们只要一次磁盘Seek操作、之后顺序读取就可以了。为了并行排序,我们先将日志分割成64MB的segments,之后在不同的Tablet服务器对segment进行并行排序。这个排序工作由Master服务器来协同处理,并且由一个Tablet服务器表明自己需要从Commit log file恢复Tablet时触发。
在向GFS中写Commit log的时候可能会引起系统颠簸,原因是多种多样的(比如,写操作正在进行的时候,一个GFS服务器宕机了;或者连接三个GFS副本所在的服务器的网络拥塞或者过载了)。为了确保在GFS负载高峰时,mutations还能顺利进行,每个Tablet服务器实际上有两个日志写入线程,每个线程都写各自的日志文件,并且在任何时刻,只有一个线程是工作的。如果一个线程的在写入的时候效率很低,Tablet服务器就切换到另外一个线程,mutations就写入到这个线程对应的log file中。每个log entry都有一个sequence number,因此,在恢复的时候,Tablet服务器能够检测出并忽略掉那些由于线程切换而导致的重复的entries。
Speeding up Tablet recovery
当Master服务器将一个Tablet从一个Tablet服务器移到另外一个Tablet服务器时,源Tablet服务器会对这个Tablet做一次Minor Compaction。这个Compaction操作减少了Tablet服务器的日志文件中uncompacted state的记录,从而减少了恢复的时间。Compaction完成之后,该服务器就停止为该Tablet提供服务。在卸载Tablet之前,源Tablet服务器还会再做一次(通常会很快)Minor Compaction,以消除前面在Minor Compaction过程中又产生的mutations记录。第二次Minor Compaction完成以后,Tablet就可以被装载到新的Tablet服务器上了,并且不需要从日志中进行恢复。(因此做一次Minor Compaction会把memtable dump为SSTable,因此memtable就不需要在内存中恢复了,commit log就不需要回放了,因为回放commit log,是为了在内存中恢复memtable)
Exploiting immutability
我们在使用Bigtable时,除了SSTable缓存之外的其它部分产生的SSTable都是不变的,我们可以利用这一点对系统进行简化。例如,当从SSTable读取数据的时候,我们不必对文件系统访问操作进行同步。这样一来,就可以非常高效的实现对行的并行操作。memtable是唯一一个能被读和写操作同时访问的可变数据结构。为了减少读操作时的竞争,memtable采用Copy-on-write机制,这样就允许读写操作并行执行。
因为SSTable是不变的,因此,我们可以把【永久清楚被标记为“删除”的数据】的问题,转换成【对废弃的SSTable进行垃圾收集】的问题了。每个Tablet的SSTable都在METADATA表中注册了。Master服务器采用“标记-删除”的垃圾回收方式删除SSTable集合中废弃的SSTable【25】,METADATA表则保存了Root SSTable的集合????。
最后,SSTable的不变性使得Tablet的split非常快捷。我们不必为每个split出来的Tablet建立新的SSTable集合,而是共享parent Tablet的SSTable集合。
7 性能评估
为了测试Bigtable的性能和可扩展性,建立了一个包括N台Tablet服务器的Bigtable集群,这里N是可变的。每台Tablet服务器配置了1GB的内存,数据写入到一个包括1786台机器、每台机器有2个400GB的IDE硬盘的GFS集群上。使用N台客户机生成压力测试Bigtable。(使用和Tablet服务器相同数目的客户机以确保客户机不会成为瓶颈)每台客户机配置2GHz双核Opteron处理器,配置了足以容纳所有进程工作数据集的物理内存,以及一张Gigabit的以太网卡。这些机器都连入一个两层的、树状的交换网络里,在根节点上的带宽加起来有大约100-200Gbps。所有的机器采用相同的设备,因此,任何两台机器间网络来回一次的时间都小于1ms。
Tablet服务器、Master服务器、Test client、以及GFS服务器都运行在同一组机器上。每台机器都运行一个GFS的服务器。其它的机器要么运行Tablet服务器、要么运行Client、要么运行使用这个池子的其它进程。
R是row keys的数量。我们精心选择R的值,保证每次benchmark对每台Tablet服务器读/写的数据量都在1GB左右。
在顺序写的benchmark中,我们使用的row keys的范围是0到R-1。这个范围又被划分为10N个大小相同的区间。核心调度器把这些区间分配给N个Client,分配方式是:只要Client处理完上一个区间的数据,调度程序就把后续的、尚未处理的区间分配给它。这种动态分配的方式有助于减少Client上同时运行的其它进程对性能的影响。我们在每个row keys下写入一段string。每个string都是随机生成的、没有被压缩的。因此,不同row key下的string也是不同的,因此也就不存在cross-row的压缩。
随机写benchmark采用类似的方法,除了row key在写入前先做Hash,Hash采用按R取模的方式,这样就保证了在整个benchmark持续的时间内,写入的工作负载均匀的分布在所有row。
顺序读benchmark生成row keys的方式与顺序写相同,区别在于,一个是写string,一个是读string。
同样的,随机读的benchmark和随机写是类似的。
scan benchmark和顺序读类似,但是使用的是BigTable提供的、从一个row range内扫描所有的value值的API。由于一次RPC调用就从一个Tablet服务器取回了大量的Value值,因此,使用scan benchmark可以减少RPC调用的次数。
随机读(内存)benchmark和随机读benchmark类似,除了LG被设置为“in-memory”,因此,读操作直接从Tablet服务器的内存中读取数据,不需要从GFS读取数据。针对这个测试,我们把每台Tablet服务器存储的数据从1GB减少到100MB,这样就可以把数据全部加载到Tablet服务器的内存中了。
以下2个图中的数据和曲线是读/写 1000-byte value值时取得的。第一个图显示了每个Tablet服务器每秒钟进行的操作的次数;第二个图显示了每秒种所有的Tablet服务器上操作次数的总和。
Single TabletServer performance
我们首先分析下单个Tablet服务器的性能:
1.随机读的性能比其它操作慢一个数量级或以上 。 每个随机读操作都要通过网络从GFS传输64KB的SSTable block到Tablet服务器,而我们只使用其中1000byte的一个value值。Tablet服务器每秒大约执行1200次读操作,也就是每秒大约从GFS读取75MB的数据。这个传输带宽足以打满Tablet服务器的CPU,因为其中包括了网络协议栈的消耗、SSTable解析、以及BigTable代码执行,这个带宽也足以打满我统中网络的链接带宽。大多数采用这种access pattern的apps通常都会减小SSTable Block的大小,如减到8KB。
2.内存中的随机读操作速度快很多,原因是,所有1000-byte的读操作都是从Tablet服务器的本地内存中读取数据,不需要从GFS读取64KB的Block。
随机和顺序写的性能比随机读要好些,原因是,每个Tablet服务器直接把写入操作的内容追加到一个Commit log的尾部,并且采用group commit,把数据以流的方式写入到GFS来提高性能。随机和顺序写在性能上没有太大的差异,这两种方式的写操作实际上都是把写入操作记录到同一个Tablet服务器的Commit log中。
顺序读的性能好于随机读,因为每取出64KB的SSTable Block后,这些数据会缓存到Block Cache中,后续的64次读操作可以直接从Cache读取数据。
Scan的性能最好,因为Client每一次RPC调用都会返回大量的value的数据,所以,RPC调用的消耗基本抵消了。
Scaling(many TabletServers performance)
将系统中的Tablet服务器从1台增加到500台,系统的整体吞吐量有了明显的增长,增长的倍率超过了100。比如,随着Tablet服务器的数量增加了500倍,内存中的随机读操作的性能增加了300倍。之所以会有这样的性能提升,主要是因为这个benchmark的瓶颈是单台Tablet服务器的CPU(就是说 单台qps上不去的原因是因为cpu,因此当server增多,可以打散请求,单台server的cpu将不再是瓶颈)。
但性能的提升不是线性的。在大多数benchmark中,当Tablet服务器的数量从1台增加到50台时,每台服务器的吞吐量会有一个明显的下降。这是由于多台服务器间的负载不均衡造成的,大多数情况下是由于其它的程序抢占了CPU和网络带宽(这里说的负载不均不是指Server上的Tablet分布不均,是指这台机器上还运行这其他别的进程,有些进程抢了cpu和带宽)。我们负载均衡的算法会尽量避免这种不均衡,但是做负载均衡也不一定能完美解决:1.负载均衡会移动Tablet,那么在短时间内(一般是1秒内)这个Tablet是不可用的;2.benchmark产生的负载会shift around(是不是说benchmark产生的负载不一定稳定,有时这个server的多,有时那个多,因此做负载均衡也fix不了)。
随机读benchmark的测试结果显示,随着Tablet服务器数量增加,随机读的性能的提升幅度最小(整体吞吐量只提升了100倍,而服务器的数量却增加了500倍)。这是因为每个1000-byte的读操作都会导致一个64KB大的SSTable Block在网络传输,占据了大一部分1 Gb link带宽。因此,随着服务器数量的增加,每台服务器上的吞吐量急剧下降。(但是这里是每台机器的网卡为1 Gb吗,那应该不会随着server的增多而有影响啊?是不是这个1Gb不是指网卡,而是指所有的机器都是共享这1Gb的链路)
8 实际应用
截止到2006年8月,Google内部一共有388个非测试用的Bigtable集群运行在各种各样的服务器集群上,大约有24500个Tablet服务器。表1显示了每个集群上Tablet服务器的大致分布情况。这些集群中,许多用于开发,因此会有一段时期比较空闲。一个由14个cluster、8069个Tablet服务器组成的group,整体的吞吐量超过了1200000次/s请求,incoming RPC traffic达到了741MB/s,outgoing RPC traffic大约是16GB/s。
表2展示了一些目前正在使用的Table。一些表存储的是用户相关的数据,另外一些存储的则是用于批处理的数据;这些表在total size、average cell size、从内存中读取的数据的比例、表的Schema的复杂程度上都有很大的差别。
8.1 Google Analytics
Google Analytics是用来帮助Web站点的管理员分析他们网站的traffic patterns的服务。它提供了整体状况的统计数据,比如每天的访问的用户数量、每天每个URL的浏览次数;它还提供了用户使用网站的行为报告,比如根据用户之前访问的某些页面,统计出几成的用户购买了商品。
为了使用这个服务,Web站点的管理员只需要在他们的Web页面中嵌入一小段JavaScript脚本就可以了。这个Javascript程序在页面被访问的时候调用。它记录了各种Google Analytics需要使用的信息,比如用户的标识、获取的网页的相关信息。Google Analytics汇总这些数据,之后提供给Web站点的管理员。
Google Analytics使用两个表:
Raw Click表(大约有200TB数据)的每一行存放了一个用户的会话。row name是一个包含Web站点名字以及用户sessioni创建时间的tuple。这种schema保证了对同一个Web站点的访问session是在BigTable中是连续存放的,并且session按时间顺序存放,这样,这个表可以压缩到原来尺寸的14%。
Summary表(大约有20TB的数据)包含了关于每个Web站点的、各种类型的预定义汇总信息。一个周期性运行的MapReduce任务根据Raw Click表的数据生成Summary表的数据。每个MapReduce工作进程都从Raw Click表中提取最新的session数据。系统的整体吞吐量受限于GFS的吞吐量。这个表的能够压缩到原有尺寸的29%。
8.2 Google Earth
Google通过一组服务为用户提供了高分辨率的地球表面卫星图像,访问的方式可以使通过基于Web的Google Maps访问接口(maps.google.com),也可以通过Google Earth定制的客户端软件访问。这些软件产品允许用户浏览地球表面的图像:用户可以在不同的分辨率下平移、查看和注释这些卫星图像。这个系统使用一个表存储preprocess数据,使用另外一组表存储用户(client)数据。
预处理流水线(Preprocessing pipeline)使用一个Imagery表存储原始图像。在预处理过程中,图像被清除,图像数据合并到最终的serving数据中。这个表包含了大约70TB的数据,所以需要从磁盘读取数据。图像已经被高效压缩过了,因此存储在Bigtable后不需要压缩了。
Imagery表的每一行都代表了一个地理区域。每个row的命名是以【确保毗邻的区域存储在一起】为目标来设计的。Imagery表中有一个CF用来记录每个区域的数据源。这个CF包含了大量的column:每个column对应一个原始图片的数据。由于每个地理区域都是由很少的几张图片构成的,因此这个CF是非常稀疏的。
预处理流水线高度依赖运行在Bigtable上的MapReduce任务传输数据。在运行某些MapReduce任务的时候,整个系统中每台Tablet服务器的数据处理速度是1MB/s。
这个服务系统使用一个表来索引GFS中的数据。这个表相对较小(大约是500GB),但是这个表必须在低延时(low latency)的前提下,针对每个数据中心,每秒处理几万个查询请求。因此,这个表必须在上百个Tablet服务器上存储数据,并且使用in-memory的CF(in-memory其实是设置在LG上的)。
8.3 个性化查询 personalized search
个性化查询(www.google.com/psearch)是一个选择性使用的服务(不是强制的 可以不用);这个服务记录用户的查询和点击,涉及到各种Google的服务,比如Web查询、图像和新闻。用户可以浏览他们查询的历史,重复他们之前的查询和点击;用户也可以根据自己的Google历史使用习惯模式(historical Google usage patterns)来生成个性化查询结果。
个性化查询使用Bigtable存储每个用户的数据。每个用户都有一个唯一的用户id,每个用户id作为row key。所有的用户action都存储在这张表中。有多个CF用来存储各种类型的action(比如,有个CF可能是用来存储所有的Web query的)。每个数据项都被用作Bigtable的时间戳,记录了相应的用户action发生的时间?????。个性化查询使用以Bigtable为存储的MapReduce任务生成user profiles。这些user profiles用来使当前的查询结果个性化。
个性化查询的数据会复制到几个Bigtable的集群上,这样就增强了数据可用性,同时减少了由客户端和Bigtable集群间的“距离”造成的延时。个性化查询的开发团队最初建立了一个基于Bigtable的、“Client侧”的复制机制为所有的replicas提供最终一致性保障。现在的系统则使用了内建的(server侧的)复制子系统。
个性化查询存储系统的设计方案可以允许其它的团队在它们自己的列中加入新的用户数据,因此,很多Google服务使用个性化查询存储系统保存用户级的配置参数和设置。但由于多个团队之间共享用户数据,因此产生了大量的CF。为了更好的支持数据共享,我们加入了一个简单的quota机制限制用户在共享表中使用的空间;quota也为使用个性化查询系统存储用户级信息的产品团体提供了隔离机制。
9 经验教训
在设计、实现、维护和支持Bigtable的过程中,我们得到了很多有用的经验和一些有趣的教训。
一个教训是,很多类型的错误都会导致大型分布式系统受损,这些错误不仅仅是通常的网络分区、或者很多分布式协议中设想的fail-stop failture(alex注:fail-stop failture,指一旦系统fail就stop,不输出任何数据;fail-fast failture,指fail不马上stop,在短时间内return错误信息,然后再stop)。比如,我们遇到过下面这些类型的错误导致的问题:内存故障、网络中断、时钟偏差(clock skew)、机器hang、扩展的和非对称的网络分区(extended and asymmetric network partitions)、我们使用的其它系统的Bug(比如Chubby)、GFS quota overflow、计划内和计划外的硬件维护。在解决这些问题的过程中学到了很多经验,通过修改协议来解决这些问题。比如,在RPC机制中加入了Checksum。在设计系统的部分功能时,不对其它部分功能做任何的假设。比如,不再假设一个特定的Chubby操作只返回某些错误码集合中的一个(可能会返回一些集合外的错误码)。
另外一个教训是,在彻底了解一个新特性会被如何使用之后,再决定是否添加这个新特性。比如,我们开始计划在我们的API中支持通常方式的事务(general-purpose transactions)。但由于我们还不会马上用到这个功能,因此并没有去实现它。现在,Bigtable上已经有了很多的实际应用,我们发现,大多是应用程序都只是需要单行事务(single-row transactions)。有些应用需要分布式事务(distributed transactions),分布式事务大多数情况下用于维护二级索引,因此我们增加了一个特殊的机制去满足这个需求。新的机制在通用性上比分布式事务差很多,但更高效(特别是在更新操作的涉及上百行数据的时候),而且非常符合我们的“跨数据中心”复制方案的优化策略。
还有一个具有实践意义的经验:系统级的监控很有用(比如,监控Bigtable自身以及使用Bigtable的Client processes)(比如,我们扩展了RPC系统,因此,每当触发一次RPC调用,它可以详细记录代表了此次RPC调用的很多重要操作)。监控这个feature帮助我们检测并修正了很多的问题,比如:
1.Tablet数据结构的锁竞争、
2.在mutation提交时,写入GFS非常慢、
3.在METADATA表的Tablet不可用时,无法访问METADATA表等问题。
4.每个Bigtable集群都在Chubby中注册了。这可以帮助我们跟踪所有的集群状态、监控它们的大小、检查集群中运行的软件的版本、监控集群流入数据的流量,以及检查是否有引发集群高延时的潜在因素。
对我们来说,最宝贵的经验是simple designs。考虑到我们目前系统的代码量(大约100000行),以及将来新的代码以各种难以预料的方式加入系统,我们发现简洁的设计和编码给维护和调试带来的巨大好处。比如,Tablet服务器成员协议(membership protocol)。我们第一版的协议很简单:Master服务器周期性的和Tablet服务器签订租约,Tablet服务器在租约过期的时候Kill掉自己的进程。不幸的是,这个协议在遇到网络问题时,可用性大大降低,并且对Master服务器恢复的时间非常敏感(也就是说如果master启动时间过长,这个时候就没有master帮助server续租,这时候Slice没有主,影响写入)。我们多次重新设计这个协议,直到它能够很好的处理上述问题。但是,协议太复杂了,并且依赖了一些Chubby很少被用到的特性。我们发现我们浪费了大量的时间在debug obscure corner cases,有些是Bigtable代码,有些是Chubby代码。最后,我们只好废弃了这个协议,重新制订了一个新的、更简单、只使用广泛使用的Chubby的特性。
10 相关工作
Boxwood【24】项目的有些组件在某些方面和Chubby、GFS以及Bigtable类似,它也提供了诸如分布式协议、锁、分布式Chunk存储以及分布式B-tree存储。虽然Boxwood与Google的某些组件功能类似,但是Boxwood的组件提供的是更底层(lower-level)的服务。Boxwood是为创建高级服务(文件系统、数据库等)提供基础构件(infrastructure),而Bigtable是直接为客户apps数据存储需求提供支持。
现在有不少项目已经攻克了很多难题,实现了在广域网上的分布式数据存储或者高级服务,通常是“Internet规模”的。这其中包括了分布式的Hash表,这项工作由一些类似CAN【29】、Chord【32】、Tapestry【37】和Pastry【30】的项目率先发起。这些系统的主要关注点和Bigtable不同,比如应对:1.各种不同的传输带宽、2.untrusted participants, 3.frequent reconfiguration,4.decentralized control, 5.Byzantine fault tolerance。
对于能提供给app开发者使用的分布式数据存储模型来说,分布式B-Tree或者分布式Hash表提供的Key-value pair方式的模型有很大的局限性。虽然Key-value pair模型是很有用的组件,但是不应该是提供给开发者的唯一组件。我们的模型提供的组件比 simple Key-value pair丰富,它支持稀疏的、半结构化的数据。另外,它也足够简单,可以高效的flat-file representation;它对于用户是透明的,允许使用者(通过LG)调整重要的系统参数。
有些数据库厂商已经开发出了并行的数据库系统,能够存储海量的数据。Oracle的RAC使用共享磁盘存储数据(Bigtable使用GFS),并且有一个分布式的锁管理系统(Bigtable使用Chubby)。IBM的Parallel Edition DB2基于a shared-nothing architecture(和Bigtable类似)。每个DB2的服务器都负责处理一张表的部分row,这张表存储在一个本地的关系型数据库中。这些产品都提供了带有事务的完整关系模型(complete relational model)。
Bigtable的LG使压缩和磁盘读取方面的性能可以与基于列的存储方案的性能相媲美;这些以列而不是行的方式组织数据的方案有:C-Store【1,34】、商业产品Sybase IQ【15,36】、SenSage【31】、KDB+【22】,以及MonetDB/X100【38】的ColumnDM存储层。AT&T的Daytona数据库,在平面文件中提供垂直和水平数据分区、并且提供很好的数据压缩率。LG不支持CPU缓存级别的优化,但Ailamaki系统中支持。
Bigtable采用memtable和SSTable存储对表的更新的方法与Log-Structured Merge Tree【26】存储索引数据更新的方法类似。这两个系统中,排序的数据在写入到磁盘前都先存放在内存中,读取操作必须从内存和磁盘中合并数据,产生最终的结果集。
C-Store和Bigtable有很多相似点:两个系统都采用Shared-nothing架构,都有两种不同的数据结构,一种用于当前的写操作(memtable),另外一种存放“long-lived”的数据(SSTable),并且提供一种机制在两个存储结构间搬运数据。两个系统在API接口函数上有很大的不同:C-Store操作更像关系型数据库,而Bigtable提供了低层次的读写操作接口,并且设计的目标是能够支持每台服务器每秒数千次操作。C-Store是个“读高性能的关系型数据库”,而Bigtable对读和写密集型应用都提供了很好的性能。
Bigtable和所有的Shared-nothing数据库一样,load balancer需要解决负载和内存均衡方面的难题。我们的问题在某种程度上简单一些:
(1)我们不需要考虑同一份数据可能有多个拷贝的问题,同一份数据可能由于视图或索引的原因以不同的形式表现出来;
(2)我们让用户决定哪些数据应该放在内存里、哪些放在磁盘上,而不是由系统动态的判断;
(3)我们的系统中没有执行复杂的查询或优化复杂的查询。
11 结论
我们已经讲述完了Bigtable,Google的一个分布式的结构化数据存储系统。Bigtable的集群从2005年4月开始已经投入使用了,在此之前,我们花了大约7人年设计和实现这个系统。截止到2006年4月,已经有超过60个项目使用Bigtable了。我们的用户对Bigtable提供的高性能和高可用性很满意,随着时间的推移,他们可以根据自己的系统对资源的需求增加情况,通过简单的增加机器,扩展系统的承载能力。
由于Bigtable提供的编程接口并不常见,一个有趣的问题是:我们的用户适应新的接口有多难?新的使用者有时不太确定使用Bigtable接口的最佳方法,特别是在他们已经习惯于使用支持通用事务的关系型数据库的接口的情况下。但是,Google内部很多产品都成功的使用了Bigtable的事实证明了,我们的设计在实践中行之有效。
我们现在正在对Bigtable加入一些新的特性,比如支持二级索引,以及支持多Master节点的、跨数据中心复制的Bigtable的基础构件。我们现在已经开始将Bigtable部署为服务供其它的产品团队使用,这样不同的产品团队就不需要维护他们自己的Bigtable集群了。随着服务集群的扩展,我们需要在Bigtable系统内部处理更多的关于资源共享的问题了【3,5】。
最后,我们发现,建设Google自己的存储解决方案带来了很多优势。通过为Bigtable设计我们自己的数据模型,是我们的系统极具灵活性。另外,由于我们全面控制着Bigtable的实现过程,以及Bigtable使用到的其它的Google的基础构件,这就意味着我们在系统出现瓶颈或效率低下的情况时,能够快速的解决这些问题。