1. 程式人生 > >【Akka】Actor引用

【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系統的結構。