扫一扫
关注微信公众号

Google Spanner 实现全球分布式的事务
2025-06-16   51CTO

今天我们来聊聊一个在分布式数据库领域鼎鼎大名的系统:Google Spanner。这篇 2012 年的论文石破天惊,因为它实现了一件被认为极其困难甚至不切实际的事情: 全球分布式的事务 。

想象一下,你的数据分布在全球各地的数据中心,但你却可以像操作一台电脑上的数据库一样,执行跨越这些数据中心的事务,并且保证数据的一致性。这听起来是不是很酷?Spanner 就做到了这一点。

核心挑战:从 F1 广告系统说起

要理解 Spanner 为何如此设计,我们得先看看它最初要解决的问题。Google 的广告后端系统,代号 F1,最初是构建在手动分片的 MySQL 数据库上的。随着业务增长,数据集达到了几十个 TB,手动对这个支撑核心收入的数据库进行分片和扩容,变成了一场噩梦。上一次折腾,花了两年多的巨大努力,协调了数十个团队。

这里的 数据分片(Sharding) 是一种数据库分区技术,也常被称为“水平分区”。想象你有一个巨大的、包含数亿用户信息的表,如果把这张表存在一台服务器上,查询和写入的压力很快就会让这台服务器不堪重负。

数据分片就是将这个大表“水平”切开,分成许多更小的、更容易管理的部分,每一部分就叫做一个 分片(shard) 。

在 F1 广告系统的旧 MySQL 架构中,分片就是 按客户(customer) 来进行的:

  • 假设 Google 有三个广告客户:A、B 和 C。
  • 系统可以将客户 A 的所有相关数据(比如广告活动、账单信息等)全部存放在  Shard 1 ,将客户 B 的所有数据存放在 Shard 2 ,将客户 C 的所有数据存放在 Shard 3 。
  • 这样一来,当需要查询客户 A 的数据时,请求会直接路由到管理 Shard 1 的服务器上,而不会影响到其他服务器。这就将巨大的读写压力分散到了多台服务器上。当客户数量和数据量持续增长时,这种手动管理和重新分片的过程变得极其复杂和昂贵,这也是 F1 团队迁移到 Spanner 的主要原因之一。

F1 团队迫切需要一个新的系统,这个系统需要满足几个关键需求:

  • 更灵活的分片 :再也不想手动搞数据分片了。
  • 同步复制和自动故障转移 :MySQL 的主从复制在故障转移时很麻烦,还可能丢数据。
  • 跨分片的强一致性事务 :业务逻辑需要跨任意数据的事务和一致性读取。

当时主流的 NoSQL 系统无法满足 F1 对强事务语义的要求。于是,Spanner 应运而生。

Spanner 蓝图:Paxos、分片与复制

Spanner 的基本架构是将数据分片(sharded),然后将每个分片的副本(replicas)分布在多个地理位置不同的数据中心。这么做的好处显而易见:

  • 高可用性 :一个数据中心挂了,服务不受影响。
  • 低延迟 :用户可以就近读取本地数据中心的副本,速度飞快。

数据的复制和一致性由 Paxos 共识算法来管理,每个数据分片都是一个独立的 Paxos 组,拥有自己的 log 。

一个 Paxos 组就是负责管理一个数据分片的一组副本(replicas)。 这组副本使用 Paxos 共识算法来就所有的数据修改达成一致,确保数据的一致性和高可用性。每个 Paxos 组都有一个 Leader,负责处理写入请求。

一个 Paxos 组的多个副本被 刻意地 分布在地理位置分散的多个数据中心里。这正是 Spanner 实现高可用性和灾难恢复能力的关键。例如,一个 Paxos 组可能有 5 个副本,分布在 3 个不同的数据中心,即使其中一个数据中心因为地震或断电而完全失效,剩下的副本依然可以选举出新的 Leader,继续提供服务。

这里你可能会问, Spanner 可以用 Raft 算法替代 Paxos 吗?

当然可以。从这篇论文的层面来看,两者没有区别。不过,在 Spanner 被设计和构建的那个年代,Raft 算法还没诞生,而 Google 内部已经有了一套调优得非常好、稳定可靠的 Paxos 实现。所以,选择 Paxos 是一个非常自然且实际的工程决策。

处理读写事务:加了“保险”的两阶段提交

