为什么选择这篇论文(Google Spanner, OSDI 2012)?
- 宽域分布式事务的罕见示例。
- 非常理想。
- 但是二阶段提交被视为太慢并且容易阻塞。
- 宽域同步复制的罕见示例。
- 巧妙的想法:
- 通过Paxos进行的两阶段提交。
- 同步时间用于快速只读事务。
- 在Google内部广泛使用。
动机用例是什么?
- Google F1广告数据库(第5.4节)。
- 之前分片在许多MySQL和BigTable DBs上;笨拙。
- 需要:
- 更好的(同步)复制。
- 更灵活的分片。
- 跨分片事务。
- 工作负载主要由只读事务主导(表6)。
- 需要强一致性。
- 外部一致性/线性一致性/可串行化。
基本组织结构:
- 数据中心A:
- “客户端”是Web服务器,例如用于gmail
- 数据在多个服务器上分片:
- a-m
- n-z
- 数据中心B:
- 拥有自己的本地客户端
- 以及数据分片的自己的副本
- a-m
- n-z
- 数据中心C:
- 相同的设置
-
通过Paxos管理复制;每个分片一个Paxos组。
-
副本位于不同的数据中心。
-
类似于Raft – 每个Paxos组都有一个领导者。
-
如实验室中一样,Paxos复制操作日志。
-
为什么采用这种安排?
- 分片通过并行性允许巨大的总吞吐量。
- 数据中心独立失败 – 不同的城市。
- 客户端可以读取本地副本 – 快速!
- 可以将副本放置在相关客户附近。
- Paxos只需要多数 – 容忍慢的/远程的副本。
面临的挑战是什么?
- 读取本地副本必须获得最新数据。
- 但是本地副本可能不反映最新的Paxos写入!
- 一个事务可能涉及多个分片 -> 多个Paxos组。
- 读取多个记录的事务必须是可串行化的。
- 但是本地分片可能反映了不同子集的已提交事务!
Spanner对读/写事务和只读事务进行不同的处理。
-
首先,读/写事务。
-
读/写事务示例(银行转账):
-
BEGIN x = x + 1 y = y - 1 END
-
我们不希望任何读取或写入x或y在我们两个操作之间偷偷进行。提交后,所有读取都应该看到我们的更新。
-
-
总结:使用Paxos复制参与者的两阶段提交(2pc)。
- (现在省略时间戳。)(这适用于读/写事务,而不是只读事务。)
- 客户端选择一个唯一的事务ID(TID)。
- 客户端将每个读取发送到相关分片的Paxos领导者(2.1)。
- 每个分片首先在相关记录上获取锁。(可能必须等待)。
- 每个分片的领导者都有单独的锁表。
- 通过Paxos不复制读锁,所以领导者失败 -> 中止。
- 客户端保持写操作私有直到提交。
- 当客户端提交(4.2.1)时:
- 选择一个Paxos组充当2pc事务协调器(TC)。
- 将写操作发送到相关分片领导者。
- 每个被写入的分片领导者:
- 在被写入的记录上获取锁。
- 通过Paxos记录一个“准备”记录,以复制锁和新值。
- 告诉TC它已准备好。
- 如果崩溃从而丢失锁表则告诉TC“不”。
- 事务协调器:
- 决定提交或中止。
- 通过Paxos将决定记录到其组。
- 告诉参与者领导者和客户端结果。
- 每个参与者领导者:
- 通过Paxos记录TC的决定。
- 释放事务的锁。
-
只读(r/o)事务
-
Spanner为r/o事务消除了两个大成本:
- 从本地副本读取,避免Paxos和跨数据中心消息。
- 但请注意,本地副本可能不是最新的!
-
没有锁,没有两阶段提交,没有事务管理器。
- 再次避免跨数据中心消息到Paxos领导者。
- 并避免减慢读/写事务。
-
表3和表6显示结果是10倍的延迟改善!
-
如何与正确性相协调?
- 从本地副本读取,避免Paxos和跨数据中心消息。
-
r/o事务的正确性约束:
-
可串行化:
- 结果与事务逐一执行相同。
- 尽管它们实际上可能并发执行。
- 即r/o事务必须本质上适合在r/w事务之间。
- 看到之前事务的所有写入,之后事务的没有。
- 结果与事务逐一执行相同。
-
外部一致性:
- 如果T1在T2开始之前完成,T2必须看到T1的写入。
- “之前”指的是实际(挂钟)时间。
- 类似于线性一致性。
- 排除了读取陈旧数据。
- 如果T1在T2开始之前完成,T2必须看到T1的写入。
-
-
为什么不让r/o事务只读取最新提交的值?
-
假设我们有两次银行转账,和一个读取两者的事务。
-
T1: Wx Wy C T2: Wx Wy C T3: Rx Ry
-
结果将不符合任何串行顺序!
- 不是T1, T2, T3。
- 也不是T1, T3, T2。
-
我们希望T3看到T2的所有写入,或者一个也不看。
-
我们希望T3的读取全部在与T1/T2相同的点发生。
-
-
想法:快照隔离(SI):
-
同步所有计算机的时钟(到实际挂钟时间)。
-
给每个事务分配一个时间戳。
- 读/写:提交时间。
- 读/只:开始时间。
-
执行时,仿佛按时间戳顺序逐一进行。
- 即使实际读取顺序不同。
-
每个副本存储每条记录的多个时间戳版本。
- 一个读/写事务的所有写入获得相同的时间戳。
-
-
一个读/只事务的读取看到的是事务时间戳时的版本。
- 记录版本的时间戳小于事务的,且是最高的。
-
-
我们的例子使用快照隔离:
-
x@10=9 x@20=8 y@10=11 y@20=12 T1 @ 10: Wx Wy C T2 @ 20: Wx Wy C T3 @ 15: Rx Ry
-
“@ 10”表示时间戳。
- 现在T3的读取将都来自@10的版本。
- T3不会看到T2的写入,即使T3的读取发生在T2之后。
- 现在结果是可串行化的:T1 T2 T3
- 串行顺序与时间戳顺序相同。
- 为什么T3读取y的旧值是可以的,即使有一个更新的值?
- T2和T3是并发的,因此外部一致性允许任何顺序。
- 记住:只读事务需要读取它们时间戳时的值,不看到之后的写入。
-
-
问题:如果T3从一个还没看到T1写入的副本读取x会怎样?
- 因为该副本不在Paxos多数派中?
-
解决方案:副本“安全时间”。
-
Paxos领导者按时间戳顺序发送写入。
- 在时间20提供读取服务之前,副本必须看到时间>20的Paxos写入。
- 所以它知道它已经看到了所有<20的写入。
- 在时间20提供读取服务之前,副本必须看到时间>20的Paxos写入。
-
如果有准备好但未提交的事务也必须延迟(第4.1.3节)。
-
因此:只读事务可以从本地副本读取 – 通常很快。
-
-
问题:如果时钟不是完美同步会怎样?
-
如果时钟没有完全同步会出现什么问题?
- 对于读/写事务,使用锁,没有问题。
- 如果只读事务的TS太大:
- 它的TS将高于副本安全时间,读取将被阻塞。
- 正确但慢 – 由于时钟误差增加了延迟。
- 如果只读事务的TS太小:
- 它会错过在只读事务开始之前提交的写入。
- 因为它的低TS将导致它使用记录的旧版本。
- 这违反了外部一致性。
- 它会错过在只读事务开始之前提交的写入。
- 如果只读事务的TS太大:
- 对于读/写事务,使用锁,没有问题。
-
-
如果只读事务的TS太小的问题示例:
-
读/写 T0 @ 0: Wx1 C 读/写 T1 @ 10: Wx2 C 只读 T2 @ 5: Rx? (C表示提交)
-
这将导致T2读取时间为0的x版本,即1。但T2在T1提交(实时)之后开始,因此外部一致性要求T2看到x=2。所以必须有解决不正确时钟可能性的方案!
-
Google的时间参考系统(第5.3节)
- [UTC, GPS卫星, 主服务器, 服务器, TTinterval]
- 每个数据中心有几个时间主服务器。
- 每个时间主服务器要么有一个GPS接收器,要么有一个“原子钟”。
- GPS接收器通常精确到微秒级别。
- 论文没有说明它所说的原子钟是什么意思。
- 可能与GPS同步,但在没有GPS的情况下也能准确一段时间。
- 如果是自由运行,误差会累积,可能是每周几微秒。
- 其他服务器与几个附近的时间主服务器通信。
- 由于网络延迟、检查之间的漂移而产生不确定性。
TrueTime
- 时间服务产生一个TTinterval = [最早时间, 最晚时间]。
- 正确的时间保证在这个区间内。
- 区间宽度根据测量的网络延迟、时钟硬件规格计算而来。
- 图6:间隔通常是微秒级,但有时超过10毫秒。
- 因此:服务器时钟并非完全同步,但TrueTime提供了服务器时钟可能有多错的保证界限。
如何确保如果读/写事务T1在只读事务T2开始之前结束,那么TS1 < TS2。
- 即,确保只读事务的时间戳不会太小。
- 两条规则(4.1.2):
- 开始规则:
- 事务TS = TT.now().latest (获取最新时间)
- 对于只读事务,是在开始时
- 对于读/写事务,是当提交开始时
- 事务TS = TT.now().latest (获取最新时间)
- 提交等待,对于读/写事务:
- 在提交之前,延迟直到TS < TS.now().earliest
- 保证TS已经过去。
- 开始规则:
使用间隔和提交等待更新的示例:
-
情景是T1提交,然后T2开始,T2必须看到T1的写入。
-
即,我们需要TS1 < TS2。
-
读/写 T0 @ 0: Wx1 C |1-----------10| |11--------------20| 读/写 T1 @ 10: Wx2 P C |10--------12| 只读 T2 @ 12: Rx? (P表示准备)
-
C保证在其TS(10)之后发生,由于提交等待。
-
Rx在C之后发生,因此在时间10之后。
-
T2选择TT.now().latest,这在当前时间之后,即在10之后。
-
所以TS2 > TS1。
-
为什么这提供了外部一致性:
- 提交等待意味着读/写TS保证在过去。
- 只读TS = TT.now().latest保证>=正确时间
- 因此>=任何之前提交事务的TS(由于其提交等待)
更一般地:
- 快照隔离为您提供可串行化的只读事务。
- 时间戳设定一个顺序。
- 快照版本(和安全时间)实现了在时间戳处一致的读取。
- 事务看到来自低TS(时间戳)事务的所有写入,来自高TS事务的则没有。
- 如果你不关心外部一致性,任何数字对于TS来说都可以。
- 同步的时间戳产生外部一致性。
- 即使在不同数据中心的事务之间也是如此。
- 即使从可能滞后的本地副本读取也是如此。
这一切为什么有用?
- 快速的只读事务:
- 从客户端数据中心的副本读取。
- 无锁定,无两阶段提交。
- 因此,在表3和表6中实现了10倍的延迟改善。
- 尽管如此:
- 由于安全时间,只读事务读取可能会阻塞,以追赶。
- 读/写事务的提交可能会在提交等待中阻塞。
- 精确(小间隔)时间最小化这些延迟。
总结:
- 很少看到部署的系统提供跨地理分布的数据的分布式事务。
- Spanner是一个令人惊讶的证明,它可以是实用的。
- 时间戳方案是最有趣的方面。
- 在Google内部广泛使用;一个商业Google服务;有影响力。