跳转到内容

TAO — Facebook 给十亿人好友列表造的专用图数据库

是什么

TAO 是 Facebook 2013 年公开的一套社交图谱专用存储。日常类比:你不是在做账本(关系型 DB 擅长这个),你是在画一张超大的人际关系网——A 加了 B 是一条线,B 给 A 的照片点赞又是一条线,这种『点 + 线』的数据模型叫

TAO 把社交图分成两类东西:

  • 对象(object):用户、照片、评论——一个 64 位 id 加一个字段 blob
  • 关联(association):有向边,带 from / to / 类型 / 时间戳 / 自定义数据

整个系统就 8 个 APIobj_get/add/del + assoc_get/add/del/count/range/time_range。背后是 sharded MySQL 做持久层 + 两级缓存挡读。

为什么重要

不理解 TAO 你解释不了几件事:

  • 为什么 Facebook / 微信 / 微博的好友列表能秒开,但同样的查询在 MySQL 里要跑几秒
  • 为什么『刚加完好友立刻刷新还看得到』这件看似简单的事,工程上要专门设计
  • 为什么大公司不买现成的 Neo4j / Nebula,要自己造一个
  • 为什么 Twitter 的 FlockDB、LinkedIn 的 LIquid 都长得跟 TAO 几乎一样

更深一层:TAO 是『关系型 DB 不是万能的』在工业界最响亮的案例之一。

核心要点

为什么关系型 DB 撑不住十亿人的好友列表

四个硬伤:

  1. 图查询要 self-JOIN:『朋友的朋友』在 SQL 里是 friends JOIN friends,friend-of-friend 是 O(N²)。一个有 1000 好友的人查二度关系要扫一百万行
  2. 明星行成锁热点:千万级粉丝的用户,他那一行被无数查询同时打,单点放大
  3. 跨数据中心 replication lag:MySQL 主从同步是异步的,写到主库后从库可能滞后几百毫秒——『刚加完好友刷新看不到』就是这个
  4. 缓存与 DB 是两套系统:传统做法 MySQL + memcached look-aside,缓存失效 + 写入之间的竞态让脏数据偶尔出现,应用层每次都要小心

第 4 点最致命:双向边(A 加 B 同时 B 也加 A)的一致性在应用层维护,漏一条就出现『我加了你但你看不到我』。

TAO 的两级缓存

+---------- leader tier (全局一个) ---------+
| 持久化 → MySQL(sharded) |
+-------------------------------------------+
↑写 ↓异步 invalidate
+--------+ +--------+ +--------+ +--------+
| follower| | follower| | follower| | follower|
| 数据中心1| | 数据中心2| | 数据中心3| | 数据中心4|
+--------+ +--------+ +--------+ +--------+
↑读 / 写转发 ↑读 / 写转发
客户端 客户端
  • follower tier:每个数据中心一个,离用户最近,挡掉 99% 的读
  • leader tier:全局一个,是 MySQL 前面的『写入门面』
  • 读路径:follower 命中即返;miss 找 leader;leader miss 查 MySQL
  • 写路径:follower 把请求转发给 leader,leader 持久化到 MySQL,再异步推到所有 follower

read-after-write 怎么保证

这是 TAO 工程上最值钱的细节。

『刚写完立刻读』要求在分布式系统里非常贵——做强一致要跨区同步等待,做弱一致用户会发现数据丢了。TAO 的折衷是『同一个 follower 读自己刚才转发的写』:

  1. 客户端发写请求给离自己最近的 follower A
  2. follower A 把写转发给 leader
  3. leader 持久化 MySQL,同步回包给 follower A,让它先 invalidate 自己缓存的旧值
  4. follower A 让客户端的下一次读穿透到 leader 拿新值
  5. 其他 follower 在毫秒级内异步 invalidate,对别的用户是最终一致

代价:用户只对自己保证 read-after-write,对别人的写仍然可能看到旧的几百毫秒。社交场景能接受。

实践案例

案例 1:查好友列表

assoc_range(user=1234, type=FRIEND, offset=0, limit=20)

