Spanner: Google的全球分布级数据库----论文摘要

Spanner中一个新奇的time api揭示了时钟的不确定性。该api及其实现对于支持外部一致性(外部观察一致性)以及一系列强力的特性至关重要,这些特性包括:对过去版本数据的无阻塞读(对于历史数据的读不加锁,且不会被其他事务阻塞)、无锁的只读事务(只读事务不加锁,但是有可能被其他事务阻塞)、以及在整个spanner集群中原子性的修改schema。

Spanner在最高层次的抽象中,它将数据分片到很多Paxos状态机中,这些状态机处于遍布全世界的很多数据中心中。副本用于全球性的可用性以及地理上的本地性。client端可以在不同副本间自动失败重试。

当数据量或Server数量发生变化时,spanner会自动的在机器之间重新分布(rehash)数据,甚至会在数据中心之间迁移数据。

spanner设计用来扩展到数百万台机器,遍布于成百上千的数据中心,以及上万亿行的数据。其中,google F1基于spanner使用五副本将自己的数据遍布美国。

大部分应用将自己的数据复制到3到5个处于相同地理区域的数据中心的副本中。这意味着,大部分应用选择更低的延迟胜于更高的可用性,它们能承担1到2个数据中心的失效。

Spanner从一个像BigTable一样的带版本的<K, V>存储演变成一个多版本数据库。数据保存在模式化的(schemed)半关系型表中;数据带有版本号,每一个版本自动的打上其提交时的时间戳;老版本的数据会被可配置的垃圾回收策略处理;应用可以读取旧时间戳的数据。作为一个NewSql数据库,spanner支持通用的事务,并提供sql查询语言。

Spanner有两个特征是基于分布式时间戳服务的分布式数据库非常难以实现的:它提供了外部一致性的读和写,以及整个db的全局一致性的读(基于时间戳的)。这些特性使得spanner支持一致性的备份,一致性的MR计算,以及原子性的schema修改————全都是在全球范围内,即使有正在执行的事务存在时。(而对于某些单点时间戳服务的分布式数据库,其扩展性受限于单个机房,Oceanbase就是典型代表)

这些特性都基于Spanner给事务分发全局有意义的提交时间戳,即使事务是分布式的。这些时间戳反映了序列化的次序,这个序列化的次序满足了外部一致性(或称为线性一致性linearizability):如果事务T1在事务T2开始之前提交(commit),那么T1的提交时间戳一定小于T2的(这就是所谓的因果关系,因为T1和T2存在因果关系:也即是T2可能由T1触发)。spanner是第一个在全球范围内提供这一保证的系统。其通过提供一个TrueAPI及其实现(使用GPS和原子钟,实现可以将不确定性保持在10ms以内,未来可以降低到1ms)。

一个Spanner的部署被称为一个universal,目前google部署了三个:test/playground, development/production, production-only。spanner组织为一系列的zones,每一个zone大体上类似于一个BigTable的布置(有一个独立的时间戳服务器)。zones是管理布局的单元。一个zone有一个ZoneMaster和从一百到数千的SpanServers。前者分配数据给SpanServers;后者向client提供数据服务。每个zone都有location proxy被client用于定位为其提供所要求的数据的服务的SpanServer。

全局的master和placement driver现在是单例。universe master主要是一个展示所有zone信息的终端。placement driver处理跨zones的数据自动迁移,在分钟级的时间尺度上。placement driver周期性的同SpanServers沟通,找出需要被移动的数据(要么就是适应更改副本数的约束,要么就是为了balance平衡)。

SpanServer底层基于的是BigTable的相关技术。在最底层,每一个SpanServer负责100到1000个tablets,抽象上类似于BigTable的tablet,表现为其实现了一堆的如下映射:(key:string, timestamp:int64) -> string不像BigTable,spanner将时间戳timestamp分配给数据data,这使得spanner更像一个多版本数据库而非K-V store。一个tablet的状态保存在一堆类似于B树(B-tree-like)的文件(sstable),以及一个WAL文件中,全部放在一个分布式文件系统colossus中。

