监督和监控

本章概述了监督(supervision)背后的概念、提供的原语及其语义。有关如何转换为真实代码的详细信息,请参阅 Scala 和 Java API 的相应章节。

示例项目

你可以查看「监督示例项目」,以了解实际使用的情况。

监督意味着什么?

正如「Actor 系统」中监督所描述的,Actor 之间的依赖关系是:supervisor将任务委托给子级,因此必须对其失败作出响应。当子级检测到故障(即抛出异常)时,它会挂起自身及其所有下级,并向其监督者发送一条消息,也就是故障信号。根据监督工作的性质和失败的性质,监督者有以下四种选择:

  • 恢复子级,保持其累积的内部状态
  • 重新启动子级,清除其累积的内部状态
  • 永久停止子级
  • 使失败升级,从而使自己失败(译者说,即继续向上一级监督者发送失败消息

始终将一个 Actor 视为监管层级的一部分是很重要的,这解释了第四个选择的存在(作为一个监督者也从属于上一级的另一个监督者),并对前三个有影响:恢复一个 Actor 恢复其所有子级,重新启动一个 Actor 需要重新启动其所有子级(如需更多详细信息,请参见下文),同样,终止 Actor 也将终止其所有子级。需要注意的是,Actor类的preRestart钩子的默认行为是在重新启动之前终止它的所有子级,但是这个钩子可以被重写;递归重新启动应用于执行这个钩子之后剩下的所有子级。

每个监督者都配置了一个函数,将所有可能的故障原因(即异常)转换为上面给出的四个选项之一;值得注意的是,该函数不将故障 Actor 的身份(identity)作为输入。这样的结构似乎不够灵活,很容易找到类似的例子,例如希望将不同的策略应用于不同的子级。在这一点上,重要的是要理解监督是关于形成一个递归的故障处理结构。如果你试图在一个层面上做的太多,就很难解释了,因此在这种情况下,建议的方法是增加一个层面(level)的监督。

Akka 实现了一种称为“父母监督(parental supervision)”的特殊形式。Actor 只能由其他 Actor 创建,其中顶级 Actor 由库提供,每个创建的 Actor 都由其父 Actor 监督。这种限制使得 Actor 监督层次的形成变得含蓄,并鼓励做出合理的设计决策。应当指出的是,这也保证了 Actor 不能成为孤儿,也不能从外部依附于监管者,否则他们可能会不知不觉地被抓起来(which might otherwise catch them unawares)。此外,这也为 Actor 应用程序(子树,sub-trees of)生成了一个自然、干净的关闭过程。

  • 警告:与监督(supervision)相关的父子通信通过特殊的系统消息进行,这些消息的邮箱与用户消息分开。这意味着与监督相关的事件并不是相对于普通消息确定的顺序。一般来说,用户不能影响正常消息和故障通知的顺序。有关详细信息和示例,请参阅「讨论:消息排序」部分。

顶级监督者

top-level-supervisors

一个 Actor 系统在创建过程中至少会启动三个 Actor,如上图所示。有关 Actor 路径的详细信息,请参阅「Actor 路径的顶级范围」。

  • /user: The Guardian Actor,最可能与之交互的 Actor 是所有用户创建的 Actor 的父级,守护者名为/user。使用system.actorOf()创建的 Actor 是此 Actor 的子级。这意味着当这个守护者终止时,系统中的所有正常 Actor 也将关闭。这也意味着守护者的监管策略决定了顶级正常 Actor 的监督方式。自 Akka 2.1 开始,可以使用akka.actor.guardian-supervisor-strategy来配置它,该设置采用了一个SupervisorStrategyConfigurator的完全限定类名。当守护者升级失败时,根守护者的响应将会终止守护者,这实际上将关闭整个 Actor 系统。
  • /system: The System Guardian,为了实现有序的关闭顺序,引入了这个特殊的守护者,当所有正常的 Actor 都终止,日志记录也保持活动状态,即使日志记录本身也是使用 Actor 实现的。这是通过让系统守护者监视(watch)用户守护者并在接收到Terminated消息时启动自己的关闭来实现的。顶层系统 Actor 使用一种策略进行监督,该策略将在所有类型的Exception(其中,ActorInitializationExceptionActorKilledException除外)上无限期重新启动,这将终止相关的子级。所有其他可抛的异常事件都会升级,这将关闭整个 Actor 系统。
  • /: The Root Guardian,根守护者是所有所谓的“顶级” Actor 的祖父(grand-parent)级,并使用SupervisorStrategy.stoppingStrategy监督「Actor 路径的顶级范围」中提到的所有特殊 Actor,其目的是在任何类型的Exception情况下终止子 Actor。所有其他可抛的异常都会升级……但是给谁?因为每个真正的 Actor 都有一个监督者,所以根守护者的监督者不能是真正的 Actor。因为这意味着它是在“气泡的外面”,所以它被称为“气泡行者(bubble-walker)”。这是一个虚构的ActorRef,它在出现问题的第一个征兆时停止其子系统,并在根守护程序完全终止(所有子系统递归停止)后将 Actor 系统的isTerminated状态设置为true

重启意味着什么?

当与处理特定消息时失败的 Actor 一起出现时,失败的原因分为三类:

  • 接收到特定的系统性(即编程)错误消息
  • 处理消息过程中使用的某些外部资源出现故障
  • Actor 的内部状态已损坏

除非能明确识别故障,否则不能排除第三种原因,这就导致了内部状态需要清除的结论。如果监督者决定其其他子级或本身不受损坏的影响,例如,由于有意识地应用了错误内核模式,因此最好重新启动子级。这是通过创建底层Actor类的新实例并将失败的实例替换为子ActorRef中的新实例来实现的;这样做的能力是将 Actor 封装在特殊引用中的原因之一。然后,新的 Actor 将继续处理其邮箱,这意味着重新启动在 Actor 除本身之外是不可见的,但有一个明显的例外,即发生故障的消息不会被重新处理。

重新启动期间事件的精确顺序如下:

  1. 挂起 Actor(这意味着在恢复之前它不会处理正常消息),并递归挂起所有子级
  2. 调用旧实例的preRestart钩子(默认为向所有子实例发送终止请求并调用postStop
  3. 等待在preRestart期间被请求终止(使用context.stop())的所有子级实际终止;就像所有 Actor 操作都是非阻塞的一样,最后一个被杀死的子级的终止通知将影响到下一步的进展。
  4. 通过再次调用最初提供的工厂来创建新的 Actor 实例
  5. 在新实例上调用postRestart(默认情况下,该实例还调用preStart
  6. 向步骤 3 中未杀死的所有子级发送重新启动请求;从步骤 2 开始,重新启动的子级将递归地执行相同的过程。
  7. 恢复 Actor

生命周期监控意味着什么?

  • 注释:Akka 中的生命周期监控通常被称为DeathWatch

与上面描述的父母和子女之间的特殊关系不同,每个 Actor 可以监视(monitor)任何其他 Actor。由于 Actor 从完全活跃地创造中出现,并且在受影响的监督者之外无法看到重新启动,因此可用于监控的唯一状态更改是从活跃到死亡的过渡。因此,监控(Monitoring)被用来将一个 Actor 与另一个 Actor 联系起来,这样它就可以对另一个 Actor 的终止做出反应,而不是对失败做出反应的监督。

生命周期监控是使用监控 Actor 要接收的Terminated消息来实现的,在该消息中,默认行为是如果不进行其他处理,则抛出一个特殊的DeathPactException。为了开始监听Terminated消息,需要调用ActorContext.watch(targetActorRef)。若要停止监听,则需要调用ActorContext.unwatch(targetActorRef)。一个重要的属性是,不管监控请求和目标终止的顺序如何,消息都将被传递,即使在注册时目标已经死了,你仍然会收到消息。

如果监督者无法重新启动其子级,并且必须终止它们(例如,在 Actor 初始化期间发生错误时),则监控特别有用。在这种情况下,它应该监控这些子级并重新创建它们,或者计划自己在稍后重试。

另一个常见的用例是,Actor 需要在缺少外部资源的情况下失败,外部资源也可能是其自己的子资源之一。如果第三方通过system.stop(child)方法或发送PoisonPill终止子级,则监督者可能会受到影响。

使用 BackoffSupervisor 模式延迟重新启动

作为内置模式提供的akka.pattern.BackoffSupervisor实现了所谓的指数退避监督策略,在失败时再次启动子 Actor,并且每次重新启动之间的时间延迟越来越大。

当启动的 Actor 失败(故障可以用两种不同的方式来表示,通过一个 Actor 停止或崩溃)时,此模式非常有用,因为某些外部资源不可用,我们需要给它一些时间重新启动。一个主要示例是当「PersistentActor」因持久性失败而失败(通过停止)时,这表明数据库可能已关闭或过载,在这种情况下,在启动持久性 Actor 之前给它一点时间来恢复是很有意义的。

下面的 Scala 片段演示了如何创建一个退避监督者,在给定的 EchoActor 因故障停止后,该监督者将以 3、6、12、24 和最后 30 秒的间隔启动:

  1. val childProps = Props(classOf[EchoActor])
  2. val supervisor = BackoffSupervisor.props(
  3. Backoff.onStop(
  4. childProps,
  5. childName = "myEcho",
  6. minBackoff = 3.seconds,
  7. maxBackoff = 30.seconds,
  8. randomFactor = 0.2, // adds 20% "noise" to vary the intervals slightly
  9. maxNrOfRetries = -1
  10. ))
  11. system.actorOf(supervisor, name = "echoSupervisor")

与上述 Scala 代码等价的 Java 代码为:

  1. import java.time.Duration;
  2. final Props childProps = Props.create(EchoActor.class);
  3. final Props supervisorProps =
  4. BackoffSupervisor.props(
  5. Backoff.onStop(
  6. childProps,
  7. "myEcho",
  8. Duration.ofSeconds(3),
  9. Duration.ofSeconds(30),
  10. 0.2)); // adds 20% "noise" to vary the intervals slightly
  11. system.actorOf(supervisorProps, "echoSupervisor");

为了避免多个 Actor 在完全相同的时间点重新启动,例如,由于共享资源(如数据库在相同配置的时间间隔后关闭和重新启动),因此强烈建议使用randomFactor为回退间隔添加一点额外的变化。通过在重新启动间隔中增加额外的随机性,Actor 将在稍微不同的时间点开始,从而避免大流量峰值冲击恢复共享数据库或他们所需的其他资源。

还可以将akka.pattern.BackoffSupervisor Actor 配置为在 Actor 崩溃且监控策略决定应重新启动时,在延迟之后重新启动 Actor。

下面的 Scala 片段演示了如何创建一个退避监督者,在给定的 EchoActor 因某些异常而崩溃后,该监督者将以 3、6、12、24 和最后 30 秒的间隔启动:

  1. val childProps = Props(classOf[EchoActor])
  2. val supervisor = BackoffSupervisor.props(
  3. Backoff.onFailure(
  4. childProps,
  5. childName = "myEcho",
  6. minBackoff = 3.seconds,
  7. maxBackoff = 30.seconds,
  8. randomFactor = 0.2, // adds 20% "noise" to vary the intervals slightly
  9. maxNrOfRetries = -1
  10. ))
  11. system.actorOf(supervisor, name = "echoSupervisor")

与上述 Scala 代码等价的 Java 代码为:

  1. import java.time.Duration;
  2. final Props childProps = Props.create(EchoActor.class);
  3. final Props supervisorProps =
  4. BackoffSupervisor.props(
  5. Backoff.onFailure(
  6. childProps,
  7. "myEcho",
  8. Duration.ofSeconds(3),
  9. Duration.ofSeconds(30),
  10. 0.2)); // adds 20% "noise" to vary the intervals slightly
  11. system.actorOf(supervisorProps, "echoSupervisor");

akka.pattern.BackoffOptions可用于自定义退避监督者 Actor 的行为,以下是一些示例:

  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. maxNrOfRetries = -1
  9. ).withManualReset // the child must send BackoffSupervisor.Reset to its parent
  10. .withDefaultStoppingStrategy // Stop at any Exception thrown
  11. )

上面的代码设置了一个退避监督者,要求子 Actor 在成功处理消息时向其父级发送akka.pattern.BackoffSupervisor.Reset消息,从而重置后退。它还使用默认的停止策略,任何异常都会导致子 Actor 停止。

  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. maxNrOfRetries = -1
  9. ).withAutoReset(10.seconds) // reset if the child does not throw any errors within 10 seconds
  10. .withSupervisorStrategy(
  11. OneForOneStrategy() {
  12. case _: MyException SupervisorStrategy.Restart
  13. case _ SupervisorStrategy.Escalate
  14. }))

