[论文阅读]FlightTracker-Facebook跨网在线存储读一致性优化(3)

143 阅读10分钟

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

[论文阅读]FlightTracker-Facebook跨网在线存储读一致性优化(3)

第45篇,终于到论文正文,这个章节继续FlightTracker内部设计信息,如果我们为每个写key都保存一个ticket,那么FT服务器将无限膨胀,而且需要较大的存储成本,所以本文讨论FT如何安全地压缩和join write set. 论文见:www.usenix.org/conference/…

4. Ticket详情

Ticket是一组写的元数据。我们使用Ticket来识别对社会图的写入,而不管这些写入是在哪里提交的。Ticket允许通用基础设施在许多独立部署的系统中跟踪和识别写,同时让数据库传达系统的具体细节。为了清楚起见,我们把保存规范化数据和生成写元数据的系统称为 "数据库",而不是其他数据存储,如缓存和索引,它们主要服务于读和代理写。

一个Ticket是作为每个数据库的自定义表示的联合体来实现的。在写入时,单数据库的Ticket仅包含新提交写入的元数据(图3)。然后,它可以与其他票证联合,或由FlightTracker和自定义应用程序使用,产生可能包含来自多个数据库的写入的票证。

Ticket的封装和Ticket包容读的语义一起给了我们在Ticket实现上极大的灵活性。由于Ticket-inclusive读将Ticket解释为下限,因此包含额外的写或比Ticket中确切编码的更新鲜的写的读结果对应用程序来说并不令人惊讶。此外,由于封装的存在,应用程序无法检查Ticket的确切内容或代表,这意味着我们总是可以安全地在Ticket中包含额外的写入。我们在Ticket压缩(§ 4.2)和FlightTracker服务的Ticket复制(§ 5)中利用了这种灵活性。

4.1 识别一组写

通过全局唯一的ID来识别写入的天真策略很容易实现,但很难使用--为阅读服务的数据存储必须跟踪所有的写入ID,以确定本地副本是否足够最新。分配一个总的写入顺序允许系统通过其顺序位置来识别写入。然而,排序意味着通过通信或定时等待来实现同步[26]。为了保持异步复制的效率优势,数据库经常选择有限范围的排序。根据我们的经验,有三个自然范围。

• 对于每个key:任何严格意义上的单调增长的值都可以与一个key结合,以识别该行或对象的特定变化。这个版本不需要是连续的--单调增长的时间戳也足够了。

• 对于每个分片:许多数据库对所有写到特定分片的数据进行排序,但对分片之间的排序没有保证。

• 全局:一些系统[26, 53]提供全局的所有写的总顺序。HLC或已知精度的时钟也可以跨分片和数据库类型进行写操作。

FlightTracker支持的所有数据库都具有以下属性,这使我们能够进一步简化我们的假设。我们的数据库提供了一个版本化的键-对象模型。它们是分片的,对分片内的所有写入保持总的顺序;此外,单个分片内的写入是以相同的顺序复制的。因此,提交时间信息(如提交时间戳或事务ID)可以指定该分片的连续前缀,并作为一个低水位标志来确定数据存储的复制状态。

Ticket中的大多数字段可以是空的,也可以是空的。当新的Ticket被铸造时,它们可以包括任何在提交时已知的信息,如每个键的版本,提交时间戳,或事务ID。虽然不是必须的,但包括更多的元数据可以灵活地解释和使用Ticket。图6显示了一个有两个数据库的Ticket结构示例。

对于数据存储或客户端来说,要确定一个读取结果是否足够最新,这个元数据需要在读取时被访问,这意味着它应该与数据一起存储。虽然这种开销看起来非同小可(例如,每个键最多8个字节),但我们的数据库已经坚持了这些版本基元,所以额外的成本可以忽略不计。

虽然我们在Facebook的数据库背景下描述了Ticket抽象,但它也适用于其他数据库并可扩展。许多数据库原生支持并存储每个键的版本基元,例如Azure SQL数据库的rowversion[13]。Per-shard或全局范围的排序也常常是现代数据库的基础,在这些数据库中,可以通过与客户端数据一起存储的序列号或时间戳来识别写内容(例如,ZooKeeper的zxid[33]、CockroachDB的hlc[2]、Kafka的offset[35]、LogDevice的LSN[4])。

4.2 Ticket joining and compaction

对Ticket的两个重要操作是连接和compaction:连接将Ticket结合起来,compaction则减少其空间占用。这两个操作都是本地操作,不需要RPC或输入Ticket以外的信息。

连接操作,本质上是集合的联合,产生的Ticket是所有输入的超集。它是Ticket的主要API。例如,数据存储客户端可以连接Ticket来结合来自多个分片或多个数据库的元数据;FlightTracker服务连接Ticket来积累每个用户最近的写入量(第5节);选择应用程序连接Ticket来表达其读取的额外约束(第7节)。

image.png

