22、聊聊akka(二)監控和監視

雖然通過充分利用多核CPU的計算能力把數據處理運算進行並行處理,提高系統整體效率,對現今大數據普遍盛行的系統計算要求還是遠遠不足的,只有通過硬件平行拓展(scale-out)形成機羣並在之上實現分佈式運算才能正真符合新環境對軟件程序的要求。

Akka程序是由多個Actor組成的。它的工作原理是把一項大運算分割成許多小任務然後把這些任務託付給多個Actor去運算。Actor不單可以在當前JVM中運行,也可以跨JVM在任何機器上運行,這基本上就是Akka程序實現分佈式運算的關鍵了。當然,這也有賴於Akka提供的包括監管、監視各種Actor角色,各式運算管理策略和方式包括容錯機制、內置線程管理、遠程運行(remoting)等,以及一套分佈式的消息系統來協調、控制整體運算的安全進行。

Actor是Akka系統中的最小運算單元。每個Actor只容許單一線程,這樣來說Actor就是一種更細小單位的線程。Akka的編程模式和其內置的線程管理功能使用戶能比較自然地實現多線程併發編程。Actor的主要功能就是在單一線程裏運算維護它的內部狀態,那麼它的內部狀態肯定是可變的(mutable state),但因爲每個Actor都是獨立的單一線程運算單元,加上運算是消息驅動的(message-driven),只容許線性流程,Actor之間運算結果互不影響,所以從Akka整體上來講Actor又好像是純函數不可變性的(pure immutable)。

消息驅動模式的好處是可以實現高度的鬆散耦合(loosely-coupling),因爲系統部件之間不用軟件接口,而是通過消息來進行系統集成的。消息驅動模式支持了每個Actor的獨立運算環境,又可以在運行時按需要靈活的對系統Actor進行增減,伸縮自如,甚至可以在運行時(runtime)對系統部署進行調配。Akka的這些鮮明的特點都是通過消息驅動來實現的。

容錯

將錯誤(崩潰)孤立出來,不會導致整個系統崩潰(隔離故障組件),備份組件可以替換崩潰組件(冗餘)(可恢復性)
容錯方式:Restart, Resume, Stop, Escalate
崩潰原因:網絡,第三方服務,硬件故障
Akka容錯:分離業務邏輯(receive)和容錯邏輯(supervisorStrategy)
父actor自動成爲子actor的supervisor
supervisor不fix子actor,而是簡單的呈現如何恢復的一個判斷==>
List(Restart, //重啓並替換原actor,mailbox消息可繼續發送,
//但是接收會暫停至替換完成,重啓默認重啓所有子actor
Resume, //同一個actor不重啓,忽略崩潰,繼續處理下一個消息
Stop, //terminated 不再處理任何消息,剩餘消息會進入死信信箱
Escalate//交給上層處理

Akka的Actor組織是一個層級結構。下層Actor是由直接上一層Actor產生,形成一種父子Actor關係。父級Actor除維護自身狀態之外還必須負責處理下一層子級Actor所發生的異常,形成一種樹形父子層級監管結構。任何子級Actor在運算中發生異常後立即將自己和自己的子級Actor運算掛起,並將下一步行動交付給自己的父級Actor決定。父級Actor對發生異常的子級Actor有以下幾種處理方式:

1、恢復運算(Resume):不必理會異常,保留當前狀態,跳過當前異常消息,照常繼續處理其它消息
2、重新啓動(Restart):清除當前狀態,保留郵箱及內容,終止當前Actor,再重新構建一個新的Actor實例,沿用原來的消息地址ActorRef繼續工作
3、徹底終止(Stop):銷燬當前Actor及ActorRef郵箱,把所有消息導向DeadLetter隊列。
4、向上提交(Esculate):如果父級無法處理子級異常,則這種情況也視爲父級出現的異常。按照規定,父級會將自己和子級Actor運算暫停掛起並把子級Actor實際產生的異常當作自己發生的異常提交給上一層父級處理(也就是說異常信息的發送者sender變成了父級Actor)。

Akka處理異常的方式簡單直接:如果發生異常就先暫停掛起然後交給直屬父級Actor去處理。這就把異常封閉在這個Actor的監管鏈條裏。Akka系統的監管鏈條實際代表一個功能的分散封閉運算,所以一個監管鏈條裏發生的異常不會影響其它監管鏈條。換句話說就是Actor發生異常是封閉在它所屬的功能內部的,一個功能發生異常不會影響其它功能。而在行令式程序中,如果沒有try-catch,任何一段產生異常的代碼都會導致整個程序中斷。

Akka提供了OneForOneStrategy和AllForOneStrategy兩種對待異常Actor的策略配置,策略中定義了對下屬子級發生的各種異常的處理方式。異常處理策略是以策略施用對象分類的,如下:

OneForOneStrategy:只針對發生異常的Actor施用策略
AllForOneStrategy:雖然一個直屬子級Actor發生了異常,監管父級Actor把它當作所有下屬子級同時發生了相同異常,對所有子級Actor施用策略
正常情況下你應該採用OneForOneStrategy,而且這也是Akka默認採用的機制。

Akka對待這種父子監管的原則保證了在Akka系統中不會出現任何孤兒,也就是說保證不會出現斷裂的監管樹。這就要求當任何一個Actor在暫停掛起前都要保證先暫停掛起它的所有直屬子級Actor,而子級則必須先暫停掛起它們的直屬子級,如此遞歸。同樣,任何Actor在重啓(Restart)時也必須遞歸式地重啓直屬子級,因爲重啓一個Actor需要先停止再啓動,我們必須肯定在停止時不會產生孤兒Actor。如果一個父級Actor無法處理子級異常需要向上提交(Esculate)的話,首先它需要採取遞歸方式來暫停掛起自身以下的監管鏈條。它的直屬父級Actor會按自己的異常處理策略來對待提交上來的異常,處理的結果將會遞歸式沿着監管樹影響屬下的所有子子孫孫。但如果這個級別的Actor異常處理策略還是無法覆蓋這個異常時,它又會掛起自己,再向上提交(Esculate)。那麼如果到達了頂級Actor又如何向上提交呢?Akka系統最終的異常處理策略可以在config文件裏配置:

# The guardian "/user" will use this class to obtain its supervisorStrategy.
# It needs to be a subclass of akka.actor.SupervisorStrategyConfigurator.
# In addition to the default there is akka.actor.StoppingSupervisorStrategy.
    guardian-supervisor-strategy = "akka.actor.DefaultSupervisorStrategy"

默認策略是DefaultSupervisorStrategy。以下是Akka提供的默認策略:

 final val defaultDecider: Decider = {
    case _: ActorInitializationException ⇒ Stop
    case _: ActorKilledException         ⇒ Stop
    case _: DeathPactException           ⇒ Stop
    case _: Exception                    ⇒ Restart
  }
final val defaultStrategy: SupervisorStrategy = {
    OneForOneStrategy()(defaultDecider)
  }

前面三種異常直屬父級直接終止子級Actor,其它類型重啓。當然我們可以在這個默認策略之上再添加自定義的一些異常處理策略

override val supervisorStrategy =
  OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) {
    case _: ArithmeticException => Resume
    case _: MyException => Restart  
    case t =>
      super.supervisorStrategy.decider.applyOrElse(t, (_: Any) => Escalate)
  }

