一个参与其中的、扮演非协调者角色的领导者,首先需要获得写锁。然后,它会选择一 个预备时间戳,这个时间戳应该比之前分配给其他事务的任何时间戳都要大(这样可以保持 单调性),并且通过 Paxos 把准备提交记录写入日志。然后,每个参与者就把自己的准备时 间戳通知给协调者。
扮演协调者的领导者,也会首先获得写锁,但是,会跳过准备阶段。在从所有其他的、扮演参与者的领导者那里获得信息后,它就会为整个事务选择一个时间戳。这个提交时间戳 s 必须大于或等于所有的准备时间戳(这是为了满足第 4.1.3 节讨论的限制条件),在协调者收到它的提交信息时,s 应该大于 TT.now().latest,并且 s 应该大于这个领导者为之前的其他 所有事务分配的时间戳(再次指出,这样做是为了满足单调性)。这个扮演协调者的领导者,就会通过 Paxos 在日志中写入一个提交记录(或者当等待其他参与者发生超时就在日志中写 入终止记录)。
在允许任何协调者副本去提交记录之前,扮演协调者的领导者会一直等待到 TT.after(s), 从而可以保证遵循第 4.1.2 节中描述的提交等待规则。因为,扮演协调者的领导者会根据 TT.now().latest 来选择 s,而且必须等待直到那个时间戳可以确保成为过去,预期的等待时间 至少是 2*ε。这种等待时间通常会和 Paxos 通信时间发生重叠。在提交等待之后,协调者就会发送一个提交时间戳给客户端和所有其他参与的领导者。每个参与的领导者会通过 Paxos 把事务结果写入日志。所有的参与者会在同一个时间戳进行提交,然后释放锁。
4.2.2 只读事务分配一个时间戳需要一个协商阶段,这个协商发生在所有参与到该读操作中的 Paxos 组之间。由此导致的结果是,Spanner 需要为每个只读事务提供一个 scope 表达式,它可以指出整个事务需要读取哪些键。对于单独的查询,Spanner 可以自动计算出 scope。
如果 scope 的值是由单个 Paxos 组来提供的,那么,客户端就会给那个组的领导者发起一个只读事务(当前的 Spanner 实现中,只会为 Paxos leader 中的只读事务选择一个时间戳), 为那个领导者分配 sread 并且执行读操作。对于一个单个位置的读操作,Spanner 通常会比 TT.now().latest 做得更好。我们把 LastTS()定义为在 Paxos 组中最后提交的写操作的时间戳。如果没有准备提交的事务,这个分配到的时间戳 sread=LastTS()就很容易满足外部一致性要求: 这个事务将可以看见最后一个写操作的结果,然后排队排在它之后。
如果 scope 的值是由多个 Paxos 组来提供的,就会有几种选择。最复杂的选择就是,和所有组的领导者进行一轮沟通,大家根据 LastTS()进行协商得到 sread。Spanner 当前实现了一个更加简单的选择。这个选择可以避免一轮协商,让读操作在 sread=TT.now().latest 时刻去 执行(这可能会等待安全时间的增加)。这个事务中的所有读操作,可以被发送到任何足够 新的副本上执行。
4.2.3 模式变更事务TrueTime 允许 Spanner 支持原子模式变更。使用一个标准的事务是不可行的,因为参与者的数量(即数据库中组的数量)可能达到几百万个。Bigtable 可以支持在一个数据中心内进行原子模式变更,但是,这个操作会阻塞所有其他操作。
一个 Spanner 模式变更事务通常是一个标准事务的、非阻塞的变种。首先,它会显式地分配一个未来的时间戳,这个时间戳会在准备阶段进行注册。由此,跨越几千个服务器的模式变更,可以在不打扰其他并发活动的前提下完成。其次,读操作和写操作,它们都是隐式地依赖于模式,它们都会和任何注册的模式变更时间戳t保持同步:当它们的时间戳小于 t 时, 读写操作就执行到时刻 t;当它们的时间戳大于时刻 t 时,读写操作就必须阻塞,在模式变更事务后面进行等待。如果没有 TrueTime,那么定义模式变更发生在 t 时刻,就变得毫无意义。
5. 实验分析我们对 Spanner 性能进行了测试,包括复制、事务和可用性。然后,我们提供了一些关于 TrueTime 的实验数据,并且提供了我们的第一个用例——F1。
5.1 微测试基准表 3 给出了一用于 Spanner 的微测试基准(microbenchmark)。这些测试是在分时机器上实现的:每个 spanserver 采用 4GB 内存和四核 CPU(AMD Barcelona 2200MHz)。客户端运行在单独的机器上。每个 zone 都包含一个 spanserver。客户端和 zone 都放在一个数据中心集合内,它们之间的网络距离不会超过 1ms。这种布局是很普通的,许多数据并不需要把数 据分散存储到全球各地)。测试数据库具有 50 个 Paxos 组和 2500 个目录。操作都是独立的 4KB 大小的读和写。All reads were served out of memory after a compaction,从而使得我们只需要衡量 Spanner 调用栈的开销。此外,还会进行一轮读操作,来预热任何位置的缓存。