Akka 实战:解析 Akka 容错机制

839 阅读21分钟

容错

容错技术在任何框架中都很重要,因为没有人能够保证系统在 100% 的运行时间中都不会出错,但我们希望系统在 100% 的时间都是可用的。而现实情况却是:软件服务器经常会出现故障或者无法使用,甚至是硬件故障。在设备众多的电信界,设备故障 "太平常" 了,如果没有良好的应对措施,将无法提供有效的服务,因此 Let it crash 的思想出现了。因为无法完全阻止故障的发生,因此要及时采取措施,比如:

  1. 系统需要容错,故障发生时系统应当保持可用并继续运行。可恢复故障不应该包含触发灾难性故障。
  2. 在某些情况下,只要系统中的核心功能可用即可。这时需要采取措施使得故障部分和系统核心部分隔离,防止产生不可预料的结果。
  3. 系统的部分故障不能破坏整个系统,需要一种方法隔离特定的故障,以便于后续处理。

本章的最后会使用基于 Akka testkit + scalaTest 的框架来验证 Actor 的一些容易混淆或是不易直观理解的容错机制。见:Akka 实战:测试驱动开发 TDD - 掘金 (juejin.cn)

Let it crash

Akka 采用了关注分离的方式,将正常的业务处理和故障排除隔离开来。Actor 在其中一个流程中执行自己的任务,而不需要考虑遇到异常时如何处理。与此同时,监视器 在另一个排错流程中准备处理随时可能发生的错误。

监视器从何而来?在专栏的第一章曾提到过,Akka 采取父母监管 ( parental supervision ) 的方法,当 Actor A 创建另一个 Actor B 时,A 就承担着监视 B 的责任。监视器本身不捕获异常,只会根据崩溃的原因而作出不同的决策,包括:

  1. 重启 ( restart );Actor 从它注册的 Props 中重新创建,下一个 Actor 实例继续处理下一条消息,但是 ActorRef 引用不变。
  2. 恢复 ( resume ):相同的 Actor 实例忽略错误,继续处理下一条消息。
  3. 停止 ( stop ):Actor 终止,不再参与处理消息。
  4. 处理升级 ( escalate ):监视器不知道作何处理,将问题上报给它的父亲 Actor,父 Actor 也是一个监视器。

在后文的监视章节会具体讨论这四个情况。总体而言,下面均为 Akka 用于构建系统的 "Let it crash" 特性:

  1. 错误隔离 ( fault isolation ) —— 监视器可以决定中止一个 Actor,将它从系统中彻底移除。
  2. 容错结构 ( structure ) —— Akka 可以在不影响其它 Actor 的情况下替换 Actor 实例。
  3. 冗余 ( redundancy ) —— 一个 Actor 可以替换为其它的 Actor。监视器可以决定剔除出错的 Actor,然后创建另一个类型进行替代。
  4. 重启 ( reboot ) —— 可以通过重新启动来实现。
  5. 组件生命周期 —— Actor 是一个活跃的组件,它可以被启动,停止,重启
  6. 挂起 ( suspend ) —— 当一个 Actor 崩溃时,它的邮箱被挂起,直到监视器做出决定。
  7. 关注分离 ( separation of concerns ) —— 正常的 Actor 消息处理和出错恢复这两个流程是正交的,它们泾渭分明。

Actor 生命周期

在 Actor 的整个生命周期中,有三个重要的事件:启动事件,停止事件,重启事件

在每一个事件中,Actor 都预留了一些钩子方法。可以在这些钩子方法当中插入自己的代码,用于重新创建新 Actor 实例的特定状态。比如:处理之前失败的消息,或者在 Actor 停用时及时释放占用的资源。虽然 Akka 对钩子的调用是异步的,但是调用顺序是能够保证的

启动事件

Actor 由 actorOf 方法创建并自动启动。通过 system.actorOf() 可以创建顶级 Actor,然后顶级 Actor 在自身的上下文中调用 context.actorOf() 创建子 Actor,见后文监视章节的图示部分。preStart 方法 在 Actor 的构造器之后被调用,可以通过重写 preStart 的方式,对 Actor 提前做一些初始化工作。