为了支持复制(replication),SpanServer为其上的每一个tablet实现一个单独的paxos状态机(早期为一个tablet实现多个paxos状态机,但是因为太复杂放弃了)。每一个paxos状态机保存它的元数据和log在它对应的tablet上(这块有些略难理解,对于raft实现而言,raft复制组本身的元数据及log放在文件中便可,其维护的状态机可以是一个rocksdb库,当然复制组的元数据和log也可以放在rocksdb中,与实际复制的数据混放在一起(cockroachdb就是这么干的))。主节点的租约为10S,支持长期保持leader地位的leader(主动限制了paxos的混乱序提交)。

我们对于Paxos的实现是pipelined的,如此以改善吞吐率,尤其是考虑到WAN的延迟,但是写会被顺序地应用在paxos上。

SpanServer对每一个leader状态的tablet副本,都实现了一个lock table锁表(cockroachdb也实现了lock table),来支持并发控制。该锁表包含了两阶段锁的状态信息:从keys的范围到锁状态的一个map(keys的范围代表锁的是规则,否则无法避免phantom read)。注意有一个长生命周期的paxos leader对有效管理锁表是至关重要的,若没有long lived leader的话,无法维护锁表。paxos本身没有长期主的概念,每一个节点都可以发起写操作,但是google在spanner中对其专门进行改造以支持长生命周期的leader。

在BigTable和Spannner中,我们都为了长生命周期的事务而设计(例如几分钟),这些事务在乐观锁并发控制下表现的非常差(由于冲突)。要求同步的操作,例如事务性的读,要获取锁表中的锁;其他操作则无视lock table。另外,SpanServer也为每一个作为leader的tablet副本实现了一个事务管理器(transactionmanager),用来支持分布式事务。transaction manager用来实现一个参与者leader,其他副本被称为参与者slave。如果一个事务只关联到了一个Paxos组(大部分事务场景下),它都可以直接忽略该事务管理器,由lock table和paxos协同提供事务性。而如果一个事务涉及到了多个paxos组,那么这些组的leaders会协作,以完成一个两阶段提交。其中的一个参与组会被选定为协调者coordinator:每一个transaction manager的状态都被保存在低层的paxos组中(因此也会被复制)[而锁表不会]。

在对key-value映射的最顶层的包装中,spanner实现支持一个称为directory的分桶抽象,其为一组连续的key值,这些key值拥有共同的前缀。对directory的支持允许了应用通过仔细选择keys来控制他们数据的本地性。directory是数据安置的单位(unit)。一个directory中的所有数据拥有相同的副本配置。当数据在paxos groups之间移动时,它一个directory一个directory的移动(一个复制组里可能有多个directories)。spanner可能因为如下原因而移动一个directory:降低一个paxos组的负担;将经常一起访问的directories放在同一个组中;将directory放置在离它的访问者更近的组中。Directories可以在client仍在操作的情况下被移动。可以预期一个50MB的directory在几秒之内移动完毕。

Paxos组可能包含多个directories的事实隐含了spanner的tablet不同于BigTable的tablet:前者并不一定是一个单一的字典序的row空间上的连续分区。相反,spanner的tablet是一个容器,该容器可能包含了row空间上的多个分区(partition)。我们做出如此设计,是为了将多个经常被一起访问的directories放置在一起。

Movedir是后台任务,用来在paxos groups之间移动directories。Movedir也用来添加或删除paxos groups中的副本,因为spanner尚不支持paxos内部的配置修改(可理解为paxos组自己不会主动寻求添加节点或删除节点,而需要由外部调用(join/leave),atomix也是这种策略)。Movedir不是被实现为一个事务一样,这样会在大量数据移动时阻塞正在进行的读和写。相反,Movedir只是记录一下它开始移动数据了,并在后台开始移动,一旦它将所有的名义上的数据全部移动完毕,它使用一个事务来原子性的改变名义上的数据并更新两个paxos组的元数据。(此处实现类似写时复制+乐观锁的方式)

