46

长版:

我是 erlang 的新手,正在考虑将它用于可扩展的架构。我发现该平台的许多支持者都在吹捧其可靠性和容错性。

但是,我很难准确理解在这个消息在瞬态内存中排队的系统中如何实现容错。我知道可以安排主管层次结构来重生已故的进程,但是我一直无法找到很多关于重生对进行中的工作的影响的讨论。正在运行的消息和在垂死节点上丢失的部分完成工作的工件会发生什么?

当消费者进程死亡时,所有生产者都会自动重新传输未确认的消息吗?如果不是,这怎么能被认为是容错的?如果是这样,是什么阻止了已处理但未完全确认的消息被重新传输,从而不适当地重新处理?

(我认识到这些问题并不是 erlang 独有的;在任何分布式处理系统中都会出现类似的问题。但是 erlang 爱好者似乎声称该平台使这一切变得“简单”..?)

假设消息被重新传输,我可以很容易地设想一个复杂的消息链的下游影响在发生故障后可能变得非常混乱的场景。如果没有某种繁重的分布式事务系统,我不明白如何在不解决每个过程中的重复的情况下保持一致性和正确性。我的应用程序代码必须始终强制执行约束以防止事务被多次执行吗?

简洁版本:

分布式 erlang 进程是否会受到重复消息的影响?如果是这样,重复保护(即幂等性)是应用程序的责任,还是 erlang/OTP 以某种方式帮助我们解决这个问题?

4

3 回答 3

138

我将把它分成我希望有意义的点。我可能会重新散列一些我在The Hitchhiker's Guide to Concurrency中写的内容。您可能想阅读那篇文章以详细了解 Erlang 中消息传递方式背后的基本原理。


1.消息传递

Erlang 中的消息传递是通过发送到邮箱(一种用于存储数据的队列)中的异步消息来完成的。绝对没有关于是否收到消息的假设,甚至没有假设它被发送到一个有效的进程。这是因为可以合理地假设 [在语言层面上] 某人可能只想在 4 天内处理一条消息,并且在它达到某个状态之前甚至不会承认它的存在。

一个随机的例子可能是想象一个长时间运行的过程,它处理数据 4 小时。如果它无法处理它,它真的应该承认它收到了一条消息吗?也许应该,也许不应该。这实际上取决于您的应用程序。因此,不作任何假设。你可以有一半的消息是异步的,只有一个不是。

Erlang 希望您在需要时发送确认消息(并在超时后等待)。与超时有关的规则和回复的格式留给程序员指定——Erlang 不能假设你想要消息接收的确认,当一个任务完成时,它是否匹配(消息当热加载新版本的代码时,可能会在 4 小时内匹配)等。

简而言之,无论您是否不希望收到消息,无论是未阅读、无法接收还是被某人拔掉插头而中断,都无关紧要。如果你想让它变得重要,你需要设计一个跨流程的逻辑。

在 Erlang 进程之间实现高级消息协议的负担交给了程序员。


2. 消息协议

正如您所说,这些消息存储在临时内存中:如果一个进程死亡,它尚未读取的所有消息都将丢失。如果你想要更多,有各种策略。其中一些是:

  • 尽可能快地读取消息并在需要时将其写入磁盘,发送回确认并稍后处理。将此与具有持久队列的 RabbitMQ 和 ActiveMQ 等队列软件进行比较。
  • 使用进程组在多个节点上的一组进程之间复制消息。此时您可能会输入事务语义。这个用于事务提交的 mnesia 数据库;
  • 在收到一切正常的确认或失败消息之前,不要假设任何事情都有效
  • 进程组和失败消息的组合。如果第一个进程未能处理任务(因为节点关闭),VM 会自动向故障转移进程发送通知,由该进程代替处理它。此方法有时与完整的应用程序一起使用以处理硬件故障。

根据手头的任务,您可能会使用其中的一种或多种。它们都可以在 Erlang 中实现,并且在许多情况下,已经编写了模块来为您完成繁重的工作。

