翻译自:http://doc.akka.io/docs/akka/2.3.8/general/actor-systems.html
Actor 系统
Actor 是封装状态和行为的对象,他们的唯一通讯方式是交换消息,交换的消息存放在接收方的邮箱里。从某种意义上来说,actor 是面向对象编程的最严格的形式,但是最好把它们看作一些人:在使用 actor 来建模解决方案时,把 actor 想象成一群人,把子任务分配给他们,将他们的功能整理成一个有组织的结构,考虑如何将失败逐级上传(受益于不实际与人交互,意思是我们不需要担心他们的情绪状态或精神问题)。这样的结果就可以在脑中形成进行软件实现的框架。这个结果可以作为构建软件实现的脚手架。
注意:一个
ActorSystem
是一个重量级结构,会分配1....N
个线程,所以每个逻辑应用创建一个即可。
分层的结构
像在一个经济组织里,actors 天生地形成层级。一个 actor,整天上看是程序的一个功能,可能想把它的任务分割成更小的,更容易管理的分块。为了这个目的,它启动由它督导(supervise)的子 actors。督导的细节将在这里 解释,我们先专注于本章的底层概念。唯一的先决条件是知道每个 actor 有且只有一个督导者,也就是创建它的 actor。
actor 系统的精髓是任务被分割和委托,直到足够小可以完整地处理。按这样做,不止任务本身被清晰地分出结构,而且使 actors 对它们应该处理什么消息、如何响应(react)和如何处理失败 更合理(译注:按这个三个角度来组织)。如果一个 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 最佳实践
-
Actor 们应该被视为非常友好的同事:高效地完成他们的工作而不会无必要地打扰其它人,也不会争抢资源。转换到编程里这意味着以事件驱动的方式来处理事件并生成响应(或更多的请求)。Actor 不应该因为某一个外部实体而阻塞(i.e. 占据一个线程又被动等待),这个外部实体可能是一个锁、一个网络 socket 等等,除非无法避免。更好的例子见下面。
-
不要在 actor 之间传递可变对象。为了保证这一点,尽量使用不变量消息。如果 actor 将他们的可变状态暴露给外界,打破了封装,你又回到了普通的 Java 并发领域并遭遇所有其缺点。
-
Actor 是行为和状态的容器,接受这一点意味着不要在消息中传递行为(例如在消息中使用 scala 闭包)。有一个风险是意外地在 actor 之间共享了可变状态,而与 actor 模型的这种冲突将破坏使 actor 编程成为良好体验的所有属性。
-
顶层 acotrs 是你的 Error Kernal 的最深处的部分,所以谨慎地创建它们,尽量形成真正的分层结构。这将有益于错误处理(综合考虑配置和性能的粒度),也会减少 guardian actor 上的负担,过度使用它会变成一个竞争的单点。
阻塞需要仔细管理
在某些情况下是没法避免阻塞操作的,比如让某个线程休眠一段不确定的时间,等待一个外部事件发生。例子如,遗留的 RDBMS 驱动或消息 API,根本的原因一般是幕后发生了(网络) I/O 。当面对这些时,你可能尝试仅仅把阻塞调用包装在 Future 里,然后用它工作。但这个方式过于简单了:当应用的负载增加时,你很可能发现瓶颈、或内存溢出、或线程不够。
一个不完整的足以解决“阻塞问题”的清单包括下面的建议:
- 在一个 actor(或一组被某个 router 管理的 actors)里做阻塞调用,确保配置一个线程池专门用于这个目的或有足够的数量。
- 在 Future 里做阻塞调用,保证在任意时候这样的调用数量有个上限(提交无限制数量的这类任务将耗尽你的内存或线程)。
- 在 Future 里做阻塞调用,提供一个有上限限制的线程池,线程数量适合于应用所运行在的硬件。
- 共献一个线程来管理一组阻塞的资源(比如一个 IO selector 驱动多个 channels),当事件发生时作为 actor 消息来分发。
第一个可能非常适合于资源天生是单线程的,例如数据库处理,传统上一次只能执行一个未解决的查询,使用内部同步来保证这个。一个通常的模式是创建一个 router 对应 N 个 actors,每个包装一个 DB 连接来处理发送给 router 的查询。数量 N 必须为最大吞吐量调整,吞吐量取决于部署在不同硬件上的哪种 DBMS。
注意:配置线程池最好委托给 Akka,在
appliaction.conf
里简单地配置,然后通过ActorSystem
实例化。
你不应该自己担忧的东西
actor 系统管理配置给它使用的资源来运行它包含的 actors。在一个这样的系统里可能存在上百万的 actors ,毕竟 mantra 视它们是充裕的,它们的开销是每个实例约 300 字节。自然地,消息在一个大系统里被处理的精确顺序对应用作者是不可控的,但这也不是刻意的。退一步并放松下,Akka 在幕后做着重活。
欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。