directory是应用可以指定数据副本放置位置的最小单元。一个管理者对副本的管理控制由两种维度:(1)数量和类型;(2)其地理位置。一个应用可以通过给database和/或单独的directory打上这些标签组合(例如:North America, replicated 5 ways with 1 witness)来控制数据被如何复制。实际上,spanner会在一个directory变得过大时,将其shard为不同的fragments(片段),Movedir实际上移动的是fragments,而非整个directory。

spanner给自己的应用提供了如下的数据特征:一个基于有模式(schematized)的半关系型表的数据模型,一个sql查询语言,以及一个通用型事务。

对于格式化半关系型表和同步复制的需求来自于Megastore的流行(BigTable只支持在数据中心之间的最终一致性复制、单行事务以及稀疏表格模式)。并且BigTable缺少跨行事务也导致了不少的抱怨:percolator部分地因此而被设计出来。

在paxos组上执行两阶段提交减轻了可用性的问题(提供了比传统两阶段提交更好的性能表现,详情可以参考一下cockroachdb的事务优化策略)。

应用的数据模型基于directory-bucketed的K-V映射之上。一个应用创建一个或多个databases(在全世界)。每一个databases包含不限数目的带格式的表,表看起来就是关系型数据库的表,有行、列和多版本数据。spanner的数据模型并不是纯关系型的,它的行(rows)必须有名字,更精确的说,每个表都要求有一个或多个PK列(主键,这种数据模型与Kudu类似)。该要求也是spanner仍看起来像一个K-V store的地方。采用这个结构是有用的,因为它允许应用通过key值的选择来控制数据的本地化。

spanner的schema定义语言类似于Megastore的,有如下额外的要求:每一个spanner的database必须被client分区为一个或多个层次结构的表。client通过“INTERLEAVE IN”声明schema中的层次结构。层次结构中的顶层的表示一个directory表。每一个directory表的行都有一个key k,伙同子孙后代表的所有以k为key开头的行,共同构成了一个directory:

create table Users {uid INT64 NOT NULL, email STRING} primary key (uid), DIRECTORY;
create table Albums {uid INT64 NOT NULL, aid INT64 NOT NULL, name STRING} 
    primary key (uid, aid)
    INTERLEAVE IN PARENT Users ON DELETE CASCADE;	//其重要意义在于构建表之间的本地性关系,其对于分片的、分布式的数据库的性能表现非常重要

TrueTime明确的将时间表示为一个TTinterval,其实一个具有明确的界限的不确定性时间间隙(不像标准的时间接口一样明确返回一个值,而不去展示不确定性的概念,实际上标准的时间接口给出的时间又是不确定的)。TrueTime采用GPS和原子钟实现,每一个datacenter都有一个time master,每台机器上有一个time slave。在我们的生产环境中,ε是一个典型的锯齿状态的函数(随时间的),从1-7ms而变化。ε平均值因此大约为4ms(大部分时间里)。由于时不时的time-master不可用,以及机器的过载及网络等都会时不时的导致小范围的ε尖刺。

 

在并发控制中,需要区分paxos复制组看到的“writes”(我们称为paxos序号),与spanner client的写“writes”。例如,在两阶段提交时,在prepare阶段会创建一个paxos写(write),该write没有对应spanner client的任何write。

spanner实现支持读-写事务、只读事务(预声明的快照隔离级别的事务)、以及快照读。单独的写被认为是读写事务;而非快照的单独的读被认为是只读事务。这两者都会在内部retried执行(client端不需要写其retry loop逻辑)。

只读事务(会先确定读操作的时间戳)是一种得益于快照隔离级别带来的性能表现的事务。一个只读事务并非简单的没有写的读写事务。在一个只读事务中,读操作基于一个系统选择的时间戳执行,并且是无锁的,因此后续的写事务不会被阻塞。只读事务中的读操作可以在任何一个足够“新”(up-to-date)的副本上执行。

快照读(snapshot read)是无锁的且非阻塞的读过去的版本数据。client可以指定一个timestamp或提供一个旧数据的上限,由spanner来选择一个时间戳,其同样可以跑在任何足够“up-to-date”的副本上。

