[论文阅读]在MongoDB中实现集群范围内的逻辑时钟和因果一致性(一)

672 阅读27分钟

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

MongoDB发表了一篇论文,号称可以实现可调一致性,在数据中心视角上,可以实现强一致、因果一致性、最终一致性。这引起了我的兴趣,为什么对因果一致性非常感兴趣?

张铁蕾老师在文章:zhangtielei.com/posts/blog-…写到:

德克萨斯大学奥斯汀分校在2011年的一项研究表明[5]:

  • 不存在比因果一致性更强的一致性模型,能够在网络分区的情况下仍然可用。
  • 在一个永远保持可用且单程收敛 (always-available, one-way convergent) 的系统里,因果一致性是可以被实现出来的。

www.jdon.com/artichect/c…也有提到:

  COPS是保序系统的集群(Clusters of Order Preserving System)的简称。在了解COPS之前,最好首先了解一下分布式系统CAC算法,从CAP-->CAC-->COPS是分布式系统的确保高性能低延迟高可扩展性的前提下追求高一致性直至事务机制的发展路径。

   COPS系统能够提供因果+一致性causal+ consistency,设计为支持复杂的在线应用,这些应用托管在少量的大规模数据中心,每个应用都是有前端服务器(COPS客户端)以及后端key-value数据存储,COPS在本地数据中心以线性化方式执行所有的put写操作和get读操作,然后会跨数据中心以因果一致性的顺序在后台进行复制。

  通过COPS我们能够获得ALPS:Availability, Low-Latency,Partition-tolerance, 和 Scalability.

  • 可用性 Availability: 所有操作读能成功,没有会无限期堵塞或返回一个显示数据不可用的错误。
  • 低延迟Low-latency: 目标响应时间在毫秒级别。
  • 分区容错Partition tolerance: 数据存储能够在网络分区的情况下持续操作。
  • 可扩展性Scalability: 数据存储能够线性水平扩展。

  按照CAP定理,这种ALPS系统必然会牺牲强一致性,比如linearizability线性一致性,但是我们还是能够在此约束下寻找到最强的一致性,也就是因果一致性。

在多机房网络分区,或者跨region服务上,我们能够得到的最强一致性模型是因果一致性,那么商用系统MongoDB居然实现了这一可选一致性,对此我们绝对应该去看看他的代码和论文。其实本文是比较简单的,可以从中看看商用系统是如何考虑这个件事情: 论文地址:dl.acm.org/doi/10.1145…

image-20221220185627612

在MongoDB中实现集群范围内的逻辑时钟和因果一致性(一)

ABSTRACT

MongoDB是一个分布式数据库,支持复制和水平分区(分片)。MongoDB复制集由一个接受所有客户端写入的主数据库组成,然后将这些写入传播到第二个数据库。复制集的每个成员都包含相同的数据集。对于水平分区,每个分片(或分区)是一个复制集。

本文讨论了MongoDB实现集群范围内逻辑时钟和因果一致性的设计和原理。该设计利用了整个研究界的想法,以确保该实现增加最小的处理开销,容忍可能的操作错误,并对非信任的客户端攻击给予保护。

虽然该团队的目标不是发现或测试新的算法,但实际的实施需要将研究界关于因果一致性、安全性和规模上最小性能开销的想法新颖地结合起来。本文描述了一个使用混合逻辑时钟的大规模、实用的因果一致性实现,在协议中加入了逻辑时间范围的签名,并引入了大规模系统所需的性能优化。该实现试图将一个事件定义为一个状态变化,因此,即使在一个数据分区没有状态变化时,也必须保证向前推进。

1 简介

许多应用要求按照因果顺序观察事件,即操作的顺序在逻辑上遵循客户事件的顺序。下面是几个应用实例。

  • ATM现金存款(写)和余额检查(读)必须保持逻辑顺序。如果余额检查请求被路由到一个没有赶上领导者的节点,它将提供不正确的余额信息,有可能允许透支。
  • 在社交媒体上,用户可以在添加内容之前更新访问控制设置,以限制对新相册或对话主题的访问。如果对访问控制设置的更新和内容的查看顺序不一致,可能会违反安全策略。
  • 考虑一个货币交易应用程序。墨西哥比索/俄罗斯卢布(MXN/RUB)的价格是由美元/卢布和美元/墨西哥货币对得出的。现货价格是高度动态的。不按顺序提供MXN/RUB和USD/RUB的价格会带来套利机会,这将给market maker带来重大损失。

