遠端參考協定¶
本筆記描述了遠端參考協定的設計細節,並逐步介紹了不同場景中的訊息流程。在繼續之前,請確保您熟悉分散式 RPC 框架。
背景¶
RRef 代表 Remote REFerence(遠端參考)。它是一個指向位於本地或遠端 worker 上的物件的參考,並且在底層透明地處理參考計數。從概念上講,它可以被視為一個分散式共享指標。應用程式可以透過呼叫 remote()
來建立 RRef。每個 RRef 都由 remote()
呼叫的被呼叫方 worker(即擁有者)所擁有,並且可以被多個使用者使用。擁有者儲存真實資料並追蹤全域參考計數。每個 RRef 都可以透過一個全域 RRefId
來唯一識別,該 RRefId
在 remote()
呼叫的呼叫方建立時被分配。
在擁有者 worker 上,只有一個 OwnerRRef
實例,它包含真實資料,而在使用者 worker 上,可以有任意數量的 UserRRefs
,並且 UserRRef
不持有資料。在擁有者上的所有使用都將使用全域唯一的 RRefId
來檢索唯一的 OwnerRRef
實例。當 UserRRef
在 rpc_sync()
、rpc_async()
或 remote()
呼叫中被用作參數或傳回值時,將會建立一個 UserRRef
,並且將會通知擁有者以更新參考計數。當全域沒有 UserRRef
實例,並且在擁有者上也沒有對 OwnerRRef
的參考時,OwnerRRef
及其資料將會被刪除。
假設¶
RRef 協定是基於以下假設設計的。
暫時性網路故障:RRef 設計透過重試訊息來處理暫時性網路故障。它無法處理節點崩潰或永久性網路分割。當這些事件發生時,應用程式應關閉所有 worker,恢復到先前的檢查點,然後恢復訓練。
非冪等的 UDF:我們假設提供給
rpc_sync()
、rpc_async()
或remote()
的使用者函數 (UDF) 不是冪等的,因此無法重試。但是,內部 RRef 控制訊息是冪等的,並且在訊息失敗時會重試。亂序訊息傳遞:我們不假設任何一對節點之間的訊息傳遞順序,因為傳送方和接收方都在使用多個執行緒。無法保證哪個訊息將首先被處理。
RRef 生存期¶
該協定的目標是在適當的時間刪除一個 OwnerRRef
。刪除 OwnerRRef
的正確時間是當沒有存活的 UserRRef
實例,並且使用者程式碼也沒有持有對 OwnerRRef
的參考時。棘手的部分是確定是否有任何存活的 UserRRef
實例。
設計原理¶
使用者可以在三種情況下獲得 UserRRef
從擁有者接收
UserRRef
。從另一個使用者接收
UserRRef
。建立一個由另一個 worker 擁有的新
UserRRef
。
情況 1 最簡單,擁有者將其 RRef 傳遞給使用者,其中擁有者呼叫 rpc_sync()
、rpc_async()
或 remote()
並將其 RRef 用作參數。在這種情況下,將會在使用者上建立一個新的 UserRRef
。由於擁有者是呼叫方,因此它可以輕鬆更新其在 OwnerRRef
上的本地參考計數。
唯一的要求是任何 UserRRef
都必須在銷毀時通知擁有者。因此,我們需要第一個保證
G1. 當任何 UserRRef 被刪除時,擁有者將會收到通知。
由於訊息可能延遲或亂序到達,我們需要另一個保證來確保刪除訊息不會太早被處理。如果 A 向 B 發送一個涉及 RRef 的訊息,我們將 A 上的 RRef 稱為父 RRef,將 B 上的 RRef 稱為子 RRef。
G2. 在擁有者確認子 RRef 之前,父 RRef 不會被刪除。
在情況 2 和 3 中,擁有者可能只有關於 RRef 分叉圖的部分或完全沒有資訊。例如,可以在使用者上建構一個 RRef,並且在擁有者收到任何 RPC 呼叫之前,建立者使用者可能已經與其他使用者共享了該 RRef,並且這些使用者可能會進一步共享該 RRef。一個不變量是,任何 RRef 的分叉圖始終是一棵樹,因為分叉 RRef 總是在被呼叫者上建立一個新的 UserRRef
實例(除非被呼叫者是擁有者),因此每個 RRef 都有一個父項。
擁有者對樹中任何 UserRRef
的視圖都有三個階段
1) unknown -> 2) known -> 3) deleted.
擁有者對整個樹的視圖不斷變化。當擁有者認為沒有存活的 UserRRef
實例時,它會刪除其 OwnerRRef
實例,也就是說,當 OwnerRRef
被刪除時,所有 UserRRef
實例可能確實被刪除或未知。危險的情況是當一些分叉是未知的,而另一些分叉已被刪除時。
G2 簡單地保證了在擁有者知道其所有子 UserRRef
實例之前,沒有父 UserRRef
可以被刪除。但是,子 UserRRef
可能在擁有者知道其父 UserRRef
之前被刪除。
考慮以下範例,其中 OwnerRRef
分叉到 A,然後 A 分叉到 Y,而 Y 分叉到 Z。
OwnerRRef -> A -> Y -> Z
如果 Z 的所有訊息(包括刪除訊息)都在 Y 的訊息之前由擁有者處理,則擁有者會在知道 Y 存在之前得知 Z 已被刪除。然而,這不會造成任何問題。因為至少 Y 的一個祖先 (A) 將會存活,並且它會阻止擁有者刪除 OwnerRRef
。更具體地說,如果擁有者不知道 Y,則由於 G2,A 無法被刪除,並且擁有者知道 A,因為它是 A 的父節點。
如果 RRef 是在使用者上建立的,事情會變得有點棘手。
OwnerRRef
^
|
A -> Y -> Z
如果 Z 在 UserRRef
上呼叫 to_here()
,則當 Z 被刪除時,擁有者至少知道 A,因為否則 to_here()
將無法完成。如果 Z 沒有呼叫 to_here()
,則擁有者有可能在收到來自 A 和 Y 的任何訊息之前收到來自 Z 的所有訊息。在這種情況下,由於 OwnerRRef
的實際資料尚未建立,因此也沒有任何東西需要刪除。這與 Z 根本不存在相同。因此,仍然沒問題。
實作¶
G1 是透過在 UserRRef
解構函式中發送刪除訊息來實作的。為了提供 G2,父 UserRRef
會在每次分叉時被放入一個上下文中,並以新的 ForkId
進行索引。父 UserRRef
只有在收到來自子節點的確認訊息 (ACK) 時才會從上下文中移除,並且子節點只有在獲得擁有者的確認後才會發送 ACK。
協定情境¶
現在讓我們討論上述設計如何在四種情境中轉換為協定。