读-写事务使用两阶段锁。因此,它们可以被赋予所有锁被获得且尚未释放之间的任意一个timestamp。对于一个事务,spanner为其指定的时间戳是paxos为代表了事务提交(transaction commit)的paxos写所指定的时间戳。(spanner由于采用了paxos,因此其可以将paxos的写时间戳与事务时间戳统一,而raft是不能的)

spanner依赖于如下的单调不变性:在每一个paxos组中,spanner单调递增的为paxos写指定timestamps时间戳,即使在跨leaders(leader切换)之间(这是基于paxos复制组允许跳跃非连续的log entry index的特性,这就允许了paxos采用和spanner事务时间戳一致的值。而raft由于其log entry index必须连续,因此无法具备这个很核心的特性)。一个leader副本也同样可以简单的指定(分发)单增的时间戳。该不变性在跨leaders(leader切换)仍有效,是因为使用了如下的不想交无关性:一个leader一定要在其取得leader lease租约后才能分发时间戳。注意到任何时候一个时间戳S被分发,Smax都确保大于S。而当一个leader被允许退位(可以重新选举)时,必须确保TT.after(Smax)为true才可以。如此一来,新当选的leader在拿到leader lease之后,当前时间戳必须是大于原来的Smax的。(此处保证的是同一个paxos组中leader切换时新老leader之间时间戳的单调递增性)

spanner同样强迫要求了如下的外部一致不变性:如果事务T2发生在事务T1的commit之后,那么T2的提交时间戳(commit timestamp)一定大于T1的提交时间戳:

Tabs(E1(commit)) < Tabs(E2(start)) => S1 < S2

执行事务和分发时间戳遵循两个规律,两者结合确保了上述的外部一致不变性。设对于写事务Ti的协调者coordinator得到了commit请求的事件为Ei(server)(也就是决定要提交的事件):

  • start规则:coordinator leader对于写事务Ti分发一个提交时间戳Si,该提交时间戳Si不小于TT.now().latest。TT.now().latest是在收到请求Ei(server)进行计算的值。也就是说Si >= Tabs(Ei(server)) //给Ti分配的时间戳一定大于Ti执行提交commit开始的绝对时间
  • commit规则:coordinator leader确保client在TT.after(Si)为true之前不能看见Ti事务的提交。也就是说commit执行完成的时间一定在TT.after(Si)之后。因此Tabs(Ei(commit)) > Si。//commit完成的绝对时间一定大于给Ti分配的时间戳

其核心在于,由于TrueTime有一定的不确定区间,因此将无法严格判断先后顺序的两个事务的commit()方法拉长,使其之间变得有交叠(overlap)。这种相互有交叠的事务相互之间就不可能有因果关系(一个事务完成了,视其结果如何又触发了另一个事务,这叫有因果关系),其产生的结果谁先谁后都是可以的。而拉长之后仍无交叠的两个事务,其相互之间有因果关系,而且其顺序也可以严格的确定。因此经过这两个规则之后,spanner通过增加单个事务的响应时间(2ε)实现了严格的外部一致性隔离级别。

敢于如此处理的原因在于TrueTime的精确性,平均4ms以内,最长不超过7ms。而对于依赖NTP的HLC算法的分布式数据库,其代价就过大而变得暂时无意义了。

 

前面spanner提供的单调不变性允许spanner来准确的决定一个副本的状态是否足够up-to-date来满足一个read。每一个副本都保留一个叫做安全时间Tsafe的值,该值代表了本副本up-to-date的最大时间戳。一个副本可以满足t <= Tsafe的时间戳的读。定义Tsafe=min(Tsafe(paxos), Tsafe(TM)),每一个paxos状态机有一个安全时间戳Tsafe(paxos),而每一个transaction manager(每个leader维护一个的)有一个安全时间戳Tsafe(TM)。

Tsafe(paxos)更简单:它代表了paxos write被应用了的最高的时间戳。因为timestamp单调递增,而且写操作会被顺序应用,因此不会再有小于等于Tsafe(paxos)时间戳的写发生。