单节点系统很自然地为客户端的读写操作提供了这些顺序排列的保证,因为所有的操作都被路由到同一个节点。从Lamport关于Lamport Clock[23]的开创性论文开始,将这种行为扩展到分布式系统一直是过去几十年的分布式系统研究之一。关于因果一致性的论文有数百篇之多。在撰写本文时,ACM数字图书馆显示了235篇参考文献,说明了社区对该主题的兴趣。造成这种兴趣的一个因素是,因果一致性是容忍分区的永远可用的单向收敛系统的最强一致性模型[26],这使得它对现代分布式应用非常有吸引力,因为可用性是最高优先级之一。

尽管商业和研究界对因果一致性的特点很感兴趣,但业界的实现却很少。正如Jepsen团队对MongoDB 3.6.4的分析[37]所指出的。 "MongoDB是我们所知的第一批提供[因果一致性]实现的商业数据库之一" 。此外,最近的一篇论文[27]指出:。 "然而,因果一致性还没有在业界得到广泛的应用"。 在这个话题上有大量成熟的研究,但是商业化的实现却很少。这有几个原因:

  • 在许多实际情况下,因果一致性是非常有可行性的[9],对于某些应用来说,性能比罕见的不正确情况更关键。
  • 因果一致性可以在客户端实现,许多需要这个功能的生产应用已经这样实现了,或者使用数据库提供的更强的一致性模型。
  • 基于依赖图跟踪的因果一致性实现引入了性能开销,这往往是生产应用需求所不能容忍的。

作为一个商业产品,MongoDB对因果一致性的实现需要解决和平衡整个需求,从性能和可扩展性到数据库的安全性,这样恶意的攻击就不能破坏或损害系统。本文将讨论MongoDB对应用解决方案的多维要求,包括性能、可扩展性和安全性。这些要求被用作实施决策的指导,并最终导致需要结合几个研究领域的各个方面。本文还将描述在实施这一功能时所作的改变如何显著提高性能并简化所有系统组件的实施。

对实施方向的基本贡献之一是围绕着这样的理解:一致性模型是分布式数据库的一个基本属性,并被用作建立分布式事务。因此,它必须易于调试和诊断。实际的问题,如提高可靠性和可维护性的代码简化,在商业产品开发中也发挥着重要作用。在实践中,代码的简单性意味着更少的系统组件。

另一个重要的要求是向后兼容。例如,较新版本的数据库必须与较旧版本的客户端驱动程序一起工作。在MongoDB的案例中,虽然有官方支持的驱动程序,但也有几个社区支持的驱动程序可能无法支持协议中的突破性变化。作为一个用于关键任务工作负载的现代数据库,MongoDB提供滚动升级--即在不停机的情况下升级到新版本。系统必须能够在一些节点已经升级到较新版本,而一些节点仍在使用旧版本的状态下运行。任何解决方案都必须提供中止升级的能力,并安全地回滚到以前的版本。此外,该方案必须是对现有客户端协议的扩展,不会破坏旧的客户端。

2017年11月宣布的MongoDB 3.6引入了因果一致性,客户已经成功使用了一年多,不需要做任何重大改变。

本文的其余部分组织如下。在 "相关工作 "部分,我们将概述相关的论文,然后在 "设计选择 "部分,我们将提出关于我们的需求和优先级如何影响选择的论据。在 "系统模型 "中,我们描述MongoDB的分布式架构。在 "使用因果一致性 "部分,我们将描述添加因果一致性如何帮助应用程序提高功能和服务。然后在 "性能优化 "中,我们将介绍在实现因果一致性时出现的挑战以及我们解决这些挑战的方法。最后,在 "性能评估 "中,我们将介绍在各种条件下使用因果一致性的性能成本评估,并得出结论。在 "附录 "部分,我们将详细描述算法、数据结构和实现的协议。

2 相关工作

