標籤:

AKKA~概念篇

Actor Model

在開始介紹Akka之前,首先需要對其理論Actor Model進行了解。

Actor Model由Carl Hewitt在1973年提出,Gul Agha在1986年發表學術報告「Actors: A Model of Concurrent Computation in Distributed Systems」,至今已有不少年頭了。在計算機科學中,它是一個並行計算的數學模型,最初為由大量獨立的微處理器組成的高並行計算機所開發,Actor Model的理念非常簡單:天下萬物皆為Actor。(PS:面向對象的萬物皆對象)

Actor Model是計算機科學領域並行計算的數學模型。An actor被看作並發的,通用的計算基元。Actors能夠接受和發送消息、做出業務邏輯判斷、創建更多的Actors並監管它們、決定在收到收到下一條消息時採用哪種行為。Actors之間只能通過消息修改自身私有狀態(避免需要任何的鎖)。

Actor Model的最根本優勢是消息發送者與已經發送的消息解耦,允許非同步通信同時又滿足消息傳遞模式的控制結構。

消息的接收者是通過地址標識的,也被稱作郵件地址。因此Actor只能和它擁有地址的Actor通信。Actor可以通過接受到的消息獲取發送者的地址,或者獲取Actor創建的並由其監管的Actors的地址。

Actors之間並發計算是Actor Model與生俱來的特點,Actors可以動態創建,Actors的地址會被包含在其發送的消息中,Actors之間的交互只能通過非同步消息,此外消息可以亂序到達。

e.g.

What is Akka?

Akka是一個免費開源的軟體工具包,使用Akka可以很容易的在JVM上構建高並發和分散式的應用程序。Akka 支持多種編程模型,但是著重於Actor Model並發模型。它的設計靈感來自於Erlang語言。

Akka的同時支持使用Java和Scala進行開發。但Akka是用Scala語言寫出來的。在Scala2.10版本中,Akka取代了Scala原有的Actor Model的實現,被收錄於Scala標準庫中。

Akka實現了獨特的混合模型

  • 對並發/並行程序的簡單的、高級別的抽象。

  • 非同步、非阻塞、高性能的事件驅動編程模型。

  • 非常輕量的事件驅動處理(1G內存可容納數百萬個actors)。

容錯性

  • 使用「let-it-crash」語義的監控層次體系。

  • 監控層次體系可以跨越多個JVM,從而提供真正的容錯系統。

  • 非常適合編寫永不停機、自癒合的高容錯系統。

持久性

  • actor接收到的消息可以選擇性的被持久化,並在actor啟動或重啟的時候重放。這使得actor能夠恢復其狀態,即使是在JVM崩潰或正在遷移到另外節點的情況下。

Akka的兩種使用方式

  • 庫(lib)方式:在web應用中使用,放到 WEB-INF/lib 中或者作為一個普通的Jar包放進classpath。

  • 以微內核的形式:可以將你的應用放進一個獨立的內核。

Hello World

行萬里路始於足下,讓我還是從最簡單的Hello World開始吧。

