在构建分布式系统时,分布式协调是否总是必要选项?本文通过一些实际的例子讨论了这一问题,并通过把问题区分为是否具有单调性做为是否需要分布式协调的标准。原文: Avoiding Coordination Cost: Three Patterns for Building Efficient Distributed Systems
在这篇文章中,我们将讨论一个在分布式系统经常会遇到的问题(需要协调协议实现程序一致性),探讨为什么这会成为一个问题(其对可伸缩性、性能和可用性的影响),什么样的系统不需要考虑这一问题,其背后简单的直观属性(单调性),以及三个可以用来构建更高效分布式系统的简单模式。本文是基于我从Joseph M. Hellerstein和Peter Alvaro撰写的一篇题为"Keeping CALM: When Distributed Consistency is Easy"的论文中得出的结论。
准备好开始这段旅程了吗?在正文之前,我们先从一个现实世界的类比开始。
现实世界的类比: 在旅行中为了对徒步路线达成"一致"而进行的协调
有一次,我和家人预定了一块露营地。就在旅行的前一天,我的两个朋友想带着家人和我们一起旅行。我预定的露营地已经被订满了,但他们在附近的两个露营地订到了位置。所以,到了旅行的那天,我们到达了各自的露营地(相隔几英里)。
到目前为止一切顺利。第二天早上,我们意识到有个问题: 我们还没有讨论/同意当天的计划。
我们需要决定去附近的哪个地方徒步。
要进行任何协调,首先需要一个协调协议(co-ordination protocol) 。分布式系统需要协调协议的原因是为了实现程序的一致性(program consistency) 。在上述情况中,我们使用的协调协议是通过电话提出并讨论徒步选项,这里的程序一致性目标是对我们当天要进行哪一次徒步达成一致。
听起来很简单,彼此打个电话决定地点,对吧?幸运的是,我的露营地有很好的网络连接,我的一个朋友(朋友#1)似乎在他的露营地也有很好的网络连接,所以我打电话给他,提出了"徒步A"的想法,而他说服我,应该去"徒步B"。但是我们都联系不上2号朋友,也没有他的消息,他的营地没有网络连接,造成了"网络分区"。因此,我们还不能决定"徒步B"作为最终计划,因为来自朋友#2的输入可能会改变最终计划。
好吧,欢迎来到"非单调(non-monotonic)"系统。
问题在于需要协调
以上就是需要协调的情况的一个例子,而且是在关键路径(critical path) 上!在这个例子中,如果我们提前约定上午9点在"徒步B"的入口见面,可能就没有问题了。
正如本文所描述的,协调协议使自治、松散耦合的机器能够共同决定如何控制基本行为,许多分布式系统利用这种协调协议来实现预期的程序结果,有许多流行的协议,如Paxos和两阶段提交。
然而,这可能会带来很高的成本,从而影响系统的可伸缩性(假设我们的朋友是10个而不是3个)、可用性(我们在等待朋友#2的时候被耽搁了)和性能(我们本可以把时间花在探索风景上)。
什么时候真正需要协调?
CALM论文提出了一个基本问题:
哪些问题可以在不需要协调的情况下以分布式方式被一致的计算,哪些问题位于该问题家族之外?
"需要"和"想要"之间是有区别的,系统真的需要使用协调协议吗,还是可以在设计时就不使用协调协议?
本文描述了系统的一个非常简单和直观的性质: 单调性(monotonicity),它决定了是否需要协调。下面让我们看看这是什么意思。
关键思想: 逻辑单调性(Logical Monotonicity)
为了理解单调性,让我们从反论开始: 什么时候一个系统不是单调的?
我们已经在上面的例子中看到了一个: 即使我和朋友#1已经同意了,我们还是不能继续选择徒步路线,因为来自朋友#2的输入可能会改变最终结果。这是一个非单调系统的例子,其中系统不能基于部分或不完整的信息做出最终决策。
分布式垃圾收集: 非单调系统的另一个例子
CALM论文将分布式垃圾收集作为一个非单调系统的例子。每个节点上都有一组对象,节点中有一个根对象,根对象可以引用其他对象,对象可以引用其他节点中的对象。在这样的系统中,多个"本地垃圾收集器"应该如何协同工作以确定哪些对象不可访问?
节点需要交换关于"边"的信息。但是本地垃圾收集器不能自主做出决定,必须进行协调!最初,机器3可能确定对象O4不可达。但稍后可以从机器1接收到新的信息,证明其可达性: 通往O4的路径(Root -> O1 -> O3 -> O4)。和上面露营的例子类似,在做决定之前需要听取每个人的意见,这是一个需要协调的例子。
该论文提出了非单调系统的另一个含义: 因为这样的系统基于新的信息"改变他们的想法/决定",他们对信息的顺序敏感,也就是说,接收信息的顺序很重要。
分布式死锁检测: 单调系统的一个例子
现在让我们看看单调是什么意思,可以归结为:
基于部分或不完整信息做出的决定是否仍然稳定?基于部分信息得出的结论是否仍然具有偶然性?
论文以单调系统为例介绍了分布式死锁检测。假设在分布式数据库的不同节点上运行事务,希望检测死锁: 例如,在一个循环中,事务T1持有锁L1,正在等待另一个节点上事务T2持有的锁,而该节点又在等待L1被释放。
与垃圾收集示例类似,这里的节点必须不断交换信息。但是,一旦机器接收到帮助它确定有一个循环的信息,即使还没有从其他节点接收到任何信息,也可以自信的确定有一个循环,这里就不需要协调!如果它从更多节点接收到新的信息,可以了解更多的循环,但不会改变存在一个循环的事实。换句话说,输出随输入单调增长。
这种单调系统的另一个重要好处是,顺序并不重要,每个参与者可以以任何顺序接收信息,输出只取决于输入的内容。
单调性给我们提供了一种不需要协调实现一致性的方法
CALM论文的关键洞见在于,如果一个系统在逻辑上是单调的,我们可以通过不需要协调的方式实现一致性。论文将其描述为:
定理1。逻辑单调的一致性(Consistency As Logical Monotonicity, CALM)。当且仅当程序是单调的时,它才具有一致的、无协调的分布式实现。
论文介绍了单调程序在信息缺失的情况下是如何"安全"的,并且可以在没有任何协调的情况下继续执行。但在非单调程序中,当接收到新信息时,结果可能会发生变化,这需要引入协调,并导致参与者在等待此类信息时"停机"。
但是"一致性"在这里到底是什么意思呢?让我们看看CALM论文是如何定义一致性的。
"程序一致性" = 我们得到了期望的系统结果吗?
CALM论文没有局限于存储级别一致性,而是缩小了范围,并关注程序一致性: 程序是否产生了期望的结果?它提出了一个很实际的问题:
尽管系统在运行时存在不确定性,但程序是否产生了确定性的结果?
它把这种程序一致性称为"合流(Confluence)"。在上面旅行的例子中,这意味着所有朋友都对当天要去哪里徒步得出了相同的结论,不管通过不可靠的电话网络交换信息的顺序和时间是否确定。
收获: 如何基于上述内容构建高效分布式系统?
就构建系统的实际模式而言,上述内容有什么意义?
1. 尽可能使用单调编程模式
单调模式并不是构建高效分布式系统的唯一方法,但如果可以将系统建模为单调模式,那么就可以在不需要协调的情况下获得程序一致性的好处。
论文中提到的一个简单例子是使用tombstone来删除存储系统中的数据项(直接删除将是非单调结构)。相反,将一个项标记为已删除(tombstoning)将使其成为一个单调结构,具有tombstone的数据项单调的从未定义转换为已定义值,并最终被删除。
论文中介绍的另一个例子是Dynamo论文中的Amazon购物车。为了获得逻辑单调性,将购物车状态建模为两个不同的集: 一个集是添加到购物车的商品集,另一个集是从购物车中删除的商品集,这使得"购物"部分是单调的,而"结帐"部分是非单调的。
2. 能否在关键路径上保持协调
如果应用程序不能以单调的方式构建,那么就需要协调。但是评估一下是否能让其远离关键路径。
例子: 在上面的徒步旅行例子中,我们可以避免在关键路径上的协调(例如,在前一天晚上协调好)。对于分布式垃圾收集器示例,由于它可以在后台执行,因此所需的协调不在关键路径中。在Amazon Dynamo论文中的购物车场景中,协调仅限于"结账"(与向购物车中添加和删除物品相比,"结账"具有不同的用户期望)。
3. 是否可以对不一致进行补偿("道歉")
能容忍系统不一致吗? 如果可以的话,就可以避免协调成本。
论文中的一个例子是关于在电商网站上订购商品: 当进行购买时,库存中的商品数量应该立即减少,从而确保显示为可购买的商品确实可用。但这需要各种系统之间的协调和集成,如库存、供应链和购物。如果没有做到这一点,购买可能就会失败。然而,可以通过一封道歉电子邮件和优惠券进行补偿。这个建议也让我想起了Gregor Hohpe的著名文章"星巴克不使用两阶段提交"。
总结
CALM(Consistency As Logical Monotonicity)方法介绍了构建高效分布式系统的不同方法。通过利用单调性属性,可以以不需要协调的方式实现程序一致性。此外还提出了一个很好的问题,即一致性是否在所有情况下都是必须的目标(例如,是否可以使用补偿行动),或者是否可以从关键路径上移除。
所以,当下次我和朋友去露营的时候,相信我会探索以上所有选择:)
你好,我是俞凡,在Motorola做过研发,现在在Mavenir做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。
微信公众号:DeepNoMind