本章概述了监督的原语语义和背后的概念。而如何转化为实际代码,请参阅相应的章节Scala和Java api。
主管的含义是什么
正如前文所言在Actor系统中主管描述了Actor之间的依赖关系:主管把任务委托给下属,因此必须响应它们的故障。在下属检测到故障时(即抛出异常)时它会暂停自已和它的下属并向主管发送单一的故障消息。按被监管的作用类别和故障类型,主管有四种选项:
- 恢复下属,保持其积累的内部状态;
- 重新启动下属,清理其积累的内部状态;
- 永久停止下属;
- 提升故障,使自已产生故障。
把Actor视为一个主管层次的一部分很重要,这也揭示了第四项存在的意义(做为一个主管同样隶属于级别更高的主管)及对前三项的影响:恢复Actor就会恢复它的所有下属,重启时也就重启它的所有下属(下面详细说明),同样停止也就停止它和所有下属。应该注意的是,默认行为preRestart
会使Actror类所有子集时在重新启动之前终止,不过它是可以覆盖。在这个钩子执行后,递归重启所有的子集。
每个主管都配置了一个函数将所有可能的故障原因(即异常)转换成上面给出的四个选项之一。值得注意的是,这个函数并不需要失败Actor的身份作为输入。很容易想出例子的结构,这看起来可能不足够灵活,比如希望策略适用于不同的下属。主管形成一个递归的故障处理结构这一点上是至关重要的。如果你在一个层级上试图做太多,它将成为难以思考,因此在这种情况下,推荐添加一个主管的层级解决这样问题。
Akka实现特定的形式称为“父级的监督”。Actor仅能被更高级的Actor创建,且被它所监管。这个限制使得Actor的形成层次隐式监督,鼓励正确的设计。应该注意的是,这也保证Actor不能孤立或从外部监管,否则可能会捕捉不到意料之外的内容。此外,这也会清除和停止Actor的子树。
警告
主管关联父子通信发生时通过特殊的系统消息,这些消息与用户消息分离使用自已的邮箱。这就使主管的相关的事件与普通消息相比存在不确定性。一般来说用户不能影响普通消息的顺序与故障通知。详细的讨论和例子看到:消息排序部分。
顶级主管
Actor系统在启动创建三个Actor,看上图。更多的信息见Actor路径影响见Actor路径高级范围。
/user: 警卫Actor
所有用户创建的Actor都受一个父Actor影响,这个警卫Actor命名为“/user”。这个Actor的子集用 system.actorOf() 创建。这就表示当这个警卫Actor终止时系统中所有普通的Actor也将被停止。这也就表示这个警卫的主管策略决定了普通高级Actor被怎么监管。从Akka 2.1可以配置使用设置akka.actor.guardian-supervisor-strategy,它的完整类名是SupervisorStrategyConfigurator
。当警卫提升故障,根警卫的响应将是终止警卫,这会停止整个Actor系统。
/system:系统警卫
这个特别的警卫是为了实现有序关闭序列日志,在所有普通Actor终止时日志仍是活跃的,尽管日志是由Actor使用。这实现了通过系统警卫观测用户警卫并实现初始化在收到终止消息时自我关闭。顶级Actor采用的策略在子集出现问题时无限重启除了抛出 ActorInitializationException和ActorKilledException
外的所有异常. 其它可扔出的升级时,关闭整个Actor系统。
/ :根警卫
根警卫是祖父级、所谓的顶级Actor和主管所有在Actor路径的顶级范围使用SupervisorStrategy.stoppingStrategy
,它的目的是终止所有子集的所有异常。所有可抛出的都会被升级,但交给谁?由于每个真实的Actor都有个主管,而根警卫的主管不是一个真实的Actor。因为它属于“域外虚化”,因此被称为“虚化穿越者”;一个综合的实体ActorRef
会在它的子集出现麻烦时关闭,并设置Actor系统isTerminated
状态为true然后根警卫将完全终止(所有子集递归停止)。
重启表示什么
当一个Actor处理时产生的故障分为三类:
- 收到体系内(如,程序上的)特定错误消息;
- 处理消息时(瞬间)产生的外部资源故障;
- 有误的Actor内部状态。
除非故障明确地可辨认,否则第三方引起内部状态将被强制的被清除。假如主管认为它的子集或自身没有受到错误的影响,比如由应用程序的内核引起,因此最好选择重启子集。由通过创建底层的Actor实例实现,替换其中的错误实例并刷新了子集的ActorRef
;这样做的目的是通过指定的引用封装Actor。新的Actor恢复处理它的邮件,这就表示重启对外部的Actor不可见且消息处理时产生的明显的异常不会再次处理。
重启期间精确的事件序列如下:
- 挂起Actor(这就使它在恢复前不会处理普通的消息),并且递归挂起它的子集;
- 回调旧实例的
preRestart
(默认向所有子集发送终止请求并调用postStop
); - 在
preRestart
时到最终停止时等待所有被要求停止的子集(使用context.stop()
);像所有的Actor操作那样,这是非阻塞的,最后被杀死的子对象的终止提示会进行下一步的处理; - 用最初提供的工厂再一次创建Actor新实例;
- 在新实例上调用
postRestart
(默认时称为preStart
); - 向所有没有在第三步被杀死的子集发送重启请求;重启的子集会用同样的方法从第2步启递归调用。
- 恢复Actor。
生命周期监测是什么意思
注意
在Akka中生命周期监测被称为“临终看护”
当上文描述的特殊的父子关系相反,每一个Actor都可能其它的Actor。不管Actor从创建到完整的生存期还是受主管影响下不可见的对外重启,从生存到死亡仅仅是状态能被监控到。监控用来绑定一个Actor到另一个上以便响应其它Actor的终止,这与主管的响应故障不同。
生命周期监控是通过监控Actor对Terminated
消息的接收而实现,它的默认行为是在没有其它处理方式抛出DeathPactException
。通过调用ActorContext.watch(targetActorRef)
开启对Terminated
消息的监听。调用ActorContext.unwatch(targetActorRef)
停止监听。有一个重要的特性是消息在监控请求与终止发生时被收到的次序无关,比如,在受关注的目标已经死时仍然会收到消息事件。
在主管不能简单的重启或终止它的子集时监控特别管用。例如,在Actor初始化时产生错误,在这种情况下监控子集或重建它们或安排它在未来某个时间重试。
另一个常见的用例是Actor在没有外部资源的情况下需要失败,这可能也是自身的子集。如果第三方终止子集通过system.stop(child)
方法或发送PoisonPill
,主管可能会受到影响。
用BackoffSupervisor
模式推迟重启
内建的akka.pattern.BackoffSupervisor
实现了所谓指数补偿主管策略,即在启动子Actor失败后下一次间隔更长的时间重启。
这种模式在Actor启动失败时非常有用【1】,因为一些外部资源无效,而我们需要给它一些时间再次启动。比如说 PersistentActor
在做持久化(停止)时失败,原因是数据库停止或超载,在这种情况下最合适的做方法是给它一些时候恢复在持久化的Actor运行前。
【1】有两种方法引起故障,在Actor停止和崩溃。
下例Scala代码显示了如何建立一个补偿主管在Actor故障后启动,增加间隔从3,6,12,24直到30秒。
1. val childProps = Props(classOf[EchoActor])
2.
3.val supervisor = BackoffSupervisor.props(
4. Backoff.onFailure(
5. childProps,
6. childName = "myEcho",
7. minBackoff = 3.seconds,
8. maxBackoff = 30.seconds,
9. randomFactor = 0.2 // adds 20% "noise" to vary the intervals slightly
10. ))
11.
12.system.actorOf(supervisor, name = "echoSupervisor")
相对应的JAVA代码是
1. import scala.concurrent.duration.Duration;
1. final Props childProps = Props.create(EchoActor.class);
2.
3. final Props supervisorProps = BackoffSupervisor.props(
4. Backoff.onStop(
5. childProps,
6. "myEcho",
7. Duration.create(3, TimeUnit.SECONDS),
8. Duration.create(30, TimeUnit.SECONDS),
9. 0.2)); // adds 20% "noise" to vary the intervals slightly
10.
11. system.actorOf(supervisorProps, "echoSupervisor");
强烈建议用randomFactor
增加些额外的补偿方差,以避免在多个Actor精确的在同一个时刻重启。如果他们因为一个共享资源如数据库停上又配置了同样的间隔。通过添加随机数使它们启动时不在同一个时点,避免从刚恢复的数据库或其它资源上有大流量的访问。
akka.pattern.BackoffSupervisor
能被配置为延时启动,当Actor崩溃或主管决定它需要重启。
下例Scala代码显示了如何建立一个补偿主管在Actor故障后启动,增加间隔从3,6,12,24直到30秒
1. final Props childProps = Props.create(EchoActor.class);
2.
3.final Props supervisorProps = BackoffSupervisor.props(
4. Backoff.onFailure(
5. childProps,
6. "myEcho",
7. Duration.create(3, TimeUnit.SECONDS),
8. Duration.create(30, TimeUnit.SECONDS),
9. 0.2)); // adds 20% "noise" to vary the intervals slightly
10.
11.system.actorOf(supervisorProps, "echoSupervisor");
akka.pattern.BackoffOptions
被用在定义补偿主管的自定义行为,如下所示:
1. val supervisor = BackoffSupervisor.props(
2. Backoff.onStop(
3. childProps,
4. childName = "myEcho",
5. minBackoff = 3.seconds,
6. maxBackoff = 30.seconds,
7. randomFactor = 0.2 // adds 20% "noise" to vary the intervals slightly
8. ).withManualReset // the child must send BackoffSupervisor.Reset to its parent
9. .withDefaultStoppingStrategy // Stop at any Exception thrown
10. )
上面的代码设置了一个补偿主这需要子Actor 发送一个akka.pattern.BackoffSupervisor.Reset
消息到父级在消息成功处理后并重置补偿。他也使用默认的停止策略,任何异常都会引起子集的停止。
1. val supervisor = BackoffSupervisor.props(
2. Backoff.onFailure(
3. childProps,
4. childName = "myEcho",
5. minBackoff = 3.seconds,
6. maxBackoff = 30.seconds,
7. randomFactor = 0.2 // adds 20% "noise" to vary the intervals slightly
8. ).withAutoReset(10.seconds) // the child must send BackoffSupervisor.Reset to its parent
9. .withSupervisorStrategy(
10. OneForOneStrategy() {
11. case _: MyException => SupervisorStrategy.Restart
12. case _ => SupervisorStrategy.Escalate
13. }))
上面的代码设置了一个补偿主管在补偿后在抛出MyException后重置子集,其它异常会提升错误等级。子集在没有抛出异常10秒后补偿会自动重置。
一对一策略和全对一策略
Akka中有两类主管策略:OneForOneStrategy
和AllForOneStrategy
。两者都配置了从异常类型到主管指令(见上文)的映射并限制的子集在终止前失败的频率。不同的是前者获得的指令只适用于失败的子集,后者适用于它和它的兄弟节点。默认采用OneForOneStrategy
,这是一个非显性的声明。
AllForOneStrategy
适用于全体子集有相当紧密的联系,即一个故障会影响其它功能。因为重启没有清理邮箱,最好在子集出现错误后停止并由主管明确的重建它们(通过观察生命周期);否则必须确保在重启前处理以后的从任一Actor接收到入队列的消息是正确的。
通常停止一个子节点(即不是响应失败)在全对一策略中不会终止其它子节点,这可通过它们的观察生命周期。如果Terminated
消息不是由主管发起,它会抛出一个DeathPactException
异常(由主管决定)重启它。默认preRestart
的行为会停止所有子集。当然,这也可以显式地处理。
请注意创建一次性的Actor从全对一的主管需要通过临时Actor提升故障会有永久影响。如果不想发生这种情况,安装一个中间的主管,并声明路由大小为1。