Akka源码分析-故障恢复
Actor故障恢复是akka中非常重要的内容,在之前的博客中虽然有介绍,但都是杂糅在其他知识点的细节中,本博客将单独介绍这一部分的故障恢复。为了简化分析的单独,本文只研究用户的actor故障恢复的步骤,系统actor也基本差不多;另外,为了简化篇幅,不重要的源码将不再整段贴出来,感兴趣的读者可以去下载源码单独分析。
上面是官方描述actor树形结构时用到的一个图,非常重要,希望读者一定要记住这个树形的层次概念。也就是说,actor一定会归属到某个父actor下面,而actor的故障恢复就是通过这种层级来实现的。这个图之所以重要,是因为如果你能够深刻理解这个图,大概就知道如何实现故障恢复了。
有些爱钻研的读者可能会问,还有一个 / 这个actor没有父actor啊,这个actor如果失败,怎么恢复?其实吧,这算是一个好问题。这是akka设计的根基啊,如果akka本身动不动都需要故障恢复,别人还怎么用。你可以这样理解,akka设计的比较好,或者说root actor功能比较少,轻易不会出现故障。虽然这样解释有点勉强,哈哈。
还记得Actor的调度模型吗,actor只有在收到消息时,才会去调用receive方法,此时actor才会被某个线程执行,也就是“活着”的。如果某个actor没有消息,那么它仅仅是一堆数据躺在内存,也就是“死的”。actor只有在“活着”的时候才可能出现故障。所以我们要从actor处理消息入手,来看看如何实现故障恢复。
//Memory consistency is handled by the Mailbox (reading mailbox status then processing messages, then writing mailbox status final def invoke(messageHandle: Envelope): Unit = { val influenceReceiveTimeout = !messageHandle.message.isInstanceOf[NotInfluenceReceiveTimeout] try { currentMessage = messageHandle if (influenceReceiveTimeout) cancelReceiveTimeout() messageHandle.message match { case msg: AutoReceivedMessage ⇒ autoReceiveMessage(messageHandle) case msg ⇒ receiveMessage(msg) } currentMessage = null // reset current message after successful invocation } catch handleNonFatalOrInterruptedException { e ⇒ handleInvokeFailure(Nil, e) } finally { if (influenceReceiveTimeout) checkReceiveTimeout // Reschedule receive timeout } }
上面是ActorCell里面处理用户消息的相关函数,这包括在一个try-catch里面,我们假设receiveMessage处理消息出现了异常。
final protected def handleNonFatalOrInterruptedException(thunk: (Throwable) ⇒ Unit): Catcher[Unit] = { case e: InterruptedException ⇒ thunk(e) Thread.currentThread().interrupt() case NonFatal(e) ⇒ thunk(e) } /** * Returns true if the provided `Throwable` is to be considered non-fatal, or false if it is to be considered fatal */ def apply(t: Throwable): Boolean = t match { // VirtualMachineError includes OutOfMemoryError and other fatal errors case _: VirtualMachineError | _: ThreadDeath | _: InterruptedException | _: LinkageError | _: ControlThrowable => false case _ => true }
出现异常则会执行catch代码块,其实就是执行了handleNonFatalOrInterruptedException函数,这个函数会检查是不是InterruptedException或NonFatal类型的异常,如果是,则会执行thunk也就是handleInvokeFailure。简单来说就是如果receive处理消息出现异常,则会执行handleInvokeFailure。
final def handleInvokeFailure(childrenNotToSuspend: immutable.Iterable[ActorRef], t: Throwable): Unit = { // prevent any further messages to be processed until the actor has been restarted if (!isFailed) try { suspendNonRecursive() // suspend children val skip: Set[ActorRef] = currentMessage match { case Envelope(Failed(_, _, _), child) ⇒ { setFailed(child); Set(child) } case _ ⇒ { setFailed(self); Set.empty } } suspendChildren(exceptFor = skip ++ childrenNotToSuspend) t match { // tell supervisor case _: InterruptedException ⇒ // ➡➡➡ NEVER SEND THE SAME SYSTEM MESSAGE OBJECT TO TWO ACTORS ⬅⬅⬅ parent.sendSystemMessage(Failed(self, new ActorInterruptedException(t), uid)) case _ ⇒ // ➡➡➡ NEVER SEND THE SAME SYSTEM MESSAGE OBJECT TO TWO ACTORS ⬅⬅⬅ parent.sendSystemMessage(Failed(self, t, uid)) } } catch handleNonFatalOrInterruptedException { e ⇒ publish(Error(e, self.path.toString, clazz(actor), "emergency stop: exception in failure handling for " + t.getClass + Logging.stackTraceFor(t))) try children foreach stop finally finishTerminate() } }
handleInvokeFailure会执行一个try语句,该语句第一句话就是执行suspendNonRecursive,从名称来看,就是要非递归的方式挂起当前actor,就是挂起mailbox对消息的处理,其实就是设置mailbox的状态。
private def suspendNonRecursive(): Unit = dispatcher suspend this
/** * After the call to this method, the dispatcher mustn't begin any new message processing for the specified reference */ protected[akka] def suspend(actor: ActorCell): Unit = { val mbox = actor.mailbox if ((mbox.actor eq actor) && (mbox.dispatcher eq this)) mbox.suspend() }
/** * Increment the suspend count by one. Caller does not need to worry about whether * status was Scheduled or not. * * @return true if the previous suspend count was zero */ @tailrec final def suspend(): Boolean = currentStatus match { case Closed ⇒ setStatus(Closed); false case s ⇒ if (updateStatus(s, s + suspendUnit)) s < suspendUnit else suspend() }
挂起当前出现异常的actor的mailbox对消息的处理之后,又通过suspendChildren挂起了子actor对消息的处理(流程跟上面差不多,都是设置子actor的mailbox状态),最后给父actor发送了Failed消息就结束了。
总结一下当前处理逻辑,如果某个actor对消息的处理出现异常,就挂起当前actor的邮箱,然后递归地挂起所有子actor的邮箱,发送Failed消息给父actor。怎么样简单吧,就是这么简单。现在要看父actor如何处理这个消息了。
final protected def handleFailure(f: Failed): Unit = { currentMessage = Envelope(f, f.child, system) getChildByRef(f.child) match { /* * only act upon the failure, if it comes from a currently known child; * the UID protects against reception of a Failed from a child which was * killed in preRestart and re-created in postRestart */ case Some(stats) if stats.uid == f.uid ⇒ if (!actor.supervisorStrategy.handleFailure(this, f.child, f.cause, stats, getAllChildStats)) throw f.cause case Some(stats) ⇒ publish(Debug(self.path.toString, clazz(actor), "dropping Failed(" + f.cause + ") from old child " + f.child + " (uid=" + stats.uid + " != " + f.uid + ")")) case None ⇒ publish(Debug(self.path.toString, clazz(actor), "dropping Failed(" + f.cause + ") from unknown child " + f.child)) } }
父actor异步收到Failed消息后会调用handleFailure方法。这个方法有两个主要逻辑,设置当前的currentMessage值,调用当前actor也就是抛异常actor的父actor的supervisorStrategy对象的handleFailure方法,如果handleFailure不处理当前异常,则通过throw继续抛出异常,这又进入了父actor的handleInvokeFailure处理逻辑。
/** * This is the main entry point: in case of a child’s failure, this method * must try to handle the failure by resuming, restarting or stopping the * child (and returning `true`), or it returns `false` to escalate the * failure, which will lead to this actor re-throwing the exception which * caused the failure. The exception will not be wrapped. * * This method calls [[akka.actor.SupervisorStrategy#logFailure]], which will * log the failure unless it is escalated. You can customize the logging by * setting [[akka.actor.SupervisorStrategy#loggingEnabled]] to `false` and * do the logging inside the `decider` or override the `logFailure` method. * * @param children is a lazy collection (a view) */ def handleFailure(context: ActorContext, child: ActorRef, cause: Throwable, stats: ChildRestartStats, children: Iterable[ChildRestartStats]): Boolean = { val directive = decider.applyOrElse(cause, escalateDefault) directive match { case Resume ⇒ logFailure(context, child, cause, directive) resumeChild(child, cause) true case Restart ⇒ logFailure(context, child, cause, directive) processFailure(context, true, child, cause, stats, children) true case Stop ⇒ logFailure(context, child, cause, directive) processFailure(context, false, child, cause, stats, children) true case Escalate ⇒ logFailure(context, child, cause, directive) false } }
handleFailure会根据当前actor也就是父actor的监督策略,判断当前的指令,然后调用processFailure方法。请注意processFailure方法的第一个参数是当前父actor的context值,第三个参数是出现异常的子actor的ActorRef。
/** * Applies the fault handling `Directive` (Resume, Restart, Stop) specified in the `Decider` * to the child actor that failed, as opposed to [[akka.actor.AllForOneStrategy]] that applies * it to all children. * * @param maxNrOfRetries the number of times a child actor is allowed to be restarted, negative value means no limit, * if the limit is exceeded the child actor is stopped * @param withinTimeRange duration of the time window for maxNrOfRetries, Duration.Inf means no window * @param decider mapping from Throwable to [[akka.actor.SupervisorStrategy.Directive]], you can also use a * [[scala.collection.immutable.Seq]] of Throwables which maps the given Throwables to restarts, otherwise escalates. * @param loggingEnabled the strategy logs the failure if this is enabled (true), by default it is enabled */ case class OneForOneStrategy( maxNrOfRetries: Int = -1, withinTimeRange: Duration = Duration.Inf, override val loggingEnabled: Boolean = true)(val decider: SupervisorStrategy.Decider) extends SupervisorStrategy
默认情况下,每个actor的监督策略是OneForOneStrategy。也就是遇到异常会执行Restart分支的代码。
def processFailure(context: ActorContext, restart: Boolean, child: ActorRef, cause: Throwable, stats: ChildRestartStats, children: Iterable[ChildRestartStats]): Unit = { if (restart && stats.requestRestartPermission(retriesWindow)) restartChild(child, cause, suspendFirst = false) else context.stop(child) //TODO optimization to drop child here already? }
其实就是会执行restartChild方法,当然我们还可以设置重试次数以防止无限重启。(默认是没有次数限制的)
/** * Restart the given child, possibly suspending it first. * * <b>IMPORTANT:</b> * * If the child is the currently failing one, it will already have been * suspended, hence `suspendFirst` must be false. If the child is not the * currently failing one, then it did not request this treatment and is * therefore not prepared to be resumed without prior suspend. */ final def restartChild(child: ActorRef, cause: Throwable, suspendFirst: Boolean): Unit = { val c = child.asInstanceOf[InternalActorRef] if (suspendFirst) c.suspend() c.restart(cause) }
restartChild比较简单,就是执行了子actor的restart函数。那么restart函数在哪里实现呢?还记得这个ActorRef的实际类型是什么吗?没错就是RepointableActorRef
def restart(cause: Throwable): Unit = underlying.restart(cause)
上面是RepointableActorRef的restart定义,就是调用了ActorCell的restart方法。
final def restart(cause: Throwable): Unit = try dispatcher.systemDispatch(this, Recreate(cause)) catch handleException
上面是ActorCell的restart方法的实现(在akka.actor.dungeon.Dispatch里面),就是又给自己发送了一个Recreate方法。读者可能会有疑问,之前不都把邮箱给suspend了么,怎么还收消息,但请注意这里发送的是系统消息。所以系统消息还是可以被路由的。
/** * Do re-create the actor in response to a failure. */ protected def faultRecreate(cause: Throwable): Unit = if (actor == null) { system.eventStream.publish(Error(self.path.toString, clazz(actor), "changing Recreate into Create after " + cause)) faultCreate() } else if (isNormal) { val failedActor = actor if (system.settings.DebugLifecycle) publish(Debug(self.path.toString, clazz(failedActor), "restarting")) if (failedActor ne null) { val optionalMessage = if (currentMessage ne null) Some(currentMessage.message) else None try { // if the actor fails in preRestart, we can do nothing but log it: it’s best-effort if (failedActor.context ne null) failedActor.aroundPreRestart(cause, optionalMessage) } catch handleNonFatalOrInterruptedException { e ⇒ val ex = PreRestartException(self, e, cause, optionalMessage) publish(Error(ex, self.path.toString, clazz(failedActor), e.getMessage)) } finally { clearActorFields(failedActor, recreate = true) } } assert(mailbox.isSuspended, "mailbox must be suspended during restart, status=" + mailbox.currentStatus) if (!setChildrenTerminationReason(ChildrenContainer.Recreation(cause))) finishRecreate(cause, failedActor) } else { // need to keep that suspend counter balanced faultResume(causedByFailure = null) }
上面是收到Recreate时调用的函数,简单来说就是调用aroundPreRestart函数,和clearActorFields、finishRecreate。从此也可以看出aroundPreRestart、preRestart是actor实例出现异常最后执行的函数,此时actor的状态还是出现异常时候的值。
final protected def clearActorFields(actorInstance: Actor, recreate: Boolean): Unit = { setActorFields(actorInstance, context = null, self = if (recreate) self else system.deadLetters) currentMessage = null behaviorStack = emptyBehaviorStack }
clearActorFields设置了context/self/currentMessage/behaviorStack几个字段为默认值或者说空值。setChildrenTerminationReason这个函数不再分析,简单来说就是递归设置子actor的失败原因,然后返回false。这实现逻辑其实也挺扯的,不返回true,返回false,简直反人类。
其实吧,setChildrenTerminationReason一般都会成功。所以会执行finishRecreate方法。
private def finishRecreate(cause: Throwable, failedActor: Actor): Unit = { // need to keep a snapshot of the surviving children before the new actor instance creates new ones val survivors = children try { try resumeNonRecursive() finally clearFailed() // must happen in any case, so that failure is propagated val freshActor = newActor() actor = freshActor // this must happen before postRestart has a chance to fail if (freshActor eq failedActor) setActorFields(freshActor, this, self) // If the creator returns the same instance, we need to restore our nulled out fields. freshActor.aroundPostRestart(cause) if (system.settings.DebugLifecycle) publish(Debug(self.path.toString, clazz(freshActor), "restarted")) // only after parent is up and running again do restart the children which were not stopped survivors foreach (child ⇒ try child.asInstanceOf[InternalActorRef].restart(cause) catch handleNonFatalOrInterruptedException { e ⇒ publish(Error(e, self.path.toString, clazz(freshActor), "restarting " + child)) }) } catch handleNonFatalOrInterruptedException { e ⇒ clearActorFields(actor, recreate = false) // in order to prevent preRestart() from happening again handleInvokeFailure(survivors, PostRestartException(self, e, cause)) } }
首先调用resumeNonRecursive,这个好像是恢复mailbox的状态的,此时就可以正常的接收消息了。重点来了:newActor。这里通过Props创建了actor的一个新的实例,一般情况下创建的actor实例与原来的都不同,不过读者是不是觉得,这里可以优化一下,弄个参数让开发者自己定义是不是重用以前的actor实例?嗯,没错,恭喜你,这就是SupervisorStrategy.Resume的作用,其实就是简单的忽略此次异常,actor实例不会有任何变化。
重建之后调用了新actor实例的aroundPostRestart、postRestart,也就是说postRestart是新实例创建成功后执行的第一个方法。新的actor实例创建成功后,后面的survivors foreach代码块就是在递归的重启子actor了。这里不再具体分析。
再总结一下,父actor收到子actor异常的消息后,根据当前的策略判断是恢复、重启、还是重用什么的,一般都是重启。如果是重启就是调用子actor的restart函数,就会给出现异常的子actor,发送Recreate系统消息,子actor收到该消息后,会执行一系列重启的操作,最后创建新的actor实例,重启成功。当然这个过程中,子actor的所有子actor也会被重启,也就是一个递归的处理过程。
其实简单的说,如果某个子actor出现异常,挂起自己的邮箱和递归子actor的邮箱,发消息通知父actor,父actor根据当前策略判断能够重启,如果能够重启,则该子actor完成重启动作,并负责递归重启它的子actor。当然还有其他策略,不同的策略这个处理逻辑会有不同。
关于Actor的生命周期,上面的图画的比较清楚,读者可以自行理解。但需要注意,Instance 1和Instance 2是两个“不同”的Actor实例,这里的不同,是指Actor内部的用户状态(即开发者自定义的字段)全部丢失,都会被重建。但两个不同实例的UID相同,这就意味着用相同的ActorRef可以跟后面重新建立的Instance 2发消息。