为什么正好一次(Exactly-Once)传递是不可能的?

这是分布是系统领域很重要的一篇文章,主要论述在消息传递中"最多一次"、"最少一次"和"正好一次"三者中正好一次传递是不可能的,也就是通过网络两个服务器之间的调用恰好通过一次就完成正确通讯是不可能的。至少一次意思是一个消息至少传递一次以上,当然会造成消息内容重复冗余,但是可靠性提高了;而至多一次是服务器的消息最多传递一次,如果再传递一次,就会造成负面影响。正好一次是通过消息接收方发送确认收到的方式试图保障每次消息传递都能可靠传递完成,这是不可能的,因为这个发送、收到和确认的过程中一旦出现问题,就无法保证传递完成。

从另外一个角度看,这是针对网络故障的不同策略,至少一次是系统会在故障时重试请求,直至调用成功;而至多一次不会重试。

至多一次实际中案例是信用卡扣款,最多只能发出一次扣款消息,如果不成功,可以让客户重新发出扣款请求,但是我们不能在分布式系统内部作出扣款一次以上的动作,会带来负面影响,客户钞票多扣了。

原文大意翻译如下:

在分布式系统环境中, 你不能仅靠一次精确exactly-once的消息传送 。 无论是Web浏览器和服务器之间, 这是分布的。 服务器和数据库也属于分布的。 服务器和消息队列也是分布的。在任何这些情况下,您不能依靠exactly-once传递语义。

正如我在过去的描述 , 分布式系统都是关于取舍平衡。基本上存在三种类型的传递语义:最多一次at-most-once,至少一次at-least-once和正好一次exactly-once。 在三个中,前两个是可行的并且被广泛使用。

你可能会极端说至少一次传递也是不可能的,因为,技术上来说,网络分区不遵循严格的时间界定。 如果你和服务器之间连接无限期中断,你就不能传递任何东西,无论你一次或N多次传递都不行。请注意,我们考虑至少一次传递时,是假定网络分区在时间上是有界,类似这种连接无限期中断有另外解决办法,比如致电ISP。

为什么正好一次传送不可能?答案就在“拜占庭将军问题”。(拜占庭时代两位相隔很远的将军希望联合一起打仗,结果是他们永远不可能约在一起打仗,问题出在通信线路上等各种环节,但是在现代战争中,这是可能的,因为两只部队在出发之前会对一下各自手表,也就是同步一下时间,约定从对表时刻以后的未来某个时间点同时进攻,但是手表时间装置在牛顿以后才变得精确,罗马拜占庭时代不具备这个条件)。比如:在我寄给你的信中,我请你一旦收到就打电话给我。你永远不会打电话给我。因为要么你不在意我的信件,没有看我的信件,要么在邮寄过程中我的信件丢失了,这是沟通的成本问题。我可以发送一封信,希望你收到它,或者我可以发送10封信,并假设你会在收到其中至少一个。但发送10封信并不真正提供任何额外的保证。

在分布式系统中,我们尝试通过等待接收到消息的确认来保证消息准确传递到,但是各种类型的事情都可能出错。 消息被删除了? Ack确认信息被丢弃了吗? 接收者是否崩溃? 他们只是变慢吗? 还是网络变慢?这些都无法确定,“拜占庭将军问题”不是设计的复杂性,他们是不可能的结果 。

人们经常扭曲“传递”的意思,以使它们的系统适合正好一次的语义,或者在其他情况下,该术语被重载以意味着完全不同的东西。 状态机复制就是一个很好的例子。原子广播协议确保消息可靠地按顺序传送。事实是不能可靠传递消息,在网络分区中,如果没有高度协调就会遭遇崩溃冲突。这种协调当然是有代价的(延迟和可用性),同时还依靠至少一次传递的语义。Zab是ZooKeeper原子广播协议 ,实施幂等操作:

状态更改是幂等的,并且多次应用相同的状态更改不会导致不一致,只要应用程序顺序与传递顺序一致即可。 因此,保证至少一次语义是足够的,并且简化了实现。

