Spanner是一个全球分布式的数据库,从数据模型来看Spanner很像BigTable,都是类似于key对应着一行数据,但是却并不一样,Spanner中衍生出了“目录”的概念(把两张表合并存储)。这并不是重点,Spanner的重是它是第一个在全球范围内传递数据且保证外部一致的分布式事务的系统,且支持几种特定的事务,这显然是一个很困难的问题,我们会在文章中加以描述,这篇文章主要对Spanner的事务以及实现事务所使用的 TrueTime API 进行分析,这些也是论文中描述最为详尽,也是比较不好懂的地方。还有之所以不分析Spanner的架构是因为我觉得论文(第二节)中此方面的描述实在是有些简略,所以直接看论文就可以。
我们在引言中提到实现分布式下的外部一致性非常困难,Spanner引入了一个新的API巧妙的解决了这个问题,这个API可以直接暴露时钟的不确定性,简单来说,就是可以给我们一个区间,保证现在的准确时间在这个区间以内,而且这个区间很小(上下浮动7ms),这就是TrueTime。
那么为什么实现分布式下的外部一致性非常困难呢?我们举两个例子来看一看:
例1: 主从复制
这是一个非常常见的模型,我们要解决的问题是Client向master发出一个修改操作,然后直接向Slave进行查询,显然如果在CA中选择可用性的话我们可能无法在Slave中看到刚刚进行的修改,这就不满足外部一致性了,有两种方法解决这个问题:
例2:跨分区的事务
当Client1执行一个跨分区的事务,也就是图中向master1,master2发送两个修改操作,然后Client向master1,master2分别发送出读取请求,一般而言我们必须把读操作也封装成一个事务,否则可能会出现读取的数据一半是修改之前,一半是修改之后,要避免这种情况也意味着读操作也会加锁,这会大幅的影响性能。
我们来看看TrueTime的API:
Method | Returns |
---|---|
TT.now() | TTinterval: [earliest, latest] |
TT.after(t) | true if t has definitely passed |
TT.before(t) | true if t has definitely not arrived |
有了这些API,实现一致性就方便了,假设“要求Client2执行的事务B一定在Client1执行的事务A之后”,使用了TrueAPI以后我们可以这么做:
t2 - t1 > 2ε
,其中ε为TrueTime最大偏移时间这样我们就可以保证事务B可以看到事务A了,这样很好证明:
(tabs代表事务a的绝对时间,t1 - ε < tabs(ea) < t1 + ε)
其中的ε为参考误差值,上限被Spanner设置为7ms,我们也可以看出精度越高,事务执行的效率就越高。
至于如此苛刻的时间需求该如何满足呢?Spanner基于GPS时钟和原子钟来实现这个功能,那么为什么要是用两种方式呢?论文中给出的解释是两种方法的失效原因并不相同,这可以最大限度的提升可用性,毕竟Spanner是一个全球性的分布式数据库。我们来看看两种方法的原理:
综合以上原因,Spanner决定同时采用GPS时钟和原子钟,并且以GPS时钟为主。这样可以既避免计时误差问题,又保证了GPS失效时的可用性。
Spanner中支持三种事务,分别为快照读
,只读事务
,读写事务
,我们来看看Spanner如何利用 TrueTime API 来实现事务的特性。
读写事务也可以只包含写事务,所以这也可以看成Spanner中对写操作的处理过程。我们会给这个事务的所有操作分配同一个时间戳(写入数据项中),我们希望所有这个读写事务提交后开始的事务的这个时间戳严格大于这个时间戳,且这个时间戳大于等于写操作的起始绝对时间,小于等于写操作commit绝对时间,流程如下:
我们可以看到读写事务的执行其实就是一个两阶段提交的过程,不过因为对每一个数据维护了时间戳,所以使得读操作可以不需要加锁,这也就在保证效率的情况下保证了读写的外部一致性。
我们说说上面提到的commit-wait
,这个机制有什么用呢?
我们知道TrueTime返回的是一个时间范围,当两个范围有交集的时候我们没办法确定它们的发生顺序。因为Spanner的支持快照读的外部一致性: 如果一个事务已经提交了,而另一个事务发起了,另一个事务是看得见已提交事务的影响的,换句话说就是**如果一个事务 T2 在事务 T1 提交以后开始执行, 那么,事务 T2 的时间戳一定比事务 T1 的时间戳大。**因为Spanner支持无锁的的只读事务和快照读,如果读事务的TrueTime和上一个事务的TrueTime有交集,那么我们没办法判断它们之间的先后顺序,所以引入了commit-wait机制,那么我们让上一个事务在after(ts)以后再commit,读事务就一定与上一个事务没有交集了。
流程如下:
因为Spanner是一个多版本的数据库,给人的感觉类似于MVCC,我们可以指定时间戳查询指定版本的数据,因为快照读引入了读等待所机制,副本节点达到足够新的状态之后才完成读取,因此即使数据复制是异步执行的,也可以保证跨副本节点的读写一致性(让人想起了zk,也是这样保证一致性的)。
只读事务是在没有指定时间戳时的读取操作,默认就是查询当前时刻的数据,流程如下:
Spanner可以看作对BigTable的改进,在paper中也提到:
尽管有许多项目可以很好地使用 BigTable,我们也不断收到来自客户的抱怨,客户反映 BigTable 无法应用到一些特定类型的应用上面,比如具备复杂可变的模式,或者对于在大范围内分布的多个副本数据具有较高的一致性要求
那么它们之间有什么大的不同呢?我认为有如下几点:
Bigtable只支持单行事务,Spanner则可以包含任意的写操作,使用2PC来实现。我并不觉得这是BigTable的缺点,相反,这在有时可以很好的提升效率(我们并不知道在Spanner中行是否存在一个paxos group中,如果是的话效率就差不多了),BigTable也提到了仅支持单行事务的初衷:
We initially planned to support general-purpose transactions in our API. Because we did not have an immediate use for them, however, we did not implement them. Now that we have many real applications running on Bigtable, we have been able to examine their actual needs, and have discovered that most applications require only single-row transactions.
BigTable做到了计算和存储的分离,这使得table迁移可以不移动底层的数据。而Spanner中数据由Paxos算法保证一致性,计算和存储在一个节点中完成。前者在table server宕机时不影响数据,master会把数据重新分配。
数据类型不同,BigTable是标准的k/v结构,且历史版本存放在一行中;Spanner中时间戳是键的一部分。
当然还有很多其他地方,仅仅列出几个比较直观的地方
其实论文对于Spanner的描述仅仅只是皮毛而已,很多重要的地方都没有讨论到,比如客户端如何确定在读或者读写事务时该向 Spanner server 内的哪些 tablt 服务器发送请求呢?在执行读写事务时锁的粒度?因为第一个问题没办法知道,为什么有这么高的可用性也就无从谈起了。但是Spanner对我们来说仍旧是一个很好的学习资料,因为它使用一个新的API做到了实现大范围分布式的外部一致性,且吞吐量并不差(异步的复制仍可保证读写一致性),打破了我对这个问题的固有认知。且事务的实现采用了2PC(2PC本身是有问题的,比如单点故障,数据不一致),这也是第一次见到2PC实际使用。
Spanner也给我了一个另外一种看CAP的角度,Spanner声称自己达到了CA,当然我们知道严格从技术上来说这是不可能的,Spanner从原理上来说这是一个CP的系统,但是Spanner称为CA的最主要因素就是,用户是否认可它的可用性,如果可用性够高,用户有时是可以忽略掉这个中断的,况且就算出现分区也不一定对用户有影响,而Spanner的可用性是超过5个9的。
参考:
http://www.nosqlnotes.com/