在分布式系统中,时间一直是一个令人头疼的问题。不同机器的时钟会产生漂移,网络延迟让时间同步变得困难,而许多一致性协议又严重依赖时间排序。Google 的 Spanner 系统通过一个大胆的想法打破了这个困境:既然软件无法完美解决时间问题,为什么不用硬件来解决?TrueTime API 就是这个思路的产物——通过 GPS 和原子钟构建全球范围内的时间同步基础设施,让分布式事务的实现变得更加优雅。
一、从 Bigtable 到 Spanner:Google 数据管理的演进
Google 的数据管理系统经历了一段漫长的演化过程。最早的 Bigtable(2006 年论文发表)是一个高性能的分布式键值存储系统,它提供了单行事务和极高的吞吐量,但缺乏跨行事务能力和 SQL 支持。许多 Google 内部团队发现,虽然 Bigtable 性能出色,但应用层需要自己实现复杂的一致性逻辑,这导致了大量重复劳动和潜在的 bug。
为了解决这个问题,Google 开发了 Megastore,它在 Bigtable 之上提供了 ACID 事务和类 SQL 的查询能力。Megastore 使用 Paxos 协议实现了跨数据中心的同步复制,但它有一个致命缺陷:性能较差。每个写操作都需要运行完整的 Paxos 共识流程,延迟较高。更重要的是,Megastore 的数据模型限制了实体组(Entity Group)的划分,跨实体组的事务需要昂贵的两阶段提交。
Spanner 的诞生就是为了同时满足多个看似矛盾的需求:全球分布、强一致性、高可用性、SQL 支持和可接受的性能。Spanner 的论文在 2012 年的 OSDI 会议上发表,标题是”Spanner: Google’s Globally-Distributed Database”。这篇论文揭示了 Google 如何利用硬件基础设施来实现外部一致性(External Consistency),这是比传统的可串行化(Serializability)更强的一致性保证。
Spanner 的核心创新在于 TrueTime API。传统的分布式系统假设时钟是不可靠的,因此设计了各种逻辑时钟和向量时钟来规避物理时钟的问题。但 Spanner 选择了相反的路径:投入巨资建设全球时间同步基础设施,让物理时钟变得足够可靠,从而简化上层协议的设计。这个决策背后的哲学是,对于 Google 这样体量的公司,硬件投资的边际成本远低于软件复杂度带来的维护成本。
二、TrueTime API:时间的置信区间
TrueTime 并不是一个普通的时间服务,它返回的不是一个确定的时间点,而是一个时间区间。这个设计直接面对了分布式系统中时钟同步的根本问题:我们无法知道”现在”的精确时间,但我们可以给出一个高概率的范围。
TrueTime 的硬件基础
TrueTime 依赖两种硬件来源:GPS 接收器和原子钟。每个 Google 数据中心都部署了多台 time master 机器,这些机器配备了 GPS 接收器或原子钟(通常称为 Armageddon masters)。GPS 接收器可以接收卫星信号,获得与 UTC 时间同步的时间源,但它们容易受到天线故障、无线电干扰和 GPS 信号欺骗的影响。原子钟则提供了极高的稳定性,但会随时间产生漂移,需要定期校准。
通过结合这两种硬件,TrueTime 系统获得了互补的优势。GPS 提供了长期准确性,而原子钟提供了短期稳定性。每个数据中心部署的 time master 机器相互之间会进行交叉校验,如果某个 time master 的时间与其他机器偏差过大,它会被自动剔除。
每个 Spanner 服务器运行一个 timeslave daemon,定期向多个 time master 进行查询。这些查询使用 Marzullo 算法来计算一个保守的时间区间,该算法可以在存在一定数量故障节点的情况下仍然计算出可靠的时间边界。
TrueTime API 的三个方法
TrueTime 对外暴露的 API 非常简洁,只有三个方法:
// TrueTime API
TTinterval TT.now()
boolean TT.after(t)
boolean TT.before(t)TT.now() 返回一个
TTinterval,它包含两个字段:earliest 和
latest。这个区间的含义是:真实的绝对时间(用
UTC 时间表示)一定落在 [earliest, latest]
这个范围内。我们用 ε 来表示误差的一半,即
ε = (latest - earliest) / 2。TrueTime
保证在调用 TT.now() 时,真实时间
t_abs 满足
t_abs ∈ [earliest, latest]。
TT.after(t) 和 TT.before(t)
是两个辅助方法。TT.after(t) 返回 true 当且仅当
t 已经明确过去了,即当前时间的
earliest 大于
t。类似地,TT.before(t) 返回 true
当且仅当 t 明确还没到来。
误差来源与量级
TrueTime 的误差 ε 通常在 1 到 7 毫秒之间。Google 的论文提到,在他们的生产环境中,ε 的分位数表现如下:大多数情况下小于 4 毫秒,99.9% 的情况下小于 7 毫秒。
误差的主要来源包括:
时钟漂移:即使是最好的晶振也会产生漂移。在两次与 time master 同步之间,本地时钟会持续漂移。Spanner 假设最坏情况下时钟漂移率为 200 ppm(parts per million),也就是每秒漂移 200 微秒。
网络延迟:查询 time master 的网络往返时间(RTT)会引入不确定性。如果一个请求的 RTT 是 x 毫秒,那么在收到响应时,我们无法确定响应中的时间戳对应的是往返过程中哪个时刻的真实时间。
Time master 的不确定性:GPS 接收器本身有一定的误差(通常在 1 微秒以下),原子钟也会有微小的漂移。
为了将 ε 保持在较小的范围内,timeslave daemon 每 30 秒与 time master 同步一次。在这个间隔内,误差会线性增长。通过频繁的同步和保守的估计,TrueTime 能够将 ε 控制在毫秒级别。
为什么 NTP 不够用?
有人可能会问,为什么不直接使用 NTP(Network Time Protocol)?NTP 是互联网上广泛使用的时间同步协议,它也能将时钟同步到毫秒级别。但 NTP 有几个致命的问题:
无法提供边界保证:NTP 会给你一个时间值,但它不会告诉你这个值的不确定性有多大。你无法知道真实时间在什么范围内。
故障处理不足:如果 NTP 服务器出现故障或者被攻击,客户端可能会收到错误的时间,但很难及时发现。
精度不够:在跨数据中心的广域网环境下,NTP 的误差可能达到几十甚至上百毫秒,这对于需要强一致性的分布式数据库来说是不可接受的。
没有冗余机制:NTP 客户端通常只查询少数几个服务器,缺乏 TrueTime 那样的多源交叉验证机制。
TrueTime 的设计哲学是:与其给出一个不知道准确度的时间点,不如给出一个有明确置信度的时间区间。这种诚实的 API 设计让上层协议能够正确地处理时间不确定性。
三、外部一致性:比线性一致性更强的保证
外部一致性(External Consistency)是 Spanner 提供的一致性保证的核心。理解外部一致性需要先理解几个相关概念。
一致性模型的层次
在分布式系统中,一致性模型形成了一个强弱有序的谱系:
- 最终一致性(Eventual Consistency):最弱的保证,只承诺在没有新更新的情况下,所有副本最终会收敛到相同的值。
- 因果一致性(Causal Consistency):保证有因果关系的操作按因果顺序可见。
- 可串行化(Serializability):所有事务的执行结果等价于某个串行执行顺序。这是数据库理论中的标准隔离级别。
- 严格可串行化(Strict Serializability)/ 线性一致性(Linearizability)+ 可串行化:事务的串行化顺序与实时时间顺序一致。
- 外部一致性(External Consistency):Spanner 使用的术语,本质上等同于严格可串行化。
外部一致性的定义是:对于任意两个事务 T1 和 T2,如果 T1 在真实时间上先于 T2 提交(即 T1 的提交时刻早于 T2 的开始时刻),那么 T1 的时间戳必须小于 T2 的时间戳,且 T2 能看到 T1 的所有写入。
用数学语言表达:如果
t_commit(T1) < t_start(T2)(真实时间顺序),那么
ts(T1) < ts(T2)(时间戳顺序)。
这个保证看起来简单,但实现起来极其困难。在没有 TrueTime 的情况下,你无法可靠地判断两个发生在不同机器上的事件的真实时间顺序。传统的做法是使用逻辑时钟(如 Lamport 时钟或向量时钟),但逻辑时钟只能捕获因果关系,无法反映真实时间。
外部一致性的意义
外部一致性的重要性在于它消除了分布式系统中的反直觉现象。考虑这样一个场景:
- 用户 Alice 在纽约的终端上提交了一个写事务 T1,更新了账户余额。
- T1 成功提交后,Alice 打电话给在东京的 Bob,告诉他”我刚转了 100 美元给你”。
- Bob 立即查询他的账户(事务 T2)。
在一个满足外部一致性的系统中,Bob 一定能看到这笔转账,因为 T1 的提交时刻早于 T2 的开始时刻(即使考虑到打电话的延迟)。但在一个只提供可串行化而不保证实时顺序的系统中,Bob 可能看不到这笔转账,因为系统可能选择了一个串行化顺序让 T2 排在 T1 之前。
外部一致性让分布式数据库的行为与单机数据库一致,用户不需要担心由于分布式带来的反直觉现象。这对于构建跨数据中心的全球服务至关重要。
Spanner 如何利用 TrueTime 实现外部一致性
Spanner 利用 TrueTime 实现外部一致性的核心思想是:为每个事务分配一个时间戳,这个时间戳既反映了真实时间,又满足外部一致性的要求。
对于读写事务,Spanner 使用如下策略:
时间戳分配:当事务准备提交时,协调者(coordinator)调用
TT.now()获取当前时间区间,并选择latest作为事务的时间戳。这确保了时间戳不会早于事务的真实提交时刻。Commit Wait:在分配时间戳
s后,协调者等待直到TT.after(s)返回 true。这意味着当前时间已经明确晚于s,任何在此之后开始的事务都会看到更晚的时间。
通过这个简单的机制,Spanner 保证了外部一致性。如果事务 T1
在真实时间上早于 T2,那么: - T1 的时间戳
s1 = TT.now().latest(在 T1 提交时) - T1
等待直到 TT.after(s1) 返回 true 才真正提交 -
此时真实时间已经晚于 s1 + ε - T2
开始时,TT.now().latest >= s1 + 2ε > s1 -
因此 s2 > s1
这个机制的代价是 Commit Wait 引入的延迟,通常是 2ε,即几毫秒到十几毫秒。但这个代价换来了全局的外部一致性,对于许多应用来说是值得的。
四、Commit Wait 机制:时间的等待艺术
Commit Wait 是 Spanner 实现外部一致性的关键机制。理解它的工作原理需要仔细分析事务提交的时序。
读写事务的提交流程
Spanner 的读写事务使用两阶段提交(Two-Phase Commit, 2PC)协议,结合两阶段锁(Two-Phase Locking, 2PL)来保证原子性和隔离性。提交流程如下:
客户端发起提交请求:事务执行完所有读写操作后,客户端向协调者发送提交请求。
Prepare 阶段:
- 协调者向所有参与者(participant)发送 prepare 消息。
- 每个参与者获取写锁,将修改记录到本地的 Paxos 日志中,但不释放锁。
- 参与者回复 prepared 消息,并告知协调者它准备的时间戳
t_prepared。
时间戳分配:
- 协调者收集所有参与者的
t_prepared。 - 协调者调用
s = TT.now().latest,并确保s大于所有t_prepared以及之前分配的任何时间戳(以保证单调性)。 - 时间戳
s被分配给这个事务。
- 协调者收集所有参与者的
Commit Wait:
- 协调者等待,直到
TT.after(s)返回 true。 - 这意味着真实时间已经确定性地超过了
s。
- 协调者等待,直到
Commit 阶段:
- 协调者将 commit 决定和时间戳
s写入自己的 Paxos 日志。 - 一旦 Paxos 复制成功,协调者向所有参与者发送 commit 消息。
- 参与者应用修改,释放锁,并回复确认。
- 协调者将 commit 决定和时间戳
返回客户端:
- 协调者向客户端返回成功响应。
可以用伪代码表示 Commit Wait 的逻辑:
def commit_transaction(coordinator, participants):
# Prepare 阶段
prepared_timestamps = []
for p in participants:
p.acquire_locks()
p.write_to_paxos_log()
prepared_timestamps.append(p.get_prepared_timestamp())
# 分配时间戳
tt_interval = TT.now()
s = max(tt_interval.latest, max(prepared_timestamps), last_timestamp + 1)
# Commit Wait:核心步骤
while not TT.after(s):
sleep(small_duration) # 等待真实时间超过 s
# Commit 阶段
coordinator.write_commit_to_paxos_log(s)
for p in participants:
p.commit(s)
p.release_locks()
return s下面的时序图展示了 Commit Wait 机制的完整时间线,从协调者获取时间区间到最终安全提交的全过程:
sequenceDiagram
participant C as 协调者(Coordinator)
participant TT as TrueTime API
participant P as 参与者(Participants)
C->>TT: TT.now()
TT-->>C: [earliest, latest]
Note over C: 分配 s = latest 作为提交时间戳
loop Commit Wait:等待真实时间超过 s
C->>TT: TT.after(s)?
TT-->>C: false(earliest <= s)
Note over C: 继续等待...真实时间流逝
end
C->>TT: TT.after(s)?
TT-->>C: true(earliest > s)
Note over C: 安全:真实时间已确定性超过 s
C->>P: 发送 Commit(s)
P-->>C: 提交完成确认
该时序图清晰展示了 Commit Wait
的核心逻辑:协调者在分配时间戳
s = TT.now().latest
之后,并不会立即提交,而是反复调用 TT.after(s)
进行轮询等待。只有当 TrueTime 确认当前真实时间已经超过
s(即
TT.now().earliest > s)时,协调者才会将提交消息发送给所有参与者。这个等待窗口的长度取决于
TrueTime 的误差 ε,通常为 2ε(约 5-10
毫秒),正是这段短暂的等待为整个系统换来了全局外部一致性的保证。
为什么要等待 2ε?
Commit Wait 的等待时间通常是 2ε,这个值来自于 TrueTime 的误差边界。让我们分析为什么需要这么长的等待时间。
假设在真实时间 t_abs 时,协调者调用
TT.now() 得到区间 [e1, l1],并选择
s = l1 作为时间戳。根据 TrueTime
的保证,t_abs ∈ [e1, l1]。在最坏的情况下,t_abs = e1,即真实时间实际上是区间的最早端点。
协调者现在需要等待直到真实时间确定性地超过
s = l1。而 TT.after(s) 返回 true
的条件是
TT.now().earliest > s。假设在真实时间
t'_abs 时,调用 TT.now() 得到区间
[e2, l2],那么需要
e2 > s = l1。
由于 TrueTime 的误差是 ε,区间的长度是 2ε,所以
l1 - e1 ≤ 2ε。从 t_abs = e1 到
e2 > l1 需要的真实时间差至少是
l1 - e1 ≤ 2ε。因此,等待时间的上界是 2ε。
在实践中,平均等待时间会短一些,因为真实时间通常不会恰好落在区间端点。Google 的论文提到,平均的 Commit Wait 时间约为 5-10 毫秒。
Commit Wait 如何保证时间戳排序
Commit Wait 保证了一个关键性质:如果事务 T1 在真实时间上完成提交早于事务 T2 开始,那么 T1 的时间戳一定小于 T2 的时间戳。
证明如下:
- 设 T1 的时间戳为
s1 = TT.now().latest(在 T1 选择时间戳的时刻)。 - T1 等待直到
TT.after(s1)返回 true,即TT.now().earliest > s1。 - 此时 T1 才真正完成提交(客户端可见)。
- T2 在 T1 完成后开始,调用
TT.now()得到区间,此时区间的earliest已经大于s1。 - T2 选择
s2 = TT.now().latest > TT.now().earliest > s1。 - 因此
s2 > s1,保证了时间戳顺序与真实时间顺序一致。
这个机制的美妙之处在于,它不需要事务之间进行通信或协调,仅仅通过每个事务独立地与 TrueTime 交互,就能保证全局的一致性。
Commit Wait 的优化
虽然 Commit Wait 引入了延迟,但有一些优化可以减少其影响:
并行处理:Commit Wait 期间,协调者可以并行地执行其他工作,如准备响应消息、更新内部状态等。
流水线化:如果客户端连续提交多个事务,可以将 Commit Wait 与下一个事务的 Prepare 阶段重叠。
只读事务免等待:只读事务不需要 Commit Wait,可以立即返回结果。
降低 ε:通过改进硬件和同步频率,可以降低 ε,从而减少等待时间。
完整事务执行走查:从请求到提交
为了更好地理解 Spanner 中 2PC、Paxos 和 Commit Wait 三大机制如何协同工作,我们通过一个具体的跨区域转账场景进行完整走查。
场景描述:用户 Alice 在美国东部发起一笔跨区域转账,将 500 美元从她在美国东部的账户(Account A,属于 Paxos Group α)转到 Bob 在欧洲的账户(Account B,属于 Paxos Group β)。Paxos Group α 的 leader 被选为本次事务的协调者。
第 1 步:客户端发送写请求
Alice 的客户端向 Spanner
发起读写事务,包含两个写操作:Account_A.balance -= 500
和 Account_B.balance += 500。客户端分别向 Paxos
Group α 和 Paxos Group β 的 leader
发送读请求,获取当前余额。
第 2 步:协调者通过 2PL 获取写锁
两个 Paxos Group 的 leader 分别在各自管理的数据上获取写锁。Group α 锁定 Account A 的余额行,Group β 锁定 Account B 的余额行。此时其他试图修改这两个账户的事务将被阻塞。
第 3 步:客户端发送提交请求
客户端向协调者(Paxos Group α 的 leader)发送 commit 请求,附带所有的写操作。
第 4 步:协调者发送 Prepare 到参与者 Paxos Group
协调者向参与者(Paxos Group β 的 leader)发送 prepare 消息。注意,协调者自身所在的 Paxos Group α 不需要 prepare 阶段——它直接在 commit 阶段处理。
第 5 步:参与者 leader 将 Prepare 写入本地 Paxos 日志(复制)
Paxos Group β 的 leader 将 prepare 记录通过 Paxos 协议写入日志。这意味着 leader 将 prepare 消息发送给 Group β 的其他副本(例如分布在亚洲和美国西部的副本),等待多数派确认。假设 Group β 有 5 个副本,则需要至少 3 个副本写入成功。此步骤确保了即使 leader 崩溃,prepare 决定也不会丢失。
第 6 步:参与者回复 t_prepared
Paxos Group β 的 leader 在 Paxos
写入成功后,回复协调者一个 prepared 消息,附带时间戳
t_prepared_β。假设此时
t_prepared_β = 1000.003(以秒为单位的简化表示)。
第 7 步:协调者选择提交时间戳 s
协调者收到所有参与者的 prepared 回复后,调用
TT.now() 获取时间区间。假设此时返回
[1000.005, 1000.011](误差 ε = 3ms),则
TT.now().latest = 1000.011。协调者按如下规则选择时间戳:
s = max(TT.now().latest, t_prepared_β, last_assigned_timestamp + 1)
= max(1000.011, 1000.003, ...)
= 1000.011第 8 步:Commit Wait——等待 TT.after(s) 返回 true
协调者开始 Commit Wait。它反复调用
TT.after(1000.011),等待
TT.now().earliest > 1000.011。假设等待了约
6ms 后,TT.now() 返回
[1000.012, 1000.018],此时
earliest = 1000.012 > 1000.011 = s,TT.after(s)
返回 true。Commit Wait 结束。
第 9 步:协调者将 Commit 决定写入自身 Paxos 日志
协调者通过 Paxos Group α 的 Paxos 协议,将 commit
决定(包含时间戳 s = 1000.011 和写操作
Account_A.balance -= 500)写入日志并复制到多数派副本。
第 10 步:协调者发送 Commit(s) 到所有参与者
协调者向 Paxos Group β 的 leader 发送 commit
消息,附带时间戳 s = 1000.011。
第 11 步:参与者应用写入,释放锁
Paxos Group β 的 leader 收到 commit 消息后,通过 Paxos 将 commit 记录写入日志并复制到多数派。然后 Group α 和 Group β 分别在各自的状态机中应用写操作:Account A 的余额减少 500,Account B 的余额增加 500。写锁被释放。
第 12 步:客户端收到成功响应
协调者向 Alice
的客户端返回成功响应。此时,任何在真实时间上晚于该响应的事务,其时间戳一定大于
s = 1000.011,因此一定能看到这笔转账——这就是外部一致性的保证。
关键时间线总结:
| 阶段 | 耗时(示例) | 说明 |
|---|---|---|
| Prepare(Paxos 复制) | ~10ms | 跨数据中心 Paxos 写入 |
| 协调者选择时间戳 | <1ms | 本地计算 |
| Commit Wait | ~6ms | 等待 TrueTime 误差消除 |
| Commit(Paxos 复制) | ~10ms | 协调者和参与者各自 Paxos 写入 |
| 总延迟 | ~27ms | 跨区域事务的典型延迟 |
这个走查揭示了 Spanner 事务的一个重要特征:Commit Wait 的 6ms 等待只占总延迟的约 22%,而 Paxos 跨数据中心复制才是延迟的主要来源。这也解释了为什么 Google 持续投资降低 ε——即使 ε 从 3ms 降低到 1ms,对整体延迟的改善也是有意义的。
五、读协议:快照读与强读
Spanner 提供了两种读操作模式,每种都有不同的一致性保证和性能特征。
快照读(Snapshot Read)
快照读允许客户端读取过去某个时间点的一致性快照。客户端可以指定一个时间戳
t,或者使用特殊的时间戳如”10 秒前”。Spanner
保证返回的数据反映的是时间戳 t
时刻的数据库状态。
快照读的优势在于:
无锁:读操作不需要获取任何锁,因为它读取的是历史数据,不会与写操作冲突。
可以读取任何副本:由于不需要获取锁,客户端可以从任何足够新的副本读取数据,这提高了读的局部性和可用性。
支持长时间运行的分析查询:快照读可以读取一个固定的历史快照,不受后续写入的影响。
快照读的时间戳选择有几种策略:
- Exact staleness:读取恰好 N
秒前的数据。例如
t = now() - 10s。 - Bounded staleness:读取不超过 N 秒前的数据,选择最新的可用快照。
- Read timestamp:客户端明确指定时间戳。
强读(Strong Read)
强读(也称为当前时间读)读取当前最新的数据。它等价于在当前时刻执行的只读事务。
强读需要获取读锁,以防止与写事务冲突。但是,对于只包含强读的只读事务,Spanner 有一个重要的优化:它不需要两阶段提交,也不需要 Commit Wait。
强读的时间戳分配如下:
- 对于只读事务,客户端向涉及到的所有 Paxos group 的 leader 发送读请求。
- 每个 leader 等待直到它的 Paxos
状态机应用到了足够新的时间戳(至少到
TT.now().latest),然后执行读操作。 - 所有读操作使用相同的时间戳,保证了事务内部的一致性。
强读的时间戳选择策略是
s = TT.now().latest(在事务开始时),但 leader
需要等待直到它的状态机应用到了至少 s
的时间戳。这个等待不是 Commit
Wait,而是等待本地状态机赶上。
Safe Time 概念
为了支持快照读和强读,Spanner 引入了 safe time
的概念。Safe time 是一个时间戳
t_safe,表示对于任何小于等于
t_safe 的时间戳 t,replica
都已经应用了所有时间戳 ≤ t 的写入。
Safe time 的计算取决于 replica 是否是 leader:
- Leader 的 safe
time:
t_safe = min(t_prepared, t_paxos_safe)t_prepared是所有已 prepare 但未 commit 的事务的最小时间戳。t_paxos_safe是 Paxos 状态机已应用的最大时间戳。
- Replica 的 safe
time:
t_safe = t_paxos_safe- 因为 replica 只从 Paxos 日志接收已提交的事务。
当客户端请求读取时间戳 t 的数据时,replica
需要等待直到
t_safe ≥ t,然后才能返回结果。这保证了读取的数据是完整和一致的。
读协议的伪代码
def snapshot_read(replica, timestamp, key):
# 等待直到 safe time 达到请求的时间戳
while replica.safe_time() < timestamp:
wait_for_state_machine_advance()
# 读取指定时间戳的数据版本
return replica.read_version(key, timestamp)
def strong_read(leader, key):
# 选择时间戳
s = TT.now().latest
# 等待状态机应用到 s
while leader.safe_time() < s:
wait_for_state_machine_advance()
# 读取数据
return leader.read_version(key, s)读优化
Spanner 对读操作有几个重要的优化:
副本选择:对于快照读,客户端可以选择距离最近的副本,减少网络延迟。
批量读取:客户端可以在一次 RPC 中读取多个 key,减少往返次数。
读取缓存:客户端可以缓存快照读的结果,如果时间戳没有变化,可以重用缓存。
Paxos Leader Lease:Leader 通过 lease 机制保证在 lease 期间它仍然是 leader,这样可以避免每次读取都运行 Paxos 协议。
六、Paxos Groups 与数据模型
Spanner 的架构是分层的,理解其数据模型和分布需要从多个层次来看。
Directory:数据放置的基本单元
在 Spanner 中,数据被组织成 directory。Directory 是数据放置和移动的基本单元,类似于传统分布式系统中的 shard 或 partition。但 directory 是自动管理的,不需要应用层显式分区。
一个 directory 包含一组连续的 key 和它们的所有副本。Directory 的大小通常在几十 MB 到几百 MB 之间。当 directory 变得过大时,Spanner 会自动将其分裂成多个更小的 directory;当 directory 过小时,可能会与相邻的 directory 合并。
Directory 的移动是通过后台任务完成的。Spanner 会监控每个 directory 的访问模式,并将频繁被同一客户端访问的 directory 移动到距离客户端更近的数据中心。这个过程对应用层是透明的。
Paxos Group:复制与共识
每个 directory 属于一个 Paxos group。一个 Paxos group 管理一组 directory,并使用 Paxos 协议在多个副本之间复制数据。
Paxos group 的 replica 分布在不同的数据中心,通常跨越三个或更多地理位置。在任何时刻,一个 Paxos group 有一个 leader,负责处理写入和强一致性读取。Leader 通过 Paxos 协议将写入复制到多数副本,然后应用到状态机。
每个 replica 运行在一个 spanserver 上。Spanserver 是 Spanner 的基本服务器进程,负责管理多个 tablet。Tablet 是 directory 在单个 replica 上的体现,包含了数据的实际存储(通常使用类似 LevelDB 或 RocksDB 的 LSM 树结构)以及事务管理逻辑。
跨 Paxos Group 的事务
当一个事务涉及多个 Paxos group 时,Spanner 使用两阶段提交协议。事务的协调者通常是涉及到的第一个 Paxos group 的 leader。
跨 Paxos group 事务的流程:
客户端读写:客户端向涉及的每个 Paxos group 发送读写请求。
Prepare 阶段:
- 协调者选择一个时间戳
s。 - 协调者向所有参与者发送 prepare 消息,附带时间戳
s。 - 每个参与者(Paxos group 的 leader)通过 Paxos 将 prepare 记录写入日志,并回复 prepared。
- 协调者选择一个时间戳
Commit Wait:协调者等待直到
TT.after(s)返回 true。Commit 阶段:
- 协调者通过 Paxos 将 commit 决定写入自己的日志。
- 协调者向所有参与者发送 commit 消息。
- 参与者通过 Paxos 将 commit 记录写入日志,并应用事务。
整个过程需要至少三次 Paxos 协议运行(每个参与者在 prepare 阶段和 commit 阶段各运行一次,协调者在 commit 阶段运行一次)。这是分布式事务的固有成本。
下面的时序图完整展示了一次跨 Paxos Group 事务的端到端消息流:
sequenceDiagram
participant Client as 客户端
participant Coord as 协调者<br/>(Paxos Group Leader)
participant P1 as 参与者1<br/>(Paxos Group)
participant P2 as 参与者2<br/>(Paxos Group)
Client->>Coord: 发起事务提交请求
par Prepare 阶段(并行)
Coord->>P1: Prepare
Note over P1: 内部运行 Paxos<br/>复制 Prepare 到多数派
P1-->>Coord: Prepared(t_prepared_1)
and
Coord->>P2: Prepare
Note over P2: 内部运行 Paxos<br/>复制 Prepare 到多数派
P2-->>Coord: Prepared(t_prepared_2)
end
Note over Coord: 分配时间戳 s = max(<br/>TT.now().latest,<br/>t_prepared_1, t_prepared_2)
Note over Coord: Commit Wait:<br/>等待 TT.after(s) = true
Note over Coord: 运行 Paxos<br/>记录 Commit 决定到自身日志
par Commit 阶段(并行)
Coord->>P1: Commit(s)
Note over P1: 运行 Paxos<br/>复制 Commit 到多数派
and
Coord->>P2: Commit(s)
Note over P2: 运行 Paxos<br/>复制 Commit 到多数派
end
Coord-->>Client: 事务成功
该时序图展示了跨 Paxos Group
事务的三个关键阶段:首先,协调者并行向所有参与者发送 Prepare
消息,每个参与者在内部通过 Paxos 协议将 Prepare
记录复制到多数派副本以确保持久性;然后,协调者基于所有
t_prepared 和 TrueTime 分配全局时间戳并执行
Commit Wait;最后,协调者将 Commit 决定写入自身 Paxos
日志后,并行通知所有参与者提交。整个过程中至少运行了三次独立的
Paxos 共识(每个参与者 Prepare 阶段各一次、协调者 Commit
阶段一次),参与者 Commit 阶段还需要额外的 Paxos
运行,这是跨分片分布式事务不可避免的通信开销。
Spanner 的半关系数据模型
Spanner 支持 SQL 和关系数据模型,但它不是传统的关系数据库。Spanner 的数据模型是半关系的(semi-relational),主要特点是支持 interleaved tables。
Interleaved tables 允许子表的行与父表的行物理上交错存储。这样可以将有父子关系的数据放在一起,减少跨 Paxos group 的访问。
例如,一个论坛应用可能有 Users 表和
Posts 表:
CREATE TABLE Users (
user_id INT64 NOT NULL,
name STRING(100),
email STRING(100)
) PRIMARY KEY (user_id);
CREATE TABLE Posts (
user_id INT64 NOT NULL,
post_id INT64 NOT NULL,
title STRING(200),
content STRING(MAX),
created_at TIMESTAMP
) PRIMARY KEY (user_id, post_id),
INTERLEAVE IN PARENT Users ON DELETE CASCADE;在这个 schema 中,每个用户的所有帖子会与该用户的记录存储在同一个 directory 中。查询某个用户的所有帖子时,只需要访问一个 Paxos group,无需跨网络查询。
Spanner 还支持 secondary indexes,可以是 local index(与基表在同一 Paxos group)或 global index(可能在不同的 Paxos group)。Global index 可以提高某些查询的效率,但写入时需要跨 Paxos group 更新索引。
七、事务类型:读写、只读与 Schema Change
Spanner 支持三种主要的事务类型,每种都有不同的实现机制和性能特征。
读写事务(Read-Write Transaction)
读写事务使用两阶段锁(2PL)和两阶段提交(2PC)协议,是 Spanner 中最重的事务类型。
两阶段锁:事务在读取或写入数据时获取锁,并在整个事务期间持有锁。读操作获取读锁,写操作获取写锁。读锁与读锁兼容,但与写锁互斥;写锁与任何锁都互斥。锁在事务提交或回滚后释放。
两阶段提交:如前所述,涉及 Prepare、Commit Wait 和 Commit 三个阶段。协调者负责协调所有参与者,确保事务的原子性。
读写事务的延迟主要来自于: 1. 网络往返(在 Prepare 和 Commit 阶段) 2. Paxos 复制延迟 3. Commit Wait 的等待时间(2ε,约 5-10ms)
对于单个 Paxos group 的事务,不需要 2PC,可以直接通过 Paxos 提交,延迟会大大降低。
只读事务(Read-Only Transaction)
只读事务是 Spanner 的一个重要优化。由于不需要写入,只读事务可以避免加锁和 2PC,大大降低了延迟和系统开销。
只读事务有两种执行模式:
1. 带时间戳边界的只读事务:客户端可以指定一个时间戳或 staleness 边界,例如”读取 10 秒前的数据”。这种事务可以在任何足够新的 replica 上执行,无需与 leader 通信。
-- 读取 10 秒前的一致性快照
SET READ_ONLY_STALENESS = EXACT_STALENESS 10s;
SELECT * FROM Users WHERE user_id = 123;2. 强一致性只读事务:读取当前最新的数据。这种事务需要在 leader 上执行(或等待 replica 应用到最新的时间戳),但仍然不需要获取锁或运行 2PC。
只读事务的时间戳分配策略: - 对于带 staleness
的事务,时间戳为 now() - staleness。 -
对于强一致性事务,时间戳为
TT.now().latest(在事务开始时)。
只读事务内的所有读操作使用相同的时间戳,保证了事务内部的一致性。由于使用 MVCC(多版本并发控制),只读事务不会与写事务产生锁冲突。
只读事务的优势: 1. 低延迟:无需 Commit Wait,无需 2PC。 2. 高吞吐:可以在任何 replica 执行,负载可以分散。 3. 不影响写入:不加锁,不阻塞写事务。
Schema Change 事务
Schema change 是分布式数据库中的一个难题,尤其是在跨数据中心的环境下。Spanner 使用一个巧妙的机制来处理 schema change,确保在 schema 演进期间系统仍然可用。
非阻塞 Schema Change:Spanner 的 schema change 不会阻塞正常的读写事务。它通过在未来的某个时间戳应用 schema change 来实现这一点。
Schema change 的流程:
客户端提交 schema change 请求:例如添加一个列、创建索引等。
分配未来时间戳:系统为 schema change 分配一个未来的时间戳
t_schema,通常是当前时间加上几秒钟。广播 schema change:系统将 schema change 和时间戳
t_schema广播到所有 Paxos group。Prepare 阶段:每个 Paxos group 在时间戳
t_schema之前完成当前的事务,并准备应用 schema change。应用 schema change:当真实时间达到
t_schema时,所有 Paxos group 同时应用 schema change。完成:Schema change 完成后,新的读写事务使用新的 schema。
通过使用未来时间戳,Spanner 确保了所有节点在同一个逻辑时刻应用 schema change,避免了不同节点看到不同 schema 的问题。
Schema change 的挑战: -
长时间运行的事务:如果有事务在
t_schema
之前开始但在之后才提交,需要特殊处理。Spanner
通过拒绝这类事务或强制它们使用旧 schema 来解决。 -
索引构建:创建新索引需要扫描全表,这可能需要很长时间。Spanner
将索引构建作为后台任务,逐步完成。
事务类型的选择
应用应该根据需求选择合适的事务类型:
- 需要强一致性的更新:使用读写事务。
- 只读查询,可以容忍少量 staleness:使用带 staleness 的只读事务,可以从任何 replica 读取,延迟最低。
- 只读查询,需要最新数据:使用强一致性只读事务,在 leader 上执行,延迟适中。
- 分析查询,长时间运行:使用带固定时间戳的只读事务,读取一个一致的历史快照。
八、CockroachDB:开源世界的 Spanner
CockroachDB 是一个受 Spanner 启发的开源分布式 SQL 数据库,由 Cockroach Labs 开发。它的设计目标是在没有 TrueTime 硬件的情况下实现类似的一致性保证。
没有 TrueTime 的挑战
CockroachDB 无法假设用户会部署 GPS 和原子钟硬件,因此需要一个纯软件的解决方案来处理时间同步问题。它使用了混合逻辑时钟(Hybrid Logical Clock, HLC)。
混合逻辑时钟:HLC 结合了物理时钟和逻辑时钟的优点。每个 HLC 时间戳包含两部分: - 物理时间部分:从本地系统时钟获取的真实时间(Unix 纳秒)。 - 逻辑计数器:用于区分在同一物理时间发生的事件。
HLC 的更新规则: 1. 当节点生成新事件时,取当前物理时间和上一个 HLC 的物理时间的最大值,逻辑计数器为 0(如果物理时间前进)或递增(如果物理时间未前进)。 2. 当节点接收到带有 HLC 的消息时,更新本地 HLC 以反映消息中的时间(如果更大)。
HLC 保证了因果一致性:如果事件 A 因果先于事件 B(例如 A 发送了一条消息,B 接收了这条消息),那么 HLC(A) < HLC(B)。
CockroachDB 的一致性保证
CockroachDB 提供的是严格可串行化(Strict Serializability),与 Spanner 的外部一致性类似。但由于缺乏 TrueTime 的时间区间保证,CockroachDB 依赖于更保守的假设:
时钟漂移边界:CockroachDB 假设不同节点的时钟偏差在一个可配置的边界内(默认 500ms)。如果检测到时钟偏差超过这个边界,节点会拒绝服务。
Uncertainty interval:当 CockroachDB 执行读操作时,它考虑一个 uncertainty interval,类似于 TrueTime 的误差区间。如果在这个区间内有写入,读操作可能需要重试。
HLC 的单调性:通过 HLC,CockroachDB 保证了时间戳的单调性,即使在时钟回拨的情况下。
架构差异
虽然 CockroachDB 受 Spanner 启发,但两者在架构上有显著差异:
1. 共识协议: - Spanner 使用 Paxos(后来可能迁移到 Raft,虽然论文中是 Paxos)。 - CockroachDB 使用 Raft,每个 range 是一个 Raft group。
2. 数据模型: - Spanner 支持 SQL 和 interleaved tables。 - CockroachDB 也支持 SQL,但其数据模型是基于 key-value 的,通过编码将 SQL 数据映射到 key-value。
3. 时间同步: - Spanner 使用 TrueTime 硬件。 - CockroachDB 使用 HLC 和 NTP。
4. 地理复制: - Spanner 原生支持跨数据中心复制,每个 Paxos group 的副本分布在不同地理位置。 - CockroachDB 也支持,但需要配置 zone configurations 来控制数据放置。
性能取舍
Spanner 的优势: - TrueTime 提供了更小的时间不确定性(1-7ms vs 500ms),减少了冲突重试的概率。 - Commit Wait 时间更短,事务延迟更低。 - 可以更精确地实现外部一致性。
CockroachDB 的优势: - 不需要特殊硬件,部署成本低。 - 开源,社区驱动,迭代速度快。 - 更灵活的配置选项,适合多种场景。
实测性能:根据公开的 benchmark,在跨数据中心的写入场景下,Spanner 的延迟通常比 CockroachDB 低 20-30%,这主要归功于更精确的时间同步。但在单数据中心或只读查询场景下,两者的性能差距不大。
CockroachDB 和 TiDB 如何在没有原子钟的情况下近似 TrueTime
Spanner 的 TrueTime 依赖专用硬件(GPS 和原子钟),这对大多数组织来说是不可获取的资源。CockroachDB 和 TiDB 分别采用了两种截然不同的策略来解决分布式时间戳问题,各有优劣。
CockroachDB:HLC + 不确定性区间(Uncertainty Interval)
CockroachDB
使用混合逻辑时钟(HLC)配合一个可配置的最大时钟偏移量(--max-offset,默认
500ms)来近似 TrueTime 的时间区间。其核心思想是:虽然无法像
TrueTime
那样精确量化时钟误差,但可以假设一个保守的上界。
当 CockroachDB 节点执行读操作时,它会计算一个不确定性区间
[read_timestamp, read_timestamp + max_offset]。如果在这个区间内遇到了一个写入的时间戳,读操作无法确定该写入是发生在”读之前”还是”读之后”——因为时钟偏移可能导致时间戳排序与真实时间不一致。此时,CockroachDB
采用读重启(Read
Restart)策略:将读时间戳推进到遇到的写入时间戳之后,然后重新执行读操作。
def cockroachdb_read(node, key, read_ts, uncertainty_limit):
value, write_ts = node.get(key)
if write_ts > read_ts and write_ts <= uncertainty_limit:
# 写入落在不确定性区间内,无法确定因果顺序
# 触发读重启:将时间戳推进到写入之后
new_read_ts = write_ts + 1
new_uncertainty = read_ts + max_offset # 不确定性上界不变
return cockroachdb_read(node, key, new_read_ts, new_uncertainty)
elif write_ts <= read_ts:
return value # 写入明确在读之前,返回该值
else:
return None # 写入明确在读之后,不可见读重启的代价是额外的延迟和重试。在高并发写入场景下,如果
max_offset 较大(如默认的
500ms),不确定性区间宽,读重启的概率显著上升,导致尾延迟增加。
TiDB:集中式时间戳预言机(TSO)
TiDB 采用了一种完全不同的策略:通过 PD(Placement Driver)组件中的 TSO(Timestamp Oracle)提供全局单调递增的时间戳。每个事务在开始和提交时都需要从 TSO 获取时间戳,TSO 保证所有分配的时间戳严格单调递增。
class TSO:
def __init__(self):
self.logical = 0
self.physical = current_time_ms()
def get_timestamp(self):
now = current_time_ms()
if now > self.physical:
self.physical = now
self.logical = 0
else:
self.logical += 1
return (self.physical << 18) | self.logical由于所有时间戳来自同一个 TSO 实例,TiDB 完全绕开了时钟同步问题——不存在不确定性区间,也不需要读重启。事务的时间戳顺序天然反映了真实的请求到达顺序。
但 TSO 的代价是引入了一个潜在的单点瓶颈和额外的网络往返。每个事务至少需要两次 TSO 调用(开始和提交各一次),在跨区域部署时,TSO 到远端数据中心的网络延迟会直接叠加到事务延迟上。TiDB 通过 TSO 批量分配和 PD leader 选举来缓解这个问题,但在全球分布的场景下,TSO 的地理位置仍然是一个关键的性能因素。
CockroachDB 处理不确定性窗口的具体机制
CockroachDB 的读重启机制包含几个关键的优化细节:
观测时间戳(Observed Timestamps):当一个节点首次与另一个节点通信时,会记录对方的 HLC 时间戳。后续读操作可以利用这个观测记录来缩小不确定性区间——如果能确认某个节点在某个时刻之前没有产生写入,就可以将该节点的不确定性上界降低。
事务级别的不确定性收敛:随着事务执行过程中与更多节点交互,不确定性区间会逐渐缩小。一个事务在刚开始时不确定性最大(整个
max_offset窗口),但随着它读取更多节点的数据,不确定性会收敛。时钟偏移检测与拒绝服务:CockroachDB 会持续监控节点间的时钟偏移。如果检测到某个节点的时钟偏移超过了
max_offset的 80%,该节点会主动停止服务,防止违反一致性保证。
TiDB 如何完全避免不确定性问题
TiDB 通过集中式 TSO 从根本上消除了不确定性:
全序时间戳:TSO 分配的时间戳构成全序关系,任意两个时间戳都可以明确比较大小,不存在”无法确定先后”的情况。
因果一致性的天然保证:如果事务 T1 在 T2 之前完成,则 T1 的提交时间戳一定小于 T2 的开始时间戳——因为 T2 的开始时间戳是在 T1 提交之后从 TSO 获取的。
无需 Commit Wait:由于时间戳来自单一源,TiDB 不需要类似 Spanner 的 Commit Wait 机制,事务可以在 Raft 提交完成后立即返回。
三种方案的量化对比
| 维度 | TrueTime(Spanner) | HLC(CockroachDB) | TSO(TiDB) |
|---|---|---|---|
| 时间不确定性 | 1-7ms | 250-500ms(可配置) | 0ms(无不确定性) |
| Commit Wait 延迟 | 2ε(约 5-10ms) | 无需(用读重启替代) | 无需 |
| 读延迟影响 | 无额外开销 | 遭遇不确定性写入时需重启,尾延迟增加 | 每次需访问 TSO,增加 1 次 RTT |
| 写延迟影响 | 增加 Commit Wait(5-10ms) | 无额外等待 | 每次需访问 TSO,增加 1 次 RTT |
| 跨区域写延迟 | Paxos RTT + Commit Wait(~30ms) | Raft RTT(~25ms) | Raft RTT + TSO RTT(~35-50ms) |
| 单点故障风险 | 无(完全去中心化) | 无(完全去中心化) | PD leader 故障需切换(秒级) |
| 硬件要求 | GPS + 原子钟 | 仅需 NTP 同步 | 仅需 NTP 同步 |
| 一致性保证强度 | 外部一致性(最强) | 严格可串行化(依赖时钟假设) | 线性一致性(依赖 TSO 可用性) |
对实际应用的影响
这三种时间方案的差异在实际应用中体现为不同的性能特征和运维要求:
高频交易/金融场景:Spanner 的低不确定性(1-7ms)意味着几乎不会遇到时间冲突,写延迟稳定可预测。CockroachDB 的 500ms 窗口在高并发写入时可能触发频繁的读重启。TiDB 的 TSO 延迟取决于 PD 的地理位置,适合同区域部署。
全球分布式应用:Spanner 的去中心化时间服务天然适合全球部署。CockroachDB 同样是去中心化的,但更大的不确定性窗口会增加跨区域读冲突。TiDB 的 TSO 架构使得远离 PD 的区域天然承受更高的延迟。
成本敏感场景:CockroachDB 和 TiDB 不需要专用硬件,部署和运维成本显著低于 Spanner。对于大多数应用,HLC 或 TSO 方案的一致性保证已经足够。
CockroachDB 的创新
CockroachDB 不仅仅是 Spanner 的开源实现,它也有自己的创新:
多租户支持:CockroachDB Serverless 提供了细粒度的多租户隔离和资源控制。
在线 Schema Change:CockroachDB 改进了 schema change 的实现,支持更复杂的 DDL 操作。
查询优化器:CockroachDB 的查询优化器(基于 Cascades 框架)在某些查询上比 Spanner 更优秀。
Kubernetes 原生:CockroachDB 与 Kubernetes 的集成非常深入,适合云原生应用。
九、总结与思考
Spanner 不仅仅是一个分布式数据库,它代表了分布式系统设计的一次重要范式转变。在 Spanner 之前,学术界和工业界普遍认为,在广域网环境下实现强一致性是不切实际的,因此 NoSQL 运动提倡放弃一致性来换取可用性和性能。但 Spanner 证明了,通过正确的工程投入,强一致性和高可用性是可以兼得的。
Spanner 的历史地位
Spanner 的论文发表于 2012 年,正值 NoSQL 运动的高峰期。当时,Cassandra、MongoDB、Riak 等系统主张使用最终一致性,认为 CAP 定理意味着必须在一致性和可用性之间做出选择。Spanner 的出现打破了这个迷思,证明了通过合理的系统设计(如 Paxos 复制、TrueTime、多版本并发控制),可以在保证强一致性的同时提供高可用性和可扩展性。
Spanner 对后续系统产生了深远影响: - CockroachDB:开源的 Spanner 替代品,使用 HLC 代替 TrueTime。 - YugabyteDB:另一个受 Spanner 启发的分布式 SQL 数据库,使用 Raft 和 MVCC。 - TiDB:中国的开源分布式数据库,使用混合时间戳(类似 HLC)。 - FoundationDB:Apple 的分布式键值存储,虽然数据模型不同,但也提供了严格可串行化。 - Azure Cosmos DB:微软的云数据库服务,提供了多种一致性级别,包括强一致性。
这些系统的共同点是,它们都认识到强一致性是可以实现的,并且对许多应用来说是值得追求的。
硬件解决软件问题的哲学
Spanner 的 TrueTime 体现了一个重要的设计哲学:有时候,硬件投资比软件复杂度更划算。在分布式系统的传统思维中,我们总是假设硬件是不可靠的、时钟是不同步的、网络是会分区的,然后设计复杂的软件协议来容忍这些问题。但 Spanner 的做法是,通过投资硬件(GPS 和原子钟)和基础设施(time master 机器、频繁的时间同步),从根本上降低了时间不确定性,从而简化了上层协议的设计。
这个思路在其他领域也有应用: - RDMA(远程直接内存访问):通过特殊的网络硬件,绕过操作系统内核,实现极低延迟的网络通信,简化了分布式系统的设计。 - Non-Volatile Memory(非易失性内存):通过持久化内存硬件,模糊了内存和存储的边界,简化了数据库的恢复逻辑。 - Programmable Switches(可编程交换机):通过在交换机上运行自定义逻辑,将某些分布式协议卸载到网络层。
这些技术的共同点是,它们通过改变底层假设(例如”时钟是不可靠的”或”网络延迟是高且不可预测的”),让上层软件设计变得更简单。
TrueTime 的局限性
尽管 TrueTime 是一个优雅的解决方案,但它也有局限性:
硬件成本:部署 GPS 接收器和原子钟需要资金投入。对于小公司或个人开发者来说,这可能是不可承受的。
地理限制:GPS 信号在某些环境下(如建筑物内部、地下设施)可能不可用。
安全风险:GPS 信号可以被欺骗或干扰,虽然 TrueTime 通过多源交叉验证来缓解这个问题,但仍然存在潜在风险。
不可移植:TrueTime 依赖于 Google 的私有基础设施,无法直接在公有云或本地部署中使用。
因此,对于大多数组织来说,CockroachDB 或 TiDB 这样的开源替代品可能更实用。
时间的本质
Spanner 引发了一个深层次的哲学问题:在分布式系统中,什么是”现在”?
在物理学中,相对论告诉我们,绝对的同时性是不存在的。两个事件是否同时发生取决于观察者的参考系。但在实际的分布式系统中,我们的速度远低于光速,相对论效应可以忽略不计。真正的问题是测量和传播的延迟。
TrueTime 的巧妙之处在于,它不试图给出”现在”的精确答案,而是给出一个诚实的不确定性区间。这种诚实让上层协议能够正确地处理时间的模糊性。Commit Wait 机制则通过主动等待,将不确定性转化为确定性。
这个思路在其他领域也有启发意义。例如,在机器学习中,我们越来越多地看到”calibrated”模型,它们不仅给出预测结果,还给出预测的置信度。在传感器融合中,卡尔曼滤波器也是通过显式地建模不确定性来融合多个传感器的数据。
未来的方向
Spanner 的设计已经过去了十多年,分布式数据库领域仍在快速演进。一些值得关注的方向包括:
确定性数据库:如 Calvin 和 SLOG,它们通过预先确定事务的顺序来避免两阶段提交的开销。
无服务器数据库:如 Aurora Serverless 和 CockroachDB Serverless,它们将计算和存储分离,支持按需扩展。
多模型数据库:同时支持关系、文档、图等多种数据模型的数据库,如 CosmosDB。
HTAP(混合事务分析处理):在同一个系统中同时支持 OLTP 和 OLAP 工作负载,如 TiDB。
量子时钟:未来可能出现更精确的时间同步技术,进一步降低 ε,使 Commit Wait 的代价变得可以忽略。
Spanner 证明了,分布式系统的”不可能三角”并非铁律,关键在于如何定义和衡量这三个维度。通过精巧的工程和合理的权衡,我们可以构建既强一致又高可用的系统。这对整个分布式系统社区来说,是一个重要的里程碑。
实践建议
对于开发者和架构师,Spanner 的经验提供了一些宝贵的教训:
不要过早放弃一致性:许多系统在设计初期就选择了最终一致性,但后来发现应用层处理不一致的复杂度更高。在选择一致性模型时,应该仔细权衡。
理解你的一致性需求:不是所有数据都需要强一致性。合理地混合使用不同的事务类型(如 Spanner 的只读事务和读写事务)可以获得更好的性能。
时间是宝贵的资源:在分布式系统中,时间不是免费的。理解时间同步的成本和延迟对于优化系统性能至关重要。
硬件不总是敌人:不要局限于纯软件的解决方案,有时候硬件投资可以带来更简单、更可靠的系统设计。
学习经典论文:Spanner 的论文值得反复阅读。它不仅描述了系统设计,还解释了设计背后的原理和权衡。
Spanner 与 TrueTime 的故事告诉我们,分布式系统的挑战是可以被攻克的,但需要创造性的思维、深厚的理论基础和大胆的工程实践。正如 Spanner 团队所证明的,有时候最优雅的解决方案来自于重新审视基本假设。
参考文献
Corbett, J. C., Dean, J., Epstein, M., et al. (2012). “Spanner: Google’s Globally-Distributed Database.” In Proceedings of OSDI ’12.
Brewer, E. (2012). “Spanner, TrueTime and the CAP Theorem.” Blog post, Google Research.
Shute, J., Vingralek, R., Samwel, B., et al. (2013). “F1: A Distributed SQL Database That Scales.” In Proceedings of VLDB ’13.
Shute, J., et al. (2012). “Building Spanner.” Presentation at Google I/O.
Lamport, L. (1998). “The Part-Time Parliament.” ACM Transactions on Computer Systems, 16(2):133-169.
Kulkarni, S. S., Demirbas, M., Madappa, D., Avva, B., & Leone, M. (2014). “Logical Physical Clocks and Consistent Snapshots in Globally Distributed Databases.” Technical Report.
Taft, R., Sharif, I., Matei, A., et al. (2020). “CockroachDB: The Resilient Geo-Distributed SQL Database.” In Proceedings of SIGMOD ’20.
Ongaro, D., & Ousterhout, J. (2014). “In Search of an Understandable Consensus Algorithm.” In Proceedings of USENIX ATC ’14.
Rao, J., Shekita, E. J., & Tata, S. (2011). “Using Paxos to Build a Scalable, Consistent, and Highly Available Datastore.” In Proceedings of VLDB ’11.
Glendenning, L., Beschastnikh, I., Krishnamurthy, A., & Anderson, T. (2011). “Scalable Consistency in Scatter.” In Proceedings of SOSP ’11.
Kraska, T., Pang, G., Franklin, M. J., Madden, S., & Fekete, A. (2013). “MDCC: Multi-Data Center Consistency.” In Proceedings of EuroSys ’13.
Lloyd, W., Freedman, M. J., Kaminsky, M., & Andersen, D. G. (2011). “Don’t Settle for Eventual: Scalable Causal Consistency for Wide-Area Storage with COPS.” In Proceedings of SOSP ’11.
上一篇:Percolator 模型
下一篇:Calvin 与确定性事务
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【系统架构设计百科】Google 基础设施:Borg、Spanner 与全球化架构
Google 在 2003 年发表 GFS 论文时,整个行业还在用单机数据库处理 Web 请求。此后二十年间,Google 内部基础设施经历了多次代际演进,从 GFS 到 Colossus,从 MapReduce 到 Flume,从 Borg 到 Kubernetes,从 Bigtable 到 Spanner。这些系统…
【分布式系统百科】混合逻辑时钟与 TrueTime:在物理和逻辑之间找到平衡
物理时钟对不齐,逻辑时钟丢物理信息,向量时钟太重。HLC 用物理时间 + 逻辑计数器找到了平衡。但 Google 选了另一条路:用原子钟和 GPS 把物理误差压到几毫秒。这篇文章从 HLC 的算法正确性证明、CockroachDB 源码实现、TrueTime 工程架构,一直讲到 AWS Clockbound 的开源方案——在物理和逻辑之间,每种选择都是一笔工程账。
【分布式系统百科】Calvin 与确定性事务:另一条路
在分布式事务处理领域,2012 年是极为特殊的一年。这一年,Google 发表了著名的 Spanner 论文,展示了如何通过 TrueTime API 和原子钟实现全球范围内的强一致性事务。同年,耶鲁大学的 Daniel Abadi 和 Alexander Thomson 在 SIGMOD 上发表了 Calvin 论文…
【分布式系统百科】分布式事务实战对比:TiDB vs CockroachDB vs YugabyteDB
自 Google 在 2012 年发表 Spanner 论文以来,分布式数据库领域掀起了一场深刻的变革。Spanner 向世界证明了一个长期被认为不可能的命题:在分布式系统中,我们可以同时获得水平扩展能力、强一致性和 SQL 语义。这打破了传统 NoSQL 系统"为了扩展性而牺牲一致性"的惯例,开启了所谓的 NewSQ…