1. 程式人生 > >Akka官方文件2.5.17——監督和監控

Akka官方文件2.5.17——監督和監控

監督意味著什麼

正如前面Actor系統所描述的一樣,監督描述了Actor之間的依賴關係:監督者將任務委託給子Actor,所以必須對它們的失敗作出響應。當一個子Actor偵測到錯誤(比如丟擲一個異常),它會暫停(掛起)自己及其所有的子Actor,然後給監督者傳送一條訊息,表名發生了錯誤。根據被監督的工作的性質以及錯誤的性質,監督者有以下4種處理策略:

  1. 恢復子Actor,並保持其內部的狀態
  2. 重啟子Actor,清除其內部的狀態
  3. 永久停止子Actor
  4. 向上傳遞錯誤

將每一個Actor視為監督層次結構的一部分是重要的,這解釋了第4種處理策略的存在(作為一個監督者同時也是另一個監督者的子Actor),並且也對前三種策略有影響。恢復一個Actor及其所有子Actor,重啟一個Actor及其所有子Actor,同樣停止一個Actor意味著停止它的所有子Actor。值得注意的是,Actor生命週期preRestart方法的預設實現是在重啟之前終止它所有的子Actor,這種預設機制可以在實現時進行重寫;這種遞迴的重啟適用於該方法執行過後留下的所有子Actor。

每個監督者都可以配置一個函式,將所有的錯誤原因(比如異常)轉換為上面的四種策略之一。值得注意的是,這個函式不會把失敗的Actor的identity作為輸入。很容易想象這種結構可能不夠靈活,比如:希望將不同的策略運用到不同的子Actor上。所以理解監督採用遞迴的錯誤處理結構是至關重要的。如果你嘗試在某一層做太多,那將會很難推敲,因此在這種情況下推薦增加一層監督者。

Akka實現了一種稱為“父類監督”的形式。Actors只能由其他Actors建立——其中頂層的Actors由Actor系統提供,每一個新建立的Actor都受其父Actor監督。這種限制使得Actor監督層次的形成是隱含的,並鼓勵合理的設計決策。值得指出的是,這保證了Actor不會被孤立或者從系統外部連線到監督者。此外,這為Actor應用程式產生了一個自然且乾淨的關閉過程。

警告

與監督相關的父子通訊是由特殊的系統訊息產生,這些訊息擁有自己的郵箱,而不是使用者訊息的郵箱。這意味著監督相關的事件相對於普通訊息是沒有確定性順序的。通常情況,使用者不能影響普通訊息和錯誤通知的順序。

頂級監督者

一個Actor系統在建立中建立了至少三個Actor,如上圖所示。

/user:Guardian Actor

可能與之互動最多的Actor就是使用者建立的所有Actor的父Actor,名字叫做“/user”,通過使用system.actorOf建立的Actor都是它的子Actor。這意味著當它終止時,所有的普通Actor都會被關閉。這也意味著它的監督策略決定了頂級的使用者定義Actor的監督方式。從Akka2.1開始,可以通過akka.actor.guardian-spervisor-strategy進行配置,該策略採用SupervisorStrategyConfigurator的完全限定名。當它向上傳遞錯誤時,root guardian的迴應將是關閉它,這實際上也會關閉整個Actor系統。

/system:System Guardian

這個特殊的guardian的引入是為了實現有序的關閉,使得logging相關的保持活動而所有普通的Actor被關閉,儘管logging本身也是由Actor實現的。這是通過讓system guardian監控user guardian,並在接收到終止訊息時啟動自己的關閉策略來實現的。頂級的系統Actor使用某種策略進行監督,該策略在接收到所有Exception型別時(除了ActorInitializationException和ActorKilledException),都會無限重啟。而所有其他的Throwable則會向上傳遞,從而導致整個Actor系統的關閉。

/:Root Guardian