CAP定理是Eric Brewer在2000年提出的一个猜想,Seth Gilbert和Nancy Lynch在2002年发表了一个正式证明[19]。该定理指出,在网络分区的情况下,系统不能同时提供可用性和原子一致性的保证。由于网络分区是大规模分布式系统的一个现实[7],分布式系统必须在一致性和可用性之间进行权衡。虽然CAP该猜想在理解分布式系统方面发挥了重要作用,但实际上大多数系统并不满足该定理的所有条件。[20].线性化要求很昂贵,虽然MongoDB支持,但由于延迟成本,使用频率低于持久性阅读。在实践中使用的系统的数据库一致性比符合CAP定理限制的系统类别更广泛。分布式数据库运动有助于更详细地了解一致性与可用性的权衡,从而导致对支持一致性模型的架构的研究,这些模型比原子一致性要弱,但仍然满足应用需求[8]。

2.1 最终一致性行为

最终一致性是指在没有数据变化的情况下,任何给定数据项的所有节点都会在未来的某个时刻收敛到相同的值。这是分布式数据库的一个有效性属性,不应该被违反。虽然最终一致性是一个弱属性,但它被许多现代分布式数据存储所支持(Cassandra [22], DynamoDB [15], Solr [4], Riak [11])。如此广泛采用的原因是。

  1. 最终的一致性不会影响系统的可用性和性能,而这是生产型分布式系统的主要要求[6]。
  2. 在实践中,用户通常可以对提供安全保证的操作指定额外的要求。例如,用户可以选择只读取提交给大多数副本的数据,以避免在分区情况下的回滚风险(在MongoDB中实现为readConcern :"majority "[31])。

然而,最终一致性并不总是能满足应用开发者的需求。因此,在某些情况下,最终一致性系统中的应用需求迫使开发者增加复杂的应用逻辑,这往往会引入不必要的延迟,降低性能和用户体验。

2.2 因果一致性行为

与最终一致性的行为相比,因果一致性保证通过以下属性体现给客户。

  • 客户端可以读取自己的写入内容

  • 客户端可以写入跟随其读取的数据

  • 单调性阅读

  • 单调性写道

这些属性在开发应用程序时很有帮助,无需在应用逻辑中编写复杂的变通方法。

因果一致性[1]被定义为保留分布式系统中事件的部分顺序的模型。如果一个事件A导致另一个事件B,因果一致性提供了一种保证,即系统中的每一个其他进程在观察事件B之前观察到事件A。因果顺序是传递性的:如果A导致B,B导致C,那么A导致C。

因果一致性的概念与分布式系统中的事件排序这一更普遍的话题密切相关。关于这个问题有许多研究论文,但最有影响力的是Leslie Lamport在1978年发表的论文[23]。这项工作定义了分布式系统中各节点事件的总顺序,建立了操作之间的 "happened-before "关系。

事件X发生在事件Y之前,如果

  1. 在按顺序处理事件的同一过程中,X在Y之前发生。
  2. X是一个信息的发送,事件Y是对事件X中发送的信息的接收

Lamport时钟(表示为LC())是一个标量值,可以捕捉到这种发生在之前的关系。如果事件X发生在LC(X),事件Y发生在LC(Y),并且X发生在Y之前,那么LC(X)< LC(Y)。反之亦然,即我们可以有LC(X)< LC(Y),但X和Y是并发的。在发送消息之前,每个进程将其LC值增加1。当一个新的消息到达一个进程时,该进程将其当前的LC值更新为其旧的LC值,或随传入消息携带的LC值,两者中较大者,然后立即将该值增加1。

2.3 实践中的因果一致性

尽管被证明是在分区存在的情况下最强的一致性模型[26],但因果一致性并没有被生产分布式系统广泛支持。由于缺乏真实世界的测试,生产型分布式数据存储中的因果一致性实现可能出现的问题并不为人所知。

经典的因果一致性模型附加了后续写入所依赖的写入,使得依赖图可能很大,其处理也很expensive。一种设计方法是[12],有一个通信通道负责按照因果顺序提供消息传递,在最坏的情况下,需要缓冲的请求是在系统中的进程数量的二次方。

