论文阅读: TAO:Facebook的社交图谱的分布式数据存储(二)

333 阅读16分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第16天,点击查看活动详情

论文阅读: TAO:Facebook的社交图谱的分布式数据存储(二)

本文继续讨论Tao,我设计过类似的缓存架构,其实这类缓存+db,而后为业务提供一致性读写一致性的sdk,请注意,缓存吞吐量非常大,而后端DB的吞吐量是很低的,我们需要时刻监测缓存层的命中率,此外,本文也设计了类似only-key、合并查询的策略,请直接阅读下文,其总体目标都是不要让缓存被击穿 论文地址www.usenix.org/system/file…

4 TAO架构

在这一节中,我们描述了组成TAO的单元,以及允许其跨数据中心和地理区域扩展的多层聚合。TAO被分离成两个缓存层和一个存储层。

image-20221213180105333

4.1 存储层

即使在TAO建立之前,objects和associations就已经存储在Facebook的MySQL中了;它是API的原始PHP实现的支持存储。这使得它成为TAO持久化存储的自然选择。

TAO的API被映射到一小部分简单的SQL查询,但它也可以通过明确地维护所需的indexes,有效地映射到非SQL数据存储系统中的范围扫描,如LevelDB[3]。然而,在评估TAO的备份存储的适用性时,必须考虑不使用API的数据访问。这些包括备份、数据的批量导入和删除、从一种数据格式到另一种数据格式的批量迁移、副本创建、异步复制、一致性监测工具和操作调试。一个替代的存储也必须提供原子写事务、有效的粒度写,以及很少的延迟异常值。

鉴于TAO需要处理的数据量远大于单个MySQL服务器所能存储的数据量,我们将数据分为逻辑分片。每个分片都包含在一个逻辑数据库中。数据库服务器负责一个或多个分片。在实践中,分片的数量远远超过了服务器的数量;我们调整了分片与服务器的映射,以平衡不同主机的负载。默认情况下,所有objects类型存储在一个表中,所有associations类型存储在另一个表中。

每个objects的ID都包含一个嵌入的分片ID,以识别其托管分片。objects在其整个生命周期内被绑定到一个分片上。一个associations被存储在其id1的分片上,因此每个associations查询都可以从一个服务器上得到服务。两个id不太可能映射到同一个服务器上,除非它们在创建时被明确地放在一起。

4.2 缓存层

TAO的缓存为客户实现了完整的API,处理与数据库的所有通信。缓存层由多个缓存服务器组成,它们共同构成一个。一个层级能够共同响应任何TAO请求。(我们也把一个区域内的数据库集合称为一个层。)每个请求都使用类似于4.1节中描述的分片方案映射到一个缓存服务器。没有要求各层有相同数量的主机。

客户端直接向相应的高速缓存服务器发出请求,然后由其负责完成读取或写。对于缓存miss和写入请求,服务器会联系其他缓存和/或数据库。

写还是会写数据库,但读,是自动穿透缓存到cache的其他分片、甚至是数据库(读穿了)

TAO的内存缓存包含objects、associations列表和associations计数。我们按需填充缓存,并使用最近最少使用(LRU)策略驱逐项目。缓存服务器理解其内容的语义,并使用它们来回答查询,即使确切的查询之前没有被处理过,例如,缓存中的计数为零就足以回答一个范围查询。

缓存中的计数为零就足以回答一个范围查询。也就是类似only-key的标志key,如此一来,就不必穿透到db。

对一个有逆向的associations的写操作可能涉及到两个分片,因为正向边缘被存储在id1的分片上,逆向边缘被存储在id2的分片上。从客户端接收查询的层级成员向托管id2的成员发出RPC调用,该成员将联系数据库以创建逆向associations。一旦反写完成,缓存服务器就会为id1向数据库发出一个写。TAO不提供这两个更新之间的原子性。如果发生故障,正向可能存在而没有反向;这些挂起的associations由异步作业安排修复。

对于双向关系,本篇论文发出时,是没有事务支持的,需要使用悬挂的作业去修复。实际上后来有一篇论文,在TAO上设计了一种新型的事务支持。

4.3 客户端通信栈

在渲染一个Facebook页面时,查询数百个objects和associations是很常见的,这很可能需要在短时间内与许多缓存服务器通信。由此产生的全对全通信的挑战与我们的memcached pool所面临的挑战相似。TAO和memcache共享Nishtala等人[21]所描述的大部分客户端栈。TAO请求的延迟可能比memcache的延迟高得多,因为TAO请求可能会访问数据库,所以为了避免多路连接上的头部阻塞效应,我们使用了一个具有无序响应能力的协议。

4.4 领导者和追随者

理论上来说,只要分片足够小,单个缓存层可以被扩展到处理任何可预见的总请求率。但在实践中,大的层(tiers)很可能是不现实的,因为它们更容易出现热点,而且它们的所有连接数会呈二次增长。

