【Akka】Actor引用

Actor系統的實體

在Actor系統中,actor之間具有樹形的監管結構,並且actor可以跨多個網絡節點進行透明通信。
對於一個Actor而言,其源碼中存在ActorActorContextActorRef等多個概念,它們都是爲了描述Actor對象而進行的不同層面的抽象。
我們先給出一個官方的示例圖,再對各個概念進行解釋。

上圖很清晰的展示了一個actor在源碼層面的不同抽象,和不同actor之間的父子關係:
Actor類的一個成員context是ActorContext類型,ActorContext存儲了Actor類的上下文,包括self、sender。
ActorContext還混入了ActorRefFactory特質,其中實現了actorOf方法用來創建子actor。
這是Actor中context的源碼:

trait Actor {
  /**
   * Stores the context for this actor, including self, and sender.
   * It is implicit to support operations such as `forward`.
   *
   * WARNING: Only valid within the Actor itself, so do not close over it and
   * publish it to other threads!
   *
   * [[akka.actor.ActorContext]] is the Scala API. `getContext` returns a
   * [[akka.actor.UntypedActorContext]], which is the Java API of the actor
   * context.
   */
  implicit val context: ActorContext = {
    val contextStack = ActorCell.contextStack.get
    if ((contextStack.isEmpty) || (contextStack.head eq null))
      throw ActorInitializationException(
        s"You cannot create an instance of [${getClass.getName}] explicitly using the constructor (new). " +
          "You have to use one of the 'actorOf' factory methods to create a new actor. See the documentation.")
    val c = contextStack.head
    ActorCell.contextStack.set(null :: contextStack)
    c
  }

ActorCell的self成員是ActorRef類型,ActorRef是一個actor的不可變,可序列化的句柄(handle),它可能不在本地或同一個ActorSystem中,它是實現網絡空間位置透明性的關鍵設計。
這是ActorContext中self的源碼:

trait ActorContext extends ActorRefFactory {

  def self: ActorRef

ActorRef的path成員是ActorPath類型,ActorPath是actor樹結構中唯一的地址,它定義了根actor到子actor的順序。
這是ActorRef中path的源碼:

abstract class ActorRef extends java.lang.Comparable[ActorRef] with Serializable {
  /**
   * Returns the path for this actor (from this actor up to the root actor).
   */
  def path: ActorPath

Actor引用

Actor引用是ActorRef的子類,它的最重要功能是支持向它所代表的actor發送消息。每個actor通過self來訪問它的標準(本地)引用,在發送給其它actor的消息中也缺省包含這個引用。反過來,在消息處理過程中,actor可以通過sender來訪問到當前消息的發送者的引用。

不同類型的Actor引用

根據actor系統的配置,支持幾種不同的actor引用:

