Akka能帮助你在单机多核处理器(纵向扩展)或分布式系统网络中(横向扩展)构建可靠应用。使这项工作得以完成的关键抽象是你代码的基本单元 ——Actor,它们之间的所有交互都通过消息传递实现。因此值得花一章的篇幅来重点介绍消息是如何在Actor之间传递的。
为了给下面的讨论提供一些上下文环境,假设一个跨越多个网络主机的应用程序。无论是发送消息到本地JVM上的Actor还是远程Actor,通信的基本机制都是相同的,但是在可靠性以及传输延迟方面会有明显的差异(可能还取决于网络带宽和消息大小)。如果发送一个远程消息,则会涉及更多的步骤,这意味着出错的可能性会更大。另一方面,发送本地消息只是传递引用给消息,并且对消息对象没有任何的限制,而远程消息则要限制消息的大小。
设计Actor时,假设每次交互都可能是远程消息,那么是安全的。这意味着仅仅依赖于已经得到保证的属性,这些属性将在下面进行详细讨论。这使得Actor的实现中有一些开销。如果你愿意牺牲完整的位置透明性——比如一组紧密合作的Actor。你可以始终将它们放在同一个JVM上,享受更严格的消息传递保证。 下面将进一步讨论这种权衡的细节。
作为补充部分,我们就如何在内置的可靠性之上构建更强的可靠性给出了一些指示。 本章最后讨论了“dead letters office”的作用。
通用规则
以下是消息发送的规则(即tell或!方法,它也是ask模式的基础):
- 最多一次交付,例如不保证的交付
- 每一对发送—接收的消息有序
第一个规则通常也可以在其他Actor实现中找到,而第二个规则特定于Akka。
讨论:“最多一次”意味着什么?
在讨论传输机制的语义时,有三个基本类别:
第一种是性能最高,实现开销最低的方式 ,因为它可以以一种即发即忘的方式完成,而不会在发送端或传输机制中保持状态。第二种需要重试以应对传输丢失,这意味着在发送端保持状态并且在接收端具有确认机制。 第三种是代价最大的——并且因此具有最差的性能——因为除了要实现第二种所做的事情外,它还需要在接收端保持状态以便过滤掉重复的传递。
- 最多一次传输意味着该机制下处理的每个消息,会被传输一次或者根本不传输;通俗来讲,这意味着消息可能会丢失
- 至少一次传输意味着该机制下处理的每个消息,可能在传输时进行多次尝试,使得至少一次成功; 通俗来讲,这意味着消息可能会重复但不会丢失
- exact-once传输意味着该机制下处理的每个消息,有且只会传输一次给接收者; 消息既不会丢失也不会重复
第一种是性能最高,实现开销最低最廉价的方式——因为可以采用fire-and-forget的方式,不需要在发送端和传输机制中保持状态。第二种方法需要重试来解决传输丢失问题,这意味着发送端需要保持发送状态,同时需要接收端确认机制来保证成功接收。第三种方式实现代价最大,所以性能也越差,因为它除了需要实现第二种方法要做的事情之外,接收端还需要过滤重复消息。
讨论:为什么不保证交付?
问题的核心在于这个保证究竟意味着什么:
- 消息已经发送到网络?
- 消息已经被另一台主机接收?
- 消息已经被放到目标Actor的邮箱?
- 消息已经在目标Actor上开始处理?
- 消息已经被目标Actor成功处理?
以上每一个问题都有不同的挑战和代价,而且很明显存在消息不满足上面所有条件的情况。比如可配置的邮箱类型以及有界邮箱如何与上述第3点进行交互,甚至如何定义上述第5点提到的“成功处理“。
另一个同样重要的理由是:没有人需要可靠传输。对于发送端来说,有意义的事情可能是接收到业务层面的通知信息来确定传输是否成功,而这不是Akka框架能够自身实现的地方,而且你可能也不希望Akka来做这件事情。
Akka拥抱分布式计算,而且他对通过消息传递可能会发生的消息丢失直言不讳,所以它没有试图撒谎并建立了一个抽象的可能泄露的模型。这个模型已经在Erlang和其他系统上取得了巨大成功,而且需要用户围绕它来设计应用程序。你可以在Erlang文档(第10.9节和第10.10节)中阅读有关此方法的更多信息。
从另一个角度看,通过提供一些基础的保证来满足那些不需要更强的可靠性场景,可以削减实现的成本。而且总是可以在基础上增加更强的可靠性,但是不可能通过降低可靠性的方式获得更高的性能。
讨论:消息的顺序
以上规则更具体地说,对于给定的一对Actor,直接从第一个发送到第二个的消息不会无序接收。直接这个词强调这种保证仅适用于使用tell操作发送到最终目的地,而不是通过中间人或者其它消息传递功能(除非另有说明)。
这种保证如下所述:
Actor
A1
发送消息M1
,M2
,M3
到A2
Actor
A3
发送消息M4
,M5
,M6
到A2
这意味着:
- 如果M1发送成功,那么必须是在M2和M3之前
- 如果M2发送成功,那么必须是在M3之前
- 如果M4发送成功,那么必须是在M5和M6之前
- 如果M5发送成功,那么必须是在M6之前
- A2可能看见A3插入到A1之后
- 因为没有保证交付,任何消息都可能丢失,如A2没有成功交付
Note
请务必注意,Akka的保证仅适用于消息进入邮箱的顺序。 如果邮箱实现不遵循FIFO顺序(例如,PriorityMailbox),则Actor的处理顺序可能与入队顺序不一致。
请注意,这个规则不具有传递性:
Actor
A
发送消息M1
到 ActorC
Actor
A
接着发送消息M2
到 actorB
Actor
B
转发消息M2
到 ActorC
Actor
C
可能以任意顺序接收M1
和M2
因果传递排序意味着在Actor C M2不会在M1之前接收(尽管它们中的任何一个可能会丢失)。 当A,B和C驻留在不同的网络主机上时,这种顺序是脆弱的,因为不同的消息存在传递延迟,请参阅下文。
Note
Actor创建被视为从父Actor发送给子Actor的消息,具有与上面讨论的相同的语义。以某种方式给Actor发送消息时,可能导致初始化消息和该消息之间的无序性,消息可能在Actor还没创建好时就已经到达。一个消息可能过早到达的示例是创建远程部署的Actor R1,然后将其引用发送到另一个远程Actor R2并让R2向R1发送消息。一个定义良好的排序的例子是父Actor创建一个Actor,立即向其发送消息。
失败消息的通信
请注意,上面讨论的排序保证仅适用于Actor之间的普通消息。 Actor的子Actor的失败消息是通过特殊系统消息来传达的,与普通消息之间没有相对顺序。 特别是:
子Actor
C
发送消息M
给它的父ActorP
子Actor 失败并发送失败消息
F
父Actor
P
可能以M
,F
或者F
,M的顺序接收到消息
原因是内部系统消息有自己的邮箱,因此用户和系统消息的入队顺序不能保证其出队时的排序。
本地JVM消息发送规则
小心使用这个部分!
不建议依赖本节中更强的可靠性,因为它会将你的应用程序绑定到仅支持本地部署:应用程序可能必须以不同的方式设计(而不是仅仅采用某些Actor本地的一些消息交换模式)才能在集群上运行。 我们的信条是“设计一次,任意部署”,为实现这一目标,你应该只依赖于“通用规则”。
本地消息发送的可靠性
Akka测试套件依赖于不丢失本地上下文中的消息(以及用于远程部署的非错误条件测试),这意味着我们确实尽最大努力确保测试的稳定。 但是,本地tell操作仍然可能因为某些原因失败,这些原因与JVM上的常规方法调用相同:
- StackOverflowError
- OutOfMemoryError
- 其他VirtualMachineError
另外,本地消息发送可能会抛出Akka特定的错误:
- 如果邮箱不接受消息(如已经满了的有界邮箱)
- 如果接收Actor在处理过程中失败或者已经被停掉
虽然第一个是配置问题,但第二个值得一些考虑:如果在消息处理时出现异常,消息的发送者得不到反馈,而是通知监督者。但是对于外部观察者来说,到底是这两者中的哪个导致消息丢失是无法区分的。
本地消息发送的顺序
对于一个严格的FIFO邮箱,假设上述警告的消息之间的不可传递性的保证在某些条件下被排除了。 正如你将注意到的,这些都非常微妙,未来的性能优化甚至可能使其无效。 不详尽的清单如下所示:
- 在收到来自上层Actor的第一个回复之前,有一个锁保护着内部临时队列,这个锁是非公平的; 这表明不同发送者的入队请求可能会重排序,这种重排序依赖于底层的线程调度机制。 由于JVM上不存在完全公平的锁,因此这是不可修复的
- 在路由构建期间使用相同的机制,更准确地说是路由的ActorRef,因此使用路由的Actor存在同样的问题
- 如上所述,问题发生在入队期间涉及锁的任何地方,这也可能存在于自定义邮箱
以上列表已经过仔细编排,但可能存在没有分析到的情况。
本地顺序与网络传输顺序是如何相关联的
对于给定的一对Actor,直接从第一个发送到第二个的消息将不会乱序接收,因为消息是基于TCP的Akka远程传输协议通过网络发送的。
如前一节所述,本地消息的发送在某些条件下遵循传递因果排序。 由于不同的消息传递延迟,可能违反此排序。 例如:
node-1上的Actor
A发送
消息M1
给 node-3上的Actor Cnode-1上的Actor
A发送
消息M2
给 node-2上的Actor Bnode-2上的Actor B转发消息
M2
给 node-3上的Actor CActor
C
可能以任意顺序接收到M1
和M2
M1可能需要更长的时间“到达”node-3相较于M2通过node-2“到达”node-3所花费的时间。
高层次的抽象
基于akka核心中内置的小而一致的工具,akka也支持更强大,更高层次的抽象。
消息传输模式
如上所述,可靠传输直接应答采用的是ACK重试协议,这种协议至少需要实现下面几点:
- 一种识别相关联消息的确认消息的方式
- 一种在指定时间内没有及时收到确认消息的消息重发机制
- 一种接收者监测和抛弃重复消息的方式
to be done
Dead Letters
我该用dead letters做什么?
我怎么收到dead letters?