【Akka】Akka中actor的生命週期與DeathWatch監控

Actor的生命週期

在Actor系統中的路徑代表一個“地方”,這可能被一個存活着的的actor佔用着。最初,路徑(除了系統初始化角色)是空的。當actorOf()被調用時,指定一個由通過Props描述給定的路徑角色的化身。一個actor化身由路徑和一個UID確定。重新啓動僅僅交換Props定義的Actor 實例,但化身與UID依然是相同的。
當該actor停止時,化身的生命週期也相應結束了。在這一刻時間上相對應的生命週期事件也將被調用和監管角色也被通知終止結束。化身被停止之後,路徑也可以重複被通過actorOf()方法創建的角色使用。在這種情況下,新的化身的名稱跟與前一個將是相同的而是UIDs將會有所不同。

一個ActorRef總是代表一個化身(路徑和UID)而不只是一個給定的路徑。因此,如果一個角色停止,一個新的具有相同名稱創建的舊化身的ActorRef不會指向新的。

在另一方面ActorSelection指向該路徑(或多個路徑在使用通配符時),並且是完全不知道其化身當前佔用着它。由於這個原因導致ActorSelection不能被監視到。通過發送識別信息到將被回覆包含正確地引用(見通過角色選擇集識別角色)的ActorIdentityActorSelection來解決當前化身ActorRef存在該路徑之下。這也可以用ActorSelection類的resolveOne方法來解決,這將返回一個匹配ActorRefFuture

Actor生命週期Hook:

Akka Actor定義了下列的生命週期回調鉤子(Hook):

  • preStart:在actor實例化後執行,重啓時不會執行。
  • postStop:在actor正常終止後執行,異常重啓時不會執行。
  • preRestart:在actor異常重啓前保存當前狀態。
  • postRestart:在actor異常重啓後恢復重啓前保存的狀態。當異常引起了重啓,新actor的postRestart方法被觸發,默認情況下preStart方法被調用。

啓動Hook

啓動策略,調用preStart Hook,一般用於初始化資源.在創建一個Actor的時候,會調用構造函數,之後調用preStart。
preStart的默認形式:

def preStart(): Unit = ()

重啓Hook

所有的Actor都是被監管的,i.e.以某種失敗處理策略與另一個actor鏈接在一起。如果在處理一個消息的時候拋出的異常,Actor將被重啓。這個重啓過程包括上面提到的Hook:

  1. 要被重啓的actor的preRestart被調用,攜帶着導致重啓的異常以及觸發異常的消息; 如果重啓並不是因爲消息的處理而發生的,所攜帶的消息爲None,例如,當一個監管者沒有處理某個異常繼而被它自己的監管者重啓時。 這個方法是用來完成清理、準備移交給新的actor實例的最佳位置。它的缺省實現是終止所有的子actor並調用postStop
  2. 最初actorOf調用的工廠方法將被用來創建新的實例。
  3. 新的actor的postRestart方法被調用,攜帶着導致重啓的異常信息。

actor的重啓會替換掉原來的actor對象;重啓不影響郵箱的內容, 所以對消息的處理將在postRestart hook返回後繼續。觸發異常的消息不會被重新接收。在actor重啓過程中所有發送到該actor的消息將象平常一樣被放進郵箱隊列中。

preRestart和postRestart的默認形式:

def preRestart(reason: Throwable, message: Option[Any]): Unit = {
  context.children foreach { child ⇒
    context.unwatch(child)
    context.stop(child)
  }
  postStop()
}

def postRestart(reason: Throwable): Unit = {
  preStart()
}

解釋一下重啓策略的詳細內容:

  1. actor被掛起
  2. 調用舊實例的 supervisionStrategy.handleSupervisorFailing 方法 (缺省實現爲掛起所有的子actor)
  3. 調用preRestart方法,從上面的源碼可以看出來,preRestart方法將所有的children Stop掉了,並調用postStop回收資源
  4. 調用舊實例的supervisionStrategy.handleSupervisorRestarted方法(缺省實現爲向所有剩下的子actor發送重啓請求)
  5. 等待所有子actor終止直到 preRestart 最終結束
  6. 再次調用之前提供的actor工廠創建新的actor實例
  7. 對新實例調用 postRestart
  8. 恢復運行新的actor

終止Hook

postStop hook一般用於回收資源。Actor在被調用postStop之前,會將郵箱中剩下的message處理掉(新的消息變成死信了)。Actor是由UID和Path來唯一標識的,也就是說ActorRef也是通過UID和Path來定位。在Actor被Stop之後,新的Actor是可以用這個Path的,但是舊的ActorRef是不能用的,因爲UID不一樣。
這個hook保證在該actor的消息隊列被禁止後才運行,i.e.之後發給該actor的消息將被重定向到ActorSystem的deadLetters中。
postStop的默認形式:

def postStop(): Unit = ()

各種Hook的順序關係圖解

