What Supervision Means
和在Akka Systems中描述的一样,supervision表示的是actor之间的依赖关系: 监督者委派任务给下级并对下属的错误事件进行相应.当下属发生错误(比如抛出异常),它会终止自己同时它的所有下属会给他们的监管者发送一个错误信号(message).根据监管性质和错误性质,监管者有四种操作可以选择:
- 恢复下属,同时保持它的内部状态
- 重启下属,同时清空它的内部状态
- 拥挤关闭下属
- 将该错误升级自自身
始终将actor作为监督层次的一部分是非常重要的,这解释了第四条的含义,即,一个监管者同时也是另一个actor的下属.同时也暗示了前三条,恢复一个actor的同时会恢复它的所有下属,重启一个actor的同时也会重启它的所有下属,同样的道理,关闭一个actor的同时也会关闭它的所有下属.
需要注意的是Actor类中preRestart的默认行为是在重启actor自身之前首先关闭它的所有子actor,当然这个hook可以被重写,这个hook被执行后会递归重启它的所有子actor.
所有的监管者都会被一个函数配置为尽可能的将所有的错误转换为上面的四中情况之一,并且,该函数并不会将错误actor的实体作为传入参数.
Akka实现了一个特殊的方式,叫做”父类监管”(parental supervision).一个Actor只能被另一个actor创建,最顶层的actor由类库提供,每个被创建的actor都被其父actor监控.
The Top-Level Supervisors
如上图所示,一个ActorSystem在创建时最少会另外启动三个actor.
/user: The Guardian Actor
该actor应该是与所有用户创家的actor相互影响最大的,监管者为”/user”.所有使用 system.actorOf() 创建的actor都是它的子actor,这表示,如果这个监管者关闭了,所有系统中的普通actor都会被关闭,同时表示了所有普通顶层acttor被这个监管者监管时的策略.从Akka 2.1开始,可以通过 akka.actor.guardian-supervisor-strategy 对这个监管策略进行设置,它接收一个SupervisorStrategyConfigurator作为入参.当这个监管者升级错误时,root监管者会以关闭这个监管者的方式作为响应,这将会关闭整个System.
/system: The System Guardian
这个特殊的actor用于实现一个有序的actor关闭日志,同时日志本身也是用actor实现的.
/: The Root Guardian
Root监管者使用 SupervisorStrategy.stoppingStrategy(策略) 监管所有被称为 “top-level”的actor,目的是用于关闭上报任何类型异常的子actor.由于所有的actor都有一个父actor,但是root的监管这不能是一个真的actor.
What Restarting Means
当一个actor在处理消息时引起一下三种类型的错误:
- 一些特殊消息引起的系统错误
- 处理消息时引用的外部资源错误
- 破坏了actor的内部状态
重启过程的准确步骤:
- 终止actor(在恢复之前不会处理普通消息),递归终止子actor
- 调用老实例的preRestart方法(默认是向所有的子actor发送终止请求并条用postStop)
- 等待所有被发送终止请求(context.stop())的子actor真正关闭,所有的actor操作都是无阻塞的,最后被关闭的actor会发出终止提醒以进行下一步
- 使用原始工厂创建一个新的实例
- 在新的实例中调用postRestart(preStart)
- 向在第3步中没有关闭的actor发送重启请求,重启后的子actor会跟随同样的进程
- 恢复actor
What Lifecycle Monitoring Means
声明循环监控在akka中又被引用为DeathWatch.
Delayed restarts with the BackoffSupervisor pattern
使用BackoffSupervisor进行延时重启,当一个actor失败时尝试再次重启,每次的时间间隔都会递增.
这个模式的用处是,当一个启动一个actor因为外部资源引用而失败,然后过一段时间后进行重试.一个主要的例子就是当一个 PersistentActor 因为一些持续性的错误(数据库关闭或超载)失败,这种场景下在重启PersistentActor之前可以适当的提供一些缓冲时间.
下面的例子展示了如何创建一个依次递增重启时间的监管,每次错误后重启的间隔为3, 6, 12, 24,最终为30:
val childProps = Props(classOf[EchoActor])
val supervisor = BackoffSupervisor.props(
Backoff.onStop(
childProps,
childName = "myEcho",
minBackoff = 3.seconds,
maxBackoff = 30.seconds,
randomFactor = 0.2 // adds 20% "noise" to vary the intervals slightly
))
system.actorOf(supervisor, name = "echoSupervisor")
比较推荐的是使用randomFactor来添加频率,以避免多个actor在同一个时间点进行同样的操作而引起负载过大,比如多个actor同时使用一个数据库时,同时过多的actor进行重新连接有可能会再次失败.
akka.pattern.BackoffSupervisor 同样可以用于当一个actor发生异常或崩溃时进行重启,下面的代码展示了使用递增的间隔对actor进行重启,依次为 3, 6, 12, 24,30:
val childProps = Props(classOf[EchoActor])
val supervisor = BackoffSupervisor.props(
Backoff.onFailure(
childProps,
childName = "myEcho",
minBackoff = 3.seconds,
maxBackoff = 30.seconds,
randomFactor = 0.2 // adds 20% "noise" to vary the intervals slightly
))
system.actorOf(supervisor, name = "echoSupervisor")
akka.pattern.BackoffOptions 可以对监管者的行为进行自定义:
val supervisor = BackoffSupervisor.props(
Backoff.onStop(
childProps,
childName = "myEcho",
minBackoff = 3.seconds,
maxBackoff = 30.seconds,
randomFactor = 0.2 // adds 20% "noise" to vary the intervals slightly
).withManualReset // the child must send BackoffSupervisor.Reset to its parent
.withDefaultStoppingStrategy // Stop at any Exception thrown
)
上面的代码展示了如何设置当一个子actor成功处理一个消息时必须向其父actor发送一个akka.pattern.BackoffSupervisor.Reset类型的消息,同时使用了一个默认停止策略,任何类型的异常都会关闭该子actor.
val supervisor = BackoffSupervisor.props(
Backoff.onFailure(
childProps,
childName = "myEcho",
minBackoff = 3.seconds,
maxBackoff = 30.seconds,
randomFactor = 0.2 // adds 20% "noise" to vary the intervals slightly
).withAutoReset(10.seconds) // the child must send BackoffSupervisor.Reset to its parent
.withSupervisorStrategy(
OneForOneStrategy() {
case _: MyException ⇒ SupervisorStrategy.Restart
case _ ⇒ SupervisorStrategy.Escalate
}))
上面的代码展示了,当抛出自定义的 MyException 类型异常时进行重启,而所有其他类型的异常被升级到父类.同时,如果该actor在10秒内没有发生任何错误时会自动重置.
One-For-One Strategy vs. All-For-One Strategy
一对一策略 或 多对一策略.
Akka中有两种类型的监管策略类: OneForOneStrategy 和 AllForOneStrategy.二者都用于配置异常类型与监管指令的映射,以及限制一个子actor在被关闭之前所允许的错误时长.二者的区别是前者只对错误的actor调用指令,后者是对所有的子actor进行指令调用.通常会使用到OneForOneStrategy,这也是在没有提供任何设置时的默认配置.