【Akka】Actor模型探索

Akka是什麼

Akka就是爲了改變編寫高容錯性和強可擴展性的併發程序而生的。通過使用Actor模型我們提升了抽象級別,爲構建正確的可擴展併發應用提供了一個更好的平臺。在容錯性方面我們採取了“let it crash”(讓它崩潰)模型,人們已經將這種模型用在了電信行業,構建出“自癒合”的應用和永不停機的系統,取得了巨大成功。Actor還爲透明的分佈式系統以及真正的可擴展高容錯應用的基礎進行了抽象。

Akka是JVM(JAVA虛擬機,下同)平臺上構建高併發、分佈式和容錯應用的工具包和運行時。Akka用 Scala語言寫成,同時提供了Scala和JAVA的開發接口。

Akka處理併發的方法基於Actor模型。在基於Actor的系統裏,所有的事物都是Actor,就好像在面向對象設計裏面所有的事物都是對象一樣。但是有一個重要區別——那就是Actor模型是作爲一個併發模型設計和架構的,而面向對象模式則不是。更具體一點,在Scala的Actor系統裏,Actor互相交互並共享信息但並不對交互順序作出預設。Actor之間共享信息和發起任務的機制是消息傳遞。

Akka在多個Actor和下面的系統之間建立了一個層次(Layer),這樣一來,Actor只需要處理消息就可以了。創建和調度線程、接收和分發消息以及處理競態條件和同步的所有複雜性,都委託給框架,框架的處理對應用來說是透明的。

Akka混合模型特點

  • Actors
    Actors爲你提供:
    對併發/並行程序的簡單的、高級別的抽象。
    異步、非阻塞、高性能的事件驅動編程模型(代碼可以異步處理請求並採用獨佔的方式執行非阻塞操作)。
    非常輕量的事件驅動處理(1G內存可容納約270萬個Actors)。
  • 容錯性
    使用“let-it-crash”語義和監管者樹形結構來實現容錯。非常適合編寫永不停機、自癒合的高容錯系統。監管者樹形結構可以跨多個JVM來提供真正的高容錯系統。
  • 位置透明性
    Akka的所有元素都爲分佈式環境而設計:所有Actor都僅通過發送消息進行互操作,所有操作都是異步的。
  • 可伸縮性
    在Akka裏,不修改代碼就增加節點是可能的,感謝消息傳遞和位置透明性(location transparency)。
  • 高彈性
    任何應用都會碰到錯誤並在某個時間點失敗。Akka的“監管”(容錯)策略爲實現自愈系統提供了便利。
  • 響應式應用
    今天的高性能和快速響應應用需要對用戶快速反饋,因此對於事件的響應需要非常及時。Akka的非阻塞、基於消息的策略可以幫助達成這個目標。
  • 事務性Actors
    事務性Actor是Actor與STM(Software Transactional Memory)的組合。它使你能夠使用自動重試和回滾來組合出原子消息流。

Actor系統

Actor本質上就是接收消息並採取行動處理消息的對象。它從消息源中解耦出來,只負責正確識別接收到的消息類型,並採取相應的行動。
Actor是封裝狀態和行爲的對象,他們的唯一通訊方式是交換消息,交換的消息存放在接收方的郵箱裏。從某種意義上來說,Actor是面向對象的最嚴格的形式,但是最後把它們看成一些人:在使用Actor來對解決方案建模時,把Actor想象成一羣人,把子任務分配給他們,將他們的功能整理成一個有組織的結構,考慮如何將失敗逐級上傳。這樣的結果就可以在腦中形成進行軟件實現的框架。

樹形結構

程序中負責某一個功能的Actor可能需要把它的任務分拆成更小的、更易管理的部分。爲此它啓動子Actor並監管它們。每個Actor有且僅有一個監管者,就是創建它的那個Actor。
Actor系統的精髓在於任務被分拆開來並進行委託,直到任務小到可以被完整地進行處理。這樣做不僅使任務本身被清晰地劃分出結構,而且最終的Actor也能按照它們“應該處理的消息類型”,“如何完成正常流程的處理”以及“失敗流程應如何處理”來進行解析。如果一個Actor對某種狀況無法進行處理,它會發送相應的失敗消息給它的監管者請求幫助。這樣的遞歸結構使得失敗能夠在正確的層次進行處理。

