翻译自:http://research.google.com/archive/spanner.html
翻译word版参见:https://docs.google.com/open?id=0B_1dRekuoYY5d2E5eXZYREdGZ3c
摘要
Spanner是Google的可扩展、多版本、全球分布、同步复制的数据库。它是第一个数据全球范围内分布的系统,且支持对外可见一致性的分布式事务。本文描述了spanner的组织方式、功能集、各种设计抉择的原理以及一个新的时间API用于暴露时钟的不确定性。这个API和它的实现对于支持对外可见一致性以及贯穿整个spanner的其它一些强大功能是至关重要的,包括历史数据的非阻塞读,lock-free的只读事务,原子的schema修改等。
1.介绍
Spanner是Google设计、建造、部署的可扩展全球分布数据库。从高层抽象来看,它是一个把数据切片分布在很多组Paxos[21]状态机器上的数据库,这些机器分布在全球的各个数据中心。复制被用来做全局的可用性和地域上的局部性,客户端会自动在多个副本间作失效切换。当数据量或者机器数有变化时,Spanner会自动对数据作重新的切片,此外,spanner还会为负载均衡和访问失效等目的自动在机器间包括跨数据中心迁移一些数据。Spanner被设计能扩展到上百万台机器、横跨数百个数据中心、上万亿行数据库的数据。
应用可以使用Spanner作为高可用的方案,甚至是应对大范围的自然灾害,通过区域内或者甚至是跨大陆的数据复制。我们的第一个客户是F1[35],Google广告后台的重构版。F1使用了横跨美国的五个副本。其它大多数应用只在一个地理区域的3至5个数据中心里复制数据,但故障模式相对独立。也就是说,大部分应用选择低延迟优于更高的可用性,能达到一两个数据中心故障还能正常工作就行。
Spanner主要工作集中在管理跨数据中心的多副本数据,当然我们也花了很长时间来在我们的分布式架构上设计和实现一些重要的数据库功能。尽管很多项目使用Bigtable[9]已经很好了,但我们也一直收到一些用户的抱怨,对于一些应用Bigtable还是太难用了,比如那些需要复杂的、不断变化的schema,或者其它需要在很大广域网范围内多副本达到强一致性的应用。(其它作者也有同样的抱怨[37]。)Google里许多应用选择使用Megastore[5],因为它是半关系化的数据模型,并且支持同步复制,尽管它的写吞吐比较差。因此,Spanner开始从类似Bigtable这种多版本的key-value存储向时间上多版本的数据库演进。数据被存在有schema的半关系型表里,数据是有版本的,每个版本自动带有提交时的时间戳,老版本的数据会被一些可配置的垃圾回收策略所回收,应用也可以读到带有老时间戳的数据。Spanner支持一般含义上的事务,也提供了基于SQL的查询语言。
作为全球分布的数据库,Spanner提供了一些有趣的功能。第一,数据的副本配置可以由应用来动态控制。应用可以指定一些约束条件来控制哪些集群拥有哪些数据、数据离用户多远(用来控制读延迟)、副本相互间的距离(用来控制写延迟)、管理多少副本(用来控制可靠性、可用性和读性能)。数据也可以动态地在数据中心间相互迁移,以便平衡数据中心间的资源使用,这个过程对应用是完全透明的。第二,Spanner有两个功能在分布式数据库中很难实现,它提供了对外的读写一致性[16],以及同一时间跨数据库的全局读一致性。这些功能使得spanner支持一致性的备份,一致性的MapReduce执行[12],原子的schema修改,而且都可以全局扩展,即使是存在执行中的事务。
这些功能之所以能实现,主要归功于Spanner给事务分配了全局意义上的提交时间戳,即使事务本身是分布的。时间戳反映了序列化的顺序。另外,序列化顺序满足对外一致性(或者说是线性化[20]),如果事务T1提交在事务T2启动之前,则T1的提交时间戳小于T2的。Spanner是第一个全局范围内提供这样保证的系统。
这些特性得以保证的关键因素是TrueTimeAPI及它的实现。该API直接暴露了时钟的不确定性,对于Spanner时间戳的保证取决于其实现所能提供的边界。如果不确定性很大,Spanner会慢下来等到不确定性过去。Google的集群管理软件提供了TrueTime API的实现。这个实现使用了多种现代的时间系统(GPS、原子时钟等),使得不确定性保持在很小的区间内(一般小于10ms)。
第二节描述了Spanner实现的结构、功能集、设计时工程上的选择。第三节描述我们新的TrueTime API及它的大概实现。第四节描述了Spanner如何使用TrueTime来实现对外一致的分布式事务、无锁的只读事务以及原子的schema更新。第五节提供了关于Spanner性能和TrueTime性能的评测,另外也讨论了F1中的经验。第六七八节描述了相关和后续的工作以及整体性的总结。
2.实现
本节描述了Spanner实现的架构和原理,然后描述了用来管理复制和位置放置的目录抽象,它也是数据移动的单元。最后描述了我们的数据模型,为什么Spanner看上去像一个关系数据库而不是一个key-value的存储,以及应用如何来控制数据的位置放置。
一个Spanner的部署称为一个universe。如果Spanner全局管理数据,那只需要少量运行中的universe。目前我们允许了一个测试试用universe、一个开发/产品用的universe,一个生产环境的universe。
Spanner以一组zone形式组织,每个zone大致类似一个Bigtable的环境。Zone是部署管理的单元。一组zone也是一组数据可以被复制的位置。一个运行中的系统可以添加也可以删除一个zone,这样新的数据中心可以加入服务也可以关闭。Zone也是物理隔离的单元,同一个数据中心里可以有一个或多个zone,如果不同的应用数据需要切分并分布到同一个数据中心的不同组服务器上。
图1描述了一个Spanneruniverse里的服务器。一个zone里有一个zonemaster以及成百上千台spanservers。前者将数据分派到spanserver上,后者具体负责client对数据的读写。每个zone里的定位代理服务器被client用来定位数据是放在哪个spanserver上的。Universe master和placement driver目前还是单点。Universe master主要是一个控制台,用来显示所有zone的状态信息以便于交互式调试。Placement driver处理自动的数据跨zone迁移,一般在几分钟时间里完成。Placement driver周期性与spanserver通信,以便于发现哪些数据需要迁移,或者是是否满足更新复制约束或负载均衡。由于篇幅限制,我们只描述spanserver。
2.1 Spanserver软件栈
本节主要描述spanserver的实现,以说明复制和分布式事务是如何架在类Bigtable实现之上的。整个软件栈如图2所示。在底下,每个spanserver负责大概100到1000个称为tablet的数据结构实例。一个tablet类似于Bigtable的tablet抽象,实现了一个如下的映射集合:
(key:string,timestamp:int64)à string
与Bigtable不同,Spanner给数据分配时间戳,这是使得Spanner更像多版本数据库而不是key-value存储的很重要的一点。Tablet的状态存在一组类似B树的文件里,并且有一个预写日志,这些都在Colossus这个分布式文件系统里(GFS的代替者[15])。
为支持复制,每个spanserver在每个tablet之上执行一个Paxos状态机。早期Spanner试图让每个tablet支持多个Paxos状态机,这样对复制的配置有更好的灵活性,但这里的设计复杂度最终让我们放弃了。每个状态机在它相应的tablet里存储它的元数据和log。我们的Paxos实现通过基于时间的领导者lease支持长期存活的领导者,时间长度默认为10秒。目前Spanner的实现每个Paxos在log中写了两次,一次在tablet的log,一次在Paxos的log。这个选择是权宜之计,我们可能最终会解决它。我们的Paxos实现是管道化的,这样可以改进Spanner在广域网延迟条件下的吞吐,但通过使用Paxos保证写是按顺序的(第四节我们所依赖的事实)。
Paxos状态机被用来执行映射组的一致复制。每个副本的key-value映射状态存储在相应的tablet里。写入者必须先在领导节点初始化Paxos协议,读取者直接从任何一个已经更新到最新的副本上的tablet里获取状态。这一组副本统称为Paxos组。
对于作为领导者的副本,spanserver通过一个锁表来保证并发控制。锁表包括两阶段锁的状态,它把key范围映射到锁状态上。(注意到一个长期存活的Paxos领导者对管理锁表效率更高)。在Bigtable和Spanner中,我们设计考虑了长期运行的事务(比如,报表生成就可能会花很多的时间),这种情况如果采用乐观并发控制方式在冲突的时候表现会比较差。需要同步的操作,比如事务性的读,可以从锁表中申请锁,其它操作可以不用锁表。
对于作为领导者的副本,spanserver还会执行事务管理器来支持分布式事务。事务管理器被用来作为领导者,组里的其它副本则作为从参与。如果一个事务只需要一个Paxos组(对大多数事务是如此),它可以不需要经过事务管理器,因为锁表和Paxos一起提供了事务性。如果一个事务需要多个Paxos组,这些组的领导者就需要执行两阶段提交。其中一个参与的组被选为协调者,这个组的领导者就作为协调领导者,组里的其它从就作为从协调者。每个事务管理器的状态被存在底下的Paxos组里(因此是有多份复制的)。
2.2 目录和放置
基于Key-value映射组,Spanner实现支持bucket的抽象,我们称之为目录,实际上是一组连续的key,它们有共同的前缀。(叫做目录是历史原因,更好的叫法是bucket。)我们将在2.3节里解释该前缀的来源。对目录的支持允许应用通过选择合适的key来控制数据的局部性。
一个目录就是数据放置的单元。同一个目录里的所有数据有同样的副本配置。当数据在Paxos组之间迁移时,它以目录为单位迁移,如图3。Spanner可能从一个Paxos组里移出一个目录以减轻负载,也会将经常被同时访问的目录移到同一个组里,或者将目录迁移到离访问者近的组里。即使是客户端在访问目录也可以被移动。一个50MB的目录在几秒内完成迁移是没有问题的。
一个Paxos组可以包含多个目录意味着Spanner的tablet和Bigtable的tablet是不一样的,前者并不要求行空间的一个分片必须字母连续。相反,Spanner tablet是一个可以容纳多个行空间分片的容器。这样做的好处是我们可以把经常被同时访问的多个目录的数据放在一起。
Movedir是一个后台任务,用来在Paxos组间移动目录[14]。Movedir也被用来给Paxos组增加或删除副本[25],因为Spanner现在还不支持Paxos里的配置变化。Movedir没有作为一个事务来执行,以避免对正要被移动数据当前读写的阻塞。相反,movedir会注册一个事件表示它后台开始移动数据。当它移动了大部分数据之后,它会使用一个事务来原子地完成移动剩下的数据和更新这两个Paxos组的元数据。
一个目录也是应用指定物理复制属性(或者就是放置)的最小单元。用于规范放置的语言在设计上与副本管理的配置是分开的。管理员可以控制两个维度:副本的个数和类型,以及副本的物理放置策略。这样在这两个维度创建了一个选择框(如,North America,replicated 5 ways with 1 witness)。应用通过给各个数据库或者目录不同的选项标记来控制数据如何复制。比如,一个应用可以存储每个终端用户的数据到他们自己的应用,并设置用户A的数据在欧洲有3份,用户B的数据在北美有5份。
为了解释的更清楚,我们已经做了很大的简化。事实上,当一个目录过大时,Spanner还会把它切分多个分片。每个分片由不同的Paxos组来负责(因此也可能在不同的服务器上)。Movedir也是在组间移动分片,而不是整个目录。
2.3 数据模型
Spanner向应用暴露出了下列数据功能:基于schema半关系型表的数据模型、一个查询语言以及通用的事务。支持这些功能的走向受很多因素的影响。支持schema半关系表和同步复制的需求主要来自于Megastore[5]的流行。在Google至少有300个应用使用Megastore(尽管它性能比较低),因为它的数据模型比Bigtable维护更简单,也因为它支持跨数据中心的同步复制。(Bigtable对于跨数据中心的副本只支持最终一致。)Google里使用Megastore的知名应用有Gmail、Picasa、Calendar、Android Market、AppEngine。Spanner支持类SQL查询语言的需求也非常明确,Dremel[28]就是一个非常受欢迎的交互式分析工具。最后,Bigtable缺少跨行事务的支持经常引起争论,Percolator[32]一部分就是为了解决这个问题而构建。一些作者争论说通用的两阶段提交太难支持了,因为其中的性能或者可用性问题[9,10,19]。我们相信让应用开发者处理事务过度使用后带来的性能问题,比缺少事务条件下编码要好很多。在Paxos运行两阶段提交减轻了可用性问题。
应用数据模型架在目录扁平化后的key-value映射之上。一个应用可以全局创建多个数据库,每个数据库可以包含无限的带schema的表。这些表看上去像关系数据库的表,有行、列以及带多个版本的值。这里就不对Spanner所用的查询语言再更细描述了,它看上去像SQL加上一些对protocol-buffer-valued域的扩展支持。
Spanner数据模型并不是纯关系型的,行必须要有名字。更准确地说,每个表需要一个拥有一个或多个主键列的有序集合。这就要求Spanner看上去仍很像key-value存储,主键形成了行的名字,每个表定义了一个从主键列到非主键列的映射。行只在行key被定义值后才认为存在,即使值是NULL。应用这个结构式有用的,因为它让应用通过key的选择来控制数据的局部性。
图4包括一个Spanner schema的示例,用于存储每个用户、每个专辑的照片元数据。这个schema描述语言类似Megastore,额外的需求是每个Spanner数据库必须被客户端切分到一个或多个表的层次结构里。客户端程序在数据库的schema里声明这种层次结构,通过INTERLEAVE IN表达式。层次结构的顶层是目录表。目录表里的每行,假定为键K,则子表所有的行以K起始,并保持字母序,形成一个目录。ON DELETE CASCADE说明在目录表里删除一行时也需要删除相关的子行。图中还显示了示例数据库的交叉布局,比如Albums(2,1)表示Albums表中的行 user_id 2,album_id 1。这种表的交叉形成目录使得客户端能够描述多张表之间的数据局部关系这对于在分片的分布式数据库里是非常必要的。没有它,Spanner就没法知道这最重要的局部关系。
3. TrueTime
这一节描述TrueTime API及其大体实现。另外会有一篇论文更详细的描述它,本文主要演示拥有该API的价值。表1列出了API的方法。TrueTime用TTinterval明确表示时间,这是一个有限时间范围内不确定的时间间隔(不像标准时间接口给客户端没有不确定性的概念)。TTinterval的端点是TTstamp类型。TT.now()方法返回TTinterval,它确保包括TT.now()调用时的绝对时间。这个时间段类似于带有闰秒拖尾现象的UNIX时间。定义瞬间的误差边界为e,它是间隔宽度的一半,平均误差边界是e平均。TT.after() 和TT.before()方法是TT.now()基础上的包装。
用函数tabs(e)表示事件e的绝对时间。更正式的术语,TrueTime保证调用tt=TT.now(),tt.earliest<=tabs(enow)<=tt.latest,其中enow是调用事件。
TrueTime底下的时间参考是GPS和原子时钟。TrueTime使用两种形式的时间参考,因为他们有不同的故障模式。GPS参考源的弱点包括天线、接收器故障,本地无线电干扰以及相关错误(比如不正确的闰秒处理和欺骗等设计缺陷),或者是GPS系统老化了。原子时钟失败的方式与GPS以及相互间无关,经常很长时间由于频率误差可能会有明显漂移。
每个数据中心都有一组time master机器来执行TrueTime,每台机器有一个timeslavedaemon。大部分master有接收GPS的专用天线,这些master是物理上隔离的以降低天线故障、无线电干扰、欺骗的影响。剩下的master(我们称之为Armageddon的master)都配备了原子时钟。一个原子时钟并不贵,Armageddon master的开销和GPS master差不多。所有master的时间也会定期相互比较。每个master也会交叉检查速率以确保本地时钟的时间,如果时间误差很大它甚至会把自己剔除出去。在同步期间,Armageddon master会慢慢地增加由于最坏情况下时钟漂移所带来的时间不确定性。GPS master宣传的不确定性会比较接近于零。
每个daemon从多个master[29]拉数据以降低单一master出错的风险,一些是从邻近数据中心所挑选的GPS master,其它的是父数据中心的GPS master和Armageddon master。Daemon进程使用Marzullo算法来检测和拒绝不正确的master,并将本地时钟同步到正确的值。为了防止受损坏的本地时钟影响,那些频率漂移超过部件规格一定范围的机器和运行环境将被剔除。
在同步期间,daemon会有缓慢的时间误差增长。E来自于最坏情况下的本地时钟漂移。E也依赖于master的误差和与master的通信延迟。在我们的生产环境中,E是一个典型的锯齿型的时间函数,随着每个轮询间隔在1到7毫秒波动。E平均因此大概在4毫秒样子。Daemon轮询间隔目前是30秒,可接受的漂移率设为200微秒/秒,整个锯齿边界在0到6毫秒。剩下1毫秒来自于与时间master的通信延迟。超过锯齿边界就可能是有故障了。比如,偶然性的时间master不可用会导致数据中心范围内增加E,同样,机器或网络的过载也可能会导致局部偶尔突破E。
4.并发控制
本节描述TrueTime怎么被用来保证并发控制的属性正确,以及这些属性如何被用来实现对外一致性事务、无锁只读事务、过往数据的非阻塞读。这些功能保证比如整个数据库审计时在时间戳t的读能看到t时刻所有提交事务后的影响。
进一步,区分Paxos所看到的写(后面我们就称之为Paxos写,除非有更清楚的说明)和Spanner客户端的写是非常重要的。比如,两阶段提交为准备阶段生成了一个Paxos写,但实际Spanner客户端没有相应的写。
4.1 时间戳管理
表2列举了Spanner支持的操作类型。Spanner实现支持读写事务,只读事务(预声明快照隔离的事务),以及快照读。单独的写通过读写事务来执行,非快照读通过只读事务来实现。这些内部都会有重试机制(客户端不需要写自己的重试循环)。
只读事务是一类通过快照隔离[6]得到性能优势的事务。只读事务需要预声明不会有写,对于读写事务来说没有写就不能简单控制了。只读事务中的读在系统选择的时间点执行不需要锁,这样进来的写不会被阻塞。只读事务中的读执行可以在任何一个已经更新到最新的副本上(4.1.3节)。
快照读是基于过去数据的读,不需要锁。客户端可以为一次快照读指定时间戳,或者提供一个所需时间戳过期的上限然后让Spanner选择一个时间戳。不管哪种情况,快照读处理的任何副本都是最新的。
对于只读事务和快照读,在一个时间戳被选择后,提交是不可避免的,除非在该时间戳的数据已经被垃圾回收。因此,客户端应该避免在重试过程中缓存数据。如果出现服务器宕机,客户端可以带着原来的时间戳和当前读到的位置继续在另一台服务器上查询。
4.1.1 Paxos领导者lease
Spanner的Paxos执行通过时间lease来使得领导权长期有效(默认为10秒)。一个潜在的领导者发送请求要求时间lease的选票,在收到大多数的lease选票后,领导者就确认自己拥有lease了。副本一次成功的写会隐式地延长lease投票,领导者也会在快到期时主动请求延长lease选票。定义一个领导者的lease期间为从它发现它有大多数的lease选票开始,到它不再有大多数的lease选票结束(因为一些到期了)。Spanner依赖于下面相互无关的不变量,对于每个Paxos组,每个Paxos领导者lease拥有期是相互独立的。附录A描述了这种不变约束是如何实现的。
Spanner执行允许Paxos领导者自己通过释放从节点的选票来退位。为保护不相交的不变约束,Spanner对何时运行退位作了限制。定义Smax是领导者用的最大时间戳。后续章节里将描述何时Smax被提前。在退位前,领导者必须等到TT.after(Smax)为真。
4.1.2 给读写事务分派时间戳
保证事务的读写使用两阶段锁,因此他们可以在所有锁获得后的任一时刻分配时间戳,但必须在任意锁释放前。对一个给定的事务,Spanner分配给它的是Paxos分配给表示事务提交的Paxos写时的时间戳。
Spanner依赖于下面的单调保证。在每一个Paxos组里,Spanner分派时间戳给Paxos写以单调递增的顺序,即使是跨领导者的。单个领导者可以很容易按单调递增顺序分配时间戳。通过使用不相交的不变量保证跨领导者之间的不变量执行,一个领导者只能在它的lease期间分配时间戳。注意到不管时间戳何时分配,Smax用来保证不相交。
Spanner也会保证下面的对外一致性:如果事务T2在事务T1提交后才开始,那么T2的提交时间必须比T1的提交时间大。定义事务Ti的开始和提交事件分别为Eistart和Eicommit,提交时的时间戳为Si。则约束条件是 Tabs(E1commit)<Tabs(E2start)=>S1<S2。执行事务和分配时间戳的协议遵循两条规则,他们共同保证下面的约束条件。对于一次写Ti,定义提交请求到达协调领导者的事件为Eiserver。
开始 协调领导者对写Ti分配的提交时间戳Si不小于在Eiserver后计算的TT.now().latest。注意到参与领导者这里没有提及,4.2.1节描述他们是怎么参与下一条规则执行的。
等待提交 协调领导者保证客户端在TT.after(Si)为真之前不会看到任何Ti提交的数据。等待提交保证Si小于Ti的绝对提交时间,或者是Si<Tabs(Eicommit)。等待提交的执行在4.2.1节描述。证明:
S1<Tabs(E1commit)
Tabs(E1commit)<Tabs(E2start)
Tabs(E2start)<=Tabs(E2server)
Tabs(E2server)<=S2
S1<S2
4.1.3 支持对某时间点数据的读
4.1.2描述的单调不变性允许Spanner正确地判断一个副本的状态是不是已更新到最新的以满足读。每个副本跟踪一个安全时间值Tsafe,表示副本更新到的最大时间戳。一个副本可以满足时间戳T<=Tsafe的读。
定义Tsafe=min(Tpaxos_safe,Ttm_safe),每个Paxos状态机有一个安全时间Tpaxos_safe,每个事务管理器有个安全时间Ttm_safe。Tpaxos_safe比较简单,它是最后应用Paxos写的时间戳。因为时间戳单调递增,且写是按顺序应用的,关于Paxos的写不会出现在Ttm_safe时间点或之前。
当一个副本没有处于准备好但还没提交(即在两阶段提交的两个阶段之间)的事务时,Ttm_safe趋向于无穷大。(对于从参与者,Ttm_safe指的即是副本的事务管理领导者,它的状态可以通过Paxos写传递过来。)如果存在这样的事务,则这些食物的状态是不确定的,参与者副本不知道该事务是否将被提交。正如我们4.2.1节讨论,提交协议保证每个参与者知道事务准备好后时间戳的下限。事务Ti每个参与的领导者(组g)分配一个准备阶段的时间戳Sprepare_i,g给它的准备记录。协调领导者保证事务提交的时间戳Si>=Sprepare_i,g,在所有的参与者组g上。因此,对于组g的每个副本、所有准备好的事务Ti,Ttm_safe=MINi(Sprepare_i,g)-1。
4.1.4 给RO事务分配时间戳
一个只读事务分两阶段执行:分配一个时间戳Sread[8],然后执行事务读作为Sread时刻的快照读。快照读可以在任何已更新到最新的副本上执行。
一个简单的赋值是Sread=TT.now().latest,可以取事务启动后的任意一个时间,通过一个类似4.1.2提到的参数。但如果Tsafe还没有到的话,这个时间戳可能会使得在Sread数据读的执行会阻塞。(此外,注意到选择Sread的值会使得Smax增大以保证不相交。)为减小阻塞的可能性,Spanner应该分配最旧的时间戳一保证外部一致性。4.2.2节解释了如何选取这个时间戳。
4.2 细节
这一节解释一些关于读写事务和只读事务的实现细节,以及执行原子schema修改的特殊事务类型的实现,然后描述了一些基本schema的优化。
4.2.1 读写事务
类似Bigtable,事务中的写会在客户端缓存直至被提交。因此,事务中的读不会看到事务中写的结果。这样的设计在Spanner中工作良好,因为一次读返回读到数据的时间戳,未提交的写都还没有分配时间戳。
读写事务中的读使用wound-wait来避免死锁。客户端向相应组的领导者副本发起读,这会请求读锁并读到最新的数据。当客户端事务保持打开时,它会发送保活的消息以防止参与的领导者把该事务超时。当客户端完成所有的读和缓存着的写,它就开始两阶段提交。客户端选择一个协调组然后发送提交消息到每一个参与的领导者,消息携带协调者的标记也所有缓存的写。客户端驱动的两阶段提交避免跨广域网两次发送数据。
一个非协调者的参与领导者首先请求一个写锁,然后选择一个比前面事务已经分配的时间戳都要大(保证单调递增)的准备时间戳,然后通过Paxos日志记录准备消息。每个参与者接着通知协调者它的准备时间戳。
协调领导者首先也请求写锁,但是不需要准备阶段。在收到其它所有的参与领导者消息之后,它为整个事务选择了一个时间戳。提交时间戳S必须大于等于所有的准备时间戳(为满足4.1.3的约束),大于协调者接收到它提交消息时的TT.now().latest,大于该领导者赋给之前事务的所有时间戳(保证单调性)。协调领导者然后通过Paxos日志化提交记录(或者是因为等待其它参与者超时而终止)。
在允许协调者副本应用提交记录时,协调领导者等到TT.after(S),这样遵循了4.1.2描述的提交等待原则,因为协调领导者基于TT.now().latest选择S,现在等到保证时间戳过去,预期等待至少2*E平均。该等待与Paxos通信是重叠的。在提交等待之后,协调者发生提交时间戳到客户端和所有其它的参与领导者。每个参与领导者日志化通过Paxos后事务的结果。所有的参与者应用同样的时间戳然后释放锁。
4.2.2 只读事务
分配时间戳时需要在参与读的所有Paxos组里有一个协商阶段。基于此,对于每个只读事务,Spanner需要一个scope表达式,这个表达式描述了整个事务阶段需要读取的所有key。Spanner自动推断独立查询的范围。
如果该范围被单个Paxos组所拥有,则客户端向那个组的领导者发起只读事务。(目前Spanner实现里在一个Paxos领导者上为只读事务只选择一个时间戳。)领导者分配Sread并执行读。对于单点的读,Spanner通常比TT.now().latest做得好。定义LastTS()为Paxos组里上一个提交写的时间戳。如果没有准备好的事务,则Sread=LastTS()满足对外一致性,事务能够看到上次写的结果,因此排在它后面。
如果该范围被多个Paxos组所拥有,则有多种选项。最复杂的是所有组的领导者之间做一轮通信基于LastTS()协商确定Sread。Spanner目前执行一个简单的选择。客户端避免了协商环节,直接在Sread=TT.now().latest执行读(这会导致等待直至安全事件到来)。所有事务中的读可以发到更新到最新的副本执行。
4.2.3 修改schema事务
TrueTime使得Spanner可以支持原子的schema修改。使用标准的事务是不可行的,因为参与者(数据库中的组数)可能以百万计。Bigtable支持在一个数据中心里作原子的schema修改,但schema修改会阻塞所有的操作。
Spanner修改schema的事务是标准事务的一个通用非阻塞变体。首先,它被显式地分配一个将来的时间戳,并在准备阶段注册。基于此,跨数千台服务器的schema修改可以以对其它并发活动最小的打断来完成。第二,依赖于schema的读和写,在时间T和注册的schema修改时间戳同步,如果它们的时间戳早于T则会被执行,而时间戳晚于T的事务则会被阻塞直至schema修改事务完成。没有TrueTime,定义schema修改发生在T时间没有任何意义。
4.2.4 细化
上面定义的Ttm_safe有一个缺点,在单个准备好的事务中防止Tsafe提前。因此,后面的时间戳就不能发生度,即使读和事务不会有冲突。这样一个错误的冲突可以被消除,通过参数Ttm_safe和一个细粒度的从key范围到处于准备好阶段事务时间戳的映射。这个信息可以存在锁表里,它已经有了一个从key范围到锁元数据的映射。当读到达时,它只需要检查细粒度的key范围对应的安全时间是否有读冲突。
上面定义的LastTS()有个类似的缺点,如果一个事务刚被提交,一个无冲突的只读事务必须被分配Sread以保证在刚才的事务之后。因此,读的执行会被延迟。这个缺点类似可以通过参数LastTS()和一个细粒度的存在锁表里的key范围到提交时间戳的映射来解决。(我们还没有执行此优化。)当一个只读事务到来时,它的时间戳可以被分配为这些事务冲突的key范围里LastTS()的最大值,除非那儿有冲突的准备阶段的事务(可以通过细粒度的安全时间来判断)。
上面定义的Tpaxos_safe有个缺点,它不能提前到Paxos写之前。也就是说,一个T时刻的快照读不能在上一个写在T时刻前发生的Paxos组里被执行。Spanner利用领导者lease间隔的不相交特性来解决这个问题。每个Paxos领导者保证Tpaxos_safe是在将来写时间戳发生后一个阈值之前,它管理一个从Paxos序列号n到可能被赋给Paxos序列号n+1的最小时间戳的映射MinNextTS(n)。一个副本可以把Tpaxos_safe提前到MinNextTS(n)-1,当它被应用序列号n时。
单个领导者很容易保证它的MinNextTS()。因为MinNextTS()保证的时间戳在领导者lease之内,不相交特性保证跨领导者的MinNextTS()。如果一个领导者希望提前MinNextTS()在领导者lease结束之前,它必须首先扩展它的lease。注意到Smax总是早于MinNextTS()里的最大值以保证不相交性。
一个领导者默认每8秒提升MinNextTS()的值。因此,在准备事务缺失时,空闲Paxos组里的健康从节点最差情况下可以处理时间戳超过8秒的读。一个领导者也可以根据从节点需要提升MinNextTS()的值。
5.评估
我们首先从复制、事务、可用性角度来评估Spanner的性能。然后我们提供一些TrueTime行为的数据,以及我们第一个客户端F1的案例。
5.1 测评
表3是Spanner的一些测评。这些测评是在共享的服务器上做的,每个spanserver运行在4GB内存和4个核(AMD Barcelona 2200MHz)的调度单元上。客户端运行在独立的机器上。每个zone包含一个spanserver。客户端和zone放在一组网络延迟小于1毫秒的数据中心里(大多数应用都选择这么放置,很少有需要把数据分布到全世界范围的)。测试数据库创建了50个Paxos组,包括2500个目录。操作时独立的4KB读和写。所有的读在压缩后也超过了内存大小,这样我们只衡量Spanner调用栈的开销。另外,我们首先做了一轮没有测量的读,用来预热各地的缓存。
对于延迟测试,客户端只发起了少量操作,以防止在服务端排队。从1个副本的实验中看出,提交延迟大概在5毫秒,Paxos延迟在9毫秒。随着副本数的增加,延迟大致保持不变(标准差很小),因为Paxos在组内个副本上是并行执行的。随着副本数的增加,得到大多数响应的延迟受单个从副本慢影响就很小了。
对于吞吐的测试,客户端发起大量操作使服务端CPU饱和。快照读可以在任何更新到最新的副本上执行,因此它们的吞吐可以随着副本的增加几乎线性增长。只有一次读的只读事务只能在领导者上执行,因为时间戳分配必须是领导者来执行。只读事务吞吐随着副本的增加而增加,因为有效的spanserver增加。在实验的设置中,spanserver的个数和副本个数相等,领导者随机在zone里随机分布。写吞吐也从同样的实验环境得到提升(这也解释了从3个到5个副本吞吐的增加),但离随每次写的工作量和副本数的增加而线性增长还是差远了。
表4演示了两阶段提交可以扩展到相对合理的参与者数量,它给出了一组实验,运行在跨过3个zone上,每个zone有25个spanserver。从平均角度和99%角度扩展到50个参与者是可行的,超过100个参与者后延迟开始明显增大。
5.2 可用性
图5描述了在多数据中心运行Spanner的可用性收益。它显示了三个当数据中心故障时吞吐上的实验,所有这些都叠加到相同的时间尺度。测试的环境包括5个zone Zi,每个都有25个spanserver。测试数据库被分到1250个Paxos组上,100个测试客户端持续进行非快照读,总体读速率在50K每秒。所有这些领导者都放在Z1。每次测试一个zone的所有服务器都会被kill 5秒时间,没有领导者的kill Z2,leader-hard的kill Z1,leader-soft的也kill Z1,但是它会通知所有的服务器它们需要先处理领导关系。
Kill Z2对读吞吐没有影响,killZ1由于需要一定的时间在另一个zone里接管领导角色,有小幅影响,吞吐的下降从图上不容易看出来,但大概有3~4%。另一方面,没有实现警告的kill Z1有很大的影响,完成的速度几乎降到0。当领导者重新选举出来后,尽管系统吞吐由于两次实验的积累又提升到100K读每秒,系统仍有其它能力,操作会被排队缓存当领导者不可用时。因此,系统吞吐重新提升到之前的速率并稳定运行。
我们另外看了下Paxos领导者lease设置为10秒时的影响。当我们killzone时,各组领导者lease的超时时间大致均匀分布在下一个10秒区间里。很快lease因为领导者死了而超时,新的领导者开始被选举。大概在kill后的10秒,所有组又有了领导者并且吞吐又恢复了。缩短lease时间可以降低服务器宕机对可用性的影响,但需要更大的lease激活网络流量。我们正在设计和实现一种机制,它使得当领导者失效时从节点释放Paxos领导者的lease。
5.3 TrueTime
TrueTime有两个问题需要回答:E是否真的能保证时钟误差在一定范围内,E最坏会到多少?对于前者,最严重的问题是如果本地时钟漂移超过200us每秒,就会打破TrueTime的假设。我们机器的统计显示坏CPU可能性是坏时钟的6倍。也就是说,时钟问题是非常罕见的,相对于其他更严重的硬件问题。因此,我们相信TrueTime的执行和Spanner以来的其它软件一样可信。
图6显示了TrueTime在几千台跨多个距离超过2200公里的数据中心里的spanserver上运行的数据。它描绘出了E分布的90%、99%、99.9%,数据从timeslave进程在刚从时间master上同步后采样。这种采样省略了由于本地时钟误差所导致的毛刺,因此衡量时间master的误差(通常是0)需要加上与时间master的通信延迟。
数据显示决定E基本值的两个因素一般不是问题。然而,还存在一些很长的延时导致E值很高。长延时的减少在3月30号开始,主要由于网络的改进减少了网络链路的拥塞。4月13号E的增长,大概在1个小时里,是因为数据中心的两个时间master日常维护而关闭。我们将继续研究和消除TrueTime尖峰的原因。
5.4 F1
Spanner在2011年早期开始在生产压力下实验性地跑起来,作为Google广告后端F1重构的一部分[35]。之前它的后端是基于MySQL并人工做分片。未压缩的数据在数十T字节,相比于许多NoSQL实例来说是很小的,但已经足够大了导致Mysql分片很困难。Mysql分片方案将每个客户及其相关数据放到一个固定的分片里。这种布局允许基于每个客户的索引及复杂查询处理,但对于应用业务逻辑来说需要知道分片的相关知识。随着客户数和数据的增长,对于这种收入很关键的数据库来说要做重新分片代价是非常大的。上一次重新分片花了两年时间的努力,包括跨多个组的协调和测试以降低风险。这个操作也是非常复杂的,最后,为了限制Mysql的增长,我们把一部分数据存到了Bigtable里,但这也损失了事务性以及跨所有数据查询的能力。
F1团队选择使用Spanner基于以下一些理由。第一,Spanner消除了手工重新分片的需求。第二,Spanner提供了同步的复制和失效自动切换。采用Mysql主从复制失效自动切换时比较困难的,也增加了数据丢失的风险和宕机时间。第三,F1需要强事务语义,其它NoSQL系统还没有支持的。应用语义需要跨任何数据的事务支持和一致性读。F1团队也需要数据上的第二索引(因为Spanner还没有提供自动的第二索引支持),它们能够使用Spanner的事务机制自己实现一致的全局索引。
所有应用的写现在默认通过F1发送到Spanner,而不是基于MySQL的应用栈。F1有两个副本在美国的西海岸,3个在东海岸。副本数据中心这样选择主要考虑自然灾害导致的停电,也包括其它前端机器。有趣的是,spanner的失效自动切换对它们来说几乎是不可见的。尽管在前几个月发生过集群整个宕的情况,F1团队要做的最多的是更新他们数据库的schema以告知spanner哪儿优先放置Paxos领导者,从而使得他们离前端机器更近。
Spanner时间戳语义使得F1维护从数据库状态计算出来的内存数据结构非常有效。F1维护了所有更改的逻辑历史日志,他们也是作为事务被写入Spanner的。F1通过某个时间点的数据快照来初始化它的数据结构,然后读取增量的更改记录并完成更新。
表5描述了F1中每个目录分段数的分布。每个目录在F1上的应用栈中对应一个客户。大部分的目录(即客户)只包含1个分段,这代表对这些客户数据的读写只发生在一个服务器上。哪些超过100个分段的目录都是包含F1辅助索引的表,对这样表多个分段的写是相对少见的。
表6显示了Spanner在F1中的操作延迟。在东海岸数据中心的副本有更高的优先权选Paxos领导者。表中的数据是通过这些数据中心的F1服务器测量的。由于锁冲突导致的长尾使得写延迟标准差很大。读延迟上标准差更大,原因是Paxos领导者跨了两个数据中心,其中只有一个有些机器是用了SSD的。另外,这些数据也包括两个数据中心的读,读平均和标准差大约在1.6KB和119KB。
6.相关工作
Megastore[5]和DynamoDB[3]作为存储服务提供了跨数据中心的一致性复制。DynamoDB提供了key-value的接口,且只能在一个region里复制。Spanner借鉴了Megastore提供的版关系型数据模型,并提供了类似的schema语言。Megastore的性能不是太好,它基于Bigtable,这也导致了很高的通信开销,另外也不支持长期存活的领导者,多个副本需要初始化写。多个副本上的所有写在Paxos协议里是冲突的,尽管他们在逻辑上并不冲突,吞吐英文Paxos组每秒只能处理几次写而极具下降。Spanner提供了高性能、通用的事务机制,以及对外一致性。
Pavlo在[31]比较了数据库和MapReduce[12]的性能。他们介绍了一些其它探索数据库功能架在分布式key-value[1,4,7,41]存储上的进展,作为两者可以合并的证据。我们同意这些结论,但也得提一下整合多层结构也有它的优势,比如整合副本并发控制减少了Spanner提交等待的开销。
在多副本存储上架事务的概念至少可以追溯到Gifford的论文[16]。Scatter[17]是最近基于DHT的key-value存储,在一致复制的基础上架了事务机制。Spanner相比于Scatter重点提供了高层接口。Gray和Lamport[18]描述了基于Paxos的非阻塞提交协议。他们的协议相比于两阶段提交增加了很多消息开销,这种广域分布的组上进一步增加了提交的开销。Walter[36]提供了快照独立性,但是没有跨数据中心。相反,我们的只读事务提供了更自然的语义,因为我们支持所有操作的对外一致性。
最近有一系列关于减少甚至取消锁开销的故障。Calvin[40]取消了并发控制:它预先分配时间戳然后以时间序执行事务。HStore[39]和Granola[11]都支持他们自己的一类事务类型,有些是可以避免锁的。这些系统都没有提供对外一致性。Spanner通过快照隔离的支持解决了争用问题。
VoltDB[42]是一个分片全内存数据库,它支持广域网上的主从复制以便于灾难恢复,但并没有更多的复制配置。有一个例子,现在被称作NewSQL,实际上是市场对支持可扩展SQL[38]的推动。一些商业数据库的实现也阅读过,如MarkLogic[26]、Oracle’s Total Recall[30]。Lomet和Li[24]描述了这样一个临时数据库的实现策略。
Farsite衍生出来的时钟不确定性边界(比TrueTime要宽松很多)是可靠时钟的一份资料[13]:Farsite中维护的服务器lease方法和Spanner维护Paxos lease一样。弱同步时间在之前的工作[2,23]里也有用于并发控制目的的。我们已经说明过TrueTime需要跨多组Paxos状态机全局时间的理由。
7.进一步的工作
去年的大部分时间我们和F1团队一起把Google的广告后台系统从MySQL迁移到Spanner上。我们现在正改进监控和支持工具,以及提升性能。另外,我们也在改进备份/恢复系统的功能和性能。目前我们在实现Spanner的schema语言,第二索引的自动维护,以及自动化的动态分片以平衡负载。长期来说,还有一组功能我们计划研究下。并行的乐观读可能是个有价值的策略,单之前的实验表明要做到一个好的实现并不太容易。另外,我们计划支出Paxos配置的直接修改[22,34]。
我们预期很多程序需要复制它们的数据到相对接近的几个数据中心里,TrueTime E可能会明显影响性能。我们并没有看到把E降到1毫秒以下有什么不可逾越的障碍。时间master的查询间隔可以降低,更好的时钟晶体相对便宜。时间master的查询延迟可以通过网络技术的改进得以降低,甚至可能通过其它时间分布技术来完全避免。
最后,还有些可改进的领域。尽管Spanner可扩展到一组节点,单节点的数据结构对于复杂的SQL查询性能相对较低,因为它们被设计面向简单的key-value访问。DB文献里的一些算法和数据结构可以很大地改进单节点的性能。第二,数据中心里的数据自动迁移以适应客户端负载的变换使我们的长期目标,但要达到这个目标,我们需要在数据中心间自动移动客户端程序处理过程的能力。移动处理过程又提出了一个更复杂的问题,管理数据中心间资源的访问和分配。
8.结论
总结一下,Spanner结合并扩展了两个研究领域的主要思想:从数据库社区,一个熟悉的、更容易使用、半关系化的接口、事务性、类SQL的查询语言;从系统社区,扩展性、自动分片、容错、副本一致性、对外一致性、广域网分布。从Spanner开始,我们已经花了超过五年的时间迭代目前的设计和实现。这阶段很大一部分时间花在了Spanner关于如何解决全球复制的命名空间以及Bigtable所缺失的数据库功能的实现上。
我们设计的一个方面展示出:Spanner功能集的关键是TrueTime。我们已经描述了在时间API里使时钟不确定性具体化,从而构建了具有更强时间语义的分布式系统。另外,作为底层系统实施越严格的时间不确定性边界,强语义的开销越降低。在业界,我们在设计分布式算法时不应该再依赖于宽松的同步时钟和弱时间API。