停止事件

在重启事件之前,先讨论停止事件。它有三种情况:

  1. 通过上下文 ( ActorSystem 或者是 ActorContext ) 的 stop 方法停止。
  2. 向 ActorRef 发送一个 PoisonPill
  3. 被监控者的 Stop 策略停止。

在 Actor 被移除并回收之前,可以重写 postStop 钩子方法释放一些珍贵的系统资源,或者保存 Actor 的最后状态到 Actor 外的某处,然后供系统的其它部分使用。需要注意,已经停止的 Actor 会和它的 ActorRef 脱离,换句话说,当 Actor 被彻底停止之后,后续发送给该 ActorRef 的消息全部会变成 死信

重启事件

当重启时,Actor 上层的 ActorRef 保持不变,但是内部的实例会被替换 ( Actor 可以通过 Props 对象在 ActorSystem 当中被重建 )。重启事件涉及两个 Actor 实例,因此这个过程要比停止和启动事件要复杂得多。Actor 在重启事件中预留了两个钩子方法: preRestartpostRestart

preRestart 能够在重建新的 Actor 实例之前保存崩溃的 Actor 的最后状态:reason 表示遇到的异常,message 保存了发生异常时处理的信息。

override def preRestart(reason: Throwable, message: Option[Any]): Unit = {
    super.preRestart(reason, message)
}

有以下两点需要注意的地方:

  1. 引发异常的消息 message 默认会被丢弃,这可以避免无效消息反复引发重启,从而其它正常消息无法得到处理的情况,简称 邮箱中毒。这并不是强制性的,除非开发者充分了解尝试重新处理异常消息的后果,比如 能够确定 引发异常的原因是来自外界的偶然错误而非消息本身。
  2. 一般情况下,重写 preRestart 方法时,主动调用 super.preRestart(reason, message) 很有必要。这会使系统主动调用当前 Actor 和它的 children 的 postStop 方法以清理它们占用的资源。这也不是强制性的,开发者同样需要充分了解不立刻回收资源的后果,比如明确令剩下的 children 继续做完剩下的工作之后再被停止。

上一个实例在被停止并销毁之前可以通过 preRestart 方法为给它的下一个实例 "遗留" 一些信息。如:

override def preRestart(reason: Throwable, message: Option[Any]): Unit = {
    super.preRestart(reason, message)
    // 这条消息将交给下一个实例处理。
    self ! Map["port":8080]
}

这条消息通过 self 投递到自身 Mailbox 的队尾,因此这条消息 会在晚些时候 才会被下一个实例收到并处理。 比如,用户坚持要再次处理引发异常的消息,那么就可以通过 preReStart 方法将 message 重新投递给下一位继任者。当新的 Actor 实例通过构造器构造之后,postRestart 方法被调用。见后文 实验 4

使用一张图可以直接串联起一个 Actor 的整个生命周期:

lifecycle_actor.png

重点:前文已经提到,若在重写 preRestart 方法时声明调用 super.preRestart 方法,那么会在执行之前额外调用它和 children 的 postStop 方法以清理资源。同理,若在重写 postRestart 时声明调用 super.postRestart 方法,那么会在它执行之前率先调用它和 children 的 preStart 以恢复资源。见后文 实验 3

监控

在 Akka 的错误处理中,监控和监视是两个概念,但是监控和监视可以同时使用,它们都和 Actor 的生命周期密切相关。

Akka系列(三):监控与容错 - 简书 (jianshu.com)

只要能获取一个 Actor 的引用 ActorRef,就能主动建立 / 取消监控关系。这不需要两者之间是父子 Actor 的关系。如:

// 建立监控关系
context.watch(otherActorRef)
​
// 解除监控关系
context.unwatch(otherActorRef)