可以將這與分層的設計方法進行比較。分層的設計方法最終很容易形成防護性編程,以防止任何失敗被泄露出來。把問題交由正確的人處理會是比將所有的事情“藏在深處”更好的解決方案。

現在,設計這種系統的難度在於如何決定誰應該監管什麼。這當然沒有一個唯一的最佳方案,但是有一些可能會有幫助的原則:

  • 如果一個Actor管理另一個Actor所做的工作,如分配一個子任務,那麼父Actor應該監督子Actor,原因是父Actor知道可能會出現哪些失敗情況,知道如何處理它們。
  • 如果一個Actor攜帶着重要數據(i.e. 它的狀態要儘可能地不被丟失),這個Actor應該將任何可能的危險子任務分配給它所監管的子Actor,並酌情處理子任務的失敗。視請求的性質,可能最好是爲每一個請求創建一個子Actor,這樣能簡化收集迴應時的狀態管理。這在Erlang中被稱爲“Error Kernel Pattern”。
  • 如果Actor A需要依賴Actor B才能完成它的任務,A應該觀測B的存活狀態並對收到B的終止提醒消息進行響應。這與監管機制不同,因爲觀測方對監管機制沒有影響,需要指出的是,僅僅是功能上的依賴並不足以用來決定是否在樹形監管體系中添加子Actor.

配置容器

多個Actor協作的Actor系統是管理如日程計劃服務、配置文件、日誌等共享設施的自然單元。使用不同的配置的多個Actor系統可以在同一個jvm中共存。Akka自身沒有全局共享的狀態。將這與Actor系統之間的透明通訊(在同一節點上或者跨網絡連接的多個節點)結合,可以看到Actor系統本身可以被作爲功能層次中的積木構件。

Actor實踐

  1. Actor們應該被視爲非常友好的同事:高效地完成他們的工作而不會無必要地打擾其它人,也不會爭搶資源。轉換到編程裏這意味着以事件驅動的方式來處理事件並生成響應(或更多的請求)。Actor不應該因爲某一個外部實體而阻塞(i.e.佔據一個線程又被動等待),這個外部實體可能是一個鎖、一個網絡socket等等。阻塞操作應該在某些特殊的線程裏完成,這個線程發送消息給可處理這些消息的Actor們。
  2. 不要在Actor之間傳遞可變對象。爲了保證這一點,儘量使用不變量消息。如果Actor將他們的可變狀態暴露給外界,打破了封裝,你又回到了普通的Java併發領域並遭遇所有其缺點。
  3. Actor是行爲和狀態的容器,接受這一點意味着不要在消息中傳遞行爲(例如在消息中使用scala閉包)。有一個風險是意外地在Actor之間共享了可變狀態,而與Actor模型的這種衝突將破壞使Actor編程成爲良好體驗的所有屬性。

Akka中的Actor模型

使用Actor就像租車——我們如果需要,可以快速便捷地租到一輛;如果車輛發生故障,也不需要自己修理,直接打電話給租車公司更換另外一輛即可。
Actor模型是一種適用性非常好的通用併發編程模型。它可以應用於共享內存架構和分佈式內存架構,適合解決地理分佈型的問題。同時它還能提供很好的容錯性。

一個Actor是一個容器,它包含了 狀態,行爲,一個郵箱,子Actor和一個監管策略。所有這些包含在一個Actor Reference裏。

Actor引用

Actor是以Actor引用的形式展現給外界的,Actor引用可以被自由的無限制地傳來傳去。內部對象和外部對象的這種劃分使得所有想要的操作能夠透明:重啓Actor而不需要更新別處的引用,將實際Actor對象放置到遠程主機上,向另外一個應用程序發送消息。但最重要的方面是從外界不可能到Actor對象的內部獲取它的狀態,除非這個Actor非常不明智地將信息公佈出去。

狀態

Actor對象通常包含一些變量來反映Actor所處的可能狀態。這可能是一個明確的狀態機(e.g. 使用 FSM 模塊),或是一個計數器,一組監聽器,待處理的請求,等等。這些數據使得Actor有價值,並且必須將這些數據保護起來不被其它的Actor所破壞。好消息是在概念上每個Akka Actor都有它自己的輕量線程,這個線程是完全與系統其它部分隔離的。這意味着你不需要使用鎖來進行資源同步,可以完全不必擔心併發性地來編寫你的Actor代碼。

