|
|
|
|
公众号矩阵

社交网络场景下大规模图存储实践之Facebook TAO

Facebook TAO[1] ,即 The Associations and Objects 的缩写,点(对象,Object)和边(联结,Associations)是”图“中最基本的抽象,用来做 Facebook 图存储名字倒是恰如其分。

作者:穆尼奥 来源:木鸟杂记|2021-10-07 16:45

概述

Facebook TAO[1] ,即 The Associations and Objects 的缩写,点(对象,Object)和边(联结,Associations)是”图“中最基本的抽象,用来做 Facebook 图存储名字倒是恰如其分。

概括来说,TAO 是 Facebook 为了解决社交场景下,超大数据的更新与关联读取问题,其核心特点如下:

  • 提供面向 Facebook 社交信息流场景特化的图 API ,比如点查、一度关联查询、按时间的范围查询。
  • 两层架构,MySQL 做存储层,MemeCache 做缓存层;缓存层又可细分为主从两层。
  • 可多机房扩展,高度面向读性能优化,只提供最终一致性保证。

历史沿革

Facebook 早期沉淀的数据就在 MySQL 上[2],MySQL 扛不住后,在 2005 年时,扎克伯格便引入了 MemCache 做缓存层,应对更高频的读请求。自此之后,MySQL 和 MemCache 便成为了 Facebook 存储层技术栈的一部分。

Facebook 数据请求负载通常符合时间局部性(即最近更新的数据最容易被访问),而非空间局部性。但 MySQL 中的数据通常不是按照时间有序存储的,因此 MySQL InnoDB 引擎自带的 block cache 并不匹配这一特点。另外,MemCache 本身只提供基于内存的 KV 访问模型,为了更高效的利用这些内存,Facebook 需要针对社交场景自己定制缓存策略,以尽可能多的让读请求命中。

将这些工程细节,包括两层存储集群,包括自行组织缓存,都暴露给应用层工程师,带来了很大的工程复杂度,引发了更多的 bug,降低了产品迭代速率。为了解决这个问题,Facebook 在 2007 年使用 PHP 在服务端做了一个抽象层,基于图存储模型,围绕点(对象)和边(联结)提供 API。由于社交场景中的喜欢、事件、页面等都可以通过图模型来方便表达,这一抽象层极大的降低了应用层工程师的心智负担。

但随着所需 API 越来越多,将图模型层(在 webserver 上)和数据层(在 MySQL和MemCache 集群)分离实现的缺点逐渐暴露了出来:

从边集合的微小更新,会导致整个边集合失效,从而降低缓存命中率。

请求边列表的一个微小子集也需要将整个边列表从存储端拉到服务端。

缓存一致性很难维持。

当时的 MemCache 集群很难协同支持实现一个纯客户端侧的惊群避免策略。

所有这些问题,都可以通过重新设计统一的、基于图模型的存储层来实现。从 2009 年开始,TAO 便在 Facebook 内部的一个团队开始酝酿。再之后,TAO 逐渐发展成了支撑每秒数十亿次读取、数百万次写入,部署于跨地区海量机器上的分布式服务。

图模型 & API

图的最基本组成就是点和边,对应到 TAO 里就是,对象(Objects)和联结(Associations)。对象和联结都可以包含一系列由键值对表示的属性。

  1. Object: (id) → (otype, (key  value)*) 
  2. Assoc.: (id1, atype, id2) → (time, (key  value)*) 

注:TAO 中的边都是有向边。

以社交网络为例,对象可以是用户、打卡、地点、评论,联结可以是朋友关系、发表评论、进行打卡、打卡于某地等等。

如下图 a),假设在 Facebook 上有这么一事件:Alice 和 Bob 在金门大桥打了个卡,Cathy 评论:真希望我也在那。David 喜欢了这条评论。

用图模型表示后,如下图 b):

一个栗子

可以看到,所有的数据条目如用户、地点、打卡、评论都被表示成了带类型的对象(typed objec),而对象间的关系如被谁喜欢(LIKED_BY)、是谁的朋友(FRIEND)、被谁评论(COMMENT),则被表示成了带类型的联结(typed associations)。

另外,尽管 TAO 中联结都是单向的,但实际中大部分关系是双向的。这时,可以增加一个反向边(inverse edges)来表示此种双向关系。

最后,由于联结是三元组,因此两个对象间可以有多条不同类型的边,但是同一类型的边,只能有一条。但在有些非社交场景中,可能需要相同类型的边也有多条。

Object API

围绕 Object 的操作,是常见的增删改查(create / delete / set-fields / get )。

同一对象类型(object type)的对象具有同样的属性集(fields,即上面提到的 (key value)*),也就是说,一个对象类型对应固定的属性集。可以通过修改对象类型的 Schema 来对其所含属性进行增删。

