Actor模型探索
最后更新于:2022-04-01 19:46:57
### Akka是什么
Akka就是为了改变编写高容错性和强可扩展性的并发程序而生的。通过使用Actor模型我们提升了抽象级别,为构建正确的可扩展并发应用提供了一个更好的平台。在容错性方面我们采取了“let it crash”(让它崩溃)模型,人们已经将这种模型用在了电信行业,构建出“自愈合”的应用和永不停机的系统,取得了巨大成功。Actor还为透明的分布式系统以及真正的可扩展高容错应用的基础进行了抽象。
Akka是JVM(JAVA虚拟机,下同)平台上构建高并发、分布式和容错应用的工具包和运行时。Akka用 Scala语言写成,同时提供了Scala和JAVA的开发接口。
Akka处理并发的方法基于Actor模型。在基于Actor的系统里,所有的事物都是Actor,就好像在面向对象设计里面所有的事物都是对象一样。但是有一个重要区别——那就是Actor模型是作为一个并发模型设计和架构的,而面向对象模式则不是。更具体一点,在Scala的Actor系统里,Actor互相交互并共享信息但并不对交互顺序作出预设。Actor之间共享信息和发起任务的机制是消息传递。
Akka在多个Actor和下面的系统之间建立了一个层次(Layer),这样一来,Actor只需要处理消息就可以了。**创建和调度线程、接收和分发消息以及处理竞态条件和同步的所有复杂性,都委托给框架,框架的处理对应用来说是透明的。**
### Akka混合模型特点
> - Actors
Actors为你提供:
对并发/并行程序的简单的、高级别的抽象。
异步、非阻塞、高性能的事件驱动编程模型(代码可以异步处理请求并采用独占的方式执行非阻塞操作)。
非常轻量的事件驱动处理(1G内存可容纳约270万个Actors)。
> - 容错性
使用“let-it-crash”语义和监管者树形结构来实现容错。非常适合编写永不停机、自愈合的高容错系统。监管者树形结构可以跨多个JVM来提供真正的高容错系统。
> - 位置透明性
Akka的所有元素都为分布式环境而设计:所有Actor都仅通过发送消息进行互操作,所有操作都是异步的。
> - 可伸缩性
在Akka里,不修改代码就增加节点是可能的,感谢消息传递和位置透明性(location transparency)。
> - 高弹性
任何应用都会碰到错误并在某个时间点失败。Akka的“监管”(容错)策略为实现自愈系统提供了便利。
> - 响应式应用
今天的高性能和快速响应应用需要对用户快速反馈,因此对于事件的响应需要非常及时。Akka的非阻塞、基于消息的策略可以帮助达成这个目标。
> - 事务性Actors
事务性Actor是Actor与STM(Software Transactional Memory)的组合。它使你能够使用自动重试和回滚来组合出原子消息流。
### Actor系统
**Actor本质上就是接收消息并采取行动处理消息的对象。它从消息源中解耦出来,只负责正确识别接收到的消息类型,并采取相应的行动。**
Actor是封装状态和行为的对象,他们的唯一通讯方式是交换消息,交换的消息存放在接收方的邮箱里。从某种意义上来说,Actor是面向对象的最严格的形式,但是最后把它们看成一些人:在使用Actor来对解决方案建模时,把Actor想象成一群人,把子任务分配给他们,将他们的功能整理成一个有组织的结构,考虑如何将失败逐级上传。这样的结果就可以在脑中形成进行软件实现的框架。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-22_56ca7f4210b71.png "")
### 树形结构
程序中负责某一个功能的Actor可能需要把它的任务分拆成更小的、更易管理的部分。为此它启动子Actor并监管它们。每个Actor有且仅有一个监管者,就是创建它的那个Actor。
**Actor系统的精髓在于任务被分拆开来并进行委托,直到任务小到可以被完整地进行处理。**这样做不仅使任务本身被清晰地划分出结构,而且最终的Actor也能按照它们“应该处理的消息类型”,“如何完成正常流程的处理”以及“失败流程应如何处理”来进行解析。如果一个Actor对某种状况无法进行处理,它会发送相应的失败消息给它的监管者请求帮助。这样的递归结构使得失败能够在正确的层次进行处理。
可以将这与分层的设计方法进行比较。分层的设计方法最终很容易形成防护性编程,以防止任何失败被泄露出来。把问题交由正确的人处理会是比将所有的事情“藏在深处”更好的解决方案。
现在,设计这种系统的难度在于如何决定谁应该监管什么。这当然没有一个唯一的最佳方案,但是有一些可能会有帮助的原则:
> - 如果一个Actor管理另一个Actor所做的工作,如分配一个子任务,那么父Actor应该监督子Actor,原因是父Actor知道可能会出现哪些失败情况,知道如何处理它们。
> - 如果一个Actor携带着重要数据(i.e. 它的状态要尽可能地不被丢失),这个Actor应该将任何可能的危险子任务分配给它所监管的子Actor,并酌情处理子任务的失败。视请求的性质,可能最好是为每一个请求创建一个子Actor,这样能简化收集回应时的状态管理。这在Erlang中被称为“Error Kernel Pattern”。
> - 如果Actor A需要依赖Actor B才能完成它的任务,A应该观测B的存活状态并对收到B的终止提醒消息进行响应。这与监管机制不同,因为观测方对监管机制没有影响,需要指出的是,仅仅是功能上的依赖并不足以用来决定是否在树形监管体系中添加子Actor.
### 配置容器
多个Actor协作的Actor系统是管理如日程计划服务、配置文件、日志等共享设施的自然单元。使用不同的配置的多个Actor系统可以在同一个jvm中共存。Akka自身没有全局共享的状态。将这与Actor系统之间的透明通讯(在同一节点上或者跨网络连接的多个节点)结合,可以看到Actor系统本身可以被作为功能层次中的积木构件。
### Actor实践
1. Actor们应该被视为非常友好的同事:高效地完成他们的工作而不会无必要地打扰其它人,也不会争抢资源。转换到编程里这意味着以事件驱动的方式来处理事件并生成响应(或更多的请求)。Actor不应该因为某一个外部实体而阻塞(i.e.占据一个线程又被动等待),这个外部实体可能是一个锁、一个网络socket等等。阻塞操作应该在某些特殊的线程里完成,这个线程发送消息给可处理这些消息的Actor们。
1. 不要在Actor之间传递可变对象。为了保证这一点,尽量使用不变量消息。如果Actor将他们的可变状态暴露给外界,打破了封装,你又回到了普通的Java并发领域并遭遇所有其缺点。
1. Actor是行为和状态的容器,接受这一点意味着不要在消息中传递行为(例如在消息中使用scala闭包)。有一个风险是意外地在Actor之间共享了可变状态,而与Actor模型的这种冲突将破坏使Actor编程成为良好体验的所有属性。
### Akka中的Actor模型
使用Actor就像租车——我们如果需要,可以快速便捷地租到一辆;如果车辆发生故障,也不需要自己修理,直接打电话给租车公司更换另外一辆即可。
Actor模型是一种适用性非常好的通用并发编程模型。它可以应用于共享内存架构和分布式内存架构,适合解决地理分布型的问题。同时它还能提供很好的容错性。
**一个Actor是一个容器,它包含了 状态,行为,一个邮箱,子Actor和一个监管策略。所有这些包含在一个Actor Reference里。**
### Actor引用
Actor是以Actor引用的形式展现给外界的,Actor引用可以被自由的无限制地传来传去。内部对象和外部对象的这种划分使得所有想要的操作能够透明:重启Actor而不需要更新别处的引用,将实际Actor对象放置到远程主机上,向另外一个应用程序发送消息。但最重要的方面是从外界不可能到Actor对象的内部获取它的状态,除非这个Actor非常不明智地将信息公布出去。
### 状态
Actor对象通常包含一些变量来反映Actor所处的可能状态。这可能是一个明确的状态机(e.g. 使用 FSM 模块),或是一个计数器,一组监听器,待处理的请求,等等。这些数据使得Actor有价值,并且必须将这些数据保护起来不被其它的Actor所破坏。好消息是在概念上每个Akka Actor都有它自己的轻量线程,这个线程是完全与系统其它部分隔离的。这意味着你不需要使用锁来进行资源同步,可以完全不必担心并发性地来编写你的Actor代码。
在幕后,Akka会在一组线程上运行一组Actor,通常是很多Actor共享一个线程,对某一个Actor的调用可能会在不同的线程上进行处理。Akka保证这个实现细节不影响处理Actor状态的单线程性。
由于内部状态对于Actor的操作是至关重要的,所以状态不一致是致命的。当Actor失败并由其监管者重新启动,状态会进行重新创建,就象第一次创建这个Actor一样。这是为了实现系统的“自愈合”。
### 行为
每次当一个消息被处理时,消息会与Actor的当前的行为进行匹配。行为是一个函数,它定义了处理当前消息所要采取的动作,例如如果客户已经授权过了,那么就对请求进行处理,否则拒绝请求。这个行为可能随着时间而改变,例如由于不同的客户在不同的时间获得授权,或是由于Actor进入了“非服务”模式,之后又变回来。这些变化要么通过将它们放进从行为逻辑中读取的状态变量中实现,要么函数本身在运行时被替换出来,见become 和 unbecome操作。但是Actor对象在创建时所定义的初始行为是特殊的,因为当Actor重启时会恢复这个初始行为。
### 邮箱
Actor的用途是处理消息,这些消息是从其它的Actor(或者从Actor系统外部)发送过来的。连接发送者与接收者的纽带是Actor的邮箱:每个Actor有且仅有一个邮箱,所有的发来的消息都在邮箱里排队。排队按照发送操作的时间顺序来进行,这意味着从不同的Actor发来的消息在运行时没有一个固定的顺序,这是由于Actor分布在不同的线程中。从另一个角度讲,从同一个Actor发送多个消息到相同的Actor,则消息会按发送的顺序排队。
可以有不同的邮箱实现供选择,缺省的是FIFO:Actor处理消息的顺序与消息入队列的顺序一致。这通常是一个好的选择,但是应用可能需要对某些消息进行优先处理。在这种情况下,可以使用优先邮箱来根据消息优先级将消息放在某个指定的位置,甚至可能是队列头,而不是队列末尾。如果使用这样的队列,消息的处理顺序是由队列的算法决定的,而不是FIFO。
Akka与其它Actor模型实现的一个重要差别在于当前的行为必须处理下一个从队列中取出的消息,Akka不会去扫描邮箱来找到下一个匹配的消息。无法处理某个消息通常是作为失败情况进行处理,除非Actor覆盖了这个行为。
### 子Actor
每个Actor都是一个潜在的监管者:如果它创建了子Actor来委托处理子任务,它会自动地监管它们。子Actor列表维护在Actor的上下文中,Actor可以访问它。对列表的更改是通过创建(tt class=”docutils literal”>context.ActorOf(…))或者停止(context.stop(child))子Actor来实现,并且这些更改会立刻生效。实际的创建和停止操作在幕后以异步的方式完成,这样它们就不会“阻塞”其监管者。
### 监管策略
Actor的最后一部分是它用来处理其子Actor错误状况的机制。错误处理是由Akka透明地进行处理的,将监管与监控中所描述的策略中的一个应用于每个出现的失败。由于策略是Actor系统组织结构的基础,所以一旦Actor被创建了它就不能被修改。
考虑对每个Actor只有唯一的策略,这意味着如果一个Actor的子Actor们应用了不同的策略,这些子Actor应该按照相同的策略来进行分组,生成中间的监管者,又一次倾向于根据任务到子任务的划分来组织Actor系统的结构。
### Actor终止
一旦一个Actor终止了,i.e. 失败了并且不能用重启来解决,停止它自己或者被它的监管者停止,它会释放它的资源,将它邮箱中所有未处理的消息放进系统的“死信邮箱”。而Actor引用中的邮箱将会被一个系统邮箱所替代,系统邮箱会将所有新的消息重定向到“排水沟”。 但是这些操作只是尽力而为,所以不能依赖它来实现“保证投递”。
不是简单地把(未处理的:译者注)消息扔掉的想法来源于我们(Akka:译者注)测试:我们在事件总线上注册了TestEventListener来接收死信,然后将每个收到的死信在日志中生成一条警告。这对于更快地解析测试失败非常有帮助。我们觉得可能这个功能也可以用于其它的目的。
### 参考资料
[让并发和容错更容易:Akka示例教程](http://www.csdn.net/article/2014-12-17/2823174)
[Akka 2.0官方文档中文版](http://www.gtan.com/akka_doc/)
**转载请注明作者Jason Ding及其出处**
[GitCafe博客主页(http://jasonding1354.gitcafe.io/)](http://jasonding1354.gitcafe.io/)
[Github博客主页(http://jasonding1354.github.io/)](http://jasonding1354.github.io/)
[CSDN博客(http://blog.csdn.net/jasonding1354)](http://blog.csdn.net/jasonding1354)
[简书主页(http://www.jianshu.com/users/2bd9b48f6ea8/latest_articles)](http://www.jianshu.com/users/2bd9b48f6ea8/latest_articles)
**Google搜索jasonding1354进入我的博客主页**
';