在幕後,Akka會在一組線程上運行一組Actor,通常是很多Actor共享一個線程,對某一個Actor的調用可能會在不同的線程上進行處理。Akka保證這個實現細節不影響處理Actor狀態的單線程性。

由於內部狀態對於Actor的操作是至關重要的,所以狀態不一致是致命的。當Actor失敗並由其監管者重新啓動,狀態會進行重新創建,就象第一次創建這個Actor一樣。這是爲了實現系統的“自癒合”。

行爲

每次當一個消息被處理時,消息會與Actor的當前的行爲進行匹配。行爲是一個函數,它定義了處理當前消息所要採取的動作,例如如果客戶已經授權過了,那麼就對請求進行處理,否則拒絕請求。這個行爲可能隨着時間而改變,例如由於不同的客戶在不同的時間獲得授權,或是由於Actor進入了“非服務”模式,之後又變回來。這些變化要麼通過將它們放進從行爲邏輯中讀取的狀態變量中實現,要麼函數本身在運行時被替換出來,見become 和 unbecome操作。但是Actor對象在創建時所定義的初始行爲是特殊的,因爲當Actor重啓時會恢復這個初始行爲。

郵箱

Actor的用途是處理消息,這些消息是從其它的Actor(或者從Actor系統外部)發送過來的。連接發送者與接收者的紐帶是Actor的郵箱:每個Actor有且僅有一個郵箱,所有的發來的消息都在郵箱裏排隊。排隊按照發送操作的時間順序來進行,這意味着從不同的Actor發來的消息在運行時沒有一個固定的順序,這是由於Actor分佈在不同的線程中。從另一個角度講,從同一個Actor發送多個消息到相同的Actor,則消息會按發送的順序排隊。

可以有不同的郵箱實現供選擇,缺省的是FIFO:Actor處理消息的順序與消息入隊列的順序一致。這通常是一個好的選擇,但是應用可能需要對某些消息進行優先處理。在這種情況下,可以使用優先郵箱來根據消息優先級將消息放在某個指定的位置,甚至可能是隊列頭,而不是隊列末尾。如果使用這樣的隊列,消息的處理順序是由隊列的算法決定的,而不是FIFO。

Akka與其它Actor模型實現的一個重要差別在於當前的行爲必須處理下一個從隊列中取出的消息,Akka不會去掃描郵箱來找到下一個匹配的消息。無法處理某個消息通常是作爲失敗情況進行處理,除非Actor覆蓋了這個行爲。

子Actor

每個Actor都是一個潛在的監管者:如果它創建了子Actor來委託處理子任務,它會自動地監管它們。子Actor列表維護在Actor的上下文中,Actor可以訪問它。對列表的更改是通過創建(tt class=”docutils literal”>context.ActorOf(…))或者停止(context.stop(child))子Actor來實現,並且這些更改會立刻生效。實際的創建和停止操作在幕後以異步的方式完成,這樣它們就不會“阻塞”其監管者。

監管策略

Actor的最後一部分是它用來處理其子Actor錯誤狀況的機制。錯誤處理是由Akka透明地進行處理的,將監管與監控中所描述的策略中的一個應用於每個出現的失敗。由於策略是Actor系統組織結構的基礎,所以一旦Actor被創建了它就不能被修改。

考慮對每個Actor只有唯一的策略,這意味着如果一個Actor的子Actor們應用了不同的策略,這些子Actor應該按照相同的策略來進行分組,生成中間的監管者,又一次傾向於根據任務到子任務的劃分來組織Actor系統的結構。

Actor終止

一旦一個Actor終止了,i.e. 失敗了並且不能用重啓來解決,停止它自己或者被它的監管者停止,它會釋放它的資源,將它郵箱中所有未處理的消息放進系統的“死信郵箱”。而Actor引用中的郵箱將會被一個系統郵箱所替代,系統郵箱會將所有新的消息重定向到“排水溝”。 但是這些操作只是盡力而爲,所以不能依賴它來實現“保證投遞”。

不是簡單地把(未處理的:譯者注)消息扔掉的想法來源於我們(Akka:譯者注)測試:我們在事件總線上註冊了TestEventListener來接收死信,然後將每個收到的死信在日誌中生成一條警告。這對於更快地解析測試失敗非常有幫助。我們覺得可能這個功能也可以用於其它的目的。

參考資料

讓併發和容錯更容易:Akka示例教程
Akka 2.0官方文檔中文版

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

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