首页
学习
活动
专区
工具
TVP
发布

Facebook如何使用ZippyDB构建通用键值存储?

ZippyDB 是 Facebook 最大的强一致性、地理分布的键值存储。自从我们在 2013 年首次部署 ZippyDB 以来,这个键值存储的规模迅速扩大,如今,ZippyDB 为许多用例服务,包括分布式文件系统的元数据、为内部和外部目的计算事件,以及用于各种应用功能的产品数据。ZippyDB 在可调整的持久性、一致性、可用性和延迟保证方面为应用程序提供了极大的灵活性,这使得它在 Facebook 内部成为存储短暂和非短暂的小型键值数据的首选。在本文中,我们将首次分享 ZippyDB 的历史和开发,以及在构建这项服务时做出的一些独特的设计选择和权衡,这项服务解决了 Facebook 的大多数键值存储场景。

ZippyDB 的历史

ZippyDB 使用 RocksDB 作为底层存储引擎。在 ZippyDB 之前,Facebook 的各个团队直接使用 RocksDB 来管理他们的数据。但是,这产生了很多重复工作,每个团队都要解决类似的挑战,例如一致性、容错、故障恢复、复制和容量管理。为满足这些不同团队的需求,我们构建了 ZippyDB,以提供一个高度持久和一致的键值数据存储,通过将所有数据和与大规模管理这些数据相关的挑战转移到 ZippyDB,使得产品的开发速度大大加快。

我们早期在开发 ZippyDB 时作出的一个重要设计决策是,尽可能地重用现有的基础设施。所以,我们最初的工作重点是建立一个可重用、灵活的数据复制库,即 Data Shuttle。将 Data Shuttle 与已有的、成熟的存储引擎(RocksDB)结合起来,在我们现有的分片管理(Shard Manager)和分布式配置服务(基于 ZooKeeper)的基础上建立了一个完全管理的分布式键值存储,共同解决了负载平衡、分片放置、故障检测和服务发现等问题。

架构

ZippyDB 被部署在所谓层的单元中。一个层由分布在全球多个地理区域的计算和存储资源组成,这使得它在故障恢复方面具有弹性。当前只有少数 ZippyDB 层,其中包括默认的“通配符”层和用于分布式文件系统元数据和 Facebook 内部其他产品组的专用层。每个层都承载着多个用例。一般来说,用例是在通配符层中创建的,该层是通用多租户层。这是首选的层,因为它可以更好地利用硬件,并减少操作开销,但有时我们也会在需要时提议使用专用层,这通常是由于更严格的隔离要求。

属于某一层上的用例的数据被分割成所谓的分片(shard)单元,这是服务器端数据管理的基本单元。每个分片都是通过使用 Data Shuttle 在多个区域进行复制(用于容错),它使用 Paxos 或异步复制来复制数据,这取决于配置。在一个分片内,一个复制子集被配置为 Paxos 仲裁组的一部分,也被称为全局范围,其中数据使用 Multi-Paxos 进行同步复制,以便在故障出现时提供高耐久性和可用性。其余的副本,如果有的话,将作为跟随者配置。

在 Paxos 术语中,这些副本与接收异步数据的学习者类似。跟随者允许应用程序在多个区域内复制,以支持低延迟的读取和宽松的一致性,同时保持较小的仲裁组以降低写操作延迟。分片内复制角色配置的这种灵活性允许应用程序能够在持久性、写操作性能和读操作性能之间取得平衡,这取决于它们的需求。

除了同步或异步复制策略外,应用程序还可以选择向服务提供“提示”,指出在哪些区域必须放置分片副本。这些提示,也被称为“粘性约束”,允许应用程序对读写的延迟有一定程度的控制,即在它们期望访问的大部分区域建立副本。ZippyDB 还提供了一个缓存层,并与一个允许订阅分片上的数据突变的 pub-sub 系统集成,这两者都可以根据用例的需求选择加入。

数据模型