由于对大多数现代应用来说,跟踪生产系统中所有可能的因果关系是太昂贵了,Balis等人建议将依赖关系缩小到重要的事件,将定义因果事件的责任交给应用[10]。在这种方法中,数据存储专注于在应用程序明确要求的事件上执行因果关系,而不是在曾经发生的事件的全局历史上执行。这种方法在 "Bolt-on Causal Consistency "论文[5]中得到进一步发展。该方案描述了一种分层的因果关系跟踪方法和数据存储层,与最终的一致性存储系统一起工作。然而,带有专用代理的分层方法导致了性能开销。开销包括需要所有到所有的复制,本地跟踪因果关系的消息,以及昂贵的依赖图的处理。

此外,Yahoo Pnuts[13]实现的每条记录的时间线一致性为从最终一致性到可序列化的各种级别提供了一个API。该实现保证了记录的所有变化都以相同的顺序在所有副本上应用。该API允许寻址一个特定的版本,因此,允许在客户端实现每条记录的因果一致性读写。然而,MongoDB的方法需要跨记录扩展这些因果保证。

其中一个研究较少的问题是因果排序可能对系统的脆弱性产生的影响。MongoDB的操作模型允许与系统认证边界之外的客户端进行通信,因此,不能被信任。换句话说,MongoDB需要假设来自客户端的消息可能是由试图破坏系统的恶意用户产生的。"实用容错"[24]提出了一种方法,它建立在容错复制核心(如Paxos或Raft)和协议提供的数据可靠性之上,以容忍拜占庭故障。在视图变化XPaxos协议中,参与的节点用自己的私钥签署他们的消息,并使用相应的公钥验证传入的消息。签名验证机制有助于检测非崩溃性故障并隔离恶意节点。MongoDB的实现采用了这种方法,为高吞吐量系统优化了性能,并将其应用于全局因果排序。最近对MongoDB的分析中探讨的另一个方面是[37],因果关系和耐久性是否应该被认为是可以独立满足的不同属性。由于MongoDB的写在本地提交到一个节点时可以被确认,在它被多数人提交和安全回滚之前,有可能写的最终结果是写失败了。在这种情况下,提交的写的因果顺序被保持,但一些次多数写可能会失败,因为它们无法提交给多数节点。例如,一些写入可能最终由于分区而失败,但提交的写入的顺序将保持因果关系。有一种观点认为,即使有些写操作是不持久的,这种行为仍然是因果一致的。这使我们能够将耐久性保证与因果性保证分开。然而,Jepsen分析[37]的作者建议使用多数写来保证耐久性。此外,该分析还观察到,MongoDB提供了顺序一致性,当与多数人读写一起使用时,会强制单个键的总顺序。

3 贡献

在大规模商业数据存储中实现因果一致性需要多维度的评价标准。这些标准被用来评估现有的方法。没有一种方法能满足每一种需求。本文贡献了一个简单、可靠和可扩展的设计,它结合了来自不同领域的几个想法。具体来说,该应用结合了混合逻辑时钟与gossip、有符号的时间范围,以及防止因操作员错误而跳到太远的未来的保护。

此外,MongoDB的实现将事件完全定义为状态变化,因此实现了一个解决方案,即使在没有状态变化的情况下,仍然有向前的逻辑时间进展保证。附录A2:在没有状态变化的情况下进行时间进展一节中讨论的noop(无操作)写入器的实现满足了这种向前进展的保证。设计选择也被详细介绍,因为它说明了生产数据库的具体要求。

4 设计选择

本节讨论了提供因果排序的实施选择和设计挑战。它概述了MongoDB工程团队应用于不同现有方法的评估标准。最终,这种评估导致必须结合各种方法的各个方面来提供一个实用的实现。

可供选择的实现如下:

  • 钟的类型
  • 依赖性跟踪
  • 时钟同步
  • 系统安全

4.1 时钟的类型

Lamport的想法有几个变种,可以归类如下。

a. 标量逻辑时钟。上面讨论的Lamport时钟。

b. 矢量逻辑时钟。Lamport时钟可以为不同节点上的独立事件分配相同的值,而矢量时钟可以区分这些跨节点的独立事件。一个由N个进程组成的系统的矢量时钟是一个由N个逻辑时钟计数器组成的阵列。进程P和Q上的事件被跟踪在不同的计数器中。然而,一个矢量时钟对每个信息需要O(N)个空间[18] 。

c. 同步时钟。同步墙时钟是在节点上使用定制硬件实现的。[14]