为了在限制最大层数的同时增加服务器,我们将缓存分成两层:一个leader层和多个follower层。TAO相对于lookaside缓存架构的一些优势(如2.1节所述)依赖于每个数据库有一个单一的缓存协调器;这种分割允许我们将协调器保持在每个区域的单一层中。与单层配置一样,每个层包含一组缓存服务器,它们共同能够响应任何TAO查询;也就是说,系统中的每个分片都映射到每个层的一个缓存服务器上。leader(leader层的成员)的行为如§ 第4.2节所述,从存储层读出和写入。

follower(追随者层的成员)将转发读miss和写miss到一个领导者。客户端与最近的追随者层进行通信,从不直接与领导者联系;如果最近的追随者不可用,他们就会failover到另一个附近的追随者层。

image-20221213183640359

这个缓存还是很简洁的。

鉴于这种两级缓存的层次结构,必须注意保持TAO缓存的一致性。每个分片都由一个领导者host,所有对分片的写入都要经过这个领导者,所以它自然是一致的。另一方面,追随者必须被明确通知通过其他追随者层进行的更新。

TAO通过异步从领导者向追随者发送缓存维护信息来提供最终的一致性[33, 35]。领导者中的objects更新将无效消息排到每个相应的跟随者中。发出写的追随者在领导者的回复中被同步更新;缓存维护消息中的版本号允许它在以后到达时被识别。

这里至关重要,失效操作(del操作)是有版本号的。

由于我们只对associations列表的连续前缀进行缓存,无效的associations可能会截断列表并丢弃许多边。相反,领导者会发送一个填充消息来通知跟随者关于associations的写入。如果追随者已经缓存了associations,那么重新填充请求就会触发对领导者的查询,以更新追随者已经过时的associations列表。§ 第6.1节讨论了这种设计的一致性,以及它是如何容忍失败的。

领导者对来自追随者的并发写入进行序列化(串行)处理。因为一个领导者调解所有对一个id1的请求,它也是保护数据库不受惊群的理想位置。leader确保它不会向数据库发出并发的重叠查询,同时也对一个分片的最大待决查询数量进行了限制。

不要重复发相同的query,常规操作。

4.5 地理上的扩展

领导者和追随者的配置允许TAO扩展到处理高的工作负载,因为读吞吐量与所有层中追随者服务器的总数成比例。然而,设计中隐含的假设是,从跟随者到领导者以及领导者到数据库的网络延迟很低。如果客户被限制在一个单一的数据中心,或者甚至是一组相距不远的数据中心,这个假设是合理的。然而,在我们的生产环境中,这并不是真的。

随着我们的社交网络应用的计算和网络需求的增长,我们不得不超越单一的地理位置:今天,我们的低层可以相隔数千英里。在这种构架下,网络往返时间很快就会成为整个架构的瓶颈。由于在我们的工作负载中,追随者的失读频率是写入频率的25倍,我们选择了一个主/从架构,要求将写入发送到master region,但允许读miss在本区域得到响应。与领导者/追随者的设计一样,我们以异步方式传播更新通知,以最大限度地提高性能和可用性,但要牺牲数据的新鲜度。

社交图是紧密互联的;不可能对用户进行分组,从而使跨分区请求很少。这意味着每个TAO追随者都必须在本地拥有一个完整的社会图的多字节副本的数据库层。在每个数据中心提供完整的副本是非常昂贵的。

我们对这个问题的解决方案是选择数据中心的位置,这些位置只集中在几个区域,区域内的延迟很小(通常小于1毫秒)。这样,每个区域存储一份完整的社会图就足够了。图2显示了主/从TAO系统的整体架构。

跟随者在所有区域的行为都是相同的,将读取的失误和写入的内容转发给本地区域的领导者层。领导者查询本地区域的数据库,不管它是主站还是从站。然而,写是由本地领导者转发到拥有主数据库的区域中的领导者那里。这意味着读取延迟与区域间延迟无关。(因为读都是在本地完成的,最终一致性)

主区域为每个分片单独控制,并自动切换以恢复数据库的故障。在切换过程中失败的写入会被报告给客户端,并不会重试。请注意,由于每个缓存承载了多个分片,一台服务器可能同时是一个主站和一个从站。我们更倾向于将所有的主数据库放在一个单独的region中。当一个逆向associations在不同的区域被掌握时,TAO必须穿越一个额外的区域间链接来转发逆向写入。

image-20211202173550190

图2:多区域TAO配置。主区域向主数据库(A)发送读取missed、写入和嵌入(embedded )一致性消息。当复制流更新从属数据库时,一致性消息被传递到从属领导(B)。从属领导向主领导(C)发送写信息,并向复制数据库(D)发送读missed。主站和从站的选择是针对每个分片单独进行的。

TAO在数据库复制流中嵌入了无效和重新填充消息。这些消息是在事务被复制到从属数据库后立即在一个区域内传递的。提前传递这些消息会造成缓存的不一致,因为从本地数据库中读取数据会提供陈旧的数据。在Facebook,TAO和memcached使用相同的管道来传递无效信息和补充信息[21]。

