Spanner 论文小结

概述

Spanner是谷歌公司研发的、可扩展的、多版本、全球分布式、同步复制数据库。它支持外部一致性(external consistency)的分布式事务,通过 Paxos 协议使得多个数据库副本达成共识。
Spanner 借助一个能保证时钟误差在一定范围(bounded time uncertainty)的 TrueTime API,实现了分布式事务的外部一致性。

TrueTime

TrueTime API 如下。
Spanner 论文小结_第1张图片

TT.now() 返回的时间区间 [earliest, latest] 中包含了调用时的绝对时间。

TT.after(t)TT.before(t) 则是对 TT.now() 的简单封装。因为调用时的绝对时间在 [earliest, latest] 区间内,TT.after(t) 只需返回 latest 就可以保证某一时间点 t 已经过去,TT.before(t) 只需返回 earliest 就可以保证某一时间点 t 还没到。

时间戳管理

用于确保 replica 足够新的 safe time

Spanner 中非 leader replica 也可以处理读请求。
只有一个 replica 足够新,才可以处理读请求。
各 replica 维护了一个它可以读取数据的时间戳上限:“安全时间”(safe time),不能从该 replica 读出高于此“安全时间”的数据。“安全时间”通过以下两者的最小值确定:

  1. 最近一次 Paxos write 的时间戳(设为 t_paxos
  2. prepared 状态的事务的时间戳最小者(当前没有 prepared transaction 时则设为无穷大)

对于 Paxos write 时间戳,取最近一次 applied 的 Paxos write 的时间戳即可;
对于 prepared 状态的事务,需要从它们中取出时间戳最小者。这是因为这些事务处于 prepared 状态,还不知道它们最终会不会提交,所以不能确定该时间戳下数据的最终结果。所以“安全时间”需要严格小于 prepared transactions 中的最小时间戳。

上述确定 safe time 的方法有以下缺点:

  • 即使一个 read 和一个 prepared transaction 不冲突,safe time 依然会被该 prepared transaction 阻碍前进。可以添加一个从 key range 到 prepared transaction timestamp 的映射。当一个读请求到达时,它只需根据 key range 检查对应 prepared transaction timestamp,以确定 safe time;
  • 如果没有出现 Paxos write 时,t_paxos 同样会阻碍 safe time 的前进。Spanner 则通过 leader-lease 区间的不相交性解决这个问题。每个 Paxos leader 维护一个时间戳门槛,表明可能会出现高于此门槛的写操作出现。它维护了一个从 Paxos 序列号 n可能分配给 Paxos 序列号 n+1 的最小时间戳的映射 MinNextTS(n),当一个副本 apply 了序列号为 n 的 log 时,可以将 t_paxos 设为 MinNextTS(n) - 1,leader 默认每8秒推进 MinNextTS()。

给 RW 事务分配时间戳

读写事务都用 2PL,他们在获取锁后分配时间戳。对于一个事务,设代表事务提交的 Paxos write 的时间戳为 t_paxos,Spanner 会将 t_paxos 分配给该事务。

Spanner 依赖以下单调性:在一个 Paxos group 中,Spanner 分配的时间戳单调递增。即使 leader 变更,这个单调性也成立,这是通过 leader-lease 区间的不相交性确保的。
Spanner 保证外部一致性:如果一个事务 T2 在 T1 提交后开始,则 T2 的时间戳一定大于 T1 的时间戳,这是通过 commit-wait 保证的。

commit-wait

对于事务 Ti,coordinator leader 等待 Ti 的提交时间戳 si 满足 TT.after(si) 后,才让 Ti 的修改对 client 可见。

事务的执行

RW 事务

提交时 client 的行为

在事务提交前,事务中发生的写是缓存在客户端中的。当一个事务的所有读、写操作都完成后(即 client 读出了所需的所有数据、缓存了所有的写操作)开始 2PC,client 先选择一个 coordinator group,给每个参与方的 leader 发送一个 commit message,并附带 coordinator 的身份、缓存的写操作。

非 coordinator 参与方 leader 的行为

一个非 coordinator 参与方 leader 收到来自 client 的 commit message 后,它选择一个 prepared timestamp 时间戳(这个时间戳必须大于它之前分配的所有时间戳),在 Paxos 中 log 一个 prepared record,然后告知 coordinator 其 prepared timestamp.

coordinator 的行为

确定 commit timestamp

coordinator 听取所有 leader 的 prepared timestamp 后,commit timestamp 必须满足以下提交:

  1. 大于等于所有收到的 prepared timestamp;
  2. 大于所有 coordinator leader 之前分配过的 timestamp;
  3. 大于 coordinator 收到 commit message 时的 TT.now().latest.

确定 commit timestamp 后,在 Paxos 中 log 一个 commit record.(或者因为没收到参与方的 prepared timestamp 而 timeout)

commit-wait

根据前述 commit-wait 规则,在 coordinator applies commit record 之前,coordinator 需要等待 TT.after(S) 为 true。在 commit-wait 后,coordinator 将 commit timestamp 发给 client 和其他参与方 leader. 各参与方 leader 将事务结果 log 到 Paxos 中,然后 apply commit record.

RO 事务

当 RO 事务所需的键都在同一个 Paxos group 时,client 将 RO 事务请求发送到该 Paxos group 的 leader。定义 LastTs() 为该 Paxos group 最近一个已提交的写操作的时间戳,对于只需一个 Paxos group 的 RO 事务,当没有 prepared transaction 时,给事务分配读时间戳 s_read = lastTS() 即可满足外部一致性(external consistency)。

如果 RO 事务需要多个 Paxos group 时有多种选择。

  1. 跟每个 Paxos group leader 协商,根据它们的 LastTS() 确定读时间戳;
  2. 不协商,直接确定读时间戳 s_read = TT.now().latest

LastTS() 的缺点:
当一个事务刚刚提交,一个与之不冲突的 RO 事务仍需将读时间戳确定为 LastTS(). 可以增强 LastTS(),添加一个 key ranges 到 commit timestamp 的映射,当一个 RO 事务到达时,只需根据 key 取出对应 key ranges 的 LastTS()

Schema-change

Spanner 中的 schema-change 作为一个事务执行。在其 prepare phase 中给该事务分配一个未来的时间戳 t。对于需要依赖 schema 的读写操作,如果它们的时间戳在 t 之前,则它们可以顺利执行;如果它们时间戳在 t 之后,则它们需要阻塞直至 schema-change 完成。

wound wait 防止死锁

RW 事务中的读操作用 wound-wait 来避免死锁。
这里分别介绍一下 wait-die 和 wound-wait。

wait-die

这是一种非抢占式死锁预防策略。当事务 Tn 请求一个当前被 Tk 占有的数据时,只有 Tn 的时间戳小于 Tk 的时间戳时,Tn 可以等待,否则 Tn 死亡(die)。
换言之,一个老事务请求一个被新事务占有的锁时,老事务将会等待;一个新事务请求一个老事务占有的锁时,新事务将会中止。

假设 T1, T2 两个事务满足 T(T1) < T(T2),wait-die 规则如下:

T1 T2
acquired the lock
try to acquire
get killed, restart later
release the lock
T1 T2
acquired the lock
try to acquire
wait util acquired
release
acquired the lock

wound-wait

这是一种抢占式死锁预防策略。它和 wait-die 相反,当 Tn 请求一个被 Tk 占有的数据时,只有 Tn 的时间戳大于 Tk 的时间戳时,Tn 可以等待,否则 Tk 中止(Tk is wounded by Tn)。
换言之,一个老事务请求一个被新事务占有的锁时,老事务会将新事务中止;一个新事务请求一个老事务占有的锁时,新事务将会等待。

假设 T1, T2 两个事务满足 T(T1) < T(T2),wound-wait 规则如下:

T1 T2
acquired the lock
try to acquire
kill T2
get killed, restart later
acquired the lock
T1 T2
acquired the lock
try to acquire
wait, util acquired
release
acquired the lock

小结

两种方法中,只有时间戳较大的事务会被杀死

总结

因为一个事务涉及的不同 key 可能属于不同的 key range,进而属于不同的 Paxos group。当多个事务执行时,可能一个新事务的修改被旧事务的修改覆盖了,进而导致外部不一致性,所以需要提供额外的手段对事务进行排序。
Spanner 借助其 TrueTime API,通过对事务的时间戳大小进行排序,实现了外部一致性。

参考资料

deadlock in DBMS

你可能感兴趣的:(database,分布式,数据库)