当被监控的 ActorRef 因为以下原因 ( 对应 Actor 生命周期的停止事件 ) 被停止时:

  1. 受父 Actor 的 Stop 策略影响,见后文监视部分。
  2. 收到 PoisonPill 消息时。
  3. 被父 Actor 的 context.stop() 方法停止。

监控者会收到 Terminate(actorRef) 消息并随之做一些处理。注意,当一个 Actor 被重启时,它不会向监控者传递这条消息,因为它的 ActorRef 本身并没有受到影响。见后文的 实验 2

监视

所有由用户创建的 Actor 都有一个共同的祖先,称之为用户守护者 user guardian,如图所示。

user_guardian.png

用户通过 context.acterOf() 或者是 system.actorOf() 方法创建一个 Actor。通过 system.actorOf() 创建的 Actor 被称之顶级 Actor。在一个 ActorSystem 内,通常只有一个或非常少个顶级 Actor

整个 Akka 系统的 Actors 构建出了树形的父子族谱,而开发者只需要专注用户空间 ( user space ) 的那部分。和主动进行监控不同,父子 Actor 之间自然地形成了监视的关系。比如 Actor A 在自己的上下文环境中 context 创建了 Actor B,那么 A 就是 B 的父 Actor,也可以说 A 是 B 的监视器。监视器能够 决定 children 遇到异常时该作何处理,或者将自己无法处理的问题抛给上一级的监视器。

【AKKA 官方文档翻译】第一部分:Actor架构_wang_wbq的博客-CSDN博客

预定义策略

即使不主动实现监视器职责,每个 Actor 也会执行默认策略 defaultStrategy,见下面的源代码。除了前三个 Akka 内部异常之外,监视器总会试图无限次的重启来解决应用异常,这在有些情况会引起阻塞。

未被捕获的异常会不断向上级抛出,直到用户守护者 ( User guardian ) 一级。但通过默认策略的代码可知,实际上只有系统级错误 Error ( 它属于 throwable 但不是 Exception ) 才有可能传递到用户守护者,这已经意味着程序遇到不可恢复的严重错误了,此时优雅地关闭整个系统才是明智的选择。

/**
 * When supervisorStrategy is not specified for an actor this
 * `Decider` is used by default in the supervisor strategy.
 * The child will be stopped when [[akka.actor.ActorInitializationException]],
 * [[akka.actor.ActorKilledException]], or [[akka.actor.DeathPactException]] is
 * thrown. It will be restarted for other `Exception` types.
 * The error is escalated if it's a `Throwable`, i.e. `Error`.
 */
final val defaultDecider: Decider = {
  case _: ActorInitializationExceptionStop
  case _: ActorKilledException         ⇒ Stop
  case _: DeathPactException           ⇒ Stop
  case _: Exception                    ⇒ Restart
}

当父 Actor 的其中的一个 child 在运行时崩溃时,它有两种策略:

  1. 只处理崩溃的 child,OneForOneStrategy。适用于 children 之间执行独立任务而不共享资源的情况。
  2. 对所有 children 进行处理,AllForOneStrategy。适用于 children 之间执行关联任务,共享资源的情况。

Akka 内部提供了另一个内建的停止策略 stoppingStrategy ,它会将任何崩溃的 Actor 直接做停止处理,是一个 one-for-one 策略。

// 下面的两个策略等价。
override def supervisorStrategy: SupervisorStrategy = OneForOneStrategy(){case _ : Exception => Stop}
override def supervisorStrategy: SupervisorStrategy = stoppingStrategy

自定义策略

每个监视器都可以根据实际情况来制定不同的策略。对于崩溃的 Actor,监视器有四种不同的策略:

  1. Resume:代价最小,处理最简单的方式。直接忽略错误,继续使用同一个 Actor 实例进行处理。
  2. Restart:移除上一个 Actor 并使用新实例代替,这段时间内 ActorRef 保持不变,Mailbox 在重启完成之前会短暂挂起。
  3. Stop:停用子 Actor,包括永久废弃它的 ActorRef
  4. Escalate:向上抛出错误,由父 Actor 决定如何处理。