root guardian是所有被稱為“頂層”Actor的父Actor,並且使用SupervisorStrategy.stoppingStrategy監督所有的特殊Actors。它的目的是終止任何丟擲Exception的子Actor,而所有其他的Throwable則會向上傳遞,不過傳遞給誰?因為每一個真實的Actor都會有一個監督者,而root guardian的監督者不可能是一個真實的Actor。這意味著它“out of the bubble”,被稱為"bubble-walker"。這是一個假的ActorRef,它實際上會在遇到麻煩時停掉其子Actor,並在root guardian完全停止後(所有子Actor遞迴停止)後立即將Actor系統的isTerminated狀態設定為true。

重啟意味著什麼

  • 接收到指定訊息的系統(即程式設計)錯誤
  • 處理訊息期間使用到的某些外部資源的故障
  • Actor內部的錯誤狀態

除非錯誤被明確的識別,否則不能排除第三個原因,這導致得出了需要清除內部狀態的結論。如果監督者認定它的其他子Actor或者它本身不會受到該錯誤的影響——比如有意識的運用了error kernel pattern——所以最好重啟子Actor。這通過重新建立一個Actor並替換掉ActorRef中原來的錯誤Actor予以實現;能夠這樣做的原因是因為將Actor封裝在了特殊的引用當中。然後新的Actor繼續處理郵箱中的訊息,這意味著重啟Actor對於外部是不可見的。有一個值得注意的點是,它不會重新處理髮生故障時的訊息。

重啟時的事件順序如下:

  1. 暫停(掛起)Actor,這意味著它不會處理普通訊息,直到恢復,並遞迴的掛起所有子Actor
  2. 呼叫舊Actor例項的preRestart方法(預設是傳送終止請求給所有子Actor),然後呼叫postStop方法
  3. 等待在preRestart期間被要求停止(使用context.stop())的所有Actor真正被停掉,這像所有其他Actor操作一樣,是非阻塞的,來自最後一個被停掉的子Actor的通知會使得進入到下一步
  4. 通過呼叫最初提供的工廠方法建立新的Actor
  5. 呼叫新的Actor物件的postRestart(預設是呼叫preStart方法)
  6. 傳送重啟請求給所有沒有在3中停掉的Actor;重啟子Actor的過程是遞迴的,從步驟2開始
  7. 恢復Actor

生命週期的監控意味著什麼

note

生命週期的監控通常被稱為DeathWatch

與上述父子Actor的特殊關係相反,每個Actor可以監控其他Actor。因為Actor從建立到完全建立以及重啟對於除監督者外的其他物件是不可見的,所以能用於監控的唯一狀態是從活動到終止。因此,監控是用於將兩個Actor繫結起來,使得可以對一個Actor的終止作出反應。這與對錯誤作出反應的監督相反。

生命週期監控是通過接收一個Terminated訊息來實現的,其預設行為是在沒有另外處理的情況下丟擲一個DeathPactException。為了偵聽Terminated訊息,需要呼叫ActorContext.watch(actorRef)。停止偵聽,則呼叫ActorContext.unwatch(actorRef)。一個重要的特性是無論偵聽的請求是在目標Actor的終止前或終止後,都將傳遞訊息。也即,即使在註冊時目標Actor已經消亡,偵聽Actor也會收到訊息。

當監督者不能重啟並想終止掉子Actor,監控機制就顯得特別有用。比如:在Actor初始化期間出錯。在這種情況下,它應該監控這些子Actor然後重新建立它們或者安排自己在一段時間後重試。

另一個常見的使用情況是,當請求不到外部資源時,一個Actor需要失敗。如果第三方通過context.stop(actorRef)方法或者傳送PoisonPill終止某個actor時,它的監督者也會被影響。

使用BackoffSupervisor模式延遲重啟

作為內建模組提供的akka.pattern.BackoffSupervisor實現了所謂的指數back-off監督策略,當一個子Actor失敗時重啟它,每次重啟之間的時間延遲增加。

