actor 系统

Actor是封装状态和行为的对象,他们的唯一通讯方式是交换消息,交换的消息存放在接收方的邮箱里。从某种意义上来说,actor是面向对象的最严格的形式,可以把它们看成一群人:在使用actor来对解决方案建模时,把actor想象成一群人,把子任务分配给他们, 将他们的功能整理成一个有组织的结构,考虑如何将失败逐级上传。结果是你可以在脑中形成构建软件实现的框架。

树形结构

如一个经济组织一样,actor自然形成树形结构。程序中负责某一个功能的actor可能需要把它的任务分拆成更小的、更易管理的部分。为此它启动子Actor并监管它们。虽然监督机制的细节将在[这里]解释,我们在这一节里将集中讲述其中的基础概念,唯一的前提是我们要知道每个actor有且仅有一个监管者,就是创建它的那个actor。

Actor 系统的精髓在于任务被分拆开来并委托,直到任务小到可以被完整地进行处理。这样做不仅使任务本身被清晰地划分出结构,而且最终的actor也能按照它们“应该处理的消息类型”,“如何完成正常流程的处理”以及“失败流程应如何处理”来进行解析。如果一个actor对某种状况无法进行处理,它会发送相应的失败消息给它的监管者请求帮助。这样的递归结构使得失败能够在正确的层级进行处理。

可以将该方法与分层的软件设计方法进行比较。分层的软件设计方法很容易形成防护性编程,以防止任何失败被泄露出来。把问题交由正确的人处理会是比将所有的事情“藏在深处”更好的解决方案。

现在,设计这种系统的难度在于决定谁应该监管什么。这当然没有唯一的最佳方案,但是有一些可能会有帮助的原则:

  • 如果一个actort管理另一个actor所做的工作,如分配一个子任务,那么父actor应该监督子actor,原因是父actor知道可能会出现哪些失败情况,知道如何处理它们。
  • 如果一个actor携带着重要数据(如它的状态要尽可能地不被丢失),这个actor应该将任何可能的危险子任务分配给它所监管的子actor,并酌情处理子任务的失败。依据请求的性质,最好的方法可能是为每一个请求创建一个子actor。这样能简化收集回复时的状态管理。这在Erlang中被称为“Error Kernel Pattern”。
  • 如果一个actor需要依赖另一个actor才能完成它的任务,这个actor应该观测另一个actor的存活状态并对收到的另一个actor的终止提醒消息进行响应。这与监管机制不同,因为观测方对监管机制没有影响,需要指出的是,仅仅是功能上的依赖并不足以用来决定是否在树形监管体系中添加子actor。

当然以上的规则都会有例外,但是无论你遵循这些规则或者打破它们,都需要有足够的理由。

配置容器

多个actor协作的actor系统是管理如日程计划服务、配置文件、日志等共享设施的固有的单元。使用不同配置的多个actor系统可能在同一个jvm中共存,Akka自身没有全局共享的状态。将这与actor系统之间的透明通讯(在同一节点上或者跨网络连接的多个节点)结合,可以看到actor系统本身可以被当作功能层次中的构件模块。

Actor最佳实践

  • Actor们应该被视为非常友好的合作者:高效地完成他们的工作而不会无必要地打扰其它人,也不会争抢资源。转换到编程里这意味着以事件驱动的方式来处理事件并生成响应(或更多的请求)。Actor不应该因为某一个外部实体而阻塞(如占据一个线程又被动等待),这个外部实体可能是一个锁、一个网络socket等等。阻塞操作应该在某些特殊的线程里完成,这个线程发送消息给可处理这些消息的actors。
  • 不要在actor之间传递可变对象。为了保证这一点,尽量使用不变量消息。如果actor将他们的可变状态暴露给外界,打破了封装,你又回到了普通的Java并发领域并遭遇所有其缺点。
  • Actor是行为和状态的容器,接受这一点意味着不要在消息中传递行为(例如在消息中使用scala闭包)。有一个风险是意外地在actor之间共享了可变状态,而与actor模型的这种冲突将破坏使actor编程成为良好体验的所有属性。
  • Top-level actors是你的Error Kernel的最深的部分。应该尽量少的创建它们,更多的使用分级系统。这对相关的错误处理有好处,也可以减少监控actor的负担。这个actor的过度使用将造成单点竞争。

阻塞需要小心管理

在某些情况下,阻塞操作是不可避免的。例如,一个线程睡眠一个不确定的时间,用来等待外部事件的发生。面对这的时候,你可能仅仅将阻塞调用封装到Future中,然后用Future工作,但是这个策略太简单:你很可能遭遇瓶颈或者超过内存或者应用程序的线程过多。

阻塞问题适当的解决方案的非详细描述有如下几点:

  • 在一个actor内部作阻塞调用,确保配置一个线程池,这个线程池要么专注于该操作,要么拥有足够的大小。
  • 在一个Future中作阻塞调用,确保在某个时间点,该调用的次数要有个上限。
  • 在一个Future中作阻塞调用,提供一个线程数量有上限限制的线程池,这个线程池与应用程序运行的计算机的硬件相适应。
  • 提供一个单独的线程管理阻塞的资源集合。当阻塞发生时,分配这些事件为actor消息。

你不应该担心的事

一个actor系统管理它所配置使用的资源,运行它所包含的actor。 在一个系统中可能有上百万个actor,不用担心,内存一定是够用的,因为每个actor实例仅占差不多300个字节。自然情况下,一个大系统中消息的处理顺序是不受应用的开发者控制的,但这并不是有意为之。让Akka去做幕后的繁重事务吧。