对于那些没有明确规定的异常处理全部默认向上级抛出 Escalate,直到用户守护者。

Restart 策略是和 Actor 的重启事件紧密相关的。Restart 次数可以显示地通过 maxNrOfRetries 参数进行指定。当重试次数超过这个阈值时,Actor 将会直接被停用 ( Stop ) 而不是向上抛出异常withinTimeRange 则限定了重试次数的时间窗口。比如:下面的策略表示 10 秒内 Actor 重启最多 5 次,否则停用它。

override def supervisorStrategy: SupervisorStrategy = OneForOneStrategy(maxNrOfRetries = 5,withinTimeRange = 10 second){
    case _ : Exception => Restart
}

当不对 Restart 做任何限制时,系统有可能因邮箱中毒而陷入阻塞式的死循环,见后文的 实验 5

ActorSystem 总会以最快速度 Restart Actor,Akka 提供另一种 "退避重启" 的方式,这种策在有些情况下更加实用,但它并不是基于重启策略的。见下文: BackoffSupervisor

Actor 在 Resume / Restart 之后默认总是从下一条消息继续处理。如果 Actor 被 Stop 策略终止,那么其它异步发往该 Actor 的消息会全部变成死信。

Akka 不容许有孤儿 Actor 存在,所以停止或重启任何一个 Actor,它下属的 children 都会 自下而上 依次停止并重启。

BackoffSupervisor

该小节参考自官方文档: Classic Fault Tolerance • Akka Documentation

退避重启策略适用于因外界的偶然事件而导致的错误:比如数据库连接偶尔中断而导致写入失败。明智的做法是令 Actor 等待一小会然后进行再尝试连接,而不是以近乎阻塞的方式地反复 Restart。

BackoffSupervisor 是一个 Actor,它在初始化时接收另一个被监视的子 Actor Props,以此建立监视关系。它相当于这个子 Actor 的消息代理:BackoffSupervisor 接收外部的消息,但实际处理的逻辑取决于被代理的 child Actor。

backoffsupervisor.png

BackoffSupervisor 不通过 Restart 的机制来重启 Actor ,而是直接停用崩溃的 Actor,然后在一个恰当的时机再另启动一个新的实例。因此,退避重启的过程中会触发 postStoppreStart 方法,但不会触发 preRestartpostRestart 方法。在等待重启的这段时间,所有继续该 BackoffSupervisor 的消息全部会变成 死信

BackoffSupervisor 有两个触发选项,它们有严格的使用场景区分:

  1. Backoff.onFailure:被监视的 Actor 在抛出异常 ( Exception ) 时触发退避重启,这不包含通过 context.stop()PoisonPill 等正常停止并关闭的情况。
  2. Backoff.onStop:被监视的 Actor 以 任何方式停止 都会触发退避重启。这个选项必须用于那些以停止自身作为异常信号而非抛出 Exception 的 Actor

在不涉及持久化 Actor 的使用场景,开发者一般只需要选择 OnFailure。下方的代码给出了 TestedActorBackoffSupervisor 监视的例子:

val childProps: Props = Props[TestedActor05]
val supervisorProps: Props = BackoffSupervisor.props{
    Backoff.onFailure (
        childProps = childProps,    // 退避监控的 child Props
        childName = "child",		// child Props
        minBackoff = 3 second,      // 最小退避时间
        maxBackoff =30 second,      // 最大退避时间
        randomFactor = 0.2          // 随机因素 0.2
    )
}
// 它是 backoffSupervisor 的 ActorRef。
// 实际处理是内部的 ActorRef。
val ref: ActorRef = context.actorOf(backoffSupervisor, "backoff-supervisor")

首次退避时间设定为 minBackoff,随后退避时间将以倍数增长。上述代码中,退避时间会逐渐累计为 3,6,12,24,30 ( 单位:s ),最大退避时间不会超过 maxBackoff