Tsafe(TM)在一个没有任何prepare但尚未提交的事务存在的副本中是∞大————在prepare与commit之间的事务正处于两阶段提交的中间阶段(对于一个参与者(participant)slave,Tsafe(TM)实际上指向副本leader的事务管理器。该slave可通过其传来的paxos writes的元数据推断出事务管理器的状态)。如果存在了这样的未提交事务,那么这些事务影响的状态就是不可确定的:一个参与者副本当下并不知道这些事务是否会提交。

  • 提交协议会确保每一个参与者(participant)都知道对于每一个prepared事务的时间戳的最低界限(low bound)(这应该就是prepare commit时执行一次写入的原因)。每一个参与者leader(对于一个group g),针对一个事务Ti会分发一个prepare timestamp称为Sprepare(i, g),为其prepare记录。协调者leader确保事务的提交时间戳Si>=Sprepare(i, g)(对每一个参与者的组g)。因此,对于group g的副本中的所有处于prepared状态的事务Ti:
Tsafe(TM) = min(Sprepare(i, g)) - 1	//所有本group中处于prepared状态的事务Ti的Sprepare(i, g)的最小值-1。

只读事务执行分两个阶段:分发一个timestamp Sread,然后通过该Sread执行快照读。快照读可以在任何足够up-to-date的副本上执行。最简单的Sread的分发方式为Sread = TT.now().latest。以和写操作同样的理由来维护外部一致性:使用这样的Sread作为时间戳一定确保了发生在只读事务开启时刻之前提交(commit)的所有事务,其Si(提交时间戳)都小于Sread(都能被看见,为了做到这一点,必须在TT.after(Sread)为true时再实际执行查询)。

然而如此一个时间戳可能要求在Sread上读的操作被阻塞,如果Tsafe尚未进展到足够的话。加入此时有prepared timestamp小于Sread而导致Tsafe尚不足以支持Sread时,其就需要被阻塞以等待Tsafe足够支持Sread。为了减少阻塞的可能性,spanner应该分发的是可保持外部一致性的最老的timestamp,后面会解释如何选择。(注意,选择一个Sread值可能会导致Smax值的增加,以保持paxos复制组时间戳的不相关一致性)

并发控制的细节

像BigTable一样,事务中的写会缓存在client端直到提交。结果是,一个事务中的读无法看见本事务中的写的效果。该设计在spanner中工作的还好,因为读会返回所读数据的时间戳,而未提交事务的写尚未被分配时间戳。

读写事务中的读使用wound-wait来避免死锁(为何使用wound-wait,读锁会被抢占,因为(1)读的时候使用的locktable,为避免phantom read锁的是范围,范围可能会非常大,若不抢占read锁可能会阻塞非常多的并发事务;(2)读锁发生在读写事务的早期,而写锁发生在commit提交阶段,因此抢占读锁代价小,抢占写锁代价大)。client会向合适的分组leader副本发读操作,申请读锁并读取最新的数据(read committed)。当一个client开启的事务仍在开着,它就通过发送KeepAlive消息来阻止参与者的leaders超时该事务。当一个client完成了所有的读并buffer缓存了所有的写,它就开始执行两阶段提交了。读写事务的读写执行阶段并无时间戳的概念,每一个读都会读取当前所能看到的最新版本,并取得读锁(因而也避免了在本事务期间该被读数据被其他并发事务修改的可能性,基于范围的读锁避免了幻读),每一个写都是直接缓存在client端本地,等到commit阶段发送到Server。(事务并发执行锁的wound-wait行为应该是读锁会等待写锁,写锁会抢占读锁)