Akka絕對不容許有孤兒Actor存在(斷裂的監管樹),所以停止任何一個Actor,它下屬的子子孫孫都會自下而上依次停止運算。爲了更好的理解Actor的監管策略,我們必須先從瞭解Actor的生命週期(lift-cycle)開始。一個Actor從構建產生ActorRef開始到徹底終止爲整個生命週期。其中可以發生多次重啓(Restart)。我們在下面對Actor的開始、終止、重啓這三個環節中發生的事件進行描述:
1、開始

@Override
public void preStart(){
    initDB
}

2、終止

@Override
public void postStop(){
    db.release
}

3、重啓
重啓是Actor生命週期裏一個最重要的環節。在一個Actor的生命週期裏可能因爲多種原因發生重啓(Restart)。造成一個Actor需要重啓的原因可能有下面幾個:

1、在處理某特定消息時造成了系統性的異常,必須通過重啓來清理系統錯誤
2、內部狀態毀壞,必須通過重啓來重新構建狀態
3、在處理消息時無法使用到一些依賴資源,需要重啓來重新配置資源

重啓是一個先停止再開始的過程。父級Actor通過遞歸方式先停止下面的子孫Actor,那麼在啓動過程中這些停止的子孫Actor是否會自動構建呢?這裏需要特別注意:因爲父級Actor是通過Props重新構建的,如果子級Actor的構建是在父級Actor的類構建器內而不是在消息處理函數內構建的,那麼子級Actor會自動構建。Akka提供了preRestart和postRestart兩個事件接口。preRestart發生在停止之前,postRestart發生在開始前,如下:

@Override
public void preRestart(Throwable reason, Option<Object> message) 

@Override
public void postRestart(Throwable reason)

很多時候由於外界原因,Actor的重啓無法保證一次成功。這種現象在使用依賴資源如數據庫、網絡連接等最爲明顯。我們前面介紹過的異常處理策略中就包含了重試(retry)次數及最長重試時間。

case class OneForOneStrategy(
  maxNrOfRetries:              Int      = -1,
  withinTimeRange:             Duration = Duration.Inf,
  override val loggingEnabled: Boolean  = true)(val decider: SupervisorStrategy.Decider)
  extends SupervisorStrategy {...}