退避算法在计算机网络的底层机制中十分常见。退避算法一般还会利用抖动(随机延迟)机制来防止连续的冲突,在 BackoffSupervisor 中也不例外,比如,这可以避免同时重启大量 Actor 而导致外部数据库的负载在某一时刻激增。使用 randomFactor 参数可以控制这种随机抖动程度。

有时,开发者可能需要更多的配置,希望遇到某些异常时直接取消退避重启而直接 Stop,或者是在一段时间保持正常之后重置退避时间。它们分别可以通过 withSupervisorStrategy()withAutoReset() 方法来实现:

val backoffSupervisor: Props = BackoffSupervisor.props {
    Backoff.onFailure(
        childProps = Props[TestedActor05],
        childName = "child-actor",
        minBackoff = 3 second,
        maxBackoff = 30 second,
        randomFactor = 0.2
    )
    // 在 10s 内正常运行后刷新退避时间。
    .withAutoReset(10 second)
    // 挂载预定义监视策略。
    .withSupervisorStrategy(
        OneForOneStrategy(){
            case FatalException => Stop
        }
    )
}

完整的测试代码见 实验 6

在大部分情境下,Actor 钩子方法,监控方法,监视者之间是混用的,它们可以构建出一个复杂有效的容错机制。后文是本章节涉及的所有单元测试。

实验 1:通过监控者捕捉 Terminated 信息

该单元测试的整体逻辑:

  1. 建立 TestActor02Supervisor 监控 TestedActor02 的关系,前者也是后者的监视器 ( 前文已经提过,监视和监控并不矛盾 )。
  2. 制造 Stop 事件。
  3. 令监视器接收到 Terminated(child) 消息。

整个事件流已经在注释处标注,TestActor02Supervisor 在这个测试用例中既是监视器,也是监控者。

class StopStrategyTest extends TestKit(ActorSystem("testSystem"))
with WordSpecLike
with MustMatchers
with StopSystemAfterAll {
    "The TestedActor02" must {
        "send 'Terminated' to its supervisor when it is broken." in {
            val ref: ActorRef = system.actorOf(Props(new TestActor02Supervisor(testActor)), "supervisor-01")
            // 1. 命令创建 child	
            ref ! NewActor
            // 3. 命令产生异常
            ref ! ThrowEx
            // 9. 验证测试成功。
            expectMsg(TestOk)
        }
    }
}

class TestActor02Supervisor(out : ActorRef) extends Actor with ActorLogging {
    // 6. 监视器捕获异常,执行 Stop 策略。
    override def supervisorStrategy: SupervisorStrategy = OneForOneStrategy(){case _ : Exception => Stop}
    override def receive: Receive = {
        // 2.创建 child,建立监控关系
        case NewActor => val child = context.actorOf(Props[TestedActor02],"child-1");context.watch(child)
        // 4.令 child 抛出异常
        case ThrowEx => context.child("child-1").get ! ThrowEx
        // 8. 接收到 Terminated 消息,向 testActor 发送 TestOk
        case Terminated(child) => {
            log.info("the child actor[{}] is terminated.",child.path)
            out ! TestOk
        }
    }
}

class TestedActor02 extends Actor with ActorLogging{
    // 5. 抛出异常
    override def receive: Receive = {case ThrowEx => throw new Exception("Designed Exception")}
    // 7. 执行 postStop,销毁前向监视者发送 Termianted 消息
    override def postStop(): Unit = log.info("TestedActor02 will shut down.")
}

实验 2:验证 Restart 策略不触发 Terminated

这个单元测试的思路比较简单:制造 Restart 事件,监视者若在此过程中接收到 Terminated() 就向单元测试的 testActor 发送测试失败的消息。为了更明显地观察实验结果,可以在 TestedActor 中的钩子方法中设置一些副作用。