client选择一个coordinator组并发送一个提交信息(commit message)给每一个参与者(participant,包含了coordinator)的leader,其中包含了coordinator的标识identify以及缓存的writes。

  • 非coordinator的参与者leader首先获取write locks,它然后选择一个比以前分发给其他事务的任何timestamp都大的时间戳作为prepare timestamp,并通过paxos记录一个prepared record(中间包含了写数据,另外也用于确定本group的Tsafe(TM))。每个participant随后将自己的prepare timestamp通知给coordinator。
  • coordinator leader首先同样获得写锁,但是略过prepare阶段。它在听取了所有其他参与者leader汇报的prepare信息后,为整个事务选择一个提交时间戳。该提交时间戳S必须大于等于所有participants的prepare timestamps(以确保Tsafe(TM)是安全的),并大于TT.now().latest(此处的now是在coordinator收到commit消息时执行的,也就是决定提交的时间),并大于本leader给以前的事务分配的最大时间戳(同样,为了维护时间戳单调性)。coordinator leader随后通过paxos记录一条commit record(或者在超时时候记录一条abort record)。

在允许任何一个coordinator副本应用该commit record之前,coordinator leader等待直到TT.after(S)为true(前面所述的commit规则,延迟提交)。因为coordinator leader选择S基于TT.now().latest,并且等待直到该S一定属于已过去的时间,因此期望的等待时间最小为2*ε。该等待时间一般与paxos复制的时间交叠overlap。在commit wait之后,coordinator发送commit timestamp(提交时间戳)给client以及所有参与者leader,也包括自己。每一个参与者leader都通过paxos记录事务的结果。所有的参与者应用这个相同的提交时间戳timestamp然后释放锁(状态机的应用apply()会把已复制的log entry应用到状态机上。对于participant而言,其待写数据已经在写prepare record时由leader复制到followers了,实际应用时只是将log entry中记载的数据应用到状态机而已)

对于Read Only事务,分配一个timestamp要求一个在本事务中读取的所有paxos groups之间的协商阶段。因此,spanner要求一个只读事务提供一个范围表达式(该范围表达式与lock table中的key范围定义的语义相同),该表达式概括了整个事务要读取的keys的范围。对于单条查询,spanner自动的推导出查询范围。

  • 如果范围的值被一个单个的paxos组所服务,那么client直接将该read-only读事务发送到该group的leader上(当前spanner的实现只通过paxos leader来选择只读事务的timestamp)。该leader会分发一个Sread并执行该read操作。(只涉及一个组时无需协商,由该组的leader自己便可以确定)

对于一个单地点(单个group)的读,spanner一般可以比使用TT.now().latest做的更好。定义LastTS()是最后一次在paxos groups中提交的paxos write的时间戳,若本group中没有prepared transactions,那么Sread = LastTS()就简单的满足外部一致性:事务能看见自己开启之前已提交的最后一次写的结果,因此本事务在最后一次写事务之后便可。(否则,若有prepared transactions,则就使用TT.now().latest了,因为并不能确定这些prepared transactions最终的提交时间戳S是否会小于本事务启动时的绝对时间,为了保证外部一致性,只能使用TT.now().latest作为Sread)

若多个paxos组服务于读的范围,则由几种不同的选项。最复杂的选项是与所有groups的leader来协商Sread,基于其LastTS()。spanner当前使用了一个简单的策略,避免了协商,仅仅取了Sread = TT.now().latest(该时间戳一定可以保证外部一致性,也即是本事务开始前提交完成的所有事务都会被读到。但是可能需要等待安全的时间之后才能执行,会被阻塞,具体场景前面有说明)。只读事务是无锁的,其自身可能会被阻塞(需要等待),但其不会对后面的写操作产生阻塞。

TrueTime允许spanner支持原子性的修改schema。由于database中的groups可能有数百万之多,因此不太可能使用标准事务来修改schema。BigTable支持在一个数据中心中原子性的修改schema,但它的schema修改会阻塞所有操作。spanner的修改schema事务是一个标准事务的无阻塞变种

  • 首先:它被明确的分发一个未来的时间戳,该时间戳在prepare阶段被注册(register)。如此一来,在数千个节点中的schema change可以最小化的打扰其他并发的行为。
  • 其次:读和写明确的依赖于schema,会与任何的已注册的schema-change的时间戳t进行同步:如果它们的时间戳在t之前,则可以以原来的schema继续执行;但是若它们的时间戳晚于t了,则必须阻塞在schema change事务之后(因此,整个database会在给定的时间戳t之后同时更改到新的schema之上)。

