【Akka】Actor模型探索

JasonDing1354發表於2016-01-21

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進入我的部落格主頁

相關文章