class TerminatedTest extends TestKit(ActorSystem("testSystem"))
with MustMatchers
with WordSpecLike
with StopSystemAfterAll {
    "The TestActor04supervisor" must {
        "get no message like `Terminated(child)`" in {
            val ref: ActorRef = system.actorOf(Props(new TestActor04Supervisor(testActor)), "supervisor-1")
            // 1. 命令创建 child
            ref ! NewActor
            // 3. 命令抛出异常
            ref ! ThrowEx
            // 12. testActor 没接受到消息,说明重启期间 supervisor 没有接受到 Terminated() 消息。
            expectNoMsg()

        }
    }
}

class TestActor04Supervisor(out : ActorRef) extends Actor with ActorLogging {
    // 6. 捕获异常,重启
    override def supervisorStrategy: SupervisorStrategy = OneForOneStrategy() {case _: Exception => Restart}
    override def receive: Receive = {
        // 2. 生成 child, 建立监控关系.
        case NewActor => {
            val child: ActorRef = context.actorOf(Props[TestedActor04], "child-1")
            context.watch(child)
        }
        // 4. 命令 child 抛出异常
        case ThrowEx => context.child("child-1").get ! ThrowEx
        // 假设重启期间接受到了 Terminated 信息,则向 testActor 发送测试失败。
        case Terminated(child) => {
            log.info("the child:{} was crashed.",child)
            out ! TestFailed
        }
        // 11. 接收到消息,程序结束。
        case Restart => log.info("the child restarted.")
    }
}

class TestedActor04 extends Actor with ActorLogging {
    // 5. 抛出异常
    override def receive: Receive = {case ThrowEx => throw new Exception("Designed Exception.")}
    // 9. 执行 preRestart
    override def preRestart(reason: Throwable, message: Option[Any]): Unit = {
    // 7. 调用 super.preRestart
        super.preRestart(reason, message)
        log.info("invoke preRestart")
    }
    // 8. 执行 postStop
    override def postStop(): Unit = log.info("invoke postStop")
    // 10. 重启后,向监视者发送 Restart 消息
    override def postRestart(reason: Throwable): Unit =  context.parent ! Restart
}


实验 3:测试 Actor 完整生命周期

该单元测试的整体思路:

  1. 重写 preStartpreRestartpostRestartpostStop 方法,每个方法内插入一点副作用即可。
  2. 调用 super.preRestartsuper.postRestart
  3. 制造一次 Restart 事件,观察日志的打印顺序。
class LifecycleTest extends TestKit(ActorSystem("testSystem"))
with WordSpecLike
with MustMatchers
with StopSystemAfterAll {
    // 这个字符串后面不要留有空格,否则会引发 Bug
    "The Tested Actor" must {
        "go through: <constructor>, preStart, postStop, preRestart, <constructor>, preStart, postRestart, postStop" in {
            val ref: ActorRef = system.actorOf(Props(new TestedActorSupervisor(testActor)), "supervisor-01")
            ref ! NewActor
            ref ! ThrowEx
        }
    }
}

class TestedActorSupervisor(out: ActorRef) extends Actor {
    override def receive: Receive = {
        case NewActor => context.actorOf(Props[TestedActor01], "child-01")
        case ThrowEx => context.child("child-01").get ! ThrowEx;
    }
    override def supervisorStrategy: SupervisorStrategy = OneForOneStrategy() { case _: Exception => Restart }
}

class TestedActor01 extends Actor with ActorLogging {
    override def receive: Receive = {
        case ThrowEx => throw new Exception("Designed Exception")
    }
    // 这里相当于 Scala 对象的实例化域。
    log.info("invoke constructor<TestedActor01>:{}", this.hashCode())

    override def preStart(): Unit = log.info("invoke preStart, the hashcode of this instance:{}", this.hashCode())
    override def preRestart(reason: Throwable, message: Option[Any]): Unit = {
        super.preRestart(reason,message)
        log.info("invoke preRestart, the hashcode of this instance:{}", this.hashCode())
    }
    override def postRestart(reason: Throwable): Unit = {
        super.postRestart(reason)
        log.info("invoke postRestart, the hashcode of this instance:{}", this.hashCode())
    }
    override def postStop(): Unit = log.info("invoke postStop, the hashcode of this instance:{}", this.hashCode())
}