import akka.actor.{Actor, ActorRef, ActorSystem, Props}nobject Sample extends App {n val system = ActorSystem("Greeting")n val benjamin = system.actorOf(Props[Benjamin], "Benjamin")n val stanley = system.actorOf(Props[Stanley], "Stanley")n benjamin ! stanleyn}nclass Stanley extends Actor {n override def receive: Receive = {n case msg: String => {n println(msg) //output: Hello Stanley!n val name = sender().path.namen sender() ! s"Hello $name!";n }n case _ =>n }n}nclass Benjamin extends Actor {n override def receive: Receive = {n case friend: ActorRef => {n val name = friend.path.namen friend ! s"Hello $name!"n }n case msg: String => {n print(msg) //output: Hello Benjamin!n context.system.terminate()n }n case _ =>n }n}n

在這段代碼範例中,我們首先創建了一個Actor 系統,Stanley 和 Benjamin兩個角色。當程序開始運行時,這段代碼

benjamin ! stanleyn

向Benjamin介紹了新朋友Stanley,當Benjamin收到了stanley的引用後立刻向stanley say Hello

case friend: ActorRef => {n val name = friend.path.namen friend ! s"Hello $name!"n }n

緊接著stanley收到了來自於benjamin的問候,立刻向benjamin也say hello。當benjamin收到來自於stanley的問候後,關閉了actor system。

case msg: String => {n print(msg) //output: Hello Benjamin!n context.system.terminate() //close system.n }n

Actor再述

前文Actor model 中闡述了Actor是應用中最小的基元,以及它們如何構成一個應用系統的。那麼讓我繼續庖丁解牛吧!看看Actor的具體概念。

Aactor是一個容器,它包含了狀態,行為,屬於它一個郵箱,child Actor和一個監管策略。所有這些封裝在一個Actor引用里,當Actor銷毀時這些資源都會被釋放。

Actor引用

Actor對象需要與外界隔離才符合Actor model的設計思想。因此Actor是以ActorRef(引用)的形式展現給外界的,ActorRef作為對象,可以被無限制地自由傳遞。Actor和ActorRef的這種劃分使得所有操作都能夠透明。e.g. 重啟Actor而不需要更新ActorRef、將實際Actor對象部署在遠程主機上和向另外一個應用程序發送消息。更重要的是,外界不可能直接得到Actor對象的內部狀態,除非這個Actor非常不明智地將內部狀態暴露。

狀態

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

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

由於內部狀態對於Actor的操作是至關重要的,所以狀態不一致是致命的。因此當actor失敗並被其監管者重新啟動時,狀態會被重新創建,就象第一次創建這個actor一樣。這是為了實現系統的「自癒合」。

可選地,通過持久化收到的消息並在重啟後重放它們,一個actor的狀態可自動恢復到重啟前的狀態。

行為

每當Actor收到一個消息,它會與Actor的當前行為進行匹配。行為是一個函數,它定義了在某個時間點處理當前消息所要採取的行為。例如如果客戶已經授權,那麼就對消息進行轉發處理,否則拒絕。這個行為可能隨著時間而改變,e.g. 不同的客戶在不同的時間獲得授權,或是由於actor進入了「非服務」模式,之後又變回來。這些變化的實現,要麼是通過將它們編碼入狀態變數中並由行為邏輯讀取,要麼是函數本身在運行時被交換出來,參考become/unbecome操作。但是actor對象在創建時所定義的初始行為是特殊的,因為actor重啟時會恢復這個初始行為。

郵箱

連接眾多Actors的紐帶是Actor的郵箱:每個Actor有且僅有一個郵箱,所有的發來的消息都在郵箱里排隊。排隊按照發送操作的時間順序來進行,這意味著由於Actor分布在不同的線程中,所以從不同的Actor發來的消息在運行時沒有一個固定的順序。從另一個角度講,從同一個Actor發送到相同目標Actor的多個消息,會按發送的順序排隊。

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

Akka與其它Actor Model實現的一個重要區別在於:當前的行為總是必須處理下一個從隊列中取出的消息,Akka不會掃描郵箱隊列來獲取下一個匹配的消息。無法處理某個消息通常被認為是失敗情況,除非這個行為被重寫。

Child Actor

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

當Actor意外終止時

當Actor終止時即失敗了且不能用重啟來解決,e.g. 自我銷毀或者被它的監管者銷毀,它會釋放其佔用的資源,並將其郵箱中所有未處理的消息放進系統的「死信郵箱「(dead letter mailbox),即將所有消息作為死信重定向到事件流中。而Actor引用中的郵箱將會被一個系統郵箱所替代,將所有的新消息作為死信重定向到事件流中。 但是這些操作只是儘力而為,所以不能依賴它實現「投遞保證」。

監管

監管描述的是Actors之間的依賴關係:監管者將任務委託給Child Actor,並對Child Actor的失敗狀況響應。當某個Child Actor出現了失敗(即拋出一個異常),它自己會將自己和自己所有的下屬掛起,然後向其自身的監管者發送一個提示失敗的消息。

Akka提供了四種處理失敗的選項

  1. 恢復下屬,保持Child Actor當前積累的內部狀態。

  2. 重啟下屬,清除Child Actor當前積累的內部狀態。

  3. 永久地停止下屬。

  4. 升級失敗(沿監管樹向上傳遞失敗),從而失敗自身。

Akka提供了兩種的監管策略:OneForOneStrategy 和AllForOneStrategy。它們之間的區別在於,前者只將所獲得的指令應用在發生故障的Child Actor上,而後者則是應用在所有Child Actor上。通常情況下,你應該使用OneForOneStrategy,這也是默認的策略。

始終要把一個Actor視為整個監管樹體系的一部分是很重要的,這解釋了第4種策略存在的意義(因為一個監管者同時也是其父監管者的Child Actor),並且隱含在前3種選擇中:恢復actor會恢復其所有下屬,重啟一個actor也必須重啟其所有下屬,類似地終止一個Actor會終止其所有下屬。

Akka實現的是一種叫「父監管」的形式。Actor只能被其它的Actor創建——頂部的Actor由庫來提供---每一個被創建的actor都由其父Actor所監管。這種限制使得actor的監管結構隱式符合其樹形層次。需要強調的是這保證了Actor不會成為孤兒或者擁有在系統外界的監管者。

actor系統在其創建過程中至少要啟動三個actor,如上圖所示

/user: 守護Actor

這個名為"/user"的守護者,作為所有用戶創建actor的父actor,可能是需要打交道最多的。使用system.actorOf()創建的actor都是其子actor。這意味著,當該守護者終止時,系統中所有的普通actor都將被關閉。同時也意味著,該守護者的監管策略決定了普通頂級actor是如何被監督的。

/system: 系統守護者

這個特殊的守護者被引入,是為了實現正確的關閉順序,即logging要保持可用直到所有普通actor終止,即使日誌本身也是用actor實現的。其實現方法是:系統守護者觀察user守護者,並在收到Terminated消息初始化其自己的關閉過程。頂級的系統actor被監管的策略是,對收到的除ActorInitializationException和ActorKilledException之外的所有Exception無限地執行重啟,這也將終止其所有子actor。而Throwable會被向上傳播,然後將導致整個actor系統的關閉。

/: 根守護者

根守護者所謂「頂級」actor的祖父,它監督所有在Actor路徑的頂級作用域中定義的特殊actor,發現任何Exception就終止所有的子actor。而Throwable都會被向上傳播,但是向上傳播給誰呢?所有的actor都有一個監管者,但是根守護者沒有父actor,因為它就是整個樹結構的根。因此這裡使用一個虛擬的ActorRef,在發現問題後立即停掉根守護者,並在根守護者完全終止之後,立即把actor系統的isTerminated置為true。

監視

與監管關係相對應的,每一個Actor都可以監視其他任意Actor。由於Actor從創建到完全可用和重啟都是除了監管者之外不可見的,只有Actor從活躍到終止的狀態變化可以被監視。監視因此被用於聯繫兩個Actor,使監視者能對另一個Actor的終止做出響應。

生命周期監視是通過監管Actor收到Terminated消息實現的,其默認行為是拋出一個DeathPactException。要開始監聽Terminated消息,需要調用ActorContext.watch(目標ActorRef)。要停止監聽,需要調用ActorContext.unwatch(目標ActorRef)。一個重要的特性是,消息將不考慮監視請求和目標終止發生的順序,也就是說,即使在登記的時候目標已經死了,你仍然會得到消息。

如果一個監管者不能簡單地重啟其Child Actor,而不得不終止它們時,監視就特別有用。例如在Actor初始化時發生錯誤。在這種情況下,它應該監視這些Child Actor並重新創建它們或安排自己在稍後重試。

另一個常見的應用情況是,一個Actor需要在沒有外部資源時失敗,該資源也可能是它的Child Actor之一。如果第三方通過system.stop(child)或發送PoisonPill(毒丸)的方式終止Child Actor,其監管者很可能會受到影響。

引用

Actor引用是 ActorRef 的子類,其最重要的目的是支持向它所代表的Actor發送消息。每個Actor通過self欄位來訪問自己的引用。在給其它Actor發送的消息中也預設包含這個引用。反過來,在消息處理過程中,Actor可以通過sender()方法來訪問到當前消息發送者的引用。

根據Actor系統的配置,支持幾種不同類型的Actor引用

純本地Actor引用,在配置為不使用網路功能的Actor系統中使用。這些Actor引用如果通過網路連接傳給遠程的JVM,將不能正常工作。

本地Actor引用,在配置為使用遠程功能的Actor系統中使用,來代表同一個JVM的Actor。為了能夠在被發送到其它節點時仍然可達,這些引用包含了協議和遠程地址信息。

本地Actor引用的另一個子類型,用在路由器中(routers,即混入 了 Router trait的Actor)。它的邏輯結構與之前的本地引用是一樣的,但是向它們發送的消息會被直接重定向到它的子Actor。

遠程Actor引用,代表可以通過遠程通訊訪問的Actor,即向他們發送消息時會透明地對消息進行序列化,並發送到別的JVM。

有幾種特殊的Actor引用類型,在實際用途中比較類似本地Actor引用:

PromiseActorRef 表示一個Promise,其目的是通過一個Actor返回的響應來完成。它是由 akka.pattern.ask 創建的。

DeadLetterActorRef是死信服務的預設實現,所有接收方被關閉或不存在的消息都被重新路由在此。

EmptyLocalActorRef是當查找一個不存在的本地Actor路徑時Akka返回的:它相當於DeadLetterActorRef,但是它保有其路徑因此可以在網路上發送,並與其它相同路徑的存活的Actor引用進行比較,其中一些存活的Actor引用可能在該Actor消失之前被得到。

然後有一些內部實現,你應該永遠不會用上。

有一個Actor引用並不表示任何Actor,只是作為根Actor的偽監管者存在,我們稱它為時空氣泡穿梭者(the one who walks the bubbles of space-time)。

在Actor創建設施啟動之前運行的第一個日誌服務,是一個偽Actor引用,它接收日誌事件並直接顯示到標準輸出上;它就是 Logging.StandardOutLogger。

路徑

由於Actor是以一種嚴格的樹形結構來創建的,所以沿著Child Actor到Parent Actor的監管鏈,一直到Actor系統的根存在一條唯一的Actor名字序列。這個序列可以被看做是文件系統中的路徑,所以我們稱之為"路徑"。

一個Actor路徑包含一個錨點(anchor),來標識Actor系統的,之後是各路徑元素的連接,從根監護者到指定的Actor,路徑元素是路徑經過的Actor的名字,以"/"分隔。

引用和路徑之間有什麼區別?

Actor引用標明了一個Actor,其生命周期和Actor的生命周期保持匹配。Actor路徑表示一個名稱,其背後可能有也可能沒有真實的Actor,而且路徑本身不具有生命周期,它永遠不會失效。你可以創建一個Actor路徑,而無需創建一個Actor,但你不能在創建Actor引用時不創建相應的Actor。

路徑錨點

每一條Actor路徑都有一個地址組件,描述訪問這個Actor所需要的協議和位置,之後是從根到Actor所經過的樹節點上Actor的名字。

e.g.

  • 純本地 : "akka://my-sys/user/service-a/worker1"

  • 遠程 : "akka.tcp://my-sys@host.example.com:9999/user/service-b"

邏輯路徑

順著Actor的Parent監管鏈一直到根的唯一路徑被稱為Actor邏輯路徑。這個路徑與Actor的創建祖先關係完全吻合,所以當Actor系統的遠程調用配置(和配置中路徑的地址部分)設置好後它就是完全確定的了。

物理路徑

Actor邏輯路徑描述它在一個Actor系統內部的功能位置,而基於配置的遠程部署意味著一個Actor可能在另外一台網路主機上被創建,即另一個Actor系統中。在這種情況下,從根守護者穿過Actor路徑來找到該Actor肯定需要訪問網路,這是一個很昂貴的操作。因此,每一個Actor同時還有一條物理路徑,從Actor對象實際所在的Actor系統的根開始。與其它Actor通信時使用物理路徑作為發送方引用,能夠讓接收方直接回復到這個Actor上,將路由延遲降到最小。

物理路徑的一個重要性質是它決不會跨多個Actor系統或跨JVM虛擬機。這意味著如果一個Actor有祖先被遠程監管,則其邏輯路徑(監管樹)和物理路徑(Actor部署)可能會分叉。


推薦閱讀:

akka有哪些比較好的學習資料?
如何學習scala,akka,play framework?

TAG:Scala | Akka |