为什么 at-most-once 交付是 actor 系统的默认值?

Why is at-most-once delivery the default for actor systems?

我正在为事件源服务使用参与者系统,我想了解为什么默认是 at-most-once 传递,即从一个参与者到另一个参与者的消息最多可能到达一次,但是可能根本没有。我正在使用 Akka,但我知道这通常是 actor 模型实现的默认设置。

在我看来,at-most-once 传递很容易导致无声的 failures/data 损坏,而 at-least-once 传递的问题可以通过版本控制和标记消息轻松解决。引自 How Akka Works 手册:

(at-most-once works) especially in situations where the occasional lose of a message does not leave a system in an inconsistent state

我不明白如何从“偶尔丢失一条消息没什么大不了”到“丢失这条特定消息没什么大不了”进行逻辑跳转。在我看来:如果我真的不关心收到这条消息,为什么要发送它呢?

示例:我们正在构建一个系统来计算文档中的单词数。我们有一个 actor (M),它接收一个文档,将其分成几行,然后将每一行发送给一个 child actor (C) 来计算该行中的单词。 M的状态是文档中有多少个词;它通过接收来自其 children C 的包含一行中单词数的消息来更新此状态,并将该数字添加到总数中。

在 at-most-once 传递中:如果 child actor 由于网络错误而丢失消息(如果是实际 actor 的错误,parent 可以使用监督来修复) ,parent 不会知道。它可以通过保存每个 child actor 的地图以及它是否完成并在收到回复时更新地图来跟踪状态,但是如果我们使计算稍微复杂一点并且来自children,这很快就会失控。此外,我们刚刚构建了一个系统来确保消息被接收,所以我们真的开始冒险走出 at-most-once 交付的世界,特别是如果我们依赖它来 re-deliver 计算消息到C 而不是只知道当前计数已损坏。

At-least-once 传递:我们可以给 M 的消息一个序列 ID,并在 parent M 中保留一个日志(C -> ID)。这让我们知道消息是否到达了两次,我们应该丢弃它。对我来说,如果 children 的任务变得更加复杂,这似乎更简单,也更普遍。

最多一次传送的最常见用例是计算(或收集其他统计数据)大量事件(例如页面浏览量)。

对于这些应用程序来说,消息的偶尔丢失确实不是什么大问题:如果您在给定分钟内发生了 999999 个事件而不是 100 万个事件,那么没有人会注意到,您也可以重申您的稍后的统计数据对任何事情都没有太大影响。

多算会是一个更大的问题:重述数据时,数字会下降(通常不只是下降 1,很容易 大量 由于丢失或超时确认而导致的多计造成的差异 - 见下文)。而且它不像你想象的那么容易避免(你会在你建议的那个“日志”中保留多少个 ID,你会保留它多长时间?这需要多少额外的内存?查找速度有多慢?如何组件重启会被处理吗?你如何确保重复的事件总是发送到同一个组件实例?另外,你必须在系统的每个组件中实现这个记录)

但更重要的是,执行至少一次合同会使系统变得更加复杂,因此系统的可靠性也会大大降低。

在这个系统中,组件不能只是将消息发送到管道中,而不管它。它必须等待接收来自它下游的每个组件的确认。如果ack没有及时到达,就必须再次发送同样的消息,一直这样下去,直到收到ack。想象一下,下游的某些组件由于某种原因(可能是内存利用率高)而变慢了。 ack 超时,上游开始一次又一次地重新发送相同的事件。与此同时,新消息不断堆积,而且没有被确认,所以他们也很反感。它呈指数级快速增长。这 增加了 已经有问题的组件 的负载 (使其变得更慢),以及 所有其他人,迅速使本已糟糕的情况变得更糟,并最终导致整个系统崩溃。

为什么它是默认值的简短回答是:

  • 在最多一次的基础上实现至少一次并不难(配对请求和回复的询问模式是其中的大部分方式),但是没有很好的方法来获得最多一次-once out of at-least-once(例如,不产生至少一次的成本)
  • 至多一次意味着或多或少一件事,与上下文无关,而至少一次意味着实际上无限多的事情(例如,在没有回复多长时间后,进程可以决定发送失败,应该重试多少次?)...没有合理的默认含义,并且很可能对于给定的 application/service.
  • 甚至没有合理的默认值

请注意,at-least-once 至少会导致与 at-most-once 一样多的不一致,尤其是在处理非幂等性时,添加幂等性的通用方法确实很重量级;相反地​​,在更高级别(以及需要的地方)设计幂等性通常能够轻得多。