Akka的actor生命週期示例代碼

下面用Kenny類演示生命週期函數的調用順序:

import akka.actor._

class Kenny extends Actor {
  println("entered the Kenny constructor")
  override def preStart: Unit = {
    println("kenny: preStart")
  }
  override def postStop: Unit ={
    println("kenny: postStop")
  }
  override def preRestart(reason: Throwable, message: Option[Any]): Unit = {
    println("kenny: preRestart")
    println(s" MESSAGE: ${message.getOrElse("")}")
    println(s" REASON: ${reason.getMessage}")
    super.preRestart(reason, message)
  }
  override def postRestart(reason: Throwable): Unit = {
    println("kenny: postRetart")
    println(s" REASON: ${reason.getMessage}")
    super.postRestart(reason)
  }
  def receive = {
    case ForceRestart => throw new Exception("Boom!")
    case _            => println("Kenny received a message")
  }
}

case object ForceRestart

object LifecycleDemo extends App{
  val system = ActorSystem("LifecycleDemo")
  val kenny = system.actorOf(Props[Kenny], name="Kenny")

  println("sending kenny a simple String message")
  kenny ! "hello"
  Thread.sleep(1000)

  println("make kenny restart")
  kenny ! ForceRestart
  Thread.sleep(1000)

  println("stopping kenny")
  system.stop(kenny)

  println("shutting down system")
  system.shutdown
}

pre*post*方法和actor的構造函數一樣,都是用來初始化或關閉actor所需的資源的。
上面的代碼中,preRestartpostRestart調用了父類的函數實現,其中postRestart的默認實現中,調用了preStart方法。

打印信息:

sending kenny a simple String message
entered the Kenny constructor
kenny: preStart
Kenny received a message
make kenny restart
kenny: preRestart
 MESSAGE: ForceRestart
 REASON: Boom!
kenny: postStop
[ERROR] [01/16/2016 21:51:46.584] [LifecycleDemo-akka.actor.default-dispatcher-4] [akka://LifecycleDemo/user/Kenny] Boom!
java.lang.Exception: Boom!
    at Examples.Tutorials.Kenny$$anonfun$receive$1.applyOrElse(Test4_LifecycleDemo.scala:24)
    at akka.actor.Actor$class.aroundReceive(Actor.scala:480)
    at Examples.Tutorials.Kenny.aroundReceive(Test4_LifecycleDemo.scala:4)
    at akka.actor.ActorCell.receiveMessage(ActorCell.scala:526)
    at akka.actor.ActorCell.invoke(ActorCell.scala:495)
    at akka.dispatch.Mailbox.processMailbox(Mailbox.scala:257)
    at akka.dispatch.Mailbox.run(Mailbox.scala:224)
    at akka.dispatch.Mailbox.exec(Mailbox.scala:234)
    at scala.concurrent.forkjoin.ForkJoinTask.doExec(ForkJoinTask.java:260)
    at scala.concurrent.forkjoin.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1339)
    at scala.concurrent.forkjoin.ForkJoinPool.runWorker(ForkJoinPool.java:1979)
    at scala.concurrent.forkjoin.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:107)

entered the Kenny constructor
kenny: postRetart
 REASON: Boom!
kenny: preStart
stopping kenny
shutting down system
kenny: postStop

Actor系統中的監管

在Actor系統中說過,監管描述的是actor之間的關係:監管者將任務委託給下屬並對下屬的失敗狀況進行響應。 當一個下屬出現了失敗(i.e.拋出一個異常),它自己會將自己和自己所有的下屬掛起然後向自己的監管者發送一個提示失敗的消息。取決於所監管的工作的性質和失敗的性質,監管者可以有4種基本選擇:

  1. 讓下屬繼續執行,保持下屬當前的內部狀態
  2. 重啓下屬,清除下屬的內部狀態
  3. 永久地終止下屬
  4. 將失敗沿監管樹向上傳遞

重要的是始終要把一個actor視爲整個監管樹形體系中的一部分,這解釋了第4種選擇存在的意義(因爲一個監管者同時也是其上方監管者的下屬),並且隱含在前3種選擇中:讓actor繼續執行同時也會繼續執行它的下屬,重啓一個actor也必須重啓它的下屬,相似地終止一個actor會終止它所有的下屬。被強調的是一個actor的缺省行爲是在重啓前終止它的所有下屬,但這種行爲可以用Actor類的preRestart hook來重寫;對所有子actor的遞歸重啓操作在這個hook之後執行。

每個監管者都配置了一個函數,它將所有可能的失敗原因(i.e.Exception)翻譯成以上四種選擇之一;注意,這個函數並不將失敗actor本身作爲輸入。我們很快會發現在有些結構中這種方式看起來不夠靈活,會希望對不同的下屬採取不同的策略。在這一點上我們一定要理解監管是爲了組建一個遞歸的失敗處理結構。如果你試圖在某一個層次做太多事情,這個層次會變得複雜難以理解,這時我們推薦的方法是增加一個監管層次。