逐步发生:

  1. 客户端 → 最近的 follower
  2. follower 缓存命中 → 返回前 20 个朋友
  3. miss → 找 leader → leader miss → MySQL → 一路回写缓存

99% 的请求停在第 2 步。MySQL 做不到这个延迟。

案例 2:加好友(双向边一致性)

assoc_add(from=A, to=B, type=FRIEND)

TAO 在 leader 一次操作里同时写 A→B 和 B→A 两条边的元数据,应用层不再手维护。少了一整类 bug。

案例 3:明星热点

『某当红明星 1000 万粉丝』的 friend list 不能整张返回——TAO 给 association list 设了上限通常 6000,超出翻页。这同时挡住了:MySQL 单行扫描爆炸 + 缓存值过大撑死内存。

案例 4:跨数据中心写

A 在亚洲数据中心写、B 在欧洲数据中心读。TAO 的 leader 在主区域(比如美国),所有写都汇过去——牺牲写的延迟换读的全球低延迟,因为读写比是 500:1。

踩过的坑

  1. TAO 不是 ACID:跨对象事务做不了。比如『把 A 移出群同时把消息记录给某管理员』这种原子动作,应用层得接受最终一致 + 偶尔重试

  2. association list 有上限:默认 6000。如果你需要『一个对象关联十万条边』,得自己分页或换模型

  3. 反向边短暂不对称:极少数情况下,A→B 已写、B→A 还在传播,这一窗口里 B 的视角看不到 A。社交场景容忍,金融场景就不行

  4. leader 区域整体故障:写要重定向到备用 leader,期间秒级窗口可能丢未持久化的写

  5. 不能跨 shard 排序:association list 在 shard 内按 time 倒排,但跨用户排序(比如『最近最热的全站动态』)要另外的 feed 系统接

适用 vs 不适用场景

适用

  • 读写比极度倾斜(500:1 以上)的图数据
  • 关系本身就是核心业务(社交、关注、点赞、评论)
  • 全球部署、多数据中心、需要单调一致而非强一致
  • 列表查询前 N 个的访问模式

不适用

  • OLAP 分析(要算每个用户的二度网络规模)→ 用图分析专用系统
  • 强 ACID(金融、计费、库存扣减)→ 关系型 DB 仍是首选
  • 写多读少的场景 → 双层缓存反而是负担
  • 需要复杂图算法(最短路径、社区发现)→ 用 Neo4j / Nebula / TigerGraph

历史小故事(可跳过)

  • 2007 年:Facebook 用 MySQL + memcached,应用层手维护双向边一致性。bug 多到团队疯了
  • 2009 年:第一版『社交图谱缓存』上线,但写入路径还是直接打 MySQL,read-after-write 不稳
  • 2012 年:内部正式叫『TAO』(The Associations and Objects),把 leader/follower 双层结构落地
  • 2013 年 6 月:USENIX ATC 论文公开,业界第一次见到这套设计的全貌
  • 之后 Twitter FlockDB、LinkedIn LIquid、Pinterest Zen 都长出了类似形状

学到什么

  1. 数据模型决定一切:用对的抽象(objects + associations)比堆机器有用。一旦把图当一等公民,MySQL 上无法表达的东西就自然了
  2. 读写比是设计起点:500:1 的场景才值得做两级缓存 + 写转发;50:50 的场景这套架构是过度设计
  3. read-after-write 不是免费的:要么牺牲跨区延迟做强一致,要么牺牲全局一致做局部强一致,TAO 选了后者
  4. 专用系统打通用系统:当一类查询的量级超过通用 DB 的承受,造一个『只擅长这一件事』的系统是值得的——前提是这件事真的够多

延伸阅读

关联

  • aurora —— Amazon Aurora,关系型 DB 在云上的现代化,与 TAO 同样面对『MySQL 撑不住』但选了不同方向
  • spanner-2012 —— Google Spanner,强一致 + 全球部署,TAO 选了相反的取舍
  • raft-2014 —— 共识协议,TAO 没用 Raft 而用『写转发到 leader tier』也能保证单调一致
  • rest-fielding-2000 —— REST,TAO 的 8 个 API 形状跟 REST 很像但更专——只服务一种数据模型