Association API

围绕 Association 的基本操作,也是增删改查。其中增删改如下:

  1. assoc_add(id1, atype, id2, time, (k→v)*) – 新增或者覆盖 
  2. assoc_delete(id1, atype, id2) – 删除 
  3. assoc_change_type(id1, atype, id2, newtype) - 修改 

值得一说的是,如果其反向边((id1, inv(atype), id2))存在,则上述 API 会同时作用于其反向边。由于多数场景下的联结是双向的,因此 Facebook 将其边的 API 默认行为同时作用于两条边。

另外,每个 Association 都会自动打上一个重要的特殊属性:联结时间(association time)。由于 Facebook 负载具有时间局部性,利用此时间戳可以对缓存数据集进行优化,以提高缓存命中率。

Association Query API

围绕 Association 的查询 API,是 TAO 的核心 API,流量最大。这负载类型包括:

  1. 指定 (id1, type, id2) 的点查,通常用来确定两个对象间是否存在对应联结,或者获取对应联结的属性。
  2. 指定 (id1, type) 的范围查询,要求结果集按时间降序排列。比如一个常见场景:该条内容最新的 50 条评论是什么?。此外,最好能提供迭代器形式的访问。
  3. 指定 (id1, type) 出边数查询。比如查询*某条评论的喜欢数是多少?*此种查询很常见,因此最好将其直接存下来,以能够在常数时间内返回结果。

尽管联结千千万,但最近的范围是重点查询对象(时间局部性),因此联结的查询 API 主要围绕时间的范围查询展开。

为此,TAO 将最基本的联结集定义为 Association List。一个 Association List 是以 id1 为起点,出边类型为 atype 的所有联结的集合,按时间降序排列。

  1. Association List: (id1, atyle) -> [a_new, ..., a_old] 

基于此,定义更细粒度的几个接口:

  1. // 返回以 id1 为起点,以 id2set 集合所包含点为终点 
  2. // 创建时间 time 满足 low <= time <= high 
  3. // 的联结集合。 
  4. assoc_get(id1, atype, id2set, high?, low?)  
  5.  
  6. // 返回联结集合的数量 
  7. assoc_count(id1, atype) 
  8.  
  9. // 返回下标满足 [pos, pos+limit) 的联结集合子集 
  10. // pos 即 Association List 中的下标 
  11. assoc_range(id1, atype, pos, limit)  
  12.  
  13. // 返回创建时间 time 满足,从 time <= high **倒序**起始, 
  14. // 到 time >= low 终止,不超过 limit 条联结 
  15. assoc_time_range(id1, atype, high, low, limit) 

为什么结果集按时间降序排列呢?因为在 Facebook 页面信息流展示时,总是先展示最新的,然后随着不断下拉,依次加载较旧的数据。

举个栗子:

  1. • “50 most recent comments on Alice’s checkin” ⇒ assoc_range(632, COMMENT, 0, 50) 
  2. • “How many checkins at the GG Bridge?” ⇒ assoc_count(534, CHECKIN) 

架构

TAO 架构

TAO 架构整体分两层,缓存层(caching layer)和存储层(storage layer)。

存储层

由于前面所说的历史原因,TAO 使用 MySQL 作为存储层。

因此,TAO 对外的 API 最终会被转化成 MySQL 语句作用于存储层,但对 MySQL 的查询语句都相对简单。当然,存储层也可以使用 LevelDB 这种 NoSQL 存储引擎,这样查询语句就会对应翻译为前缀遍历。当然,选择存储引擎不止要看 API 翻译方便与否,还要看数据备份、批量导入导出、多副本同步等非 API 因素。

单个 MySQL 服务肯定存不下所有 TAO 数据,因此 TAO 使用了 MySQL 集群支撑存储层。为了将数据均匀的分到多个 MySQL 机器上,TAO 使用一致性哈希算法将数据在逻辑上进行了切片(shard)。每个切片存到一个 MySQL db 中。每个 Object 在创建时会关联一个 shard,并将 shard_id 做到 object_id 中,因此在 Object 整个生命周期中其 shard 都不会再改变。

具体来说,MySQL 中所存数据主要包括两张表,一个点表,一个是边表。其中,点和其出边会存在同一个 MySQL db 中,以最小化关联查询代价。所有的点属性在保存时,会被序列化到一个叫做 data 的列。如此,可以将具有不同类型的 Object 保存到一张表中。边和点保存时类似,但是会额外在 id1,atype,andtime 字段上做索引,以方便基于某个点的出边的范围查询。此外,为了避免对边的数量的查询所带来的高昂开销,会额外用一张表来保存 associations 的数量。

缓存层