這個模式在啟動Actor失敗是特別有效,因為外部某個資源不可用,我們需要給它一段時間重啟。其中一個有用的主要的示例是PersistentActor因為持久化錯誤而失敗,這表明資料庫可能以及宕機或者過載。在這種情況下, 在persistent actor啟動之前,給它多一點時間恢復是有意義的。

失敗可以用兩種不同的方式表示; Actor停止或崩潰。

下面的Scala片段展示瞭如何建立一個back-off監督者,它將在因為失敗而停止後啟動給定的echo actor,增加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
    maxNrOfRetries = -1
  ))

system.actorOf(supervisor, name = "echoSupervisor")

強烈建議使用randomFactor為back-off的間隔增加一些額外的方差,以避免多個Actor在相同的時間點重啟。比如,由於共享同一個資源(比如資料庫),而資料庫宕機時,actor也會停止,然後在相同的時間間隔內重新啟動。通過向重啟的間隔增加額外的隨機性,Actor會在稍微不同的時間點啟動,從而避免大量的流量衝擊剛剛恢復的共享資料庫或者其它共享的資源。

akka.pattern.BackoffSupervisor Actor同樣可以配置當Actor崩潰,並且監督策略決定應該重啟該Actor時在一定的延遲後重啟該Actor。

下面的Scala片段展示瞭如何建立一個back-off監督者,它將在由於某些異常而導致崩潰後啟動給定的echo 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
    maxNrOfRetries = -1
  ))

system.actorOf(supervisor, name = "echoSupervisor")

akka.pattern.BackoffOptions可用於自定義back-off監督者Actor的行為,下面是一些示例:  

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

上面的程式碼設定了一個back-off監督者,它要求子Actor在成功處理訊息時向其父節點發送akka.pattern.BackoffSupervisor.Reset訊息,重置back-off。 它還使用預設停止策略,任何異常都會導致子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
    maxNrOfRetries = -1
  ).withAutoReset(10.seconds) // reset if the child does not throw any errors within 10 seconds
    .withSupervisorStrategy(
      OneForOneStrategy() {
        case _: MyException ⇒ SupervisorStrategy.Restart
        case _              ⇒ SupervisorStrategy.Escalate
      }))

如果丟擲了MyException,則會設定了一個back-off監督者進行重啟,其他的異常則會向上傳遞。 如果在10秒內沒有丟擲任何錯誤,則自動重置back-off。

一對一策略 vs 多對多策略

Akka有兩類監督策略:OneForOneStrategy和AllForOneStrategy。兩者都配置了從異常型別到監督指令的對映(見上文),並且限制了在終止前允許失敗的次數。它們之間的區別在於前者僅將獲得的指令應用於失敗的子節點,而後者也將其應用於所有兄弟節點。 通常,應該使用OneForOneStrategy,如果沒有顯式的指定,同時它也是預設值。

AllForOneStrategy適用於子Actor之間存在緊密依賴關係的情況,一個子Actor的失敗會影響其他子Actor的功能,也即它們是密不可分的。由於重啟不會清除郵箱,因此通常最好在故障時終止子節點並從監督者那明確的重新建立它們(通過監控子節點的生命週期),否則你必須確保任何一個Actor不會在重啟之前接收到訊息而在重啟後對其進行處理。

通常,當採用OneForOneStrategy時,停止一個子Actor不會自動停止其他子Actor;這可以通過監控他們的生命週期來完成:如果監督者沒有處理Terminated訊息,那麼它會丟擲DeathPactException並重啟(取決於它的監督者),而預設的preRestart方法會終止所有子Actor。當然,這也可以顯式的進行處理。

請注意,在AllForOneStrategy中建立的臨時Actor導致的失敗,該臨時Actor丟擲的錯誤也將會影響到其它永久的Actor。如果這不是你想要的,建立一箇中間監督者,這可以為worker宣告一個大小為1的路由來完成,參見路由