一个分片包含一部分特定的数据,比如客户 A、B、C 的数据。这些数据 整体 由一个 Paxos 组来管理。数据 D、E、F 可能属于另一个分片,因此由 另一个独立的 Paxos 组 来管理。所以,数据 ABC 不会 同时存在于其他分片上。一个事务可能会需要 同时访问 多个分片的数据(比如从客户 A 的账户转账到客户 D 的账户),这种情况下,该事务就需要与多个 Paxos 组(管理客户 A 的组和管理客户 D 的组)进行协调。

对于需要修改数据的 读写事务(read-write transactions) ,Spanner 沿用了经典的 两阶段提交(Two-Phase Commit, 2PC) 协议,并将其构建在 Paxos 之上,以保证原子性。

过程大致如下:

  1. 客户端选择一个 Paxos 组作为 事务协调者(Transaction Coordinator, TC) 。
  2. 所有要写入的分片(参与者),首先会在其 Leader 副本上获取 锁(locks) 。
  3. 接着,参与者通过 Paxos 把“准备”记录(prepare record)写入日志,这相当于把锁和要写入的新值持久化并复制到了多数副本。
  4. 所有参与者都“准备”好后,协调者会通过 Paxos 记录一个最终的“提交”或“中止”决定。
  5. 最后,协调者通知所有参与者,参与者记录最终决定并释放锁。

传统 2PC 的一个巨大痛点是,如果协调者宕机,所有参与者都会持有锁并陷入阻塞状态,整个系统可能被卡住。Spanner 的巧妙之处在于, 协调者本身的状态也是通过 Paxos 复制的 。这意味着即使协调者的 Leader 挂了,Paxos 组内部会选出一个新的 Leader,它能从 log 中恢复状态,继续推动事务完成,从而解决了 2PC 的阻塞问题。

当然,这套流程涉及多次跨数据中心的网络通信,所以读写事务的延迟并不低,在美国东西海岸之间完成一次事务,延迟大约在 100 毫秒左右。

快速只读事务的魔法:快照隔离与时间戳

F1 的工作负载中,绝大部分是 只读事务(read-only transactions) 。为了极致的性能,Spanner 希望只读事务能够:

  • 无锁 :不使用锁,避免和读写事务互相阻塞。
  • 本地读 :直接读取本地数据中心的副本,避免跨数据中心通信。

这就带来一个核心难题:本地副本的数据可能不是最新的,如何保证读取到一致的数据呢?

Spanner 的答案是 快照隔离(Snapshot Isolation) 。简单来说,就是给每个事务分配一个 时间戳(timestamp) 。读写事务在提交时获得一个时间戳,其所有写入操作都将关联这个时间戳。只读事务在开始时获得一个时间戳,它只能看到所有时间戳早于它的事务的写入结果,就像在那个时间点给整个数据库拍了一张快照。

这样一来,只读事务就可以在不加锁的情况下,读到一份完整且一致的数据。

Spanner 的定海神针:TrueTime API

快照隔离解决了并发事务的视图问题,但一个新的、更棘手的问题出现了: 时间 。分布式系统中的每台机器都有自己的时钟,它们不可能完美同步。如果时钟不准,会发生什么?

  • 如果只读事务的时间戳 过大 ,它可能需要等待数据同步,导致延迟增加,但结果是对的。
  • 如果只读事务的时间戳 过小 ,它可能会读到一份“旧”的快照,从而错过一些实际上已经提交的修改。这就违反了 Spanner 承诺的 外部一致性(external consistency) 。

这里,我们就要引出 Spanner 的核心创新,也是它的“定海神针”—— TrueTime 。

TrueTime API 并不返回一个精确的时间点,而是返回一个时间区间 TT.now() = [earliest, latest] 。它保证 真实的绝对时间 一定落在这个区间内。这个区间的宽度(epsilon)代表了时钟的不确定性,通常只有几毫秒。Google 通过在每个数据中心部署带有 GPS 接收器或原子钟的时间主服务器(time master)来实现这一点。

有了 TrueTime ,Spanner 通过两条规则来保证外部一致性:

  1. 时间戳分配 :为读写事务分配的提交时间戳 ts,是调用 TT.now().latest 的结果。
  2. 提交等待(Commit Wait) :一个读写事务在提交并释放锁之前,必须等待,直到 TT.now().earliest > ts 。这个等待确保了当事务的写入结果对外部可见时,它的时间戳 ts 在真实时间中已经成为过去时。

这个“提交等待”机制是关键。它保证了如果事务 T1 在真实时间里先于 T2 完成,那么 T1 的时间戳一定小于 T2 的时间戳。这就保证了外部一致性。

Spanner 关于时间的假设与证明

Spanner 的天才之处在于,它并 不追求 完美的时钟同步,而是 承认并量化 时钟的不确定性,并在此基础上构建出严格的数学保证。

