跳转到内容

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

待复核

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

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

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

API 是固定的一小套(不是任意图查询):对象的增删改查,加上关联的增删、改类型、计数、按位置取一段、按时间取一段。背后是分片(shard = 把超大表切成多块,各块可放不同机器)MySQL 做持久层,再加两级缓存挡读。

一句话记住:TAO 不追求『什么图查询都能跑』,只把社交产品每天要打十亿次的那几类读写做到又快又稳。

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

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

更深一层:TAO 是『关系型 DB 不是万能的』在工业界最响亮的案例之一——不是 MySQL 差,是负载形状不对。

  1. 关系型 DB 的四个硬伤:图查询要 self-JOIN(『朋友的朋友』像把两本通讯录交叉对一遍,O(N²));明星行成锁热点;跨机房 MySQL 异步复制有 lag(刚写完别处还看不到);MySQL + memcached look-aside(旁路缓存:应用自己决定先查缓存还是先查库)会在『撕缓存』与『写库』之间竞态。类比:账本和便利贴是两套系统,便利贴撕晚了就会看错数。

  2. 两级缓存:本区 follower + 本区 leader:每个区域有离用户近的 follower 挡绝大多数读;本区 leader 管本区库与写协调。跨区域时,写还要再转发到持有 master 库的那个区域的 leader——不是『全球只有一个 leader 盒子』。

区域 A (master 库) 区域 B (slave 库)
follower ←→ leader ←→ MySQL follower ←→ leader ←→ MySQL副本
↑写转发到 master 区 leader ←———————┘

读路径:follower 命中即返 → miss 找本区 leader → 再 miss 查本区 MySQL。写路径:follower 转发 → 本区 leader →(必要时)master 区 leader → MySQL,再异步通知其他 follower invalidate(作废旧缓存 = 撕掉过期便利贴)或 refill(把新边列表补回来)。

  1. read-after-write 只保证『自己刚写的』:发起写的那个 follower 会同步拿到更新,让该客户端下一次读能看到新值;其他机房是毫秒级最终一致。类比:你在本店柜台改完地址,本店立刻改底册;外地分店隔一会儿才同步——社交能接受,金融不行。论文负载里读约占 99.8%、写约 0.2%(约 500:1),所以这套折衷划算。
assoc_range(user=1234, type=FRIEND, offset=0, limit=20)

逐步发生:

  1. 客户端打到最近的 follower
  2. follower 缓存命中 → 直接返回前 20 个朋友(绝大多数请求停在这)
  3. miss → 本区 leader;leader 再 miss 则查 MySQL,再一路回写缓存

MySQL 单独扛不住这种延迟与 QPS;缓存命中才是『秒开』的原因。 好友列表几乎总是『只要最近/最相关的一小段』,所以 range 比『整表 JOIN』对口得多。

案例 2:加好友(配置了反向边)

Section titled “案例 2:加好友(配置了反向边)”
assoc_add(from=A, to=B, type=FRIEND)

逐步发生:

  1. follower 把写转给本区 leader;若本区库是 slave,再转到 master 区 leader
  2. 若 FRIEND 配置了 inverse 类型,系统会再写 B→A。两条边可能落在不同 shard不保证原子——失败时可能短暂只剩一条(hanging association),靠异步任务修补
  3. 发起写的 follower 同步更新缓存;其他 follower 异步收到 invalidate/refill

应用层不用手维护双向边,但仍要容忍极短窗口的不对称——这是论文明确写出的取舍,不是『一次事务写完就绝对对称』。

『千万粉丝』的 friend / like 列表不能整张塞进一次响应。TAO 给 association 查询设了每类型上限通常约 6000,超出必须用 pos 或时间游标翻页——同时挡住:单次扫描爆炸、缓存条目过大撑死内存。

跨区时:亚洲写、欧洲读,写走 master 区、读留本地——用写延迟换读的全球低延迟。热点对象还可在客户端侧再挡一层(带版本号),避免把单个 follower 打穿。

  1. TAO 不是 ACID:跨对象事务做不了;『移出群 + 记一条管理日志』只能最终一致 + 重试
  2. association list 有上限:默认约 6000;要十万条边得自己分页或换模型
  3. 反向边可能短暂不对称:跨 shard 写 inverse 失败会留下 hanging association,社交可忍、金融不行
  4. leader / master 切换窗口:写失败会直接报错给客户端,不自动重试;未持久化的写可能丢
  5. 不能跨 shard 排序:列表在 shard 内按 time 倒排;全站热榜要另接 feed 系统
  6. follower 故障切换可能打破 read-after-write:读打到备份 tier 时,可能暂时看不到自己刚写的值

适用

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

不适用

  • OLAP 分析(算每个用户的二度网络规模)→ 图分析专用系统
  • 强 ACID(金融、计费、库存)→ 关系型 DB 仍是首选
  • 写多读少 → 双层缓存反而是负担
  • 复杂图算法(最短路径、社区发现)→ Neo4j / Nebula / TigerGraph
  • 需要跨多对象原子提交 → TAO 明确不做,别硬拧
  • 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,同样面对『MySQL 撑不住』但选了关系型现代化
  • spanner-2012 —— Google Spanner,强一致 + 全球部署,TAO 选了相反取舍
  • raft-2014 —— 共识协议;TAO 用写转发到 master leader,没用 Raft
  • rest-fielding-2000 —— REST;TAO 的固定 API 更专,只服务一种数据模型
  • bigtable —— 宽表存储另一极;TAO 是图 API + MySQL,不是任意 KV
  • dynamo —— 高可用最终一致的另一条工业路线,可对照一致性光谱
  • pnuts-2008 —— 另一套按应用选一致性的大规模存储,和 TAO 同属『不为强一致付满价』
  • farm-2015 —— FaRM — 把一排机器的内存当成一个低延迟仓库