d. 混合逻辑时钟(HLC)。Lamport时钟不提供对物理时间的参考,因此无法按物理时间搜索事件。HLC是一个带有分布算法的标量值,可以将该值与物理时间绑定。[21]

我们考虑了(a-d)的优点和缺点,使用以下产品和用户需求来指导设计决策。

  1. 对于不需要逻辑时间的操作,性能必须保持不变。
  2. 对于利用逻辑时间的事件,性能开销最小。
  3. 围绕MongoDB的现有工具应该保持向前兼容。
  4. 外部客户必须是不被信任的,必须不能破坏系统,或影响其可用性。
  5. 该解决方案必须允许扩展到数以千计的分片而不出现明显的性能下降。
  6. 该解决方案必须对操作员的错误操作可以复原。
  7. 该解决方案必须在现成的硬件或我们的用户可以使用的流行商业云服务上运行。

对选项(a)的评价。标量逻辑时钟(Lamport Clock)的一个缺点是,它与物理时间脱节。在这种限制下,流行的MongoDB工具,如备份和恢复,不能使用物理时间作为参考。我们拒绝了这个选项,因为它违背了我们的第三个要求,并且会导致对工具的破坏性改变。

对选项(b)的评价。矢量时钟可以区分不同节点上的事件,这提高了在多个分片上建立快照,或一致的时间点的效率。然而,每个消息的大小与分片的数量成正比。这在许多分片之间不能很好地扩展,破坏了我们的第五个要求。

对选项(c)的评价。TrueTime不适合可以在任何地方部署的开源数据库,因为它需要原子钟/GPS来实现时钟的严格保证边界。基于时钟同步的方法为读或写操作引入了强制性的等待,可能会变得很重要。对特殊硬件的需求并不满足要求7。

对选项(d)的评价。混合逻辑时钟与标量逻辑时间具有相同的空间和计算要求。即使在因果一致性之前,MongoDB也使用了类似于混合逻辑时间的排序,用于维持用于复制单一分片的操作日志的秩序。因此,我们已经有了一些在集群中同步事件所需的基础。这种方法与我们的产品要求最为一致。

此外,我们在对事件的定义上与之前的工作有一个重要的偏离。在大多数论文中,从[23]开始,一个事件是消息到达一个节点的过程。这有助于对底层系统进行抽象,并考虑到任何事件都可能产生后果。在MongoDB中,我们可以利用能够检测哪些消息真正改变了系统的状态,哪些是简单的观察者的优势。只有当有新的操作日志条目时,逻辑时钟才会递增,代表系统状态的变化。这使得系统状态和逻辑时间之间有了联系。这种方法也大大简化了实施,如下图所示,用逻辑时间对OPLOG条目进行索引。

4.2 依赖性跟踪

对于依赖性跟踪,工程团队的选择是。

a. 全面的依赖性跟踪

b. 明确的依赖性跟踪

c. 整体式或Bolt-on

d. 对客户端(驱动)的依赖性跟踪

由于需要附加和处理所有事件的依赖图而造成的性能开销,我们拒绝了(a)的方法。

对于(c),"Bolt-on Causal Consistency "论文[5]描述了一种实现shim的方法,它不保存状态,并解决了因果关系。这使得现有的最终一致的系统可以在不改变其实现的情况下解决因果排序问题。我们有两个选择。"将因果一致性作为一个单独的代理来实现,或者将其作为分布式数据库的核心功能的一部分"。

方法(d)有可能适用于旧版本的数据库,因为它假设服务器和协议没有改变。然而,客户端的实现需要所有客户端参与者的明确应用支持,而客户端可以使用任何数量的语言的驱动程序。一个实现可能需要所有参与者传递因果关系令牌来建立发生-之前的关系,因此不能普遍使用。

虽然用因果关系分离可用性、liveness、分区容忍性和可扩展性(ALPS)[25]的分层方法听起来非常吸引人,但这种方法如果作为一个单独的代理实现,会增加复杂性、开销和额外的故障点。虽然MongoDB没有实现因果一致性代理或shim,但它仍然实现了数据复制、分片和因果一致性的独立组件,也就是说,我们可以改变复制协议,而不会影响因果一致性代码。同样地,改变因果一致性的实现方式也不会影响分片或复制。使用HLC值作为操作日志唯一ID的一部分,对于实现因果一致性非常方便。甚至可以尝试使用矢量混合时钟(即为每个时间附加一个节点ID,以提供更好的分区支持)以及时钟同步来实现外部一致性;这一切都可以在不改变复制或分片协议的情况下进行,这对于提供向后兼容性和滚动升级到新版本非常重要。

