Akka幫助您在多核的單機(jī)上(“向上擴(kuò)展”或縱向擴(kuò)展)或分布式計(jì)算機(jī)網(wǎng)絡(luò)中(“向外擴(kuò)展”或橫向擴(kuò)展)構(gòu)建可靠的應(yīng)用程序。這里關(guān)鍵的抽象是,你的代碼單元——actor——之間所有的交互都是通過消息傳遞完成,這也是為什么“消息是如何在actor之間傳遞”的準(zhǔn)確語義應(yīng)該擁有自己的章節(jié)。
為了給出下面討論的一些背景,考慮一個(gè)跨越多個(gè)網(wǎng)絡(luò)主機(jī)的應(yīng)用。首先通信的基本機(jī)制是相同的,無論是發(fā)送到一個(gè)在本地JVM中的actor,還是一個(gè)遠(yuǎn)程actor,不過當(dāng)然在投遞延遲上會(huì)有可觀察到的差異(也可能決定于網(wǎng)絡(luò)帶寬和消息大?。┖涂煽啃浴h(yuǎn)程消息發(fā)送,顯然會(huì)有更多步驟,從而意味著更多出錯(cuò)的可能。另一方面,本地消息發(fā)送只會(huì)傳遞一個(gè)本地JVM中消息的引用,所以沒有對發(fā)送的底層對象上做任何限制,而遠(yuǎn)程傳輸將對消息的大小進(jìn)行限制。
如果你在編寫actor時(shí),認(rèn)為每一次消息交互都可能是遠(yuǎn)程的,這是安全但悲觀的賭注。這意味著,只依賴那些始終被保證的特性(下面將詳細(xì)討論這些特性)。這樣做當(dāng)然會(huì)在actor的實(shí)現(xiàn)中帶來一些額外的開銷。如果你愿意犧牲完全的位置透明性——例如有一組密切合作的actor——你可以總是將它們放在同一個(gè)JVM中,并享受更加嚴(yán)格的消息傳遞保證。這種折衷的細(xì)節(jié)在下面會(huì)進(jìn)一步討論。
一般規(guī)則
這些是消息發(fā)送的規(guī)則(即tell或!方法,這也是ask的底層實(shí)現(xiàn)方式):
- 至多一次投遞,即不保證投遞
- 對每個(gè) “發(fā)送者-接收者” 對,有消息排序
第一條規(guī)則是典型的,并在其他actor框架中有出現(xiàn),而第二個(gè)則是Akka獨(dú)有的。
討論:“最多一次”是什么意思?
當(dāng)涉及到描述傳遞機(jī)制的語義時(shí),有三種基本類型:
- 至多一次 <=1投遞的意思是對該機(jī)制下的每條消息,會(huì)被投遞0或1次;更隨意的說法就是,它意味著消息可能會(huì)丟失。
- 至少一次 >=1投遞的意思對該機(jī)制下的每條消息,有可能為投遞進(jìn)行多次嘗試,以使得至少有一個(gè)成功;更隨意的說法就是,消息可能重復(fù),但不會(huì)丟失。
- 恰好一次 == 1投遞的意思對該機(jī)制下的每條消息,接收者會(huì)正好得到一次投遞;消息既不能丟,也不會(huì)重復(fù)。
第一種是最廉價(jià)的——性能最高,實(shí)現(xiàn)開銷最少——因?yàn)樗梢杂么蚝蟛还埽╢ire-and-forget)的方式完成,不需要在發(fā)送端或傳輸機(jī)制中保留狀態(tài)。第二種方式要求重試來對抗傳輸丟失,這意味著需要在發(fā)送端保持狀態(tài),并在接收端使用確認(rèn)機(jī)制。第三種是最昂貴的——并因此表現(xiàn)最差——因?yàn)槌诵枰诙N方式的機(jī)制以外,還需要在接收端保持狀態(tài),以過濾重復(fù)的投遞。
討論:為什么沒有投遞保證?
這個(gè)問題的核心在于這個(gè)擔(dān)保究竟具體是什么意思:
- 消息被發(fā)送在網(wǎng)絡(luò)上?
- 消息被其他主機(jī)接收?
- 消息放到目標(biāo)actor的郵箱中?
- 消息開始被目標(biāo)actor處理?
- 消息被目標(biāo)actor成功處理?
以上每個(gè)擔(dān)保都具有不同的挑戰(zhàn)和成本,并且很明顯,存在一些情況導(dǎo)致任何消息傳遞框架都將無法遵守這些擔(dān)保;想象例如可配置郵箱類型,以及一個(gè)有界的郵箱將如何與第3點(diǎn)互動(dòng),甚或第5點(diǎn)的“成功”由什么決定。
在上面的話中包含著同樣的推理——沒人需要可靠地消息機(jī)制。對發(fā)送者來說,確定交互是否成功的唯一有意義的方式是收到業(yè)務(wù)層級的確認(rèn)消息,這不是Akka可以做到的(我們不會(huì)寫一個(gè)“按我的意思來做”的框架,也沒有人會(huì)想讓我們這樣做)。
Akka擁抱了分布式計(jì)算,將消息傳遞的不可靠性明確化,因此它不會(huì)嘗試說謊和實(shí)現(xiàn)一個(gè)有問題的抽象。這是一個(gè)已經(jīng)在Erlang中大獲成功的模型,它要求用戶圍繞它進(jìn)行設(shè)計(jì)。
對這個(gè)問題的另一個(gè)角度是,通過只提供基本保障,那些無需更強(qiáng)可靠性的用例也就不要支付額外的實(shí)施成本;總是可以在基本的基礎(chǔ)上添加更強(qiáng)的可靠性,但不可能相反移除可靠性,來獲得更高的性能。
討論:消息順序
該規(guī)則更具體的講是,對于給定的一對actor,從第一個(gè)actor直接發(fā)送到第二個(gè)actor的消息不會(huì)被亂序接收。直接這個(gè)詞強(qiáng)調(diào)通過tell操作符直接發(fā)給最終的目的地,而沒有使用中介者或其他信息傳播特性時(shí)(除非另有說明)。
該擔(dān)保說明如下:
Actor A1 發(fā)送消息 M1, M2, M3 到 A2
Actor A3 發(fā)送消息 M4, M5, M6 到 A2
意味著:
- 如果 M1 被投遞,則它必須在 M2 和 M3 前被投遞
- 如果 M2 被投遞,則它必須在 M3 前被投遞
- 如果 M4 被投遞,則它必須在 M5 和 M6 前被投遞
- 如果 M5 被投遞,則它必須在 M6 前被投遞
- A2 可以交織地看到 A1 和 A3 的消息
因?yàn)闆]有投遞保證,以上任意消息都有可能被丟棄,即沒有到達(dá) A2
注意
需要注意的是Akka保證消息被排隊(duì)到收件人郵箱的順序是很重要的。如果郵箱的實(shí)現(xiàn)不遵循FIFO的順序(例如,一個(gè)PriorityMailbox),則actor的處理順序可能偏離排隊(duì)順序。
請注意,這條規(guī)則不具有傳遞性:
Actor A 發(fā)送消息 M1 給 actor C
Actor A 然后發(fā)送消息 M2 給 actor B
Actor B 轉(zhuǎn)發(fā)消息 M2 給 actor C
Actor C 可以以任何順序接收 M1 和 M2
因果型順序傳遞性意味著M2永遠(yuǎn)不會(huì)再M(fèi)1之前到達(dá)actor C(盡管它們中的任何一個(gè)都可能會(huì)丟失)。這種順序性無法保證,因?yàn)橄⒕哂胁煌膫鬟f延遲,例如當(dāng)A,B和C位于不同的網(wǎng)絡(luò)主機(jī)時(shí),詳見下文。
注意:Actor的創(chuàng)建被視為是從父節(jié)點(diǎn)發(fā)送到孩子的消息,和上面的討論具有相同的語義。以消息可以被重排序的方式發(fā)送一個(gè)消息到一個(gè)actor,會(huì)導(dǎo)致消息丟失,因?yàn)閯?chuàng)建的消息也許沒有發(fā)送導(dǎo)致actor還不存在。一個(gè)消息可能過早到來的例子是,創(chuàng)建一個(gè)遠(yuǎn)程部署的actor R1,將其引用發(fā)送給另一個(gè)遠(yuǎn)程actor R2,并且讓R2發(fā)送消息給R1。一個(gè)明確定義排序的例子是一個(gè)父節(jié)點(diǎn)創(chuàng)建一個(gè)actor,并立即向它發(fā)送消息。
失敗消息的傳達(dá)
請注意,上面只討論了actor之間的用戶消息的順序保證。一個(gè)actor的孩子的失敗是通過特殊的系統(tǒng)消息傳達(dá)的,與普通用戶發(fā)送的消息沒有順序關(guān)系。特別是:
子 actor C 發(fā)送 M 給其父節(jié)點(diǎn) P
子 actor 失敗并發(fā)送失敗消息 F
父 actor P 可能以M, F或F, M的順序收到兩個(gè)事件
這樣做的原因是,內(nèi)部系統(tǒng)消息有其自己的郵箱,因此用戶和系統(tǒng)信息的排隊(duì)的順序不能保證其出隊(duì)的時(shí)間順序。
JVM內(nèi)(本地)消息發(fā)送規(guī)則
對本節(jié)介紹的內(nèi)容要小心使用!
不建議依托本節(jié)所介紹的更強(qiáng)可靠性,因?yàn)樗鼤?huì)綁定您的應(yīng)用程序只能進(jìn)行本地部署:為了適應(yīng)在機(jī)器集群上運(yùn)行,應(yīng)用程序可能需要不同的設(shè)計(jì)(而不是僅僅是對actor采用一些本地消息交換模式)。
我們的信條是“一次設(shè)計(jì),按照任何你希望的方式部署”,要實(shí)現(xiàn)這一點(diǎn),你應(yīng)該只依靠一般規(guī)則。
本地消息發(fā)送的可靠性
Akka測試套件依賴于本地上下文沒有消息丟失(以及對遠(yuǎn)程部署的非錯(cuò)誤條件測試也成立),也就是說,我們實(shí)際中確實(shí)是以最大努力來保證我們測試的穩(wěn)定性。然而,就像一個(gè)方法調(diào)用可能在JVM上失敗一樣,本地的tell操作也可能會(huì)因?yàn)橥瑯拥脑蚨。?/p>
StackOverflowErrorOutOfMemoryError- 其他
VirtualMachineError
此外,本地傳輸可以以Akka特定的方式失?。?/p>
- 如果郵箱不接收消息(例如已滿的BoundedMailbox)
- 如果接收的actor在處理消息時(shí)失敗,或actor已終止
第一個(gè)顯然是配置的問題,不過第二個(gè)是值得一些思考的:如果處理的時(shí)候有異常,則消息的發(fā)送者不會(huì)得到反饋,而是將通知發(fā)送給其父監(jiān)管者了。對外部觀察者來說,這和丟失這個(gè)消息沒有區(qū)別。
本地消息發(fā)送順序
假設(shè)使用嚴(yán)格的先進(jìn)先出郵箱,則前面提到的消息非傳遞的排序擔(dān)保,在一定條件下可以被消除。你會(huì)注意到,這些是很微妙的,甚至未來的性能優(yōu)化有可能將本節(jié)的所有內(nèi)容變?yōu)闊o效。反標(biāo)志的一些可能如下:
- 在收到頂層actor的第一個(gè)回應(yīng)之前,有一個(gè)鎖用于保護(hù)內(nèi)部的臨時(shí)隊(duì)列,并且該鎖是非公平的;言下之意是,在actor的構(gòu)造過程中從不同的發(fā)送者發(fā)來的入隊(duì)請求(這里只是比喻,細(xì)節(jié)會(huì)更為復(fù)雜),也許會(huì)因低級別的線程調(diào)度導(dǎo)致重新排序。由于完全公平鎖在JVM上并不存在,這是不可修復(fù)的。
- 路由器(更準(zhǔn)確地說是路由ActorRef)的構(gòu)造過程也是使用相同的機(jī)制,因此對使用路由部署的actor也存在同樣的問題。
- 如上所述,在入隊(duì)過程中任何涉及鎖的地方都會(huì)有此問題,這也適用于自定義郵箱。
這份清單經(jīng)過精心編制,但其他有問題的場景仍然可能會(huì)逃過我們的分析。
本地消息排序和網(wǎng)絡(luò)消息排序如何關(guān)聯(lián)
正如上一段所解釋的,本地消息發(fā)送在一定條件下服從傳遞因果順序。如果遠(yuǎn)程信息傳輸也遵從這個(gè)排序規(guī)則,這將轉(zhuǎn)化為跨越單個(gè)網(wǎng)絡(luò)鏈接的傳遞因果順序,也就是說,如果正好只有兩個(gè)網(wǎng)絡(luò)主機(jī)參與。涉及多個(gè)環(huán)節(jié)則無法作此保證,如上面提到的位于三個(gè)不同節(jié)點(diǎn)的三個(gè)actor。
目前的遠(yuǎn)程傳輸不支持此排序規(guī)則(這同樣是由于鎖的喚醒順序不滿足FIFO,此時(shí)是指連接建立的序列)。
從一個(gè)投機(jī)觀點(diǎn)來看,未來有可能支持這種排序的保證,通過用actor完全重寫遠(yuǎn)程傳輸層來實(shí)現(xiàn);同時(shí)我們正在研究提供如UDP或SCTP的底層傳輸協(xié)議,這將帶來更高的吞吐或更低的延遲,不過將再次刪除此保證,這將意味著在不同的實(shí)現(xiàn)之間進(jìn)行選擇就是在順序擔(dān)保和性能之間進(jìn)行折中。
更高層次的抽象
基于Akka的核心中小而一致的工具集,Akka也在其上提供了強(qiáng)大的,更高層次的抽象。
消息模式
上面討論的實(shí)現(xiàn)可靠投遞的問題,一個(gè)直截了當(dāng)?shù)拇鸢甘鞘褂妹鞔_的ACK-RETRY協(xié)議。其最簡單的形式需要
- 一種方法來識別個(gè)體信息,并將它與確認(rèn)進(jìn)行關(guān)聯(lián)
- 一個(gè)重試機(jī)制,如果沒有及時(shí)確認(rèn),將重新發(fā)送消息
- 一種接收方用來檢測和丟棄重復(fù)消息的方法
第三步是必要的,因?yàn)榇_認(rèn)消息本質(zhì)上也是不能確保到達(dá)的。 一個(gè)企業(yè)級確認(rèn)的ACK-RETRY協(xié)議,在Akka Persistence模塊中以至少一次投遞的方式支持了。至少一次投遞的消息可以通過跟蹤標(biāo)識符的方式進(jìn)行重復(fù)的檢測。實(shí)現(xiàn)第三步的另一種方式是在業(yè)務(wù)邏輯中實(shí)現(xiàn)消息處理的冪等性(譯者注:即每次消息處理的結(jié)果都是一樣的)。
實(shí)現(xiàn)所有三個(gè)要求的另一個(gè)例子在可靠的代理模式中展示了(現(xiàn)在被至少一次投遞所取代)。
事件源
事件源(和分片)使得大型網(wǎng)站能擴(kuò)展到支持?jǐn)?shù)以十億計(jì)的用戶,并且其想法很簡單:當(dāng)一個(gè)組件(想象為actor)處理一個(gè)命令,它會(huì)生成表示該命令效果的一組事件的列表。這些事件除了被應(yīng)用到該組件的狀態(tài)之外,也被存儲(chǔ)。這個(gè)方案的好處是,事件永遠(yuǎn)只會(huì)被附加存儲(chǔ)上,沒有什么是可變的;這使得完美的復(fù)制和擴(kuò)展這一事件流的消費(fèi)者群體得到支持(即其他組件也可以消費(fèi)這個(gè)事件流,只需要復(fù)制組件的狀態(tài)到一個(gè)新的空間中,并且對變化進(jìn)行反應(yīng)即可)。如果組件的狀態(tài)丟失——由于某臺(tái)機(jī)器的故障,或者是被淘汰出緩存——它仍然可以很容易地通過重播事件流(通常使用快照來加快處理)進(jìn)行重建。event-sourcing由Akka Persistence支持。
具有明確確認(rèn)功能的郵箱
通過實(shí)現(xiàn)自定義郵箱類型,有可能在接收actor結(jié)束時(shí)重試消息處理,以處理臨時(shí)故障。這種模式一般在本地通信上下文非常有用,否則投遞擔(dān)保不足以滿足應(yīng)用程序的需求。
請注意,”JVM內(nèi)(本地)消息發(fā)送規(guī)則“中的警告仍然有效。
實(shí)現(xiàn)這種模式的示例展示在郵箱確認(rèn)中。
死信
不能被投遞(并且可以被確定沒有投遞成功)的消息,會(huì)被投遞到一個(gè)名為/deadLetters的人造actor。該投遞以盡力而為為基礎(chǔ);它甚至可以在本地JVM中失?。ɡ鏰ctor終止時(shí))。在不可靠的網(wǎng)絡(luò)傳輸丟失的消息將會(huì)被丟棄,而不會(huì)作為死信處理。
死信應(yīng)該被用來做什么?
這個(gè)組件的主要用途是調(diào)試,特別是如果一個(gè)actor的發(fā)送始終沒有送達(dá)的時(shí)候(通常查看死信,你會(huì)發(fā)現(xiàn)發(fā)件者或接收者在某個(gè)環(huán)節(jié)上設(shè)置錯(cuò)了)。為了能更好地用于該目的,最好的實(shí)踐是,盡可能避免發(fā)送消息到deadLetters,即使用一個(gè)合適的死信日志記錄器(詳見下文)來運(yùn)行應(yīng)用程序,并時(shí)不時(shí)地清理日志輸出。這個(gè)實(shí)踐——和其他所有實(shí)踐類似——需要按照常識構(gòu)建明智的應(yīng)用程序:有可能避免發(fā)送消息給一個(gè)已終止的actor,給發(fā)送者代碼增加的復(fù)雜性,多于調(diào)試輸出帶來的清晰度。
死信服務(wù)與其他所有消息投遞一樣,對于投遞保證遵循相同的規(guī)則的,因此它不能被用來實(shí)現(xiàn)投遞保證。
怎樣接收死信?
一個(gè)actor可以在事件流中訂閱類akka.actor.DeadLetter,請參閱事件流(java)或事件流(scala)來了解如何做到這一點(diǎn)。那么訂閱的actor會(huì)從該點(diǎn)起收到(本地)系統(tǒng)中發(fā)布的所有死信。死信不會(huì)在網(wǎng)絡(luò)上傳播,如果你想在一個(gè)地方收集他們,你將不得不在每個(gè)網(wǎng)絡(luò)節(jié)點(diǎn)上使用一個(gè)actor訂閱,并手動(dòng)轉(zhuǎn)發(fā)。同時(shí)注意,在該節(jié)點(diǎn)上生成的死信,能夠確定一個(gè)發(fā)送操作失敗,這對于遠(yuǎn)程發(fā)送來說,可以是本地系統(tǒng)(如果不能建立網(wǎng)絡(luò)連接)也可以是遠(yuǎn)程系統(tǒng)(如果你要發(fā)送的目標(biāo)actor在該時(shí)間點(diǎn)不存在的話)。
(通常)不用擔(dān)心死信
每當(dāng)一個(gè)actor不是通過自身決定終止的,則存在這樣的可能——它發(fā)送到自身的一些消息丟失了。這在復(fù)雜關(guān)閉場景下是很容易發(fā)生的,而這些場景通常也是良性的:看到一個(gè)akka.dispatch.Terminate消息被丟棄意味著,兩個(gè)中斷請求被發(fā)送,當(dāng)然只有一個(gè)可以成功。同樣道理,在停止一個(gè)子樹的actor時(shí),如果父節(jié)點(diǎn)在終止的時(shí)候仍然觀察著子節(jié)點(diǎn),則你可能會(huì)看到從孩子發(fā)出的akka.actor.Terminated消息會(huì)轉(zhuǎn)變?yōu)樗佬拧?/p>