kka提供了context.watch和context.unwatch來設置通過ActorRef對任何Actor的終止狀態監視,無須父子級別關係要求。下面是Akka提供的這兩個函數:

  def watch(actorRef: ActorRef): Unit = {
    watching += actorRef
    actorRef.asInstanceOf[InternalActorRef].sendSystemMessage(Watch(actorRef.asInstanceOf[InternalActorRef], this))
  }

  def unwatch(actorRef: ActorRef): Unit = {
    watching -= actorRef
    actorRef.asInstanceOf[InternalActorRef].sendSystemMessage(Unwatch(actorRef.asInstanceOf[InternalActorRef], this))
  }
worker java.lang.NullPointerException: run 
[INFO] [04/16/2018 10:09:01.153] [FaultHandlingTest-akka.actor.default-dispatcher-4] [akka://FaultHandlingTest/user/supervisor/child] Worker starting.
meet NullPointerException , restart.
preRestart 0   hashCode=1876167812
[WARN] [04/16/2018 10:09:01.155] [FaultHandlingTest-akka.actor.default-dispatcher-2] [akka.tcp://[email protected]:2551/system/cluster/core/daemon/downingProvider] Don't use auto-down feature of Akka Cluster in production. See 'Auto-downing (DO NOT USE)' section of Akka Cluster documentation.
postRestart 1  hashCode=801569476
[ERROR] [04/16/2018 10:09:01.159] [FaultHandlingTest-akka.actor.default-dispatcher-15] [akka://FaultHandlingTest/user/supervisor/child] run 
java.lang.NullPointerException: run 
    at org.akka.faulttolerance.Listener.onReceive(Listener.java:44)
[INFO] [04/16/2018 10:09:01.159] [FaultHandlingTest-akka.actor.default-dispatcher-15] [akka://FaultHandlingTest/user/supervisor/child] Worker stoping..
[INFO] [04/16/2018 10:09:01.183] [FaultHandlingTest-akka.actor.default-dispatcher-15] [akka://FaultHandlingTest/user/supervisor/child] Worker starting.

監督者面對其下屬的失敗,有不同的策略。不過大致可以分爲已下的四類:

1)恢復下級Actor,並且保持下級Actor的內部狀態(略過處理)
2)重新啓動下級Actor,並且清除下級Actor的內部狀態 /重啓並替換原actor,mailbox消息可繼續發送,不能處理異常信息
3)永久的停止下級Actor
4)將錯誤逐層上傳,從而暫停自己(有點像java中的throw exception)

其中需要知道的是,Akka中的每一個Actor都在監督樹中,他們既可以扮演監督者的角色,也可以扮演被監督者的角色。上級Actor的狀態直接影響着下級Actor的狀態,因此對上面的前三條策略可以做如下補充(詮釋)

1)當恢復某個Actor的時候同時也要恢復它的所有下級Actor
2)重新啓動某個Actor的時候也要重啓它所有的下級Actor
3)停止某個Actor的時候也需要停止它所有的下級Actor。

Actor的preRestart方法的默認行爲就是:在這個Actor重啓前,先終止它所有的下級Actor,這個過程其實就是一個遞歸的過程。但是這個方法是可以重寫的,因此在重寫的時候需要謹慎。

 // 定義監督策略:
    private static SupervisorStrategy strategy = new OneForOneStrategy(-1,
            Duration.Inf(), new Function<Throwable, Directive>() {  //AllForOneStrategy
        @Override
        public Directive apply(Throwable t) {
            if (t instanceof Exception) {
                return SupervisorStrategy.restart();
                //return SupervisorStrategy.resume();
            } else {
                return escalate();
            }
        }
    });

    @Override
    public void preStart() throws Exception {
        for (int i = 0; i < 10; i++) {
            all[i] = getContext().actorOf(Actor2.props());
            getContext().watch(all[i]);
        }
        getSelf().tell(MSG.START, getSelf());
    }
    @Override
    public void onReceive(Object message) throws Exception {
        System.out.println(name + "_" + index + " receive message" + message.toString());
        if (message instanceof MSG) {
            if (message.equals(MSG.START)) {
                System.out.println("****START****");
                all[(int) (Math.random() * 10)].tell(MSG.HI + ""+ count, getSelf());
                self().tell(MSG.GOON, self());
            } else if (message.equals(MSG.GOON)){
                all[(int) (Math.random() * 10)].tell(MSG.HI + ""+ count, getSelf());
                self().tell(MSG.GOON, self());
                if (++count == 100) {
                    throw new ActorKilledException("Teminate all actor!");
                }
            } else {
                unhandled(message);
            }
        } else if (message instanceof Terminated) {
            System.out.println(getSender() + " terminate");
        } else {
            unhandled(message);
        }

    }

參考:http://www.cnblogs.com/tiger-xc/p/6760658.html
https://github.com/akka

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