IT 论文精读 · PAPER 24
Corbett 等 · Google · OSDI 2012
2012 年,Google 造出了 Spanner——一个把数据摊在横跨几个大洲的机房里、却依然像一本人人都认同顺序的账本一样工作的数据库。它撑起了 Google 广告这类最核心的业务,也是后来 Google Cloud Spanner、CockroachDB、TiDB 这些「全球数据库」的思想源头。它最出名的一招,是一个叫 TrueTime 的「诚实时钟」。
数据库想又快又不丢,就得把数据复制好几份、分散到不同城市的机房。可一旦散开,就冒出一个头疼问题:到底谁先谁后? 你在北京下的单,和同事在纽约改的价,哪个先生效?靠各机房自己的钟来判断吗——可每台机器的钟都走得略有快慢,谁也不敢拍胸脯说自己的时间绝对准。于是当时的全球数据库只能二选一:要么挤在一个地区、换来「顺序清楚」,要么摊向全球、换来「偶尔读到过期数据、顺序错乱」。Spanner 偏要两个都拿。
普通时钟的毛病是假装精确——它告诉你「现在是 3 点整」,可它自己其实心里没底。Spanner 反其道而行:让每台机器的钟老实交代自己的误差,不报一个点、而报一小段区间——「现在大概在 3 点整前后那几毫秒之间,反正真时间一定落在这段里」。这个「带着诚实误差的时间」,就是 TrueTime。
光有诚实区间还不够,关键在成交时「多等一拍」。每当一笔改动要落定,Spanner 先给它盖一个时间戳,然后故意等上那么几毫秒——等到连最「糊涂」的钟都承认「这个时间戳已经是过去时了」,才把改动对外公开。这一等,就保证了:任何在它之后才开始的改动,拿到的时间戳一定更大。 于是全球所有机房,哪怕彼此从没商量,也会对「谁先谁后」给出完全一致的答案——那本账本的顺序,人人都认。至于容错,Spanner 把每份数据在几个机房各存一份、靠一轮「投票」保证副本一致,坏掉一两台也照常跑。
Spanner 第一次证明:「全球铺开」和「强一致的事务」可以兼得,不必二选一。你能在横跨大洲的数据上,像用一台单机数据库那样放心地转账、下单、改库存,系统替你保证顺序不乱、读到的永远是说得通的最新状态。诚实地说,这份「多等一拍」给每次写都添了几毫秒延迟,而且它靠的是每个机房里真装了 GPS 天线和原子钟——这套家当不是随便谁都搭得起。
别再假装全球的钟都分毫不差——让每台钟诚实报出「时间在这一小段区间里」,再在每次成交时多等一小拍、等到误差被「等」了过去,全世界就能对「谁先谁后」给出同一个答案。于是一个铺满地球的数据库,用起来却像一本顺序人人认同的单机账本。
想看 TrueTime 的区间与 commit-wait 时序、Paxos 组 + 两阶段提交的结构图和实验数字? → 切到精读版
Spanner 是第一个把数据分布到全球尺度、同时还支持外部一致(externally-consistent)分布式事务的数据库。它的核心创新是 TrueTime——一个把时钟不确定性显式暴露成时间区间的 API;靠它,Spanner 能给每个事务一个尊重真实先后的全局时间戳,从而在跨洲复制的数据上实现强一致的读写事务、无锁快照读,并支撑了 Google 广告后端 F1。
作者是 James Corbett、Jeffrey Dean、Wilson Hsieh 等一大批人,来自 Google,论文发在 OSDI 2012(获最佳论文)。它上承 Bigtable(2006)与 Megastore,底层数据落在 Colossus(GFS 的后继)上、副本一致靠 Paxos(Lamport);下启 Google 广告后端 F1、商用的 Google Cloud Spanner,以及 CockroachDB、YugabyteDB、TiDB 这一整代「NewSQL / 全球数据库」。
Bigtable 好用,但只保证单行原子,跨行、跨表的事务要应用自己扛,写起来痛苦;后来的 Megastore 补上了同步复制与事务,却写吞吐很低。当时业界近乎共识:「全球复制」与「强一致事务」不可兼得——这既是 CAP 的取舍,更卡在一个最根本的时间难题上。
要让分散在各大洲的机器对「事件谁先谁后」达成一致,就得有个大家都信的时间基准。可物理时钟总在漂移,NTP 对时的误差可达几十到上百毫秒、而且没有可靠的上界——你拿到一个时间戳,却不知道它到底偏了多少。没有可信的时间,就没法用时间戳给事务定序,也就做不出跨机房的强一致。 以往系统只能回避:要么放弃全局事务,要么把数据锁在一个地区。Spanner 决定正面啃下这块硬骨头。
传统的时钟 API 返回一个时间点,却对自己的误差绝口不提——这正是万恶之源。TrueTime 反过来:TT.now() 返回一个区间 [earliest, latest],并保证真实时间一定落在这个区间内。区间的半宽记作 ε(epsilon),代表「此刻的不确定性」。
这个保证怎么来的?每个机房部署一批时间主机,用 GPS 接收机和原子钟两类失效模式不同的时间源交叉校准(GPS 可能天线故障、原子钟可能缓慢漂移,但很难同时出错),再叠加已知的本地晶振漂移上界推算出 ε。实测 ε 通常只有几毫秒(约 1–7ms、均值约 4ms)。关键不在「消灭误差」——误差消不掉——而在把误差量化、并交给系统去处理。TrueTime 还配两个判断:TT.after(t)(t 是否已确定成为过去)与 TT.before(t)。
Spanner 的最强承诺是外部一致性(external consistency,即事务级的线性一致):若事务 T1 在真实时间上先于 T2 提交完成,则 T1 的时间戳 < T2 的时间戳——全世界看到的顺序,与事情真实发生的顺序完全吻合。
怎么保证?提交时协调者选一个时间戳 s = TT.now().latest(取区间上界,宁可偏大),然后故意等到 TT.after(s) 为真——即等到连最保守的钟都确认「s 已经成为过去」——才放锁、让这次写对外生效。这一步就叫 commit wait,大约要等两倍不确定性(≈ 2ε,约 10ms 量级)。这一等,保证任何后开始的事务读到的 TrueTime 都已越过 s,于是它必然拿到一个更大的时间戳。 换句话说,Spanner 没有假装时钟完美,而是把不确定性老老实实「等」了出去。
一个 Spanner 部署叫 universe,切成若干 zone(约等于一个机房,是物理隔离与部署的单位)。数据切成 tablet(多版本的 (key, 时间戳) → 值 映射,底层落在 Colossus 上)。每个 tablet 的若干副本分处不同 zone,组成一个 Paxos 组并选出一个 leader:所有写经 Paxos 复制到多数派副本,坏掉少数副本仍能继续服务。一批连续、同前缀的 key 组成一个 目录(directory),是数据放置与跨组迁移的最小单位。
单个 Paxos 组内的事务好办;跨多个组的事务用两阶段提交(2PC)协调。经典 2PC 的死穴是协调者一宕机,参与者就被卡住、锁迟迟放不掉。Spanner 的巧劲在于:2PC 里的每个参与者(以及协调者本身)都是一个 Paxos 组——任何单点宕机,都由该组内的 Paxos 自动选出新 leader 补位。于是 2PC 不再有单点、也不会永久阻塞。读写事务用两阶段锁(2PL)做并发控制(悲观加锁)。
因为每次写都带一个全局有意义的时间戳、且旧版本都留着(多版本),只读事务和快照读可以完全不加锁:指定一个时间戳 t,就读「截至 t 的那个版本」,任何「数据已追上 t」的副本都能就近服务——读不阻塞写、写不阻塞读,还能把读分摊到离用户最近的副本上。这正是 TrueTime 给全局时间戳赋予真实意义后,水到渠成的好处。
Spanner 打破了「全球分布 vs 强一致事务只能二选一」的教条,证明只要有一个把时钟不确定性讲清楚的 API,就能在全球尺度上做外部一致的事务。它把「时间」——分布式系统里历来最不可靠的东西——变成了可以拿来给事务定序的一等公民。工程上,它成了商用 Google Cloud Spanner 的内核;思想上,它点燃了 NewSQL / 全球数据库这一整代系统(CockroachDB、YugabyteDB、TiDB 等),只是这些追随者大多没有原子钟,改用更宽松的时钟界或混合逻辑时钟(HLC)去近似 TrueTime 的效果。
① 一句话:Spanner 是第一个既全球分布、又支持外部一致分布式事务的数据库,钥匙是 TrueTime。
② 痛点:全球复制下机器的钟各自漂移、NTP 误差无上界,没有可信时间就无法用时间戳给跨机房事务定序——于是「全球 vs 强一致」被认为不可兼得。
③ 核心 idea:TrueTime 不返回时间点,而返回区间 [earliest, latest] 并保证真时间在其中;ε 是不确定性,靠 GPS + 原子钟压到几毫秒。
④ 定序机制:外部一致性 = 真实上先提交者时间戳更小;靠 commit wait——选 s=now().latest 后等到 TT.after(s)(约 2ε)再放锁,把不确定性「等」出去。
⑤ 架构:数据切成 tablet,副本跨机房组成 Paxos 组(多数派写);连续同前缀 key 成一个目录,是放置/迁移单位。
⑥ 跨组事务:2PC 架在 Paxos 之上——每个参与者/协调者都是一个 Paxos 组,单点宕机被组内共识兜住,2PC 不再永久阻塞。
⑦ 红利:全局时间戳 + 多版本 → 只读事务和快照读完全无锁,可就近读任一追上的副本。
⑧ 影响与局限:证明「全球 + 强一致」可兼得,催生 NewSQL/全球数据库;但写有 ~2ε 延迟下限、依赖 GPS+原子钟、跨组走 2PC、分区时选 C 舍 A、初版 SQL 能力有限。