ZippyDB 支持一个简单的键值数据模型,它的 API 可以获取、放置和删除键以及它们的批处理变体。它支持遍历键的前缀和删除键的范围。这种 API 非常类似于底层 RocksDB 存储引擎所提供的 API。另外,我们还支持对基本的读-改-写操作和事务进行测试和设置的 API,对更通用的读-改-写操作进行条件写操作(后面将详细介绍)。

事实证明,这个最小的 API 集足以满足大多数用例在 ZippyDB 上管理它们的数据。对于短暂的数据,ZippyDB 有原生的 TTL 支持,允许客户端在写操作时指定对象的到期时间。通过对 RocksDB 的定期压实支持,我们可以有效地清除所有过期的键,同时在压实操作过程中过滤掉读取端的死键。在 ZippyDB 上,很多应用程序实际上是通过 ORM 层来访问 ZippyDB 上的数据,该层将这些访问转换为 ZippyDB 的 API。在其他方面,这个层的作用是抽象出底层存储服务的细节。

分片是服务器端的数据管理单元。分片到服务器的最佳分配需要考虑到负载、故障域、用户限制等因素,这由 ShardManager 处理。ShardManager 负责监控服务器的负载失衡、故障,并启动服务器之间的分片移动。

分片,通常被称为物理分片(physical shard,p-shard),是一种服务器端的概念,不会直接向应用程序公开。取而代之的是,我们允许用例将它们的键空间划分为更小的相关数据单元,称为微分片(μshard)。一个典型的物理分片的大小为 50~100GB,承载着几万个微分片。这个额外的抽象层允许 ZippyDB 透明地充分分区数据,而无需更改客户端。

ZippyDB 支持两种从微分片到物理分片的映射:紧凑型映射和 Akkio 映射。紧凑型映射是在一个相当静态的分配,并且只有在分割过大或者过热的分片时才会更改映射。在实践中,与 Akkio 映射相比,这是一种非常少见的操作,在 Akkio 映射中,微分片的映射由名为 Akkio 的服务管理。Akkio 将用例的键空间分割成微分片,并将这些微分片放置在信息通常被访问的区域。Akkio 有助于减少数据集的重复,并为低延迟访问提供一个明显比在每个区域放置数据更有效的解决方案。

如前所述,Data Shuttle 使用 Multi-Paxos 将数据同步复制到全局范围内的所有副本。从概念上讲,时间被细分成一个单位,称为轮数(epoch)。每个轮数都有一个唯一的领导者,它的角色是通过名为 ShardManager 的外部分片管理服务分配的。一旦领导者被分配,它在整个轮数的持续时间内都有一个租约。周期性的心跳用于保持租约的活跃性,直到 ShardManager 将分片上的轮数调高(例如,用于故障转移、主负载平衡等)。

当故障发生时,ShardManager 会检测到故障,分配一个具有更高的轮数的新领导者,并恢复写操作可用性。在每个轮数内,领导者通过给每个写操作分配一个单调增加的序列号,生成对分片的所有写操作的总排序。然后,通过使用 Multi-Paxos 将这些写操作写入到一个复制的持久日志中,以实现对排序的共识。一旦写入达成共识,它们就会在所有副本中按顺序排出。

为了在最初的实施中简化服务设计,我们选择了使用外部服务来检测故障并分配领导者。但是,在将来,我们计划完全在 Data Shuttle 内部检测故障(“带内”),并且更加主动地重新选择领导者,而不必等待 ShardManager 并产生延迟。

一致性

ZippyDB 为应用程序提供了可配置的一致性和持久性级别,可以在读写 API 中指定为选项。这样,应用程序就可以在每个请求级别上动态地权衡持久性、一致性和性能。

在大多数副本的 Paxos 日志中,默认情况下写操作包括持久化数据,并且在确认向客户端之前向主服务器的 RocksDB 写入数据。在默认的写操作模式下,在主服务器上的读操作将总是看到最近的写入。一些应用程序不能容忍每次写的跨区域延迟,因此 ZippyDB 支持快速确认模式,即写操作一旦在主服务器上被排队复制就被确认。这种模式的持久性和一致性保证显然较低,这是对更高的性能的折衷。

