IT 论文精读 · PAPER 25
Shute 等 · Google · VLDB 2013
2013 年,Google 一队工程师公开了 F1——一个撑着 AdWords 广告业务(Google 当时最赚钱的产品)跑的数据库。它厉害在同时拿到了两样过去被认为不能兼得的东西:像大互联网系统那样能无限扩容,又保留了老式关系数据库的完整 SQL、事务和强一致。它取代了 AdWords 底下那套手工拆分、维护到令人崩溃的 MySQL。
过去十几年,做大系统的人被逼着做一道二选一。一边是老牌关系数据库(会 SQL、能开事务、数据永远对得上),可一旦数据大到一台机器装不下,就得手工把表「切成很多块」摊到很多机器上——切完之后跨块的事务、跨块的查询、再切一次,全都是噩梦。另一边是新兴的 NoSQL(像 Bigtable),天生能摊到几千台机器、随便扩,可代价是丢掉了事务、丢掉了 SQL、数据还常常「暂时对不上」。Google 自己就是从 MySQL 逃到 NoSQL 的,工程师们逃过去之后天天怀念以前的好日子。
F1 的赌注是:不用二选一。底下先垫一个天生就能扩、又天生强一致的存储层——上一篇讲过的 Spanner(Google 那个用「诚实的时钟」做全球一致事务的数据库);然后在它之上,把关系数据库那套完整 SQL、事务、索引原封不动地盖回来。换句话说:把扩容这件难事甩给下面的 Spanner 扛,F1 自己专心把「好用的数据库」这层做漂亮。
难点在一处:Spanner 为了让全球的数据永远一致,每次写入都要跨机房投票拍板,慢——一次提交要几十到上百毫秒,比本地数据库慢一大截。F1 用三招把这份慢摊平:
一、把一家客户的整棵数据装进「同一个文件夹」。一个广告主,底下有很多广告系列,每个系列又有很多广告组——F1 让这一整棵树在磁盘上物理挨着放、归到同一格。于是「取出这个客户的全部资料」是一次本地取件,改动也在一格里就地完成,不用满机房乱跑。
二、改数据不上锁,保存时再核对。就像多人编辑一份共享文档:你不锁住它慢慢改,而是先随便读、随便改,到「保存」那一刻才检查一下——这些内容有没有被别人动过?没动就落盘,动了就重来。这样慢吞吞的客户端不会一直占着锁堵住别人。
三、既然每次问答都是慢速的「越洋电话」,就别一句一句地问。F1 把应用改成一次批量多问、能并行的同时问,用问的次数变少、变并行,把每次的慢摊薄。结果 AdWords 网页整体反而不比老 MySQL 慢。
F1 证明了「能无限扩容」和「有完整 SQL+事务+强一致」不必二选一:把扩容甩给底下的 Spanner,自己专心盖回好用的关系数据库;再用「整棵数据归一格、改动不上锁只在保存时核对、批量并行地问」这三招,把跨机房同步带来的慢摊平。它撑起了 Google 最赚钱的 AdWords。
想看架构图、层级表怎么摆、乐观事务怎么跑,以及真实延迟数字? → 切到精读版
F1 是 Google 为 AdWords 打造的分布式关系数据库:它建在 Spanner 之上,既继承了 Spanner「能水平扩展 + 全球强一致」的存储底座,又在其上重新提供了完整 SQL、ACID 事务、一致的二级索引——正面推翻了「要扩展就必须放弃 SQL 与事务」的行业成见。为吞下同步复制带来的高提交延迟,它用层级式 schema(把相关数据物理聚簇成一棵树)、Protocol Buffer 列类型、乐观事务、以及批量+并行的客户端访问模式把延迟摊平,并把变更历史做成一等公民来维护一致索引。它自 2012 年起在生产环境管理着 AdWords 的全部数据。
作者是 Jeff Shute 等一大队 Google 工程师,论文发在 VLDB 2013。它上承 Google 自家的 Spanner(2012,提供强一致、可扩展的存储与事务)——F1 是盖在 Spanner 上的 SQL 层;它要替换的是 AdWords 底下那套手工分片的 MySQL(一次重新分片耗时以年计、动用大批人力)。它也是「NewSQL」浪潮的标志性系统之一:与它同时代的 Google Spanner、以及后来的 CockroachDB、TiDB 等,都在回答同一个问题——能不能既要扩展、又要 SQL 和事务。
2000 年代,做大规模系统的人被逼进一道二选一。传统关系数据库给你 SQL、事务、强一致,但扩不动:数据超过单机就得手工分片,之后跨分片的事务、跨分片的 JOIN、以及业务增长后的再分片,都是巨大的运维负担。Google 的 AdWords 正卡在这里——它跑在一套人工切分的 MySQL 上,一次重新分片要动员一个团队、耗时约两年。
另一条路是 NoSQL(Google 自家的 Bigtable 是代表):天生能摊到几千台、随便扩,但代价是放弃了跨行事务、放弃了 SQL、只给最终一致。逃到 NoSQL 的应用开发者,得在应用层自己拼事务、自己维护索引一致、自己写查询逻辑——把数据库本该干的脏活全背到身上。
F1 的动机就是:这道二选一是假的。只要底层能提供「可扩展 + 强一致」的存储,就能在其上把关系数据库最有价值的东西(SQL、事务、索引)重新装回来。而这样的底层,Google 刚好有了——Spanner。作者明确列出四个必须同时满足的目标:可扩展、可用、一致、好用——尤其强调「好用」,因为要让几百个工程师顺手把 AdWords 迁上来。
F1 的第一层设计哲学是分工。最难的「水平扩展 + 跨机房强一致复制」交给下面的 Spanner:数据按目录切成许多片,每片由一组副本(通常 5 个、跨多机房)用 Paxos 同步复制。F1 自己则是一层基本无状态的 SQL 引擎:客户端可以连到任意一台 F1 server,server 本身几乎不存数据,因此可以随意增删、按需扩容。大查询则交给一个 F1 slave pool(分布式查询 worker 池)并行执行。
这是 F1 最关键的设计,直接决定它能不能扛住延迟。传统关系模型里各张表互相独立、物理上四散;F1 允许把表声明成父子层级:子表的主键以父表主键为前缀,于是一个父行和它名下的所有子孙行被物理聚簇(clustering)在一起、交错存放,归入同一个 Spanner 目录、也就是同一个 Paxos 组。
以 AdWords 为例:Customer(广告主)→ Campaign(广告系列)→ AdGroup(广告组)。一个广告主的整棵数据树落在同一格里。好处是决定性的:取一个客户的全部资料是一次本地读取(不必跨组四处捞),改动这棵树内部的数据是一次单组本地事务(不必发起昂贵的跨组分布式提交)。因为真实业务里绝大多数事务恰好都局限在「一个客户」范围内,这一步就把大部分写操作变成了便宜的本地操作。
F1 的列不止能存数字和字符串,还能直接存 Protocol Buffer 对象——包括可重复字段(一个字段里装一串值)。这意味着许多本该拆成子表、查询时要 JOIN 的一对多关系,可以直接内嵌进父行的一个 protobuf 列里,少建几张表、少连几次表。而且 Google 的应用代码本来就用 protobuf 传数据,让数据库原生理解 protobuf,抹掉了对象与关系表之间的阻抗失配,开发体验更顺。
代价躲不掉:Spanner 的同步复制让一次提交要跨机房投票,延迟约 50–150 毫秒(读则约 5–10 毫秒)。F1 用两手化解。
其一是乐观事务(optimistic transaction),也是默认模式。它分两段:读阶段可以任意长、不加任何锁,只默默记下读过的每一行的「最后修改时间戳」;随后一个很短的写阶段把要写的改动打包,提交前重新核对那些时间戳有没有变——没变就落盘,变了(说明被人抢先改过)就中止重试。类比多人协作编辑共享文档:不锁文档,只在保存那一刻查一下「我读的内容有没有被人动过」。好处一串:慢吞吞或崩掉的客户端不会一直占着锁堵别人;重试幂等、服务端好实现;配合无状态的 F1 server 天然合拍。(另有快照事务做只读一致快照、悲观事务直接用 Spanner 的加锁事务,按需选用。)
其二是改造应用的访问模式。既然每次访问都像一通慢速越洋电话,就别一句一句地串行问:F1 鼓励应用批量地、并行地发起读取,用「次数更少、彼此并行」抵消单次的慢。配合层级式 schema(一次读回整棵树),AdWords 迁到 F1 后,网页端到端延迟反而与旧 MySQL 相当、部分页面还更快——单次操作虽慢,但并行度上来了。
索引有两种物理形态:本地索引与被索引的行同处一个目录,随行的事务一起更新,便宜;全局索引横跨众多目录、独立分片,更新它要发起跨组的分布式事务,昂贵,因此 F1 会限制一个事务里能改多少条全局索引。两种索引都随事务同步维护、保证一致——这正是 NoSQL 交给应用层自己扛、而 F1 替你扛下的活。此外,F1 把变更历史(change history)做成一等公民:每个事务都把自己的改动作为变更记录一并写下,下游可以订阅这些变更用于增量维护索引、失效缓存、同步到其它系统——一个内建、一致的「变更数据捕获」。
最有说服力的结果是它真的在跑 Google 最赚钱的业务:F1 自 2012 年初起在生产环境管理 AdWords 的全部数据,取代了之前手工分片的 MySQL,规模达上百 TB 级、承载持续的高并发 OLTP 负载。性能上,论文诚实地给出数字:得益于同步复制,提交延迟约 50–150 毫秒,明显高于单机数据库;但通过层级 schema、乐观事务与批量并行读,AdWords 的用户可见延迟与旧系统相当。可用性上,靠 Spanner 的多副本(典型 5 副本跨多机房),系统能扛住整个机房下线而不丢数据、几乎不中断。扩展性上,加机器即扩容,彻底告别了「以年计的重新分片」。
F1 与 Spanner 一起,把行业从「扩展就得放弃 SQL 与事务」的宿命里拉了出来,亲手证明了「可扩展」与「强一致 + 完整 SQL + 事务」可以并存——只要肯为一致性付出一点延迟、并在设计上把它摊平。它是 NewSQL 潮流的旗帜之一,直接启发和印证了后来一批分布式 SQL 数据库(CockroachDB、TiDB、YugabyteDB 等)的路线:底层做可扩展强一致存储,上层重铸 SQL。它示范的几样具体工程——层级聚簇换数据局部性、乐观事务对抗高延迟与不良客户端、内建变更历史维护一致索引——也成了后来系统反复借鉴的套路。对 Google 自己,它更是把最核心业务的数据底座换血成功的一次实战。
① 一句话:F1 建在 Spanner 之上,把「可扩展 + 强一致」的存储底座,重新配上完整 SQL、ACID 事务与一致索引,撑起了 AdWords。
② 痛点:过去只能二选一——关系库有 SQL/事务但扩不动(要手工分片、再分片以年计),NoSQL 能扩但丢了事务、SQL 与强一致。
③ 骨架:把「扩展」甩给 Spanner,F1 自己做基本无状态、可随意扩容的 SQL 引擎;大查询交给 slave pool 并行跑。
④ 层级 schema:子表主键以父键为前缀,父子行物理聚簇成一棵树、归同一 Paxos 组 → 取整棵树一次本地读、树内改动是单组本地事务。
⑤ Protobuf 列:把一对多关系内嵌进一行、少建表少 JOIN,并消除对象-关系阻抗失配。
⑥ 抗延迟:默认乐观事务(读不加锁只记时间戳、提交时核对有没有被改),加上批量+并行的客户端访问;变更历史做成一等公民维护一致索引。
⑦ 结果:生产管理 AdWords 全部数据(上百 TB);提交延迟 50–150ms 但用户可见延迟与旧系统相当;扛机房下线不丢数据。
⑧ 局限:提交延迟天生高、需应用配合改写;资源成本更高;全局索引与高冲突下的乐观事务受限;强依赖 Spanner 与 TrueTime。