角色是封装了状态与行为的对象,它们通过交换放入接收者信箱的消息实现两两之间的通讯。从某种意义上说,角色是最严格的面向对象编程,不过最好还是把它们当作人来看待:当用角色为一个方案建模时,想象有一群人,并给他们分配了任务,他们在一个组织结构中发挥职能作用,并想象如何做到故障升级(就像在不需要考虑实际利益的情况下与人打交道,也就是说我们不需要关心他们的情绪变化或道德问题)。这样的结果可以充当构建软件的心理脚手架。
注意:一个角色系统是一个会分配1…N个线程的重量级结构,因此为每个逻辑上的应用创建一个角色系统即可。
层次结构
就像在一个经济组织内,角色自然形成了层次结构。一个程序中监控特定功能的角色,可能想把自己的任务分解为更小,更容易管理的片段。基于这一目的,它启动了子角色,并管理它们。本节我们关注基本概念,更多细节在这里。惟一的前提是每个角色都有一个管理者,也就是它的创建者。
角色系统的典型特性是任务的拆分与委派,直到任务拆分的足够小。这样做,不只任务自己有清晰的结构,而且作为结果产生的角色也可以决定它们可以处理以及如何处理哪些消息,还有故障如何解决等等。如何一个角色遇到不能处理的情况,它会向管理者发送一条失败消息,去寻求帮助。这种递归结构允许在合适的级别处理故障。
将这种思想与分层的软件设计比较,后者更容易演变为防御性编程,以开发没有故障的软件为目标:程序如何与正确的对象(译者注:原文为person,但本人认为此处应该是指程序之间或模块之间的交互方式)交互;一个更好的方案是如何发现故障,而不是把一切“都藏在地毯下面”。
现在设计这样一个系统的困难之处在于如何决定谁应该管理什么。当然没有一个最好的方案,但是有一些准则可能会有帮助:
- 如果一个角色管理其它角色的工作,比如通过传递子任务,这个管理者就应当管理子角色。理由是管理者知道会发生哪些故障以及如何处理它们。
- 如果一个角色持有数据(也就是说它的状态要避免丢失),这一角色应当找出它监管的所有子角色处理的任何可能有危险的子任务,并适当处理这些子角色的故障。根据请求的性质,为每个请求创建一个新的子角色可能是最好的方案,这样简化了为收集应答的状态管理。这种模式来自Erlang,被称做错误内核模式。
- 如果一个角色依赖于另一个才能履行它的职责,它应当监视其它角色的活跃度,并在收到终止通知时行动。这与监管不同,监视方对监管策略没有影响,而且应当注意到的是,单纯的功能性依赖不是决定是否要在层次结构的什么位置放置一个子角色的标准。
对于这些规则,当然总有例外;但是不论你应当有充足的理由决定遵守这些规则还是破坏它们。
配置环境
作为一个角色的协作集合的角色系统是管理共享设施的天然单元,比如调度服务、配置、日志等。拥有不同配置的多个角色系统可能无碍的共存于同一个JVM中,在Akka内部没有全局共享状态。还有一点,角色系统之间通讯的透明性——单节点内部或跨网络节点的通讯——可以构建功能层次的模块。
角色最佳实践
- 角色应该像好同事:高效的完成工作而且不打扰他人,避免占用资源。翻译成编程行为就是以事件驱动的方式处理事件生成响应(或更多请求)。除非是不可避免的,否则角色不应被外部的实体阻塞(也就是占用着一个线程的被动等待)——可能是一把锁,一个网络套接字等等。对于不可避免的情况请见下文。
- 不在可变对象之间传递可变对象。为了确保这一点,最好不改变消息。如果角色的封装是因向外暴露自己的可变状态而遭到破坏,你就退回到了通常的所有Java并发编程缺陷的境地。
- 角色是状态和行为的容器,接受这一点意味着不常发送消息内的行为(可能使用Scala闭包很有诱惑力)。风险之一就是不小心就在角色之间共享了可变状态,而这一点违反了角色模型的做法破坏了基于角色编程的良好体验的一切特性。
- 顶级角色处于你的错误内核(Error Kernel)的最深处,所以尽量不要创建它们,喜欢真正的分层系统。这一点对故障处理有好处(同时考虑到配置与性能的粒度),还降低了监护人角色的重要性(译者注:原文没有重要性一词,此处原文为strain,即血统,本人认为这句话原义是指监控护人角色的家族成员数量,后面半句指出过度使用这种角色会形成单点争用),如果过度使用这就形成了单点争用。
阻塞需要仔细的管理
在某些情况下阻塞操作不可避免,也就是令一个线程进入不定时间的休眠,等待一个外部事件发生。传统的RDBMS驱动或消息API就是例子,深层的原因通常是幕后发生了(网络)I/O。面对这一点,你可能倾向于仅仅用Future对象包装这个阻塞调用,并用跟此对象代替直接与IO之间的交互,但是这个策略实在是太简单了:当应用的负载增加时,你很可能会发现瓶颈所在,或耗尽内存,或线程过多。
下面是“阻塞问题”恰当方案的不完全清单:
- 在一个角色(或由路由器管理的角色组【Java,Scala】)内部执行阻塞调用,确保配置一个足够大的线程池专门用于这一目的。
- 在一个Future对象内执行阻塞调用,确保此类调用在任意时间点内的上限(无限制的提交任务会耗尽你的内存或线程数)。
- 在一个Future对象内执行阻塞调用,提供一个线程池,这个线程池的线程上限要与运行应用程序的硬件相符。
- 专门用一个线程管理一组阻塞资源(比如说NIO选择器驱动多个通道),以角色消息的形式调度事件。
第一种方案可能尤其适用于单线程模型,比如数据库句柄传统上一次只能执行一个查询,并使用内部同步方式确保这一点。一个常见的模式是为N个角色创建一个路由器,每个角色包装一个数据库连接,而查询是发送到路由器的。数字N必须是最大吞吐量,而数字大小取决于在什么样的硬件上部署了哪种数据库管理系统(DBMS)。
注意
配置线程池的工作最好委托给AKKA,简单配置在application.conf文件里并通过一个ActorSystem对象实例化【JAVA,Scala】。