在读操作方面,最流行的三个一致性级别是最终一致性、读写一致性(Read-your-writes Consistency)和强一致性。ZippyDB 所支持的最终一致性级别,实际上比更著名的最终一致性的级别要强得多。ZippyDB 为分片内的所有写操作提供总排序,并确保读操作不会由落后于主/仲裁超过某个可配置阈值的副本提供(心跳用于检测延迟),因此由 ZippyDB 支持的最终读操作更加接近文献中的有界过时一致性。

对于读写操作,客户端缓存服务器返回的最新序列号用于写操作,并在读取时使用该版本来运行或稍后的查询。版本的缓存是在同一个客户进程中。

ZippyDB 还提供了强一致性或线性化能力,无论这些写操作或读操作来自何处,客户端都可以看得到最近的写操作的效果。目前强读操作是通过将读操作路由到主服务器来实现的,以避免需要进行仲裁对话,这主要是出于性能方面的考虑。主服务器依靠拥有租约,以确保在提供读操作之前没有其他主服务器。在某些例外情况下,如果主服务器没有听说过租约续期,那么主服务器的强读操作就会变成一个仲裁检查和读操作。

事务与条件写操作

ZippyDB 支持事务和条件写操作,以满足需要对一组键进行原子式读-改-写操作的用例。

在分片上,所有事务默认为可序列化,我们不支持更低的隔离级别。这样可以简化服务器端实现和客户端并发执行事务的正确性推理。事务使用乐观的并发控制来检测和解决冲突,其作用如上图所示。一般情况下,客户端读取二级数据库的快照中所有的数据,组成一个写操作集,然后把读写操作集全部发送到一级数据库提交。

当接收到一个读写操作集,并接受了读操作的快照之后,主服务器检查是否对其他正在执行的事务执行了冲突的写操作。当没有冲突时,才能接受事务;然后,如果服务器不发生故障,则确保事务成功。主服务器上的冲突解决取决于跟踪之前被接受的事务在主服务器上同一轮数内执行的所有最新写操作。跨轮数的事务将被拒绝,因为这简化了写操作集跟踪,而不需要复制。在主服务器上维护的写操作历史也会被定期清除,以保持低空间使用率。因为不会维护完整的历史记录,主服务器需要维护一个最低跟踪版本,并拒绝所有针对较低版本的快照进行读操作的事务,以保证可序列化。只读操作事务的工作方式完全类似于读写操作事务,除了写操作集为空。

通过“服务端事务”实现条件写操作。它提供了一个更加友好的客户端 API,用于客户端希望根据一些常见的前提条件(例如 key_presentkey_not_presentvalue_matches_or_key_not_present)原子化地修改一组键的情况。如果主服务器收到有条件的写请求,它会建立事务上下文,并将前提条件和写操作集转换为服务器上的一个事务,重复使用所有的事务机制。在客户端可以计算前提条件而不需要读操作的情况下,条件写操作 API 可能比事务 API 更有效。

ZippyDB 的未来

分布式键值存储有很多应用,在构建各种系统时,从产品到为各种基础设施服务存储元数据,经常会出现对分布式键值存储的需求。构建可扩展的、强一致性的、容错的键值存储是一项挑战,往往需要通过许多权衡思考,以提供规划好的系统功能和保证的组合,从而在实践中有效地处理各种工作负载。

本文介绍了 Facebook 最大的键值存储 ZippyDB,它已经生产了六年多,为很多不同的工作负载服务。该服务自从推出以来得到了很高的采用率,主要是因为它在效率、可用性和性能权衡方面具有灵活性。该服务也使我们能够作为一家公司高效地使用工程资源,并作为一个单一的池有效地利用我们的键值存储容量。ZippyDB 仍在不断发展,目前正在经历重大的架构变化,比如存储-计算分解、成员管理的根本变化、故障检测和恢复以及分布式交易,以适应不断变化的生态系统和产品要求。

作者介绍:

Sarang Masti,Facebook 软件工程师。

原文链接:

https://engineering.fb.com/2021/08/06/core-data/zippydb/

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/sM6BY1MkwRkc081i1VvX
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券