为了验证重启后的两个 Actor 不是同一个实例,钩子方法中总会将 this.hashCode 打印到日志当中,这个单元测试不需要设置断言,只需要观察日志输出的顺序即可 ( 和前文 Actor 生命周期中的图示相对应 )。

实验 4:通过 preRestart 向下一个实例传递信息

该单元测试的大体思路:

  1. TestActorSupervisorTestedActor 抛出异常并崩溃。
  2. 制造重启事件。
  3. 在上一个实例被移除之前,记录崩溃发生的时间,通过 self 传达给下一个实例。
  4. 下一个实例接收并记录最后一次崩溃发生的时间到 lastMsg

注意,如果没有重写 supervisorStrategy 监视器方法,那么 ActorSystem 默认情况下总是会无限次数地重启崩溃的 Actor。

class RestartTest extends TestKit(ActorSystem("testSystem"))
with WordSpecLike
with MustMatchers
with StopSystemAfterAll {
    "A Tested Actor" must {
        "send crashTime to next Actor instance by `preRestart` method when it brakes." in {
            val ref: ActorRef = system.actorOf(Props(new TestActorSupervisor(testActor)))
            // 1. 命令创建 child
            ref ! NewActor
            // 3. 命令抛出异常
            ref ! ThrowEx
            // 10. 接受到 testActor 的 TestOk,测试成功。
            expectMsg(TestOk)
        }
    }
}

class TestActorSupervisor(out: ActorRef) extends Actor with ActorLogging {
    // 默认策略会自动尝试重启 TestedActor,可以不用重写。
    // 6. 捕获异常,默认执行 Restart
    // override def supervisorStrategy: SupervisorStrategy = OneForOneStrategy(){}...
    override def receive: Receive = {
        // 2. 创建 child
        case NewActor => context.actorOf(Props[TestedActor], "child-1")
        // 4. 令 child 抛出异常
        case ThrowEx => context.child("child-1").get ! ThrowEx
        // 9. 向 testActor 回传 TestOk
        case TestOk => out ! TestOk
    }
}

class TestedActor extends Actor with ActorLogging {
    var lastMsg: String = "ok"
    override def receive: Receive = {
        // 8. 重启后收到 ExInfo 消息,回传 TestOk。
        case ExInfo(exMessage) => {
            lastMsg = exMessage
            log.info(s"this actor has crashed in ${lastMsg} yet.")
            context.parent ! TestOk
        }
        // 5. 抛出异常
        case ThrowEx => throw new Exception("Designed Exception")
    }
    // 7. 上一个实例销毁,给下一个实例发送 ExInfo 消息。
    override def preRestart(reason: Throwable, message: Option[Any]): Unit = {
        super.preRestart(reason, message)
        val crashTime: String = new SimpleDateFormat("yyyy-MM-dd").format(new Date)
        self ! ExInfo(crashTime)
    }
}

实验 5:重现邮箱中毒

这个单元测试基于实验 4 做了一个简单的改动,以制造邮箱中毒的场景:上一个 Actor 实例在重启时会给下一个 Actor 实例再次发送一条 ThrowEx 消息。

class PoisonMailboxTest extends TestKit(ActorSystem("testSystem"))
with MustMatchers
with WordSpecLike
with StopSystemAfterAll {
    "A TestActor03" must {
        "struggles in loop exception with default strategy" in {
            val ref: ActorRef = system.actorOf(Props[TestActor03Supervisor])
            // 1. 命令创建 child
            ref ! NewActor
            // 3. 命令抛出异常
            ref ! ThrowEx
            // 这里不进行断言,让主线程稍进入超时等待 TIMED_WAITED 状态,
            // 观察其它线程的工作。
            Thread.sleep(10000)
        }
    }
}

class TestActor03Supervisor extends Actor with ActorLogging {

    // 6. 默认策略 Restart
    
