我将把这分成几个点,希望能让您理解。我可能会重复一些我在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定理,它基本上给你三个选择,你必须选择其中的两个:
- 一致性
- 分区容错性
- 可用性
根据你的位置不同,需要采取不同的方法。CAP定理通常用于描述数据库,但我相信在处理数据时,无论何时都应该提出类似的问题,以获得一定程度的容错能力。