一.Spanner功能概要
在Spanner面世篇中简单的介绍过:Spanner具有高扩展性,多版本(multi-version)、世界级分布(globally-distributed)及同步复制(synchronously-replicated)等特性。
Spanner立足于高抽象层次,使用Paxos协议横跨多个数据集把数据分散到世界上不同数据中心的状态机中。世界范围内响应,出故障时客户副本之间的自动切换。当数据总量或服务器的数量发生改变时,为了平衡负载和处理故障,Spanner自动完成数据的重切片和跨机器(甚至跨数据中心)的数据迁移。
Spanner可以轻松的横跨数百个数据中心将万亿级数据库行扩展到数百万台机器中。高可靠性更是让应用程序如虎添翼,即使面对大范围的自然灾害,可靠性仍然能得到良好的保障(因为Spanner有着世界级的数据转移)。最初的用户来自F1 — 使用了美国境内的5个拷贝。多数其他应用程序都是在同一个地理区域将数据复制3到5份,使用相对独立的故障模式。也就是说多数的应用程序会选择低延迟超过高有效性,只用一两个数据中心来保障数据的可靠性。
Spanner的主旨是数据中心的管理,但是在分布系统基础设施的特色上同样是下足了功夫。尽管Bigtable很讨一些项目欢心,我们还是收到了一些Bigtable在某些应用程序(复杂的、不断变化的架构或者需要在大区域响应中保持强一致性)中使用会异常艰难的投诉。许多Google的应用程序都在使用Megastore,因为它的半关系数据模型支持同步复制,尽管它有着可怜的吞吐量。
因此,Spanner已经从Bigtable-like版本的键值存储进化到现在的多版本数据库。数据存放在系统化的半关系表格中;数据被版本化了,每个版本都会用提交时间进行标注;旧版本的数据服从结构的垃圾回收策略;应用程序可以通过数据以前的时间标记来读取。
Spanner支持多用途的事务处理,并且提供了一个基于SQL的查询语言。作为世界级分布的数据库,Spanner更有一些令人感兴趣的特色:
1. 应用程序可以通过复制装置动态的对数据进行微控制。还可以通过制定约束条件来指定数据中心和其中包含的数据(无视数据与用户间的距离,数据与数据间的距离及数据保持的份数)。系统动态的和透明的在数据中心之间转移数据来保证资源的平衡利用。
2. Spanner有两个特性是很难在分布式数据库中实现的:读写的外部一致性和基于时间标记的全局读一致性。这让Spanner可以在全球范围内保持数据的一致备份,MapReduce一致执行和原子的Schema修改,即使是连续操作。
这些特性保证了Spanner可以有序的在世界范围内响应事务处理,即使是分散式的事务。时间标记反应了事务的顺序。另外,序列化的时间确保了外部一致性:如果事务T1在另一个事务T2之前提交,那么T1提交的时间标记是小于T2的。
Spanner是首个提供如此保证的系统。促成这项跨越的关键是TrueTime API(具有原子时钟和GPS)。TrueTime API直观的揭示了时钟的不可靠性,它运行提供的边界更决定了时间标记。如果不确定性很大,Spanner会降低速度来等待不确定因素的消失。Google集群管理软件更奠定了TrueTime的实施的基础。通过新型原子时钟将不确定性无限的放小。
二.Spanner的设计及一些重要组件(本部分特别感谢EMC研究院 颜开提供翻译支持)
由于Spanner是全球化的,所以有两个其他分布式数据库没有的概念。
如图所示。一个Spanner有上面一些组件。实际的组件肯定不止这些,比如TrueTime API Server。如果仅仅知道这些知识,来构建Spanner是远远不够的。但Google都略去了。这里做一下简单介绍:
可以看出来这里每个组件都很有料,但是Google的论文里只具体介绍了Spanserver的设计,所以也就只能介绍到这里。下面详细阐述Spanserver的设计。
1. Spanserver
本章详细介绍Spanserver的设计实现。Spanserver的设计和BigTable非常的相似。参照下图
从下往上看。每个数据中心会运行一套Colossus (GFS II) 。每个机器有100-1000个tablet。Tablet概念上将相当于数据库一张表里的一些行,物理上是数据文件。打个比方,一张1000行的表,有10个tablet,第1-100行是一个tablet,第101-200是一个tablet。但和BigTable不同的是BigTable里面的tablet存储的是Key-Value都是string,Spanner存储的Key多了一个时间戳(时间标记):
(Key: string, timestamp: int64) ->string。
因此spanner天生就支持多版本,tablet在文件系统中是一个B-tree-like的文件和一个write-ahead日志。
每个Tablet上会有一个Paxos状态机。Paxos是一个分布式一致性协议。Table的元数据和log都存储在上面。Paxos会选出一个replica做leader,这个leader的寿命默认是10s,10s后重选。Leader就相当于复制数据的master,其他replica的数据都是从他那里复制的。读请求可以走任意的replica,但是写请求只有去leader。这些replica统称为一个paxos group。
每个leader replica的spanserver上会实现一个lock table来管理并发。Lock table记录了两阶段提交需要的锁信息。但是不论是在Spanner还是在BigTable上,但遇到冲突的时候长时间事务会将性能很差。所以有一些操作,如事务读可以走lock table,其他的操作可以绕开lock table。
每个leader replica的spanserver上还有一个transaction manager。如果事务在一个paxos group里面,可以绕过transaction manager。但是一旦事务跨多个paxos group,就需要transaction manager来协调。其中一个Transaction manager被选为leader,其他都听其指挥。这样事务就得到了保证。
2. Directories and Placement
之所以Spanner比BigTable有更强的扩展性,在于Spanner还有一层抽象的概念directory, directory是一些key-value的集合,同个directory里的key有着一样的前缀。更妥当的叫法是bucketing。Directory是应用控制数据位置的最小单元,可以通过谨慎的选择Key的前缀来控制。不难猜出,在设计初期,Spanner是作为F1的存储系统而设立的,甚至还设计有类似directory的层次结构,这样的层次有很多好处,但由于实现起来太复杂而被摒弃了。
Directory作为数据放置的最小单元,可以在paxos group里面移来移去。Spanner移动一个directory一般出于以下几个原因:
Directory可以在不影响client的前提下,在后台移动。移动一个50MB的directory大约只需要的几秒钟。
那么directory和tablet又是什么关系呢。可以理解为Directory是一个抽象的概念,管理数据的单元;而tablet是物理的东西,数据文件。由于一个Paxos group可能会有多个directory,所以spanner的tablet实现和BigTable的tablet实现有些不同。BigTable的tablet是单个顺序文件。Google有个项目,名为Level DB,是BigTable的底层,可以看到其实现细节。而Spanner的tablet可以理解成一些基于行的分区的容器。这样就可以将一些经常同时访问的directory放在一个tablet里面,而不用太在意顺序关系。
在paxos group之间移动directory是后台任务。这个操作还被用来移动replicas。移动操作设计的时候不是事务的,因为这样会造成大量的读写block。操作的时候是先将实际数据移动到指定位置,然后再用一个原子的操作更新元数据,完成整个移动过程。
Directory还是记录地理位置的最小单元。数据的地理位置是由应用决定的,配置的时候需要指定复制数目和类型,还有地理的位置。比如(上海,复制2份;南京复制1分) 。这样应用就可以根据用户指定终端用户实际情况决定的数据存储位置。比如中国队的数据在亚洲有3份拷贝,美国队的数据在全球都有拷贝。
前面对directory还是被简化的,还有很多无法详述。
3. 数据模型
Spanner的数据模型来自于Google的内部实践。在设计之初,Spanner就决定了以下几个特性:
为何会这样决定呢?在Google内部还有一个Megastore,尽管要忍受性能不够的折磨,但是在Google有300多个应用在用它,因为Megastore支持一个类似关系数据库的schema,而且支持同步复制 (BigTable只支持最终一致的复制) 。使用Megastore的应用有大名鼎鼎的Gmail, Picasa, Calendar, Android Market和AppEngine。 而必须对Query语句的支持,来自于广受欢迎的Dremel。 最后对事务的支持是必不可少的,BigTable在Google内部被抱怨的最多的就是其只能支持行事务,再大粒度的事务就无能为力了。Spanner的开发者认为,过度使用事务是造成性能下降的恶果,应该由应用的开发者承担。应用开发者在使用事务的时候,必须考虑到性能问题。而数据库必须提供事务机制,而不是因为性能问题,就干脆不提供事务支持。
数据模型是建立在directory和key-value模型的抽象之上的。一个应用可以在一个universe中建立一个或多个database,在每个database中建立任意的table。Table看起来就像关系型数据库的表。有行,有列,还有版本。Query语句看起来是多了一些扩展的SQL语句。
Spanner的数据模型也不是纯正的关系模型,每一行都必须有一列或多列组件。看起来还是Key-value。主键组成Key,其他的列是Value。但这样的设计对应用也是很有裨益的,应用可以通过主键来定位到某一行。
上图是一个例子。对于一个典型的相册应用,需要存储其用户和相册。可以用上面的两个SQL来创建表。Spanner的表是层次化的,最顶层的表是directory table。其他的表创建的时候,可以用interleave in parent来什么层次关系。这样的结构,在实现的时候,Spanner可以将嵌套的数据放在一起,这样在分区的时候性能会提升很多。否则Spanner无法获知最重要的表之间的关系。
TrueTime API 是一个非常有创意的东西,可以同步全球的时间。上表就是TrueTime API。TT.now()可以获得一个绝对时间TTinterval,这个值和UnixTime是相同的,同时还能够得到一个误差e。TT.after(t)和TT.before(t)是基于TT.now()实现的。
TrueTime API实现的基础是GPS和原子钟。之所以要用两种技术来处理,是因为导致这两个技术的失败的原因是不同的。GPS会有一个天线,电波干扰会导致其失灵。原子钟很稳定。当GPS失灵的时候,原子钟仍然能保证在相当长的时间内,不会出现偏差。
实际部署的时候。每个数据中心需要部署一些Master机器,其他机器上需要有一个slave进程来从Master同步。有的Master用GPS,有的Master用原子钟。这些Master物理上分布的比较远,怕出现物理上的干扰。比如如果放在一个机架上,机架被人碰倒了,就全宕了。另外原子钟也不是很贵。Master自己还会不断比对,新的时间信息还会和Master自身时钟的比对,会排除掉偏差比较大的,并获得一个保守的结果。最终GPS master提供时间精确度很高,误差接近于0。
每个Slave后台进程会每个30秒从若干个Master更新自己的时钟。为了降低误差,使用Marzullo算法。每个slave都会计算出自己的误差。这里的误差包括的通信的延迟,机器的负载。如果不能访问Master,误差就会越走越大,直到重新可以访问。
4.Google Spanner并发控制
Spanner使用TrueTime来控制并发,实现外部一致性。支持以下几种事务。
例如一个读写事务发生在时间t,那么在全世界任何一个地方,指定t快照读都可以读到写入的值。
上表是Spanner现在支持的事务。单独的写操作都被实现为读写事务;单独的非快照被实现为只读事务。事务总有失败的时候,如果失败,对于这两种操作会自己重试,无需应用来实现重试循环。
时间戳的设计大大提高了只读事务的性能。事务开始的时候,要声明这个事务里没有写操作,只读事务可不是一个简单的没有写操作的读写事务。它会用一个系统时间戳去读,所以对于同时的其他的写操作是没有Block的。而且只读事务可以在任意一台已经更新过的replica上面读。
对于快照读操作,可以读取以前的数据,需要客户端指定一个时间戳或者一个时间范围。Spanner会找到一个已经充分更新好的replica上读取。
还有一个有趣的特性的是,对于只读事务,如果执行到一半,该replica出现了错误。客户端没有必要在本地缓存刚刚读过的时间,因为是根据时间戳读取的。只要再用刚刚的时间戳读取,就可以获得一样的结果。
5.读写事务
正如BigTable一样,Spanner的事务会将所有的写操作先缓存起来,在Commit的时候一同提交。这样的话,就读不出在同一个事务中写的数据了。不过这没有关系,因为Spanner的数据都是有版本的。
在读写事务中使用wound-wait算法来避免死锁。当客户端发起一个读写事务的时候,首先是读操作,他先找到相关数据的leader replica,然后加上读锁,读取最近的数据。在客户端事务存活的时候会不断的向leader发心跳,防止超时。当客户端完成了所有的读操作,并且缓存了所有的写操作,就开始了两阶段提交。客户端闲置一个coordinator group,并给每一个leader发送coordinator的id和缓存的写数据。
leader首先会上一个写锁,他要找一个比现有事务晚的时间戳。通过Paxos记录。每一个相关的都要给coordinator发送他自己准备的那个时间戳。
Coordinatorleader一开始也会上个写锁,当大家发送时间戳给他之后,他就选择一个提交时间戳。这个提交的时间戳,必须比刚刚的所有时间戳晚,而且还要比TT.now()+误差时间还要晚。这个Coordinator将这个信息记录到Paxos。
在让replica写入数据生效之前,coordinator还有再等一会。需要等两倍时间误差。这段时间也刚好让Paxos来同步。因为等待之后,在任意机器上发起的下一个事务的开始时间,都不会比这个事务的结束时间早了。然后coordinator将提交时间戳发送给客户端还有其他的replica。他们记录日志,写入生效,释放锁。
6.只读事务
对于只读事务,Spanner首先要指定一个读事务时间戳。还需要了解在这个读操作中,需要访问的所有的读的Key。Spanner可以自动确定Key的范围。
如果Key的范围在一个Paxos group内。客户端可以发起一个只读请求给group leader。leader选一个时间戳,这个时间戳要比上一个事务的结束时间要大。然后读取相应的数据。这个事务可以满足外部一致性,读出的结果是最后一次写的结果,并且不会有不一致的数据。
如果Key的范围在多个Paxos group内,就相对复杂一些。其中一个比较复杂的例子是,可以遍历所有的group leaders,寻找最近的事务发生的时间,并读取。客户端只要时间戳在TT.now().latest之后就可以满足要求了。
三.与Google其他项目的关系
Megastore和DynamoDB已经提供了横跨数据中心复制一致性服务。DynamoDB呈现了一个键值界面,并可以在小范围内进行复制。Spanner继承了Megastore的半关系数据模型,甚至是相似的语言。Megastore的性能一直不是很高。它处于Bigtable的上一层,而Bigtable加强了在通信上的资源消耗。而在长期的调度中Bigtable同样不适合:多份拷贝可能同时写入数据。Paxos协议下,不同拷贝上的写入必然发生冲突,即使它们在逻辑上没有冲突:在同一个Paxos组中同时间发生不同写入,吞吐量必将崩溃。虽然Bigtable在Google中被广泛使用,但是其不能提供较复杂的Schema和跨数据中心的强一致性。而Spanner提供了高性能、多用途事务和外部一致性。集合了复制的并发控制更是减少了Spanner中提交的等待。Spanner提供的快照隔离更是解决了争用问题。
四.未来的工作
2011年的大部分时间都用于协助F1小组将Google的广告后台从MySQL搬到Spanner上。Google现在正致力于将它的监视工具和支持工具提到与它通信性能同等的高度。此外,Google还在改善备份系统的功能及性能。当下,正在构建Spanner模式语言、第二索引的自动维护及基于负载的再分片。
晚一点时间,Google还会在OSDI 2012 12vestigate会议上发布一对新特性。此外,还计划在未来直接改变Paxos配置。鉴于许多应用程序都在非常近的数据中心之间复制数据,TrueTime将会对性能有着显著的影响。但是将延迟降到1毫秒以下的所有障碍都已经被克服。降低查询的主要延迟可以通过改善网络技术,更甚者通过交替时间分配技术来避免。
当然还有很明显的地方需要改进。首先:尽管Spanner的节点数量是可伸缩的,在复杂SQL查询中本地节点的数据结构性能仍然很差,因为它们是为简单的键值访问而设计的。DB著作中的算法可以大幅度的提高单模性能。其次:在数据中心间移动数据来改变客户端负载仍然是长期目标。而为了实现这一目标,还必须能够在数据中心间以自动的和协调的方式来转移客户端进程。在数据中心间移动进程中的资源获取和分配更是个巨大的攻坚。(编译/仲浩 责编/包研)