    override def receive: Receive = {
        // 2. 创建 child
        case NewActor => context.actorOf(Props[TestedActor03],"child-1")
        // 4. 令 child 抛出异常
        case ThrowEx => context.child("child-1").get ! ThrowEx
    }
}

class TestedActor03 extends Actor with ActorLogging {
    // 5. 抛出异常
    // 8. 再次抛出异常,引发邮箱中毒,程序在 step 6-8 陷入死循环。
    override def receive: Receive = {case ThrowEx => throw new Exception("Designed Exception")}
    // 7. 将这个异常信息传递给下一个实例。
    override def preRestart(reason: Throwable, message: Option[Any]): Unit = {
        super.preRestart(reason, message)
        log.info("send message to next instance:{}",message)
        self ! message.get
    }
}

如笔者在前文所提到的:在默认情况下,系统会无限次地执行重启策略。因此若不对此加以限制,那么系统就有陷入阻塞的风险。另一方面,谨慎考虑处理引发 Actor 崩溃的异常消息 message

实验 6:测试 BackoffSupervisor

该单元测试是前文介绍 BackoffSupervisor 的完整代码。注意观察:

  1. TestedActorpreRstartpostRestart 均未触发,说明 BackoffSupervisor 的重启机制和 Restart 不同。
  2. 由于设置 withAutoReset(10 second) 的原因,通过日志的打印时间可以发现两次退避重启时间均为 3s 左右。
  3. 由于额外设置了 withSupervisorStrategy 的原因,BackoffSupervisor 在接受到 Actor 抛出的 FatalException 之后选择停止而非退避重启。
class BackoffSupervisorTest extends TestKit(ActorSystem("system"))
with WordSpecLike
with MustMatchers
with StopSystemAfterAll{
  "A BackoffSupervisor" must {
    "waiting for a moment after crashed." in {

      val backoffSupervisor: Props = BackoffSupervisor.props {
        Backoff.onFailure(
          childProps = Props[TestedActor05],
          childName = "child-actor",
          minBackoff = 3 second,
          maxBackoff = 30 second,
          randomFactor = 0.2
        )
          .withAutoReset(10 second)
          .withSupervisorStrategy(
            OneForOneStrategy(){
              case FatalException => Stop
            }
          )
      }

      // 这里为了方便测试,直接将它声明为顶级 Actor。
      val ref: ActorRef = system.actorOf(backoffSupervisor, "backoff-supervisor")

      // 向这个 BackoffSupervisor Ref 发送消息,它实际上会转发给内部的 child Actor.
      ref ! ThrowEx
      // 耐心等待一会,令 BackoffSupervisor 重置退避时间。
      Thread.sleep(15000)
      // 再次抛出异常
      ref ! ThrowEx

      // 考虑到一点抖动,令等待时间比 3s 稍长一些
      // 如果没有重置退避时间 (4s < 6s),那么它将丢失这条消息。
      Thread.sleep(4000)
      ref ! ThrowFatalEx

      // 预留一点时间观察程序运行
      Thread.sleep(10000)
    }
  }
}

class TestedActor05 extends Actor with ActorLogging {

  override def receive: Receive = {
    case ThrowEx => throw new Exception("Designed Exception")
    case ThrowFatalEx => throw FatalException
  }

  override def postStop(): Unit = {log.info("TestedActor shuts down.")}
  override def preStart(): Unit = {log.info("TestActor starts.")}

  // 这两个钩子方法不会被调用
  override def preRestart(reason: Throwable, message: Option[Any]): Unit = {log.info("pre-Starting.")}
  override def postRestart(reason: Throwable): Unit = {log.info("post-Starting.")}
}

object FatalException extends Exception("Fatal Exception")

参考资料

Akka延迟重启《nineteen》_weixin_33743703的博客-CSDN博客

Akka框架基本要点介绍 - 知乎 (zhihu.com)

(18条消息) 22、聊聊akka(二)监控和监视_llianlianpay的博客-CSDN博客_akka 监控

Akka框架基本要点介绍 - 知乎 (zhihu.com)