“简化实现”是作者微妙尝试。状态机复制就是这样,复制状态。如果我们的消息有副作用,所有这些都会出问题了。

看看最多一次交付:当消息传递时,它在接受者处理它之前立即被确认。发送者终会接收到确认ack。但是,如果接收者在其处理这个消息之前或期间崩溃,则该数据将永远丢失。客户交易丢了? 只能对客户说,很抱歉,似乎没有收到您的订单。这是最多一次交付的世界观。说实话,根据具体情况,实现最多一次的语义比这更复杂。 如果有多个工作程序处理任务或工作队列被复制,代理必须保证强一致性(或CAP在CAP定理中的CP),以确保一旦被确认的任务不会被传递给任何其他工作程序。 Apache Kafka使用ZooKeeper来处理这种协调。

另一方面,我们可以在消息被处理后确认消息。如果这个处理在接受消息之后但在确认之前崩溃(或者确认没有被递送),则发送者将重新传送。 您好,请至少一次传递吧。 此外,如果你想以邮件传递到多个站点,你需要一个原子广播,但吞吐量带来巨大的负担。快速或一致性,你就慢慢权衡吧,欢迎来到分布式系统的世界。

现在每个主要消息队列提供至少一次传送作为保证。如果它宣称正好仅一次传递,那他们就在撒谎,希望你不会购买或他们自己都不明白的分布式系统。 不管怎样,这不是一个好的指标。

RabbitMQ的尝试提供担保沿着下面这些路径:

当使用确认时,从信道或连接失败中恢复的生产者应该重传任何没有被接收确认的消息。 这里存在消息重复的可能性,因为代理可能已经发送了一个确认,但是从未到达生产者(可能由于网络故障等)。 因此,消费者应用程序将需要执行重复数据消除或以幂等方式处理传入的消息。

我们在实践中实现正好一次性传递的方式是模拟它。 消息本身应该是幂等的,这意味着它们可以被多次应用而没有不利影响,或者我们通过重复数据删除来消除对幂等性的需求。理想情况下,我们的消息不需要严格的排序,而是交换就可以。无论你采取什么路线都有设计意义和权衡,但这是我们必须生活的现实。

重新考虑将操作作为幂等行为说说很容易,但是做起来很难,大多数需要改变我们对状态的思考方式。 这最好通过重温复制状态机来描述。相对于发布操作到各个节点并应用执行它们,如果我们只是分布状态改变state changes它们自己本身呢?而不是将状态发布到各个节点,,这样我们只是及时报告各个点的发生事实 。 这是Zab工作原理。

想象一下,我们想告诉朋友来接我们。我们向他发送一系列带有转弯路线的简讯,但其中一则讯息会传送两次!我们的朋友会不太高兴,因为他发现自己在城市里兜圈了。相反,我们只是告诉他,我们在哪里 ,让他看着办吧。如果消息被传递多次,这没关系。

这个意义(幂等操作)是非常深远,因为我们仍然在关注消息的顺序,这就是为什么像交换commutative和收敛convergent复制的数据类型的解决方案正在变得越来越受欢迎。 也就是说,我们可以通过外在手段如sequencing传统顺序、向量时钟或其他部分排序机制等方式解决这个问题。它通常是因果顺序,虽然不是立即实时,但是反正以后一直都是这样。那些否定因果顺序的人实际不明白在分布式系统没有“现在now”这个精确概念或这个时间点 ,(需要各个服务器精确对表,也就是校对时钟)。

重申一下,没有正好一次传递这样的东西。我们必须在两种罪恶之中进行选择,在大多数情况下选择至少是一次传递。 可以通过确保幂等性或以其他方式消除操作的副作用来模拟一次性语义。同样,重要的是了解设计分布式系统时所涉及的权衡。异步比比皆是,这意味着你不能指望同步机制来保证行为。 针对这种异步自然属性为故障恢复和弹性进行设计。


You Cannot Have Exactly-Once Delivery – Brave New

[该贴被banq于2016-11-27 19:07修改过]