如果一个转发的写是成功的,那么本地领导者将用新的值更新其缓存,即使本地从属数据库可能还没有被异步复制流更新。在这种情况下,跟随者将从写中收到两个无效或重新填充,一个是写成功时发送的,一个是写的事务被复制到本地从属数据库时发送的。

TAO的主/从设计确保所有的读取都能在一个区域内得到满足,代价是有可能向客户返回过时的数据。只要用户持续查询相同的追随者层,用户通常会对TAO状态有一个一致的看法。我们将在下一节讨论这方面的例外情况。

5 implement

前几节描述了TAO服务器是如何处理大量数据和查询率的。本节详细介绍了对性能和存储效率的重要优化。

5.1 缓存服务器

TAO的内存管理是基于Facebook定制的memcached,如Nishtala等人[21]所述。TAO有一个slab分配器来管理同等大小的slabs item项,一个线程安全的哈希表,同等大小的items之间的LRU驱逐,以及一个动态的slabs rebalancer 平衡器来保持所有类型的slab之间的LRU驱逐age相似。一个slab项可以容纳一个节点或一个边缘列表。为了提供更好的隔离,TAO将可用的RAM划分为arenas,通过objects或associations类型选择arenas。这使得我们可以延长重要类型的缓存寿命,或者防止不良的缓存item驱逐更好的类型的数据。到目前为止,我们只是手动配置了arenas来解决特定的问题,但应该可以自动地设置arenas的size,以提高TAO的整体命中率。

对于小的固定大小的item,例如association计数,主哈希表中的bucket item的指针的内存开销变得很重要。我们将这些item分开存储,使用直接映射的8路associations缓冲区,不需要指针。每个项目中的LRU顺序通过简单地将条目向下滑动来跟踪桶。我们通过增加一个表来实现额外的内存效率,该表将每个活动的atype映射到一个16位的值。这让我们可以在14个字节内将(id1, atype)映射为32位的计数;一个负的条目,即记录一个(id1, atype)没有任何id2的情况,只需要10个字节。这种优化使我们在给定的系统配置下,可以在缓存中多容纳大约20%的item。

5.2 MySQL映射

回顾一下,我们将objects和associations的空间划分为分片。每个分片被分配到一个逻辑的MySQL数据库,该数据库有一个objects表和一个associations表。一个objects的所有字段都被序列化为一个单一的 "数据 "列。这种方法允许我们将不同类型的objects存储在同一个分片中,受益于不同数据管理策略的objects被存储在不同的自定义表中。

associations的存储方式与objects类似,但为了支持范围查询,它们的表有一个基于id1、type和time的额外索引。为了避免潜在的昂贵的SELECT COUNT查询,associations计数被存储在一个单独的表中。

5.3 缓存分片和热点

使用一致的hash[15]将碎片映射到一个层中的缓存服务器上。这简化了层的可扩展性和请求路由。然而,这种半随机地将碎片分配给缓存服务器的做法会导致负载不平衡:一些跟随者会比其他跟随者承担更多的请求负载。TAO通过分片克隆来重新平衡追随者之间的负载,其中对分片的读取由一个层中的多个追随者提供。克隆的分片的一致性管理信息被发送到所有托管该分片的跟随者。

在我们的工作负载中,一个受欢迎的objects被查询的频率比其他objects高几个数量级是很常见的。克隆可以将这种负载分配给许多跟随者,但是这些objects的高命中率使得将它们放在一个小型的客户端缓存中是值得的。当追随者对一个热点项目的查询做出反应时,它包括该objects或associations的访问率。如果访问率超过一定的阈值,TAO客户端会缓存数据和版本。通过在随后的查询中包括版本号,如果数据自上一个版本以来没有变化,追随者可以在回复中省略该数据。访问率也可以用来节制客户端对非常热的objects的请求。

5.4 High-Degree Object

许多objects都有超过6,000个相同类型的associations从它们身上发出,所以TAO不缓存 的完整associations列表。同样常见的是,在执行assoc_get查询时,结果是空的(指定的id1和id2之间不存在边)。不幸的是,对于高等级的objects,这些查询会以各种方式进入数据库,因为被查询的id2可能在associations列表的未缓存尾部。我们通过修改被观察到的发出有问题的查询的客户端代码来解决缓存实施中的这种低效率。解决这个问题的一个办法是使用assoc_count来选择查询方向,因为检查逆向的边是等价的。在某些情况下,如果一条边的两端都是高度节点,我们也可以利用应用域的知识来提高缓存能力。许多associations将时间字段设置为其创建时间,许多objects将其创建时间作为一个字段。由于一个节点的边缘只能在该节点被创建之后被创建,我们可以将id2搜索限制在时间比objects的创建时间长的associations上。只要缓存中存在比objects更早的边,那么这个查询就可以由TAO的跟随者直接回答。