上面的代码设置了一个退避监督者,如果抛出MyException,在退避后重新启动子级,而任何其他异常都将被升级。如果子 Actor 在 10 秒内没有抛出任何错误,则会自动重置后退。

One-For-One 策略 vs. All-For-One 策略

Akka 提供了两种监管策略:一种是OneForOneStrategy,另一种是AllForOneStrategy。两者都配置了从异常类型到监督指令(见上文)的映射,并限制了在终止之前允许子级失败的频率。它们之间的区别在于前者只将获得的指令应用于失败的子级,而后者也将其应用于所有的子级。通常,你应该使用OneForOneStrategy,如果没有明确指定,它也是默认的。

AllForOneStrategy适用于子级群体之间有很强的依赖性,以至于一个子 Actor 的失败会影响其他子 Actor 的功能,即他们之间的联系是不可分割的。由于重新启动无法清除邮箱,因此通常最好在失败时终止子级,并在监督者(通过监视子级的生命周期)中显式地重新创建它们;否则,你必须确保任何 Actor 都可以接受在重新启动之前排队但在重新启动之后处理消息。

All-For-One策略中,通常停止一个子级将不会自动终止其他子级;通过监控他们的生命周期可以完成:如果监督者不处理Terminated消息,它将抛出DeathPactException(这取决于它的监督者),它将重新启动,默认的preRestart将终止其所有的子级。

请注意,创建的一次性 Actor 来自一个all-for-one监督者,通过临时 Actor 的失败升级影响所有其他的 Actor。如果不需要这样做,可以安装一个中间监督者;这可以通过为worker声明一个大小为 1 的路由器来实现,详见「路由」。


英文原文链接Supervision and Monitoring.