Akka實現的是一種叫“父監管”的形式。Actor只能由其它的actor創建,而頂部的actor是由庫來提供的——每一個創建出來的actor都是由它的父親所監管。這種限制使得actor的樹形層次擁有明確的形式,並提倡合理的設計方法。必須強調的是這也同時保證了actor們不會成爲孤兒或者擁有在系統外界的監管者(被外界意外捕獲)。還有,這樣就產生了一種對actor應用(或其中子樹)自然又幹淨的關閉過程。

生命週期監控(臨終看護DeathWatch)

在Akka中生命週期監控通常指的是DeathWatch。
除了父actor和子actor的關係的監控關係,每個actor可能還監視着其它任意的actor。因爲actor創建後,它活着的期間以及重啓在它的監管者之外是看不到的,所以對監視者來說它能看到的狀態變化就是從活着變到死亡。所以監視的目的是當一個actor終止時可以有另一個相關actor做出響應,而監管者的目的是對actor的失敗做出響應。

監視actor通過接收Terminated消息來實現生命週期監控。如果沒有其它的處理方式,默認的行爲是拋出一個DeathPactException異常。爲了能夠監聽Terminated消息,你需要調用ActorContext.watch(targetActorRef)。調用ActorContext.unwatch(targetActorRed)來取消對目標角色的監聽。需要注意的是,Terminated消息的發送與監視actor註冊的時間和被監視角色終止的時間順序無關。例如,即使在你註冊的時候目標actor已經死了,你仍然能夠收到Terminated消息。 當監管者不能簡單的重啓子actor而必須終止它們時,監視將顯得非常重要。例如,actor在初始化的時候報錯。在這種情況下,它應該監視這些子actor並且重啓它們或者稍後再做嘗試。
另一個常見的應用案例是,一個actor或者它的子actor在無法獲得需要的外部資源時需要失敗。如果是第三方通過調用system.stop(child)方法或者發送PoisonPill消息來終止子actor時,監管者也將會受到影響。

說明

爲了在其它actor結束時(i.e.永久終止,而不是臨時的失敗和重啓)收到通知,actor可以將自己註冊爲其它actor在終止時所發佈的 Terminated消息的接收者。這個服務是由actor系統的DeathWatch組件提供的。

註冊一個監控器的代碼:

import akka.actor.{ Actor, Props, Terminated }

class WatchActor extends Actor {
  val child = context.actorOf(Props.empty, "child")
  context.watch(child) // <-- 這是註冊所需要的唯一調用
  var lastSender = system.deadLetters

  def receive = {
    case "kill"              ⇒ context.stop(child); lastSender = sender
    case Terminated(`child`) ⇒ lastSender ! "finished"
  }
}

要注意Terminated消息的產生與註冊和終止行爲所發生的順序無關。多次註冊並不表示會有多個消息產生,也不保證有且只有一個這樣的消息被接收到:如果被監控的actor已經生成了消息並且已經進入了隊列,在這個消息被處理之前又發生了另一次註冊,則會有第二個消息進入隊列,因爲一個已經終止的actor註冊監控器會立刻導致Terminated消息的發生。
可以使用context.unwatch(target)來停止對另一個actor的生存狀態的監控,但很明顯這不能保證不會接收到Terminated消息因爲該消息可能已經進入了隊列。

DeathWatch代碼示例:

DeathWatch的作用是,當一個actor終止時,你希望另一個actor收到通知。
使用context.watch()方法來聲明對一個actor的監控。
下面是示例代碼:

import akka.actor._

class Jason extends Actor {
  def receive = {
    case _ => println("jason got a message")
  }
}

class Parent extends Actor {
  // start Jason as a child, then keep an eye on it
  val jason = context.actorOf(Props[Jason], name="Jason")
  context.watch(jason)

  def receive = {
    case Terminated(jason) => println("OMG, they killed jason")
    case _ => println("parent received a message")
  }

}

object DeathWatchDemo extends App{
  val system = ActorSystem("DeathWatchDemo")
  val parentActor = system.actorOf(Props[Parent], name="Parent")

  // look up jason, then kill it
  println("kill the child actor")
  val jasonActor = system.actorSelection("/user/Parent/Jason")
  jasonActor ! PoisonPill

  Thread.sleep(5000)
  println("calling system.shutdown")
  system.shutdown
}

當Jason被殺死後,Parent actor收到Terminated(jason)消息。

轉載請註明作者Jason Ding及其出處
Github博客主頁(http://jasonding1354.github.io/)
GitCafe博客主頁(http://jasonding1354.gitcafe.io/)
CSDN博客(http://blog.csdn.net/jasonding1354)
簡書主頁(http://www.jianshu.com/users/2bd9b48f6ea8/latest_articles)
Google搜索jasonding1354進入我的博客主頁

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章