F1 2013 — 把 Spanner 包成 SQL,扛起 AdWords 全部账单
是什么
F1 是 Google 2013 年公开的一个分布式 SQL 数据库,目的非常具体:把 AdWords(卖广告那套)原来跑在一堆 MySQL 分片上的核心库整体搬走。
日常类比:原来公司财务用了几十本账本,每本管一个分公司,跨账本对账要人肉抄;F1 给你一本单本账册,但内部自动复制到全球十几个仓库,你写一笔,所有仓库自动同步、强一致,断电丢仓库也不丢钱。
它建在 spanner-2012 之上——Spanner 提供”全球一致存储 + Paxos 复制”,F1 在上面套一层 SQL 引擎、二级索引、查询执行器、ORM。
简单说:Spanner 是底盘 + 发动机,F1 是方向盘 + 仪表盘 + 整车,开起来像 MySQL,跑起来全球一致。
为什么重要
不读 F1 没法回答这些事:
- 为什么”全球一致 SQL + ACID + 高可用 + 扩缩容”这堆需求第一次被同时满足——之前要么牺牲一致性(Dynamo 系),要么牺牲扩展性(单机 Postgres)
- 为什么 cockroachdb / TiDB / YugabyteDB 这一票”NewSQL”几乎都是 F1 + Spanner 的开源致敬版
- 为什么 Google 敢让广告这种每秒上万、错一笔就赔钱的系统跑在跨洲同步复制上,commit 50-150ms 还能接受
- AdWords 是 Google 命根子收入;F1 上线证明”全球一致 SQL 真能扛 OLTP”,给整个工业界吃了定心丸
核心要点
F1 的 5 个关键设计:
-
建在 Spanner 上:底层不自己搞,直接用 Spanner 的 Paxos 跨机房同步复制 + TrueTime + 全局事务。F1 只做 SQL 层。
-
层级 schema(hierarchical schema):子表的行物理穿插在父表行下面。比如
Customer下挂Campaign,再下挂AdGroup,所有同一 customer 的数据在 Spanner 里聚成一块,落到同一个 Paxos group。跨 group 事务才慢,本地事务就快。 -
三种事务模式:
- snapshot read(只读、无锁、读历史快照)
- pessimistic(拿锁,类 MySQL)
- optimistic(先读、提交时检查冲突、失败重试)—— F1 默认推这个,client 端写完整业务再一次性 commit
-
分布式 SQL 查询引擎:能跨 shard 做 join、aggregate;同时支持低延迟 OLTP 查询和大规模并行扫描。
-
ProtoBuf 列 + change history:F1 列里可以直接存 protobuf 对象(schemaless 风格),且每次 commit 自动生成 change history,下游订阅做实时同步。
承认的代价:单次 commit 50-150ms(同步 Paxos 跨洲),比 MySQL 慢一个数量级。F1 用”批量 + 并行 + 异步 ORM”把这个延迟藏起来。
实践案例
案例 1:AdWords 一个广告主的所有 campaign
广告主 ID = 12345,下面挂 1 万个 campaign,每个 campaign 下挂 N 个广告组:
CREATE TABLE Customer (CustomerId INT64, ...) PRIMARY KEY (CustomerId);CREATE TABLE Campaign (CustomerId INT64, CampaignId INT64, ...) PRIMARY KEY (CustomerId, CampaignId), INTERLEAVE IN PARENT Customer;CREATE TABLE AdGroup (CustomerId INT64, CampaignId INT64, AdGroupId INT64, ...) PRIMARY KEY (CustomerId, CampaignId, AdGroupId), INTERLEAVE IN PARENT Campaign;INTERLEAVE IN PARENT 让 Spanner 把这一家子的行物理上塞在一起。一次”读这个广告主全部数据”的查询不用跨 Paxos group,本地一次扫描搞定。
案例 2:optimistic 事务怎么写
# F1 推荐的 ORM 用法(伪码)with f1.optimistic_txn() as txn: customer = txn.read("Customer", id=12345) # 读快照,不锁 campaigns = txn.read_all("Campaign", customer=12345) # 批量读,不锁 for c in campaigns: c.budget = c.budget * 1.1 # 内存里改 txn.commit() # 这里才走 Paxos,50-150ms关键:所有读不阻塞,业务逻辑完整跑完再一次性 commit。Paxos 慢的那 100ms 只占整个事务一次,不是每次读都付。
案例 3:分布式 join 怎么办
跨 shard 的 join F1 不是不能做,是不鼓励。论文实测:跨数据中心 join 一千万行,用并行扫描 + 哈希 join 几秒能出。但日常 OLTP 不会跑这种,只有报表和分析才跑。OLTP 几乎都吃层级 schema 的本地化红利。
伪码示意:
-- 这种跨广告主的扫描会被 F1 切成多个 shard 的并行扫描SELECT customer_id, SUM(daily_spend)FROM CampaignWHERE date BETWEEN '2013-01-01' AND '2013-01-31'GROUP BY customer_id;F1 的执行计划会自动 fan-out 到所有 Spanner shard,每个 shard 本地聚合后再汇总——经典 volcano 风格的迭代器加分布式扩展。
案例 4:change history 喂下游
F1 每次 commit 自动生成一条变更记录(含 before/after),订阅这个流的下游系统可以做实时数据同步、缓存失效、广告反作弊检测。比传统 MySQL binlog 干净,因为 F1 是强一致写入,不存在主从延迟问题。
踩过的坑
-
当 MySQL 用 = 性能崩盘:直接把现有 ORM(先 SELECT 1 再 SELECT N)搬到 F1,每个 SELECT 一次 Paxos,N+1 问题被放大 100 倍。必须改成批量 + 并行 + 异步。
-
schema 不分层 = 跨 group 事务暴增:如果不用
INTERLEAVE,所有表都平铺,事务大概率跨 Paxos group,2PC 跨洲,延迟从 100ms 涨到 500ms+。 -
二级索引也走 Paxos:F1 支持强一致二级索引,但每次写都要同步更新索引——索引多了写入慢。AdWords 团队反复权衡哪些该建索引。
-
client 库要重写:MySQL 那套 client 假设”读便宜、写贵”,F1 是”读快、写也不便宜、但写一次包很多操作划算”。整套 ORM、应用层连接管理、重试逻辑都重做。
适用 vs 不适用场景
适用:
- 全球分布业务、要强一致 + ACID + SQL(金融、广告、订单、库存)
- 数据量从 TB 长到 PB、QPS 从万长到十万的成长期系统
- 不能丢任何一笔交易、机房挂了要秒级切换的场景
- 业务能改 ORM 写法(批量 + 异步)的团队
不适用:
- 单机房单实例就够、QPS 几百、数据 GB 级 — 上 F1/Spanner 是用核武器打蚊子
- 极低延迟 OLTP(< 10ms commit)— 同步 Paxos 跨洲做不到,留给本地 MySQL/Postgres 或内存数据库(tigerbeetle)
- 复杂 OLAP 报表为主 — 用专门数仓(BigQuery / Snowflake),F1 不擅长大扫
- 没法改应用代码的遗留系统
历史小故事(可跳过)
- 2007-2011:AdWords 用 MySQL 分片集群,分片管理团队几十人,每次扩容、加新分片维度都是大手术。
- 2011 年起:Google 内部 spanner-2012 项目成熟,提供”全球一致 KV”。AdWords 团队决定在它上面建 SQL 层,叫 F1。
- 2013 VLDB:F1 论文公开,宣布 AdWords 已经在 F1 上跑。这是 Google 第一次承认”用全球同步复制扛 OLTP 是可行的”。
- 2014 起:F1 启发的开源派系冒出——CockroachDB(2015)、TiDB(2016)、YugabyteDB(2017)、Vitess on Spanner-like 模型,都在抄 F1 + Spanner 的核心思路。
学到什么
- 强一致全球 SQL 不是不可能,是要重新算账:把 100ms commit 摊到批量操作上,端到端可能比 MySQL 还快。
- Schema 设计决定一致性成本:层级 schema 让”哪些事务本地、哪些跨 group”在 schema 里就定了,比运行时再判断便宜得多。
- 同步复制 + Paxos 是新基线:在 Spanner/F1 之后,“异步复制 + 偶尔丢数据”在大公司核心系统逐渐被替换。
- NewSQL 是真存在的第三条路:CAP 不是非此即彼,工程能在”全球一致 + 高可用 + SQL”三角里找到平衡点(代价是延迟)。
- 延迟可以摊掉,丢数据摊不掉:F1 选择把延迟难题留给应用层(异步 ORM、批量),把”绝不丢数据”的责任独占。这种边界划分思路通用——把不可摊的成本放在系统底层。
延伸阅读
- 论文 PDF(VLDB 2013,14 页):F1: A Distributed SQL Database That Scales
- spanner-2012 —— F1 的底盘,必须先理解 Spanner 才能理解 F1
- cockroachdb —— F1 思想的开源版,文档里直接说”灵感来自 Spanner/F1”
- paxos —— Spanner 复制依赖的共识协议
- brewer-cap-2000 —— F1 是对 CAP “必须二选一”的工程反例
- 后续论文:F1 Query (VLDB 2018),把查询引擎抽象成独立组件
- mapreduce —— Google 老一代大数据范式,与 F1 互补:MapReduce 跑批,F1 扛在线
- gfs —— Spanner 间接依赖的存储基石
关联
- spanner-2012 —— F1 的存储层;F1 = Spanner + SQL + ORM + 查询引擎
- bigtable —— Spanner 的前身;F1 是从 NoSQL 时代回到 SQL 的标志
- paxos —— 同步复制的共识基石
- system-r-1976 —— 关系型 SQL 的鼻祖,F1 是它的全球分布式重生
- aries-1992 —— 单机日志恢复算法;F1 用 Paxos 替代了 ARIES 那种主从 redo log
- brewer-cap-2000 —— F1 是对 CAP 的工程反例:CP + 可用性可同时拿
- cockroachdb —— F1 思想的开源继承
- aurora —— AWS 的另一条路:单 region 共享存储而非跨 region 同步
- tigerbeetle —— 极端低延迟金融 OLTP,与 F1 在另一端