遠端引用協定¶
本說明文件描述了遠端引用協定的設計細節,並逐步介紹了不同情境下的訊息流。在繼續之前,請確保您熟悉 分散式 RPC 框架。
背景¶
RRef 代表遠端引用。它是位於本地或遠端工作節點上物件的引用,並在底層透明地處理引用計數。從概念上講,它可以被視為分散式共享指標。應用程式可以通過調用 remote() 來建立 RRef。每個 RRef 都由 remote() 調用的被調用工作節點(即所有者)擁有,並且可以由多個使用者使用。所有者儲存實際資料並追蹤全域引用計數。每個 RRef 都可以通過全域唯一的 RRefId 來唯一標識,該 ID 在 remote() 調用的調用者上建立時分配。
在所有者工作節點上,只有一個 OwnerRRef 實例,其中包含實際資料,而在使用者工作節點上,可以根據需要建立任意數量的 UserRRefs,並且 UserRRef 不持有資料。所有者上的所有使用都將使用全域唯一的 RRefId 來檢索唯一的 OwnerRRef 實例。當 UserRRef 在 rpc_sync()、rpc_async() 或 remote() 調用中用作參數或返回值時,將建立一個 UserRRef,並且將通知所有者以更新引用計數。當全域沒有 UserRRef 實例並且所有者上也沒有對 OwnerRRef 的引用時,將刪除 OwnerRRef 及其資料。
假設¶
RRef 協定是在以下假設下設計的。
暫時性網路故障:RRef 設計通過重試訊息來處理暫時性網路故障。它無法處理節點當機或永久性網路分割區。當這些事件發生時,應用程式應關閉所有工作節點,恢復到先前的檢查點,然後繼續訓練。
非冪等 UDF:我們假設提供給
rpc_sync()、rpc_async()或remote()的使用者函數 (UDF) 不是冪等的,因此無法重試。但是,內部 RRef 控制訊息是冪等的,並且在訊息失敗時會重試。無序訊息傳遞:我們不假設任何一對節點之間的訊息傳遞順序,因為發送方和接收方都使用多個執行緒。無法保證哪個訊息會先被處理。
RRef 生命週期¶
協定的目標是在適當的時間刪除 OwnerRRef。刪除 OwnerRRef 的正確時間是當沒有活動的 UserRRef 實例並且使用者程式碼也沒有持有對 OwnerRRef 的引用時。棘手的部分是如何確定是否存在任何活動的 UserRRef 實例。
設計思路¶
使用者可以在三種情況下獲得 UserRRef
從所有者處接收
UserRRef。從另一個使用者處接收
UserRRef。建立一個由另一個工作節點擁有的新
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。
協定情境¶
現在讓我們在四種情境中討論上述設計如何轉換為協定。