所以这可能会回答你的问题。因为您自己实现了协议,所以您可以选择是否多次发送消息。


3.什么是容错

选择上述策略之一确实取决于容错对您意味着什么。在某些情况下,人们的意思是说“没有数据丢失,没有任务失败”。其他人使用容错说“用户永远不会看到崩溃”。在 Erlang 系统的情况下,通常的含义是保持系统运行:可以让单个用户挂断电话而不是让所有人挂断电话。

这里的想法是让失败的东西失败,但让其余的东西继续运行。为了实现这一点,VM 为您提供了一些东西:

  • 您可以知道一个进程何时死亡以及它为什么会死亡
  • 如果其中一个出错,您可以强制相互依赖的进程一起死亡
  • 您可以运行一个自动为您记录每个未捕获的异常的记录器,甚至定义您自己的
  • 可以监视节点,以便您知道它们何时关闭(或断开连接)
  • 您可以重新启动失败的进程(或失败的进程组)
  • 如果一个失败,让整个应用程序在不同的节点上重新启动
  • 还有更多关于 OTP 框架的内容

使用这些工具和一些标准库的模块为您处理不同的场景,您可以在 Erlang 的异步语义之上实现几乎您想要的东西,尽管能够使用 Erlang 的容错定义通常是值得的。


4.一些注意事项

我个人的观点是,除非你想要纯事务语义,否则很难有比 Erlang 中存在的假设更多的假设。您总是会遇到的一个问题是节点出现故障。您永远无法知道它们是否因为服务器实际崩溃或网络故障而关闭。

在服务器崩溃的情况下,只需重新执行任务就很容易了。但是,对于净拆分,您必须确保一些重要操作不会重复执行,但也不会丢失。

它通常归结为CAP 定理,它基本上为您提供 3 个选项,您必须从中选择两个:

  1. 一致性
  2. 分区容错
  3. 可用性

根据您的定位,将需要不同的方法。CAP 定理通常用于描述数据库,但我相信每当您在处理数据时需要一定程度的容错能力时,都会提出类似的问题。

于 2010-07-05T01:49:14.493 回答
5

erlang OTP 系统是容错的。这并不能免除您在其中构建同样容错的应用程序的需要。如果您使用 erlang 和 OTP,那么您可以依赖一些东西。

  1. 当一个进程死亡时,该进程将重新启动。
  2. 在大多数情况下,进程崩溃不会导致整个应用程序崩溃
  3. 发送消息时,只要接收者存在,它就会被接收。

据我所知,erlang 中的消息不会重复。如果您发送消息并且进程接收到消息,则该消息将从队列中消失。但是,如果您发送一条消息并且该进程收到该消息但在处理它时崩溃,那么该消息将消失并且未处理。在您的系统设计中应该考虑这一事实。OTP 通过使用进程将基础设施关键代码(例如主管、gen_servers 等)与可能会崩溃的应用程序代码隔离开来,帮助您处理所有这些问题。

例如,您可能有一个将工作分派到进程池的 gen_server。池中的进程可能会崩溃并重新启动。但是 gen_server 仍然处于运行状态,因为它的全部目的只是接收消息并将它们分派到池中进行处理。这允许整个系统在池中出现错误和崩溃的情况下保持正常运行,并且总有一些东西在等待您的消息。

仅仅因为系统是容错的并不意味着你的算法是容错的。

于 2010-07-04T20:53:33.433 回答
3

我认为答案与 Erlang 完全无关。它存在于客户端-服务器交互的语义中,您可以选择在客户端-服务器协议中实现“至少一次”、“最多一次”或“恰好一次”保证。所有这些调用语义都可以通过在发送或执行之前在客户端和服务器上组合唯一的标签、重试和记录客户端请求来实现,以便在崩溃后可以被服务器拾取。除了重复,您可能会丢失、孤立或延迟消息。

于 2011-01-15T01:05:37.007 回答