读写穿透。TAO 的存储层实现了所有对外 API,对客户端( Client )完全屏蔽了存储层。即,Clients 只和缓存层进行交互,缓存层负责将数据同步到存储层。缓存层也是由多个缓存服务器构成,能够 Serve 任意 TAO 请求的一组缓存服务器称为一个 Tier。单个请求会路由到单个缓存服务器,不会跨多个服务器。

缓存策略使用经典的 LRU。值得一提的是,由于 TAO 的边默认是双向的,在 Client 写入边时,由缓存层变成负责将其变为写去边和回边的两个有向边,但 TAO 并不保证其原子性。失败了会通过垃圾回收来删除中间结果。

两层架构。TAO 中的每个逻辑分片(Shard)基本是同构的。每个逻辑分片的缓存层包括一组缓存服务器,由单个 Leader 缓存服务器和一组 Follower 缓存服务器构成。

其中,Followers 缓存服务器是外层,Leader 服务器是内层。所有客户端只和 Followers 打交道,Followers 缓存服务器本身只负责读请求,如果发现读未命中或者有写请求,就将其转发给所对应 Leader 缓存服务器。

如果读请求负载持续增加,对 Follower 缓存服务器扩容即可。

如果对某些 object 访问显著高于其他,TAO 会通过记录访问频次对其识别,然后进行客户端侧的缓存,并通过版本号来维持一致性。

一致性。Leader 收到多个 Follower 的并行写请求后会将其进行定序,序列化后到存储层进行同步读写后返回;对于写请求来说,还会异步的通知其他 Follower 服务进行对应数据的更新,因此 TAO 最终只能提供最终一致性保证。这样做的好处是换来了读请求的高吞吐。

多地扩展。由于 TAO 的读请求频次约为写频次的 25 倍,而单地数据中心(datacenter)又不能满足 Facebook 全球场景。因此 TAO 整体上使用了主从架构,两个 datacenter 都部署一套存储层+缓存层作为主从(Primary-Secondary),所有写请求都要由从数据中心的 Leader Cache 路由到主数据中心(见上图),然后由主数据中心存储层异步传回从数据中心。但从数据中心的 Leader Cache 并不等本地存储层同步回数据,即进行更新,并通知 Followers 到自己这 Refill。TAO 的这种设计,能够最大化的保证一个读取请求在一个 DataCenter 内被满足,代价是客户端可能会读到过时数据。即牺牲一致性,来降请求低延迟,提高吞吐。

一致性

TAO 在一致性和可用性取舍方面时,选择了后者。为了高可用性和极致的性能,选择了弱化的一致性模型——最终一致性。因为在 Facebook 的大部分场景下,不可用要比不正确更加糟糕。在大部分常见场景下,TAO 能做到更强的写后读一致性(read after write consistency)。

TAO 中同一份数据,首先,会进行 Master-Slave Region 进行主从备份;其次,在同一 Region 中,会使用 Leader-Follower Cache 做两层缓存。更新时,不同位置的数据不同步,便会造成数据的不一致。在 TAO 中,在更新后给足够时间间隔,所有的数据副本都会趋向一致,并且体现最新更新。而通常,这个时间间隔不会超过 1s 。这在 Facebook 中大多数场景是没有问题的。

对于那些对一致性有特殊要求的场景,应用层可以将请求标记为 critical。TAO 在接到有此标记的请求时,会将其转发到 Master Region 进行处理,进而获取强一致性。

参考

[1] TAO 论文:https://www.usenix.org/system/files/conference/atc13/atc13-bronson.pdf

[2] Facebook 技术博客,TAO——图的威力:https://engineering.fb.com/2013/06/25/core-data/tao-the-power-of-the-graph/

[3] meetup TAO:https://www.notion.so/Meetup-1-Facebook-TAO-28e88836a3f649ba9b3e3ea83858c593

[4] stanford 6.S897 课件:https://cs.stanford.edu/~matei/courses/2015/6.S897/slides/tao.pdf

【编辑推荐】

  1. 打破线上线下存储的边界,阿里云推出云定义存储产品
  2. 什么是大数据系统存储及管理?
  3. 赌你看不懂:分布式存储系统的数据强一致性挑战
  4. Ceph 基础篇 -对象存储使用
  5. 如何正确配置Linux存储库
【责任编辑:武晓燕 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

订阅专栏+更多

视频课程+更多

linux基础知识和项目实战部署课程

linux基础知识和项目实战部署课程

讲师:陈槐14135人学习过

AWS云计算实战-基础篇视频课程

AWS云计算实战-基础篇视频课程

讲师:陈涛12885人学习过

金蝶K3 WISE 视频教程全集

金蝶K3 WISE 视频教程全集

讲师:金速鹏100122人学习过

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊

51CTO服务号

51CTO官微