TrueTime 的保证是什么?

TrueTime API 的核心承诺是:它返回的区间 [earliest, latest]一定包含 真实的绝对时间。我们不知道真实时间在区间的哪个点,但可以 100% 确定它就在这个区间内。这个保证依赖于底层的物理设施—— GPS 和原子钟的稳定运行,以及对网络延迟等不确定因素的精确计算。

可以假设时间主服务器(time master)一定准确吗?

在 Spanner 的模型中,我们必须 信任 这个时间体系能够提供一个正确的时间 区间 。如果时间主服务器本身发生故障,给出了一个完全错误的、与真实时间毫无交集的区间,那么 Spanner 的外部一致性保证确实会被打破。这相当于整个系统的“信任根基”被动摇了。因此,Google 在时间基础设施的稳定性和纠错能力上投入了巨大努力,来确保这个基础假设的成立。

Spanner 如何基于“不确定”的时间实现“确定”的一致性?

关键在于 提交等待(Commit Wait) 规则。Spanner 并没有神奇地消除时间的不确定性,而是通过 等待 来抵消掉这份不确定性。

举个例子:

  • 一个读写事务 T1 准备提交,它获得了一个时间戳 ts = TT.now().latest
  • 假设此刻的 TrueTime 区间是 [10:00:00.004, 10:00:00.006],那么 ts = 10:00:00.006
  • Commit Wait 规则要求,T1 必须暂停,不能马上让其他事务看到它的写入结果。它必须等到 TT.now().earliest 越过 10:00:00.006 这个时间点。
  • 比如,它一直等到 TrueTime 区间变为 [10:00:00.007, 10:00:00.009] 时,因为 007 > 006,等待结束,T1 的提交才真正对外可见。

通过这个等待,Spanner 保证了当 T1 的写入结果可见时,它的时间戳 ts 在真实世界里 一定已经成为了过去时 。任何在这之后开始的新事务 T2,获取到的时间戳一定会晚于 ts,从而保证了 T2 一定能看到 T1 的写入,实现了外部一致性。

总结一下 :Spanner 并不是假设时钟是完美的。它将时钟的不确定性(epsilon)转化为性能开销(等待时间)。不确定性越大,Commit Wait 的时间就越长,性能就越差。但无论如何,只要 TrueTime 提供的区间是正确的,外部一致性的 正确性 就能得到保证。

外部一致性到底是什么?

我们刚才反复提到外部一致性。 它和我们常说的线性一致性、可串行化有什么关系呢?

外部一致性(External Consistency) 要求,如果事务 T1 在真实时间中先于事务 T2 开始执行并完成,那么在数据库的事务历史中,T1 也必须排在 T2 前面。它的效果等同于将 线性一致性(linearizability) 的概念应用于整个事务,而不仅仅是单个操作。它也等同于 严格可串行化(strict serializability) ,即可串行化的顺序必须与真实时间的发生顺序一致。

那为什么外部一致性如此重要呢?

我们来看一个生动例子。假设你在圣何塞的数据中心,通过一个网页服务修改了你们团队共享账号的密码。然后你马上转身,把新密码告诉了坐在你旁边的同事。你的同事立即在圣马特奥的另一个数据中心尝试用新密码登录。

如果没有外部一致性,你的同事可能会连接到一个还没同步到新密码的数据副本上,导致登录失败,系统提示密码错误。这显然不符合用户的直觉和预期。外部一致性则保证了你的同事一定能看到你刚刚完成的密码修改,登录成功。它保证了系统的行为和真实世界的时间顺序是一致的。

Spanner 在真实世界中的应用

最后,我们来回答一个大家可能关心的问题: 真的有人在用 Spanner 吗?

答案是肯定的,而且是大规模使用。

  • Google 内部有数百个服务依赖 Spanner,包括我们前面提到的 F1 广告系统,以及 Zanzibar 授权系统等。
  • 它作为 Cloud Spanner ,已经成为 Google Cloud 平台上的一项核心服务,开放给外部客户使用。
  • 它的设计思想也深刻影响了业界,比如开源分布式数据库 CockroachDB 就是基于 Spanner 的设计理念构建的。

Spanner 的出现,雄辩地证明了在全球范围内提供具有强一致性保证的分布式事务是切实可行的。它的核心思想,特别是 TrueTime 的设计,为构建更强大的分布式系统开辟了新的道路。



 


热词搜索:分布式 事务 系统

上一篇:Gartner发布云技术发展的六大趋势
下一篇:最后一页

分享到: 收藏