优化改良

Tsafe(TM)有一个问题,任何一个处于prepared的事务都会直接阻止Tsafe的进展,也就是说,但凡有prepared的事务存在,Tsafe都只能取值到min(Sprepare(i, g))-1。而不管后面提交了多少其他事务,以及新开的只读事务是否与该prepared的事务有冲突(并发控制的粒度有些粗)。可通过细粒度的由key range到prepared transaction timestamp的映射来去掉这种不必要的冲突。这个映射信息可以保存在lock table中,因为在lock table中已经为prepared的事务维护了key range映射到lock的元数据了。

LastTS()有同样的问题:如果一个事务刚刚提交(开始提交执行),一个没有冲突的只读事务同样要被分发Sread。那么该Sread要被排在该提交事务之后。因此,该只读事务就可能会被delayed。该问题可以被同样的修正,使用一个从key range到提交时间戳的细粒度的映射,同样放在lock table中。

Tsafe(paxos)有一个问题,就是若没有后续的paxos写,则它无法被增加。意思是,一个以t为时间戳的快照读不能在最后一次写的时间戳小于t的paxos组上执行(因为不够up-to-time)。spanner通过利用leader租约无关性来解决。每一个paxos leader通过保持一个值来增加Tsafe(paxos),该值指出了一个未来的写操作被分配的时间戳一定要比它大:paxos leader维护了一个MinNextTs(n)映射,从paxos的顺序号n到可以被分发给paxos顺序号n+1的最小时间戳。一个副本可以在应用了n之后将其Tsafe(paxos)增加到MinNextTs(n)-1。一个leader可以简单的维护其MinNextTs()承诺,由于MinNextTS()承诺的timestamp依赖于leader的lease,因此不想交无关性就可以确保在leader切换时MinNextTS()仍可以被保证。一个leader默认每8秒推进一下其MinNextTs(),如此一来,在没有prepare事务的情况下,一个空闲的paxos组中的健康的slaves在最坏情况下会在8秒之内可以服务读操作。

测试

性能基准测试

  • 每个SpanServer运行在分时调度的系统上,4GB内存+4核(AMD Barcelona 2200MHz)。
  • client运行在独立的机器上(不占用spanner内部资源)
  • 每个zone包含一台SpanServer。client和zones放置在网络距离1ms以内的数据中心中(300公里以内或是同城,这种是最常见的场景)
  • 测试数据库创建了50个paxos groups以及2500个directories。操作是单个的4KB大小的读和写。
  • 所有的read数据都在compaction之后刷入磁盘,因而不会存在于内存表(MemTable)中。如此一来我们只测试spanner调用栈的负载。另外,先执行不计入测量的读预热来加载缓存。
  • 对于延迟测试,client发出足够少的操作以避免在server中排队:
  1.   在1副本测试中,提交wait是大概5ms,paxos写延迟大概在9ms,共14ms左右的延迟(从提交到收到响应)
  2.   随着副本数增加,延迟大体保持平坦,因为paxos组在组内副本间复制时是并行的。随着副本数增加,达成一致的延迟时间反而对单个慢副本的敏感度下降了。

对于吞吐量测试,client发出足够多的操作,因此使CPUs的负载饱和。快照读可以执行在任何up-to-date的副本上,因此其吞吐量随着副本数增加几乎呈线性增加。单个读的只读事务会在leader上执行,因为timestamp必须由leader来分发。只读事务随着副本数增加而增加,这是因为有效节点增加了:在本实验的设置下,SpanServer的个数与replica副本的个数是一样的。而总的paxos组不变,因此平均分到每台SpanServer上的paxos组leader就减少了。该有利条件又被随着副本数增加导致的工作量增加给中和一下。

两阶段提交可以扩展到一个可接受的参与者(participant)数量规模:

  • 在3个zone,每个zone含有25台SpanServer的集群中
  • 将事务扩展到50个participants,对于平均及99th-percentile的延迟都增长的偏慢。而当扩展到100时才开始显著提升了