compaction有助于Ticket克服写集合跟踪技术的可扩展性问题(scale)。这个想法很直截了当。Ticket表示应该是可见的写,即一种下限;我们可以提高下限,以换取更紧凑的表示。直观地说,用所产生的Ticket做Ticket-包容的读,使它们的约束同样或更严格。

image-20211108153808663

从形式上看,Ticket是CRDT[50],我们使用的compaction技术是CRDT膨胀操作[18]:每范围排序和子集-上集关系定义了≤部分顺序。根据这个顺序,Ticket膨胀产生一个≥输入Ticket的Ticket。产生的Ticket可能需要更少的字节来表示。并非所有的膨胀都能减少Ticket的大小,但我们下面使用的三种膨胀都能做到。

**按范围压缩:**为每个键保留最高的版本,为每个分片保留最高的事务ID,这让我们可以丢弃具有旧版本或事务ID的元数据。这种compaction是在每次连接时进行的。

**跨范围压缩:**一些数据库有静态的分片分配,并在Ticket中包括每个键的版本和每个分片的事务ID(如图6中的DatabaseA)。用每个分片的事务ID代替每个键的元数据可以大大减少Ticket的大小,特别是对于有许多单独写的分片。假设§3.1中的例子中的边缘和其他许多写都在分片X上,那么我们可以压缩Ticket T1 = {W3,W4,...,W100} 到T2 = {shardX : txn_id|-> 8985}。

跨范围compaction为我们提供了一种权衡,在Ticket的大小和服务于Ticket-inclusive读的成本之间进行权衡。由于压缩后的Ticket在语义上编码了更多的写入。读取不太可能在本地得到服务。例如,T2要求在分片X上的所有写操作(txn_id 8985)要被复制。因此,在FlightTracker中,这种类型的压缩是启发式地进行的,并且 在FlightTracker服务中很少执行。

**全局compaction:**我们可以将一张Ticket膨胀为一个全局范围的时间戳,以代表所有具有较早时间戳的写入。全局compaction只发生在FlightTracker服务中超过60秒的写入,因为我们假设我们的数据存储的复制滞后以及时钟的倾斜要小得多。鉴于阈值较长,全局compaction不必精确--较旧的写入不需要立即从Ticket中删除,因为它们纯粹是为了减少Ticket的大小。因此,用于compaction的时间戳可以是数据库生成的逻辑提交时间戳,也可以是数据存储在写入完成后生成的物理时间戳。全局compaction让FlightTracker只存储60s的数据,这大大减少了其工作集。

此外,我们还定义了一个包含Ticket的阅读,其中有一个空Ticket隐含地编码了返回数据的约束,即相对于提供读取服务的副本的当前物理时间戳而言,不超过60秒的时间。这样一来,在大多数情况下,我们就避免了传递只包含一个旧的全局时间戳的Ticket的需要。

4.3 Physical representation

作为一个跨系统的基元,Ticket提出了许多有趣的软件工程挑战。Ticket处理代码在部署频率差异很大的系统中运行,因此Ticket必须既能向前兼容又能向后兼容。Ticket封装了来自多个系统的特定实施细节,但由于客户可以加入Ticket,因此它不能将编码和解码留给数据存储。Ticket必须具有可扩展性松散耦合性,允许在不影响现有系统的情况下添加新系统的元数据。Ticket还必须有足够的效率,以便在我们的重读环境中的每个查询中使用。

我们通过使用Thrift[8]来解决上述一些挑战,Thrift是一种最初为高效和可移植的RPC设计的序列化格式。我们使用Thrift接口定义语言(IDL)来定义Ticket数据结构。

Facebook是默认使用thrift IDL的

**兼容性:**Thrift处理了大部分向前和向后兼容的问题,因为未来版本的未知字段会被默默地跳过。当我们将新的元数据字段引入到Ticket中的现有系统时,仍然必须小心。例如,如果两个具有相同密钥和时间戳的写入被事务ID所区分,不知道事务ID的旧代码可能会惊讶地看到一个重复的写入。

**序列化:**我们目前支持两种序列化格式,由前缀标识。默认的是Thrift Compact编码的LZ4-压缩[25]。这是在所有的RPC边界上使用的,使Ticket很容易通过其他系统的隧道。第二种是Thrift JSON编码,用于调试和记录的可读性。

**封闭性。**一个Ticket的内部结构可以被那些铸造新Ticket或执行包括Ticket在内的读取的系统所访问。不需要检查Ticket的客户端可以将其视为不透明的令牌。我们为需要检查Ticket内部结构的代码提供语言绑定和实用函数。

我们选择了Ticket这个名字,以减少开发人员对其语义的假设。基础设施建设者经常将对特定写入的可见性保证(Transaction 8980)与对连续前缀的保证(Transaction 1. . 8980)混为一谈;新的名称可以减少这种倾向。

**可扩展性和松散耦合性。**如图6所示,每个数据库都可以定制自己的表示方法。扩展Ticket结构以支持额外的数据库,只需在主结构中增加一个字段并更新连接函数即可。数据库之间的松散耦合通过为每个人使用不同的字段提供。