我将把它分成我希望有意义的点。我可能会重新散列一些我在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 个选项,您必须从中选择两个:
- 一致性
- 分区容错
- 可用性
根据您的定位,将需要不同的方法。CAP 定理通常用于描述数据库,但我相信每当您在处理数据时需要一定程度的容错能力时,都会提出类似的问题。