消息传递可靠性
akka帮助我们在单机多核处理器或多服务器分布式系统中构建可靠性强的的应用系统。为了实现这一点,关键是在你的代码单元的交互中(也就是actor)的交互只通过消息传递来实现。因此值得花一章的篇幅来重点介绍消息时如何在actor之间传递的。
这里先假设一个跨多网络服务器的系统作为下面将讨论的内容的上下文环境,因为跨网络消息传递需要更多的可靠性保证。虽然发送消息给同一个jvm上的actor和发送给另一台服务器上的actor,基本的通信机制是一样的,但是在延迟和可靠性上有明显的差别,当然这种差别也可能受网络带宽和消息大小的影响。远程消息的发送涉及的步骤明显要多,这也意味着发生错误的概率就越大;而发送本地消息只是传递引用给消息,并且对消息对象没有任何的限制,而远程消息则要限制消息大小。
在设计actor的时候,他们之间的交互如果都是远程消息传递,那么是非常安全的,这意味着仅仅依赖于已经得到保证的因素,这些得到保证的因素将在在后面详述。不过,实现这样的系统当然会有一些开销。如果你想牺牲完全位置透明性(比如一组紧密合作的actor),你可以把他们放在同一个jvm里,但是需要更严格的消息传递可靠性保证。关于这些可靠性权衡也将在后面讨论。
大体规则
消息发送(比如tell,!,ask方法)规则大体有下面2则:
1.最多传输一次:非确信传输。
2.每一对发送-接收者的消息有序。
第一条在其他actor模型实现系统中非常常见,第二天只对akka系统有效。
讨论:“最多一次”意味着什么
一般讨论传输机制的时候,大致可以分为下面3类:
1.最多传输一次:采用这种机制传输消息时,每一天消息传输0或1次,更通俗的说,就是消息可能丢失。
2.最少传输一次:采用这种机制传输消息时,可能多次重试传输,直到最少有一次成功。通俗的说,就是确保消息不会丢失,但是接受到的消息有可能重复。
3.精确传输一次:采用这种机制传输消息时,对于处理的每一条消息,接收者会且只会接收到一次,也就是消息既不会丢失也不会重复。
在上面3中方式中,第一种是性能最高,实现开销最低最廉价的方式。它这种发送出去就忘记的方式不需要保持发送端和传承机制的状态。第2种方法需要重试来计数传输丢失数,这意味着发生端需要保持发送状态,同时也需要机制来保证发生端获得接收端的状态。第3种方式实现代价更大,所以性能也越差,它除了需要实现第二种方法要做的事情之外,接收端还需要过滤重复消息。
讨论:为什么采用非确信传输?
采用确信传输需要面对下面几个核心问题:
1.消息是否是在网络上传输?
2.消息的接收端是否是另一台服务器?
3.消息时投递到另一个actor的邮箱吗?
4.消息是否是立即被目标actor处理?
5.消息是否被目标actor成功处理?
上面任何一个问题的解决都有不同的挑战和代价,而且很明显任何一个消息传输库都不可能满足上面的所有条件。比如可配置的邮箱类型,有界邮箱是如何与第三方交互的,甚至如何定义第5点提到的“成功处理“。
另一个同样重要的理由是:没有人需要非确信传输不可。对于发送端来说,有意义的事情可能是通过接收到通知信息来知道传输是否成功。而这不是akk框架需要实现的地方,而且你可能也不希望akka来做这件事情。
akka支持分布式计算,而且他对通过消息传递可能会发生消息丢失的直言不讳,所以它没有试图建立一个抽象的泄露模型。因为这个模型已经在erlang和其他系统上取得了巨大成功。
从另一个角度看,通过提供一些基础的保证来满足那些不需要更强的可靠性场景,可能削减实现成本。而且在未来可能会继续加强基础可靠性,而不会因为获得高性能而降低可靠性。
讨论:消息顺序
akka中一个比较特殊的规则是,对于一组给定的actor,从第一个actor直接发送到第二个actor的消息是有序的。这里强调下直接这个词:消息从发送者到接收者之间没有经过其他actor的转发。下面使用例子来解释下这个规则:
假如actor A1发送M1,M2,M3给actor A2;actor A3发送M4,M5,M6给actor A2。那么M1一定在M2,M3前面,M2一定在M3前面,M4,M5,M6亦然。A2接收到A1,A2的消息之间没有必然的顺序。当然因为消息传输是非确信传输的,所以任何一个消息都可能丢失。注意:这里的顺序是指接收端邮箱进队列的顺序,如果邮箱使用的不是FIFO而是优先级邮箱,那么消息的处理顺序跟进邮箱的顺序不一样。最后必须注意这种有序是不能传递的。比如 A发送消息M1给B,A发送消息M2给C,C在转发M2给B,那边B收到的M1,M2之间是无序的。这是因为消息传输可能存在不同的延迟,特别是ABC在不同的服务器上时。
注意:由于actor的创建也可以认为是父actor发送消息给子actor,因此初始发送的创建信息与其他消息的顺序可能于发送时不一致,所以其他消息可能由于actor还没创建而丢失。
通信失败
上面讨论的顺序保证只适用于用户消息。子actor的错误消息是通过特殊的系统消息通信来传递的,它与用户消息之间没有必然的顺序关系。因为内部系统消息有自己专用的邮箱,所以用户消息队列和系统消息队列两个队列之间不能保证顺序关系。
本地JVM消息通信规则
本地JVM之间通信可靠度更高,但是并不建议依赖于这种可靠性性来设计你的系统,因为这会把你的系统局限在本地actor之间消息传递上,或者局限于单机部署。但是一般来说系统必须设计得支持集群部署。AKKA的信条是“一次设计,任意部署”。所以当你在依赖本地通信之间的高可信度设计系统的时候,必须清醒的知道你在做什么。
akka在不发生错误的本地jvm通信测试中,消息的丢失率为0,但是这种测试结果是在测试环境稳定的情况下得到的。由于stackoverflowerror,OOM以及其他jvm错误,可能会导致本地tell操作也发生错误。另外akka的某些特殊的使用场景也可能发生错误。比如使用有界邮箱,邮箱满了会导致消息丢失,接收actor发生错误或者已经终止,等等。对于前者,很清楚是由于邮箱配置的关系导致,但是对于后者,如果接收actor处理消息时发生错误,发送actor并不会接收到反馈,而是接收actor的监护者获得错误消息。但是对于外部观察者来说,到底是这两者中的哪个导致消息丢失是无法区分的。
本地消息发送顺序
前面提到,严格的FIFO邮箱是没有传递性的,不过特定条件下本地消息顺序是有传递性的。这些成立条件也是非常微妙的,甚至将来的性能优化可能会颠覆整个现有的条件。不过可以确定不会变的条件如下:
1.在接收到高层actor返回的回复信息之前,会有一个非公平锁来保护内部临时队列。这表明不同发送者的进队请求在进队时可能会重排序,这种重排序依赖于底层的线程调度,因为绝对的公平锁在JVM上是不存在的。
2.actor的消息机制也适用于路由层,更准确的说是被路由的actor引用,因此路由也存在第1条中同样的问题。
3.只要在进队时存在锁的地方(比如客户邮箱),上面的问题就会存在。
上面的3条是经过谨慎考虑总结出来的,也可能存在漏洞的情况。
本地顺序与网络顺序是如何协调的
前面的段落中提到了,在特定条件下,本地消息发送顺序具有传递性。如果远程消息传输也适用这条规则,这将转化为跨一个网络链路的传递顺序性,即刚好只有两个网络主机。如果有多条链路,比如在第三个节点上的actor,它的顺序是无法保证的。虽然目前无法支撑这样顺序,但是以发展的眼光来看将来未必不能通过重新实现传输层来实现。另外,我们也期待AKKA提供其他诸如UDP和SCTP等底层通信协议来提高吞吐率和降低延迟。虽然这些协议会降低消息的可靠性,但是能够使得用户根据自己的场景来权衡消息可靠性和性能,并作出相应的选择。
更高层次的抽象
基于akka内核内置的小而连续的工具,akka也支持更强大,更高层次的抽象。
消息模式
前面讨论了可靠传输直接应答采用的是ACK重试协议,这种协议至少需要实现下面几点:
1.从一组相关信息中能鉴别某个消息的确认信息
2.如果没有及时收到确认信息,需要提供某种重试机制来重发消息。
3.接收端能检测并丢失重复消息。
由于确认消息不能保证一定到达接收端,因此第3点也是必不可少的。业务级确认信息的ACK重试由akka的持久模块中的“至少传输一次”来支持。通过跟踪消息的身份信息能够识别消息的重复。另一种第三方的实现方法是通过检测业务逻辑信息的幂等性来检测重复消息。另一个以上3点都实现的例子可以从可靠性代理模式中看到。
事件源
事件源使得网页容纳的用户规模可以扩张到几十亿。其实这个想法非常简单:一个组件处理一条命令时,它将会产生一系统代表命令结果的事件。这些事件除了应用到组件的状态之外,还被存储起来。这种方案好的一面是事件可以顺序的存储到容器中,而且不会产生突变。这使得这些事件流的消费者可以完美的复制和扩展。如果由于机器错误或者内存丢失,导致组件的状态丢失了,通过重新执行事件流可以重新恢复组件的状态。akka的事件源由持久模块支持。
带有明确应答信息的邮箱
通过实现客户端的邮箱类型,可以使得消息接收actor为了处理暂时性错误而能重试消息处理。这种模式在确定传输的本地通信环境中尤为有用。如果没有这种机制,应用的性能需求可能得不到满足。请注意上面提到的“本地JVM消息通信规则”的注意事项。
死信消息
当消息不能投递到目标actor时,它将会投递到一个叫做死信actor的虚拟actor。虽然死信会尽量投递,但是即使是在本地jvm上也不保证一定能投递成功,更不要说通过不可靠的远程网络传输了。
死信的作用
死信最大的作用就是用来调试,尤其是发送actor发送到某个actor的消息一直失败时,那么死信可以告诉这条发送actor到接收actor之间的通道上的哪个地方肯定设置错误了。为了更合理的使用这一目的,尽量不要发送死信消息,另外在应用中把及时把死信消息打印到日志中。但是最重要的一点是:尽量不要发送消息给已经不工作的actor,虽然这样的判断会使代码在一定程度上更复杂一些,但是却比通过死信debug输出要简单。
如何接收死信消息
actor可以在事件流中订阅akka.actor.deadLetters类。被订阅的死信actor可以接收到本地actor系统上所有的死信消息。死信消息不会再网络上传播,如果你想再一个地方收集整个集群的死信消息,你要在每个网络节点上订阅一个死信actor,并且通过它手动的转发死信消息。
哪些死信消息不用担心
如果一个actor不是自己正常停止工作,那么它发送给自己的消息就有可能丢失。这个场景在系统启动或shutdown非常复杂的情况下特别容易发生。如果看到akka.dispatch.Terminate 消息丢失了,那么说明这个停止消息发送了2遍,当然只有1条成功了。同样的,如果父actor要终止它下面的所有子actor,当父actor已经终止了,但是还在监护子actor时,你可能会收到来自子actor的转变为死信的akka.actor.Terminated消息。上面的几种死信可以忽略,而且一般也不会经常发生。