让用户明确跟踪依赖关系(b)的想法可以以因果关系段的形式呈现,客户阅读的所有数据都是客户观察到的最新事件之后发生的-。 该团队采取了方法(b)。假设客户进行了一次写入,并且返回的数据已经被添加到带有因果标记T的副本A中。同一客户从副本B的后续读取必须满足这样的条件:副本B上的数据集有被提交到副本A的写入。一种类似于依赖性跟踪的方法是在[2]中讨论的是标量物理时钟,在[16]中讨论的是矢量混合逻辑时钟。

正如在 "时钟类型 "子节中所讨论的,MongoDB使用标量混合逻辑时钟。来自因果一致的会话中的每个数据节点的每个消息--即一个执行线程都会收到operationTime最后已知的逻辑时间,该节点上的OPLOG中持续存在。源于该会话的后续读或写操作将最高的已知操作时间附加到请求元数据上。收到请求的数据节点等待其操作日志赶上请求的操作时间。

4.3 时钟同步化

在研究论文中,时钟同步往往意味着gossip[19],[23],或心跳[17],[16]。然而,由于低延迟的要求,我们需要一种更快的方式来同步所有参与操作的数据节点上的集群时间。例如,考虑一个有分片X和分片Y的系统。根据水平分区的定义,它们包含不重叠的关键域。由于MongoDB中的集群时间是标量的,系统必须提供一个解决方案,当客户写到分片X,然后从分片Y读取时,要求operatingTime(X)大于Y上的最新持久化集群时间的场景。时钟同步可以通过以下方式解决。

a. 使用矢量时钟

b. 物理时钟同步

c. 使用心跳

d. 稳定集群时间(SCT)的强制提前 - 在OPLOG中持续存在的逻辑时间

选项(a)和(b)在前面已经被排除了,在发送频繁的心跳和推进SCT之间做出决定。为了推进SCT,MongoDB执行了一次无操作的写入,因为这是增加稳定集群时间的唯一方法。它不会代表冲突,因为关键域是独立的(没有多主站),因此每个读或写操作序列都会有严格增加的SCT序列。因此,推进SCT允许为所有类型的工作负载提供低延迟,只受限于节点的写吞吐量。MongoDB采取的方法是(d)。

4.4 系统安全

MongoDB将数据的安全性和对恶意攻击的抵抗放在首位。虽然在oplog键中使用HLC有助于大大简化代码,同时保持分层方法,但它也引入了一个显著的安全风险。预先的SCT算法将所有系统参与者的最新已知逻辑时间写入OPLOG,即使是安全边界之外的参与者,即外部客户端。由于SCT值是严格的单调和持久的,一个恶意的客户端可以简单地发送最大可能的逻辑时钟值。一旦最大的时间被写入操作日志,系统将不能接受未来的修改,因为时间不能再被递增。有几种方法可以使用。

a. 对逻辑时间的值进行签名,以便它们只能由MongoDB的数据节点发起。

b. 使用一个单独的代理,它将跟踪明确附加的依赖关系--即不在数据节点中嵌入因果关系。

正如前面所讨论的,方法(b)不能满足提供最佳性能和可靠性/简单性的要求。论文[24]中描述了方法(a),保护复制协议免受杂乱的故障。我们使用了类似的方法来防止用户改变消息,以便将集群时间推进到最大值。然而,我们可以使用相同的安全密钥来生成消息签名和验证,因为MongoDB架构有一个集中的数据目录,非特权用户无法使用。虽然这种设计的好处是只在服务器上实现,但它增加了系统的复杂性。主要的工程困难是实现可靠和安全的密钥生成和同步,以及确保它在分区、升级、降级和多版本操作中工作。

即使在客户不遵守因果gossip的情况下,例如发送旧的HLC值,也只有不遵守的会话受到影响。所有的因果排序都是由服务器执行的。如果一个修改带来了一个旧的HLC值,服务器将使用该值或它所看到的最新时间中较大的一个,并从那里开始递增。