  1. 純本地引用被配置成不支持網絡功能的,這些actor引用發送的消息不能通過一個網絡發送到另一個遠程的JVM。
  2. 支持遠程調用的本地引用使用在支持同一個jvm中actor引用之間的網絡功能的actor系統中。爲了在發送到其它網絡節點後被識別,這些引用包含了協議和遠程地址信息。
  3. 本地actor引用有一個子類是用在路由(比如,混入了Router trait的actor)。它的邏輯結構與之前的本地引用是一樣的,但是向它們發送的消息會被直接重定向到它的子actor。
  4. 遠程actor引用代表可以通過遠程通訊訪問的actor,i.e. 從別的jvm向他們發送消息時,Akka會透明地對消息進行序列化。
  5. 有幾種特殊的actor引用類型,在實際用途中比較類似本地actor引用:
    • PromiseActorRef表示一個Promise,作用是從一個actor返回的響應來完成,它是由akka.pattern.ask調用來創建的
    • DeadLetterActorRef是死信服務的缺省實現,所有接收方被關閉或不存在的消息都在此被重新路由。
    • EmptyLocalActorRef是查找一個不存在的本地actor路徑時返回的:它相當於DeadLetterActorRef,但是它保有其路徑因此可以在網絡上發送,以及與其它相同路徑的存活的actor引用進行比較,其中一些存活的actor引用可能在該actor消失之前得到了。
  6. 然後有一些內部實現,你可能永遠不會用上:
    • 有一個actor引用並不表示任何actor,只是作爲根actor的僞監管者存在,我們稱它爲“時空氣泡穿梭者”。
    • 在actor創建設施啓動之前運行的第一個日誌服務是一個僞actor引用,它接收日誌事件並直接顯示到標準輸出上;它就是Logging.StandardOutLogger

獲得Actor引用

創建Actor

一個actor系統通常是在根actor上使用ActorSystem.actorOf創建actor,然後使用ActorContext.actorOf從創建出的actor中生出actor樹來啓動的。這些方法返回指向新創建的actor的引用。每個actor都擁有到它的父親,它自己和它的子actor的引用。這些引用可以與消息一直髮送給別的actor,以便接收方直接回復。

具體路徑查找

另一種查找actor引用的途徑是使用ActorSystem.actorSelection方法,也可以使用ActorContext.actorSelection來在actor之中查詢。它會返回一個(未驗證的)本地、遠程或集羣actor引用。向這個引用發送消息或試圖觀察它的存活狀態會在actor系統樹中從根開始一層一層從父向子actor發送消息,直到消息到達目標或是出現某種失敗,i.e.路徑中的某一個actor名字不存在(在實際中這個過程會使用緩存來優化,但相較使用物理actor路徑來說仍然增加了開銷,因爲物理路徑能夠從actor的響應消息中的發送方引用中獲得),這個消息傳遞過程由Akka自動完成的,對客戶端代碼不可見。
使用相對路徑向兄弟actor發送消息:

context.actorSelection("../brother") ! msg

也可以用絕對路徑:

context.actorSelection("/user/serviceA") ! msg

查詢邏輯Actor層次結構

由於actor系統是一個類似文件系統的樹形結構,對actor的匹配與unix shell中支持的一樣:你可以將路徑(中的一部分)用通配符(«*» 和«?»)替換來組成對0個或多個實際actor的匹配。由於匹配的結果不是一個單一的actor引用,它擁有一個不同的類型ActorSelection,這個類型不完全支持ActorRef的所有操作。同樣,路徑選擇也可以用ActorSystem.actorSelection或ActorContext.actorSelection兩種方式來獲得,並且支持發送消息。
下面是將msg發送給包括當前actor在內的所有兄弟actor:

context.actorSelection("../*") ! msg

與遠程部署之間的互操作

當一個actor創建一個子actor,actor系統的部署者會決定新的actor是在同一個jvm中或是在其它的節點上。如果是在其他節點創建actor,actor的創建會通過網絡連接來到另一個jvm中進行,結果是新的actor會進入另一個actor系統。 遠程系統會將新的actor放在一個專爲這種場景所保留的特殊路徑下。新的actor的監管者會是一個遠程actor引用(代表會觸發創建動作的actor)。這時,context.parent(監管者引用)和context.path.parent(actor路徑上的父actor)表示的actor是不同的。但是在其監管者中查找這個actor的名稱能夠在遠程節點上找到它,保持其邏輯結構,e.g.當向另外一個未確定(unresolved)的actor引用發送消息時。

因爲設計分佈式執行會帶來一些限制,最明顯的一點就是所有通過電纜發送的消息都必須可序列化。雖然有一點不太明顯的就是包括閉包在內的遠程角色工廠,用來在遠程節點創建角色(即Props內部)。
另一個結論是,要意識到所有交互都是完全異步的,它意味着在一個計算機網絡中一條消息需要幾分鐘才能到達接收者那裏(基於配置),而且可能比在單JVM中有更高丟失率,後者丟失率接近於0(還沒有確鑿的證據)。

Akka使用的特殊路徑

在路徑樹的根上是根監管者,所有的的actor都可以從通過它找到。在第二個層次上是以下這些:

  • "/user"是所有由用戶創建的頂級actor的監管者,用ActorSystem.actorOf創建的actor在其下一個層次 are found at the next level。
  • "/system" 是所有由系統創建的頂級actor(如日誌監聽器或由配置指定在actor系統啓動時自動部署的actor)的監管者。
  • "/deadLetters" 是死信actor,所有發往已經終止或不存在的actor的消息會被送到這裏。
  • "/temp"是所有系統創建的短時actor(i.e.那些用在ActorRef.ask的實現中的actor)的監管者。
  • "/remote" 是一個人造的路徑,用來存放所有其監管者是遠程actor引用的actor。

附錄-Actor模型概述:

Actor模型爲編寫併發和分佈式系統提供了一種更高的抽象級別。它將開發人員從顯式地處理鎖和線程管理的工作中解脫出來,使編寫併發和並行系統更加容易。Actor模型是在1973年Carl Hewitt的論文中提的,但是被Erlang語言採用後才變得流行起來,一個成功案例是愛立信使用Erlang非常成功地創建了高併發的可靠的電信系統。

Actor的樹形結構

像一個商業組織一樣,actor自然會形成樹形結構。程序中負責某一個功能的actor可能需要把它的任務分拆成更小的、更易管理的部分。爲此它啓動子Actor並監管它們。要知道每個actor有且僅有一個監管者,就是創建它的那個actor。

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

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

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

  • 如果一個actort管理另一個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和一個監管策略。所有這些包含在一個Actor引用裏。

狀態

Actor對象通常包含一些變量來反映actor所處的可能狀態。這可能是一個明確的狀態機,或是一個計數器,一組監聽器,待處理的請求,等等。這些數據使得actor有價值,並且必須將這些數據保護起來不被其它的actor所破壞。

好消息是在概念上每個Akka actor都有它自己的輕量線程,這個線程是完全與系統其它部分隔離的。這意味着你不需要使用鎖來進行資源同步,可以完全不必擔心併發性地來編寫你的actor代碼。

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

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

行爲

每次當一個消息被處理時,消息會與actor的當前的行爲進行匹配。行爲是一個函數,它定義了處理當前消息所要採取的動作,例如如果客戶已經授權過了,那麼就對請求進行處理,否則拒絕請求。

郵箱

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

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

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

子Actor

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

監督策略

Actor的最後一部分是它用來處理其子actor錯誤狀況的機制。錯誤處理是由Akka透明地進行處理的。由於策略是actor系統組織結構的基礎,所以一旦actor被創建了它就不能被修改。

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

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

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