Akka官方文件2.5.17(四)——Actor引用、路徑、地址
目錄
本節描述如何在一個可能的分散式Actor系統中識別和定位Actor。
上圖展現了一個Actor系統中最重要的實體之間的關係,請繼續閱讀以獲得更詳細的資訊。
什麼是Actor引用?
一個Actor引用是ActorRef的子類,其最主要的目的是向它所表示的Actor傳送訊息。每個Actor都可以通過self欄位訪問它的canonical(本地)引用;當傳送訊息給其他Actors,這個引用也將作為預設的傳送引用。相反地,在處理訊息時,Actor可以通過sender()方法獲取到當前訊息傳送者的Actor引用。
依據Actor系統的配置,可以支援幾種不同型別的Actor引用:
- 純本地Actor引用在未配置支援網路功能的Actor系統中使用。這些Actor引用通過網路傳輸到遠端JVM時,不會起任何作用
- 本地Actor引用,當啟用遠端功能時,被Actor系統用來支援那些代表同一JVM中的Actor引用的網路功能。為了能夠訪問其他網路節點,這些引用包含了協議和遠端地址的資訊
- 有一種本地Actor引用的子類被用來當作路由(Actor中混入Router特質),它的邏輯結構與前面提到的本地引用相同,但是向它們傳送訊息會直接轉發給它們的一個子節點
- 遠端Actor引用表示使用遠端通訊可達的Actor,即向它們傳送訊息時,會透明地序列化該訊息並將它們傳送到遠端的JVM
- 有幾種特殊型別的actor引用,其行為類似於本地actor引用:
- PromiseActorRef是Promise的特殊表示,它的目的是接收Actor返回的響應。akka.pattern.ask
- DeadLetterActorRef是dead letters服務的預設實現,Akka會將所有接收者已經關閉或不存在的訊息路由到該服務
- EmptyLocalActorRef是Akka在查詢不存在的本地Actor路徑時返回的東西:它等同於DeadLetterActorRef,但它保留了它的路徑,以便Akka可以通過網路傳送它並將其路徑與其他存在的Actor引用路徑進行比較,這些Actor引用可能在該Actor消失之前得到
- PromiseActorRef是Promise的特殊表示,它的目的是接收Actor返回的響應。akka.pattern.ask
- 然後有一些一次性的內部實現:
- 有一個Actor引用不代表一個Actor,但作為Root Guardian的偽監督者,我們稱之為“bubble-walker”
- 在實際啟動Actor的建立之前啟動的第一個日誌記錄服務是一個偽Actor引用,它接受日誌事件並將它們直接列印到標準輸出; 它是Logging.StandardOutLogger
什麼是Actor路徑?
由於Actor是以嚴格的層次方式建立的,每個Actor存在一個獨特的序列,這個序列是通過遞迴的從根節點朝下查詢定義的。此序列可以看作是檔案系統中的資料夾,因此我們採用名稱“路徑”來表示它,儘管Actor層次結構與檔案系統層次結構存在一些根本上的區別。
一個Actor路徑包含了anchor,它標識了Actor系統,然後是路徑元素,從Root Guardian到指定的Actor; 路徑元素是遍歷所經過的Actor的名字,並用斜槓分隔。
Actor引用和路徑之間有什麼區別?
一個Actor引用指定了一個單一的Actor,引用的生命週期與Actor的生命週期匹配;一個Actor路徑代表了一個名字,該名字可能是也可能不是指向一個特定的Actor,而且路徑本身沒有生命週期,永遠不會失效。你可以建立一個Actor路徑而不建立一個Actor,但不建立Actor就無法建立Actor引用。
你可以建立一個Actor,終止它,然後建立一個具有相同Actor路徑的新Actor。 新建立的Actor是Actor的一個新例項, 這兩個不是同一個Actor。一個指向舊例項的Actor引用對新例項不起作用。即使具有相同的路徑,傳送到舊Actor引用的訊息,也不會傳遞給新的例項。
Actor路徑anchor
每個Actor路徑都有一個地址部分,描述了訪問這個Actor時所需要的協議和位置,然後是從根遍歷到指定Actor的路徑上的Actor名字(以斜槓分隔),如以下示例:
"akka://my-sys/user/service-a/worker1" //純本地
"akka.tcp://[email protected]:5678/user/service-b" //遠端
這裡,akka.tcp是2.4版本的預設遠端傳輸協議,其他傳輸協議也是可行的。主機和埠部分的解析(即示例中的host.example.com:5678)取決於所使用的傳輸協議的機制, 但它必須遵守URI結構規範。
Actor邏輯路徑
通過遵循從指定Actor沿著父監督者連結到Root Guardian得到的唯一路徑稱為Actor的邏輯路徑。因此只要設定了Actor系統的遠端配置(以及路徑的地址部分),它就可以完全確定了。
Actor物理路徑
雖然Actor邏輯路徑描述了一個Actor系統內的功能位置,但是基於配置的遠端部署方式意味著一個Actor可能在另外一臺網路主機上被建立,即在另一個Actor系統內建立遠端Actor。在這種情況下,遵循從Root Guardian到指定Actor的Actor路徑需要訪問網路,這是一項代價高昂的操作。因此,每個Actor也有一個物理路徑,從Actor物件實際所在的Actor系統的Root Guardian開始。在查詢其他Actor時使用此路徑作為傳送方引用將允許它們直接回復此Actor,從而最大限度地減少路由引起的延遲。
Actor路徑別名或者符號連結?
在某些真實的檔案系統中,你可能會想到一個Actor可能擁有“路徑別名”或“符號連結”,即一個Actor可以使用多個路徑訪問。但是,你應該注意到Actor層次結構與檔案系統層次結構不同。你不能自由地像建立符號連結那樣建立Actor路徑來指向任意的Actor。如上面Actor邏輯和物理路徑部分所述,Actor路徑必須是表示監督層次結構的邏輯路徑,或表示Actor部署的物理路徑。
怎麼獲取Actor引用
一共有兩種方式獲取Actor引用:通過建立或者通過查詢,後面一種又可以具體分為兩種形式:通過具體的Actor路徑獲取Actor引用和查詢Actor的邏輯層次
建立Actors
Actor系統通常是通過使用ActorSystem.actorOf方法在Guardian Actor下建立Actor,然後在創建出的Actor中使用ActorContext.ac-torOf來擴充套件Actor樹。這些方法返回新建立的Actor的Actor引用。 每個Actor都可以直接訪問(通過其ActorContext)其父Actor,自身及其子Actor的引用。這些引用可以通過訊息傳送給其他Actor,使得它們能夠直接回復。
通過具體地址查詢Actor
此外,可以使用ActorSystem.actorSelection方法查詢Actor引用。selection可以用於發出selection請求的Actor與返回的Actor進行通訊,這種selection在每次傳送訊息時都會重新獲取返回的Actor。
要獲取繫結到特定Actor生命週期的ActorRef,你需要向Actor傳送訊息(例如內建的Identify訊息),返回的sender()即為所求。
絕對路徑 vs 相對路徑
除了ActorSystem.actorSelection之外,還存在ActorContext.actorSelection,在任何Actor中都可通過context.actorSelection使用。這會產生一個Actor查詢,就像在ActorSystem上查詢一樣,但它不是從Actor層級結構的根路徑開始查詢,而是從當前Actor開始。由兩個點(“..”)組成的路徑元素可用於訪問父Actor。 例如:向指定的兄弟傳送訊息:
context.actorSelection("../brother") ! msg
也可以使用絕對路徑的方式在上下文中查詢,即
context.actorSelection("/user/serviceA") ! msg
查詢Actor邏輯層次結構
由於Actor系統結構類似檔案系統,因此可以採用與Unix shell支援的相同方式匹配路徑: 你可以用萬用字元(*«*»*和«?»)替換(部分)路徑元素名稱來匹配零個或多個真正的Actors。因為結果不是單一的Actor引用,所以它返回ActorSelection型別,並且不支援ActorRef的完整操作集。可以使用ActorSystem.actorSelection和ActorContext.actorSelection方法制定選擇,並支援傳送訊息:
context.actorSelection("../*") ! msg
這將會把msg傳送給包括當前Actor在內的所有兄弟姐妹。 對於使用actorSelection獲得的引用,通過遍歷監督層次結構以獲得需要執行訊息傳送的Actor引用。由於訊息在傳送過程時,與selection匹配的Actor集合也可能會發生變化,因此無法檢測到選擇的動態變化。為了做到這一點,通過傳送請求並收集所有響應,提取傳送者的引用,然後監控所有發現的具體Actor來解決不確定性。在將來的版本中可以改進這種解析選擇的方案。
總結:actorOf
vs. actorSelection
Note
以上部分詳細描述的內容可以概括和記憶如下:
- actorOf只建立一個新的Actor,並將其建立為呼叫此方法的上下文的直接子節點(可以是任何Actor或Actor系統)
- actorSelection僅在傳遞訊息時查詢現有的Actor,即在選擇建立時不建立Actors或驗證Actors的存在
Actor引用和路徑相等
ActorRef的相等性與ActorRef對應於目標Actor例項的相等性相匹配。當兩個Actor引用具有相同的路徑並且指向同一個Actor的例項時,它們相等。指向已終止Actor的Actor引用與具有相同路徑的另一個(重新建立的)Actor的Actor引用不相等。請注意,由失敗引起的Actor重啟仍然意味著它是相同的Actor例項,即對於ActorRef的使用者來說,重啟是不可見的。
如果你需要跟蹤集合中的Actor引用並且不關心確切的Actor例項,那麼可以使用ActorPath作為鍵,因為在比較Actor路徑時不考慮目標Actor的識別符號。
重用Actor路徑
當Actor被終止時,它的引用將指向dead letter郵箱,DeathWatch將進行最後的處理,並且通常不會再次恢復到活動狀態(因為Actor生命週期不允許這樣)。雖然有可能在以後建立一個具有相同路徑的Actor——不過這並不是一個很好的實踐:通過actorSelection獲取的Actor引用在死亡之後又突然重新開始工作了,並且這個轉變與任何其他的事件沒有任何的順序保證,因此新的Actor可能接收到傳送給之前停掉的Actor的訊息。
在非常特殊的情況下做這件事可能是正確的,但是必須確保只有這個Actor的監督者才有權做這種處理。因為只有它的監督者才能可靠的保證在新的Actor建立之前,舊的Actor名字從系統中登出了。
當測試項依賴於特殊路徑的例項時,也可能需要複用Actor路徑。在這種場景下,最好mock它的監督者,以便在合適的時機傳遞Terminated訊息給測試流程,並確保後續複用的Actor路徑等待路徑名字被登出。
與遠端部署的互操作
當一個Actor建立子Actor時,Actor系統的部署者知道(通過配置檔案或程式設計方式配置)新的Actor物件是在同一個JVM中還是在另一個節點上。在第二種情況下,Actor的建立將通過網路連線觸發,新的Actor在不同的JVM中、不同的Actor系統中被建立。遠端系統將會把該Actor物件放置在專門為遠端建立的Actor保留的特殊路徑上,它的監督者是觸發建立這個Actor的那個遠端Actor。在這種場景下,context.parent(監督者的引用)和context.path.parent(在Actor路徑中的父節點)代表不同的Actor物件。然而,通過檢視其Actor名字時會發現它在遠端節點上,而且仍然保持了邏輯結構。
地址部分的作用是什麼?
通過網路傳送Actor引用時,是由其路徑表示。 因此路徑中的地址字串必須包含協議、host地址、埠號等所有必要的資訊才能正確傳送訊息給底層的Actor。 當Actor系統接收到遠端節點的Actor路徑時,它會檢查該路徑的地址是否與Actor系統的地址匹配,如果匹配,它將被解析為Actor的本地引用。 否則,它會被當作遠端Actor引用。
Actor路徑的頂層作用域
在Actor路徑層級結構的根部是Root Guardian,通過它可以找到所有的Actor。根路徑的名字是一個斜槓“/”,下一層包含了以下Guardian:
- “/user”是所有使用者建立的頂層Actor的Guardian Actor。通過ActorSystem.actorOf建立的Actor在“/user”的下一層
- “/system”是所有系統建立的頂層Actor的Guardian Actor。比如:日誌監聽器或Actor系統啟動時通過配置建立的Actors
- “/deadLetters”是dead letter Actor,所有傳送到停掉的或者不存在的Actor的訊息會被重新路由到該Actor(基於盡力交付:即使在同一個JVM中,訊息也可能丟失)
- “/temp”是所有系統建立的臨時的Actor的Guardian。比如:ActorRef.ask中用到的PromiseActorRef
- “/remote”是一個偽路徑,所有監督者是遠端Actor引用的Actor都存放在這個路徑下
上面這套構造Actor的名稱空間的需求源於一個非常簡單集中的目的:系統層級中的每一項都是一個Actor,並且所有的Actor都以同樣的方式工作。因此,你不僅僅能查詢你自己建立的Actor,你還可以查詢System Guardian並向它傳送訊息(雖然它會把你的訊息丟掉)。這個原則意味著沒有特例需要額外記憶,使得整個系統更加統一和一致。