Erlang/OTP的消息可靠吗?消息是否会重复?

48
长版:
我是erlang的新手,并考虑将其用于可扩展的架构。我发现许多支持该平台的人都吹捧它的可靠性和容错性。
然而,我正在努力理解在这种消息在短暂内存中排队的系统中如何实现容错性。我了解到可以组织监督器层次结构以重新生成已死亡的进程,但我一直未能找到有关重新生成对正在进行的工作的影响的讨论。在垂死节点上丢失的飞行中的消息和部分完成的工作的工件会发生什么?
当消费者进程死亡时,所有生产者是否会自动重传未被确认的消息?如果不是,如何认为这是容错的?如果是,又有什么防止已处理但尚未确认的消息被重复发送,从而被错误地再次处理?
(我意识到这些问题并非仅适用于erlang;任何分布式处理系统都会出现类似的问题。但erlang爱好者似乎声称该平台使这一切都“轻松”..?)
假设消息被重新传输,我很容易想象在错误后复杂消息链的下游效应可能变得非常混乱。如果没有一些重型分布式事务系统,我不明白在不解决每个进程中的重复情况的情况下如何维护一致性和正确性。我的应用程序代码是否必须始终强制执行约束以防止事务执行多次?
短版:
分布式erlang进程是否会受到消息重复的影响?如果是这样,幂等性(即重复保护)是应用程序的责任,还是erlang/OTP在某种程度上帮助我们处理这个问题?

我的理解是,当一个进程死亡并重新启动时,任何没有传回的操作都需要重新执行。 - James Black
很明显,未完成的工作需要重新做...我认为你是在暗示应用程序有责任重新启动未完成的任务(可能是通过跟踪完成情况和重新发送失败的消息)。这是你的意思吗?有人可以从经验上确认吗? - joshng
3个回答

144

我将把这分成几个点,希望能让您理解。我可能会重复一些我在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定理,它基本上给你三个选择,你必须选择其中的两个:

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

根据你的位置不同,需要采取不同的方法。CAP定理通常用于描述数据库,但我相信在处理数据时,无论何时都应该提出类似的问题,以获得一定程度的容错能力。


6
我们需要一种方法将我们的一些积分捐赠出来,以将某些答案的点赞数提高超过+1。这里的工作真是太好了。 - JUST MY correct OPINION

5
erlang OTP系统具有容错性,但这并不意味着您无需在其中构建同样具备容错性的应用程序。如果使用erlang和OTP,则可以依赖于以下几点:
1. 当进程死亡时,该进程将被重新启动。 2. 大多数情况下,进程崩溃不会导致整个应用程序崩溃。 3. 发送消息时,只要接收者存在,消息就会被接收。
据我所知,erlang中的消息不会重复。如果您发送一条消息并且该进程接收到它,那么该消息将从队列中删除。但是,如果您发送一条消息并且该进程在处理消息时崩溃,则该消息将消失且未处理。这一事实应考虑在系统设计中。OTP通过使用进程将基础架构关键代码(例如监控器、gen_servers等)与可能受到崩溃影响的应用程序代码隔离开来,帮助您处理所有这些问题。
例如,您可能拥有一个gen_server,用于将工作分派给进程池。进程池中的进程可能会崩溃并重新启动。但是,由于gen_server的唯一目的是接收消息并将其分派到池中进行处理,因此gen_server保持运行。这使得整个系统即使在池中出现错误和崩溃时也能保持运行,并且始终有东西等待您的消息。
仅因为系统具有容错性并不意味着您的算法也具有容错性。

3

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接