Availability可用性测试

universe包含5个zone Zi,每一个zone有25个SpanServer。整个测试database分片成1250个paxos groups。100个测试client持续的执行非快照读,共同加起来的速率为50K/S(QPS)。所有的leader明确的置于Z1。(计算出单个client在500/S的请求速率,因此有Server一定余量,上面测试的只读请求的响应时间在1.5ms以内)。

5秒之后,一个zone中的所有Servers都被kill掉:

  • 在non-leader场景中,Z2中的Servers被kill
  • 在leader-hard场景中,Z1中的Servers被kill
  • 在leader-soft场景中,Z1中的Servers被kill,但是会先给它们发送切换leadership的通知

kill掉Z2并不影响读的吞吐量。给leader时间让其切换leadership然后kill掉Z1的情况只有很小的影响,大约为3-4%。而直接kill掉Z1会造成一个严重的影响:读事务完成率会降到接近0。随着重新选举出来leader,系统的吞吐量会升到大约100K/S,这是因为我们的试验有两个特点:

  • 系统由额外的处理能力(如上所述50K/S并未达到系统的边界)
  • leader不可用的时候,操作会被加入队列以等待可用的leader执行

我们同样可以看到paxos leader租约设置为10S所造成的影响(cockroachdb也使用租约来避免读取时还要完成一次共识)。当我们kill掉Z1时,leader租约过期时间应该平均的分布在接下来的10S内。大约在kill time之后的10S后,所有的分组都有leaders了,吞吐量就恢复了。

更小的lease时间可以降低Server挂掉对可用性的影响,但是会要求更多的租约更新lease-renewal网络开销。我们在设计并实现一种由slave在发现leader失效之后释放leader租约的机制。

F1的实验

从2011年开始,在实际产品的工作负担下对spanner实验性的评估就开始了。我们选用的是F1。F1原来将数据保存在手动以各种方式分片的Mysql中。其未压缩的数据在数十TB,该数据量对于很多NoSQL实例而言不算大,但是对于分片的MySQL而言很大了。MySQL将用户数据及其关联的其他数据分片到一个固定数目的shards中,如此布局可以有效的使用索引,以及设计对一个以用户为中心的复杂查询。但需要应用application能够了解业务逻辑。随着用户及相关数据的增加,对该应用执行resharding是非常昂贵的。

F1的布局为东海岸3副本,西海岸2副本,在选举时东海岸的副本有更高的优先级。

F1实测时平均read延迟在8.7ms,但是std标准差在376ms。写延迟的较大的标准差是由于锁冲突所导致的肥尾现象引起的。更大的读偏差一部分是因为paxos组的leaders分布在不同的数据中心中,只有其中的一个配备了SSD;另外,读取的数据大小也不一样。

相关工作

跨数据中心的一致性复制,已经由Megastore和DynamoDB提供了。DynamoDB提供一个KV接口,并且只在一个region区域中进行复制。

spanner遵循了Megastore提供的半关系型数据模型,甚至是相似的schema定义语言。Megastore无法达到较高的性能,Megastore基于BigTable,其采用了较高的通信代价。它同样不支持长生存周期的leaders:多个副本都可能发起写操作。不同副本发起的写操作必然会在paxos协议层面冲突,即使它们逻辑上并不冲突:一个paxos group在每秒几次写入时其吞吐量就会坍塌。spanner提供了更高的性能,标准的事务和外部一致性。

集成多层的结构有其优越性:在spanner中集成并发控制与复制,将事务构建于一致性复制之上,降低了commit等待。

spanner虽然在扩展性上做的非常好,但是在单机的复杂SQL查询上性能还是很一般。因为使用的是简单的K-V访问。Database课程中的算法和数据结构可能会对单点的性能优化很有帮助。

总结而言,spanner结合并扩展了两个社区的研究:

  • 从数据库社区而言:一个熟悉的、易用的、半关系型数据模型接口,事务,以及SQL查询语言
  • 从分布式系统社区而言:扩展性、自动分片、错误容忍、一致性复制、外部一致性,以及大区域部署

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值