Ray2-0 架構 - 中文翻譯版(Part2)
原文地址:https://docs.ray.io/en/latest/ray-contribute/whitepaper.html
Object 管理
通常,小的對象存儲在所有者的進程內存儲中,而大的對象存儲在分佈式對象存儲中。爲了減少每個對象的內存佔用和解析時間。注意,在後一種情況下,佔位對象(placeholder object)存儲在進程內存儲中,以表明該對象實際上存儲在分佈式對象存儲中。
進程內存儲中的對象可以通過內存複製快速解析,但由於額外的複製,當許多進程引用時,可能會佔用更大的內存。單個工作程序的進程內存儲的容量也受限於該機器的內存容量,從而限制了在任何給定時間可以引用的此類對象的總數。對於多次引用的對象,吞吐量也可能受到所有者進程處理能力的限制。
相比之下,解析分佈式對象存儲中的對象需要至少一個從工作程序到工作程序的本地共享內存存儲的 RPC。如果工作進程的本地共享內存存儲尚未包含對象的副本,則可能需要其它 RPC 連接。另一方面,因爲共享內存存儲是用共享內存實現的,所以同一節點上的多個工作人員可以引用對象的同一副本。如果對象可以,這可以減少總內存佔用。分佈式內存的使用還允許進程引用對象,而不需要對象本地,這意味着 zero-copy 反序列化可以使用超過單個機器的內存總容量限制。吞吐量可以隨分佈式對象存儲中的節點數量而變化,因爲對象的多個副本可能存儲在不同的節點上。
Object 轉換(Object resolution)
Object resolution 是將 ObjectRef
轉換爲普通值的過程,例如在使用 get 或者作爲任務參數傳遞時會自動轉換。
ObjectRef
包含兩個字段:
-
一個唯一的 28 字節,詳情見 https://github.com/ray-project/ray/blob/master/src/ray/design_docs/id_specification.md
-
對象所有者( worker 進程)的地址。這包括 worker 進程的唯一 ID、IP 地址和端口以及本地 raylet 的唯一 ID。
通過直接從所有者的進程內的存儲複製小對象來解析小對象。例如,如果所有者調用 “ray.get”,系統將查找並反序列化本地進程內存儲中的值。如果所有者提交了一個含有依賴任務,它將通過將值直接複製到 task specification 中來內聯對象。類似地,如果借用者試圖解析值,則對象值將直接從所有者處複製。
如果是個大對象會經過上面圖中的流程進行解析。
對象 x 是在 Node 2 創建的,當 Owner 調用這個對象時會先查找對象的位置,併發出副本請求從 Node 2 中複製,然後 Node 1 接收這個對象。
大型對象存儲在分佈式對象存儲中,必須使用分佈式協議進行解析。如果對象已經存儲在引用持有者的本地共享內存存儲中,則引用持有者可以通過 IPC 檢索對象。這將返回一個指向共享內存的指針,該內存可能同時被同一節點上的其它 worker 引用。
如果該對象在本地共享內存存儲中不可用,則引用持有者會通知其本地 raylet,然後它會嘗試從遠程 raylet 獲取副本。raylet 從對象目錄中查找位置,並從這些 raylet 之一請求傳輸。自 Ray v1.3+ 起,對象目錄存儲在所有者處(以前存儲在 GCS 中)。
Memory management
對於遠程任務,對象值由正在執行的工作程序計算。如果值很小,worker 將直接向所有者回復值,並將其複製到所有者的進程內存儲中。一旦所有引用超出範圍,此值將被刪除。
主副本與可收回副本。主副本(節點 2)不符合逐出資格。但是,節點 1(通過 “ray.get” 創建)和節點 3(通過任務提交創建)上的副本可以在內存不夠時被逐出。
如果該值較大,則執行工作程序將該值存儲在其本地共享內存存儲中。共享內存對象的初始副本稱爲主副本。主副本是唯一的。
因爲只要範圍中有引用,它就不會被釋放。raylet 通過保存對存儲對象的物理共享內存緩衝區的引用來 “鎖定” 主副本,從而防止對象存儲區將其逐出。相反,如果在本地內存不夠時,對象的其它副本可能會被 LRU 逐出,除非開發人員在使用這個對象。
在大多數情況下,主副本是要創建的對象的第一個副本。如果初始副本因故障而丟失,所有者將嘗試根據對象的可用位置指定新的主副本。
一旦對象引用計數變爲 0,對象的所有副本最終都會被自動垃圾收集。所有者會立即從進程中存儲中刪除小對象。raylets 將從分佈式對象存儲中異步刪除大型對象。
raylets 還管理分佈式對象傳輸,該傳輸基於對象當前需要的位置創建對象的其他副本,例如,如果依賴於對象的任務被調度到遠程節點。
由於以下任何原因,對象可能存儲在節點的共享內存對象存儲中:
-
它是由本地工作進程通過 “ray.get” 或 “ray.wait” 請求的。一旦工作進程完成 “ray.get” 請求,就可以釋放這些資源。注意,對於可以是 zero-copy,從 “ray.get” 返回的 Python 的值直接引用共享內存緩衝區,因此對象將被 “固定”,直到該 Python 值超出範圍。
-
它由在該節點上執行的前一個任務返回。一旦沒有對對象的更多引用,或者一旦對象被引用,這些對象就可以被釋放。
-
它是由該節點上的本地工作進程通過 “ray.put” 創建的,一旦不再引用對象(上圖中節點 1 上的對象 A、B 和 C),就可以釋放這些對象。
-
它是在該節點上排隊或執行的任務的參數。一旦任務完成或不再排隊,就可以釋放這些資源。節點 2 上的對象 B 和 C 都是這樣的例子,因爲它們的下游任務 g 和 h 尚未完成。
-
此節點以前需要它,例如,已完成的任務需要它。節點 2 上的對象 A 就是一個例子,因爲 f 已經完成了執行。如果內存不足,這些對象可能會基於本地 LRU 被逐出。當 ObjectRef 超出範圍時,它們也會被快速釋放(例如,在 f 完成並調用 “del A” 之後,A 從節點 2 中刪除)。
內存不足的情況
對於小對象,Ray 當前不會對每個工作進程的進程存儲施加內存限制。你需要確保小對象不會太多,導致所有者進程因內存不足而被終止。
Ray 對共享內存對象施加限制由 raylet 負責強制執行此限制。下面是可以存儲在節點上的不同類型的共享內存對象的可視化,具有基本的優先級。
對象創建請求由 raylet 排隊,並在(6)中有足夠的內存用於創建對象時提供服務。如果需要更多內存,raylet 將選擇要從(3)-(5)中逐出的對象以騰出空間。即使在所有這些對象被逐出後,raylet 也可能沒有空間用於新對象。如果應用程序所需的總內存大於集羣的內存容量,就會發生這種情況。
如果驅逐後需要更多的空間,raylet 首先會在整個集羣中的每個 worker 處觸發特定於語言的垃圾收集。在語言前端看到的 ObjectRef 似乎很小,因此不太可能觸發通常的特定語言垃圾回收機制。然而,ObjectRef 的實際內存佔用可能非常大,因爲物理值存儲在 Ray 的對象存儲中的其它位置,並且可能存儲在與語言級別 ObjectRef 不同的節點上。因此,當任何 Ray 對象存儲達到容量時,我們會在所有工作線程上觸發語言級別的垃圾收集,這將清除所有不需要的 ObjectRef,並允許從對象存儲中釋放物理值。
raylet 會在觸發溢出之前,讓 worker 有時間異步垃圾收集 ObjectRef。溢出允許從對象存儲中釋放(2)中的主副本,即使對象仍然可以被引用。如果禁用溢出,則應用程序將在可配置超時後接收 ObjectStoreFullError。溢出可能代價很高,並且增加任務執行的長時間;因此,一旦對象存儲達到可配置閾值(默認爲 80%),Ray 也會急快速溢出對象,以確保可用空間。
注意,即使啓用了對象溢出,對象存儲仍可能耗盡內存。如果同時使用的對象太多(1),則會發生這種情況。爲了減輕這種情況,raylet 限制了正在執行的任務的參數的總大小,因爲在任務完成之前無法釋放參數。默認上限爲對象存儲內存的 70%。這確保了只要沒有其他對象因 “ray.get” 請求而被活動鎖定,任務就可以創建一個對象存儲容量的 30%。
目前,raylet 沒有爲 worker 的 “ray.get” 請求的對象實現類似的上限,因爲這樣做可能會導致任務之間的死鎖。因此,如果對大型對象有過多的併發 “ray.get” 請求,raylet 仍可能耗盡共享內存。發生這種情況時,raylet 會將對象分配爲本地磁盤上的內存映射文件(默認情況下爲 / tmp)。由於 I/O 開銷,這樣分配的對象性能較差,但即使對象存儲已滿,它也允許應用程序繼續運行。如果本地磁盤已滿,則分配將失敗,之後應用程序將收到 OutOfDiskError。
對象溢出(Object spilling)
Ray 默認支持在對象存儲的容量用完後將對象溢出到外部存儲。
外部存儲通過可插拔接口實現。默認情況下支持兩種類型的外部存儲:
本地存儲。默認情況下選擇本地磁盤,這樣 Ray 用戶就可以使用對象溢出功能,而無需任何額外配置。
分佈式存儲(實驗性,目前提供 Amazon S3)。訪問速度可能較慢,但這可以提供更好的容錯性,因爲數據可以在工作節點故障後存活。
對象溢出由幾個部分構成:
raylet 內
-
本地對象管理器:跟蹤對象元數據,例如外部存儲中的位置,並協調 IO woker 和與其它 raylet 的通信。
-
共享內存對象存儲
IO workers
用於溢出和恢復對象的 python 進程。
外部存儲
用於存放無法放入共享內存對象存儲的對象。
raylet 管理一個 I/O worker 池。I/O worker 從本地共享內存對象存儲和外部存儲進行讀 / 寫。
當 Ray 沒有足夠的內存容量來創建對象時,它會引發對象溢出。請注意,Ray 只溢出對象的主副本:這是通過執行任務或通過 “Ray.put” 創建的對象的初始副本。非主副本可以立即被逐出,這種設計確保了集羣中每個對象最多有一個溢出的副本。只有在對象溢出後,或者應用程序中沒有更多引用時,主副本纔可收回。
協議如下所示,重複執行,直到留出足夠的空間來創建任何需要的對象:
-
Raylet(本地對象管理器)查找本地對象存儲中的所有主副本。
-
Raylet 將這些對象的溢出請求發送給 IO worker。
-
IO worker 將對象值及其元數據寫入外部存儲。
-
一旦主副本溢出到外部存儲,raylet 將使用溢出對象的位置更新對象目錄。
-
對象存儲區收回主副本。
-
一旦對象的引用計數變爲 0,所有者就會通知 raylet 可以刪除該對象。raylet 向 IO worker 發送請求,以從外部存儲中刪除對象。
溢出的對象將根據需要恢復。當請求對象時,Raylet 要麼通過向本地 IO worker 發送恢復請求從外部存儲恢復對象,要麼從不同節點上的 Raylet 獲取副本。遠程 Raylet 可能會將對象溢出到本地存儲(例如,本地 SSD)上。在這種情況下,遠程 raylet 直接從本地存儲讀取對象並將其發送到網絡。
由於 IO 開銷,每個文件一個對象溢出許多小對象是低效的。對於本地存儲,操作系統將很快耗盡 inode。如果對象小於 100MB,Ray 會將對象融合到單個文件中以避免此問題。
Ray 還支持多目錄溢出,這意味着它使用安裝在不同位置的多個文件系統。當多個本地磁盤連接到同一臺機器時,這有助於提高溢出帶寬和最大外部存儲容量。
目前存在的限制:
-
使用本地文件存儲時,如果存儲溢出對象的節點丟失,則溢出對象將丟失。在這種情況下,Ray 將嘗試恢復對象,就像它從共享內存中丟失一樣。
-
如果所有者丟失,則無法訪問溢出的對象,因爲所有者存儲對象的位置。
-
應用程序當前正在使用的對象被 “pinned”。例如,如果 Python 的 Driver 有一個指向 ray.get 獲得的對象的原始指針(例如,共享內存上的 numpy 數組),則該對象將被固定。在應用程序釋放這些對象之前,它們是不可使用溢出機制的。正在運行的任務的參數也固定在任務的持續運行的時間內,運行結束後纔可以。
引用計數
每個 worker 存儲其所擁有的每個對象的引用計數。所有者的本地引用計數包括本地 Python 引用計數和作爲所有者提交的任務所依賴的對象。當 Python 的 “ObjectRef” 被釋放時,前者將遞減。當依賴於對象的任務成功完成時(注意,以應用程序級異常結束的任務視爲成功),後者將遞減。
ObjectRef
也可以通過將它們複製到另一個進程。接收 “ObjectRef” 副本的過程稱爲借用者。例如:
@ray.remote
def temp_borrow(obj_refs):
# Can use obj_refs temporarily as if I am the owner.
x = ray.get(obj_refs[0])
@ray.remote
class Borrower:
def borrow(self, obj_refs):
# We save the ObjectRef in local state, so we are still borrowing the object once this task finishes.
self.x = obj_refs[0]
x_ref = foo.remote()
temp_borrow.remote([x_ref]) # Passing x_ref in a list will allow `borrow` to run before the value is ready.
b = Borrower.remote()
b.borrow.remote([x_ref]) # x_ref can also be borrowed permanently by an actor.
通過跟蹤這些引用。簡言之,每當引用 “逃離” 本地作用域時,所有者就會添加到本地引用計數中。例如,在上面的代碼中,當調用 “temp_borrow.remote” 和“b.borrow.remoto” 時,所有者會增加 x_ref 的掛起任務計數。一旦任務完成,它會向所有者回復一個仍在借用的引用列表。例如,在上述代碼中,“temp_borrow” 的 worker 會回答說,它不再借用 “x_ref”,而 “Borrower” 的 worker 會回答說它仍在借用 “x_ref”。
如果 worker 仍在借用任何對象,所有者會將 worker 的 ID 添加到本地的 borrowers 列表中。borrowers 保持第二個本地參考計數,與所有者類似,一旦 borrowers 的本地參考計數變爲 0,所有者要求 borrowers 回覆。此時,所有者可以將 worker 從 borrowers 列表中刪除並收集對象。在上述示例中,“borrowers” 的 worker 正在永久借用引用,因此所有者在 “borrowers” 自身超出範圍或死亡之前不會釋放對象。
borrowers 也可以遞歸地添加到所有者列表中。如果 borrowers 本身將 “ObjectRef” 傳遞給另一個進程,就會發生這種情況。在這種情況下,當 borrowers 響應所有者其本地引用計數爲 0 時,它還包括其創建的任何新 borrowers 。所有者反過來使用相同的協議聯繫這些新的 borrowers。
根據上述一共包含下面不同的引用計數:
本地 python 引用計數
等於 worker 進程中 python 的引用技術,在取消或者分配 python 中的 ObjectRef 遞增或遞減。
提交任務計數
依賴於尚未完成執行的對象的任務數。當 worker 提交任務時遞增。當任務完成時遞減。如果對象足夠小,可以存儲在進程內存儲中,則在將對象複製到 Task specification 中時,此計數會提前遞減。
借用者(Borrowers)
當前借用 “ObjectRef” 的進程的一組工作 ID。借用者是一個 worker 但不是所有者並且擁有 Python 本地實例的 ObjectRef,每個借用者還會維護一個本地的借用者列表,允許借用者將 “ObjectRef” 發送給另一借用者,而無需聯繫所有者 。當任務被傳遞一個 ObjectRef 並在任務結束後繼續使用它時,該任務通知其調用方它正在借用該對象。然後,被調用的 worker 將任務 的 worker 的 ID 添加到此集合中。
當 ObjectRef 的引用計數爲 0 時,如果是所有者本身會自動刪除,所有者向每個 ### 借用者發送異步 RPC。借用者在收到後將其刪除,如果無法聯繫到借用者,會從列表中刪除。
如果借用者被移除,worker 會等待來自所有者的 rpc,一旦 worker 本地的引用計數爲 0,worker 就會將借用者彈出並告知所有者。
嵌套計數(Nested count)
在作用域中且其值包含有 ObjectRef
的 ObjectRef
數。
譜系計數(Lineage count)
啓用對象重建時使用。依賴於此 “ObjectRef” 且其值存儲在分佈式對象存儲中(可能在失敗時丟失)的任務數。在提交依賴於對象的任務時遞增。如果任務返回的 “ObjectRef” 超出範圍,或者任務完成並在進程內存儲中返回值,則遞減。
Corner cases
x_ref = foo.remote()
@ray.remote
def capture():
ray.get(x_ref) # x_ref is captured. It will be pinned as long as the driver lives.
創建引用的常規做法是將 ObjectRef 作爲任務參數直接傳遞給其它 worker,或者在數據結構(如列表)內部傳遞。也可以通過使用 “ray.cloudpickle” 對 “ObjectRef” 進行額外引用。在上面代碼下,ray 無法跟蹤對象的序列化副本或確定 ObjectRef 何時已反序列化(例如,如果 ObjectRef 由非 Ray 進程反序列化)。因此,將向對象的計數添加一個永久引用,以防止對象超出範圍。
帶 out-of-band 序列化的其它方法包括使用 “pickle” 或自定義序列化方法。與上述類似,Ray 無法跟蹤這些引用。訪問反序列化的 ObjectRef(即通過調用 “ray.get” 或作爲任務參數傳遞)可能會導致引用計數異常。
Actor handles
用於跟蹤(非分離)Actor 的生命週期。虛擬對象用於代表 Actor。此對象的 ID 是根據 Actor 創建任務的 ID 計算的。Actor 的創建者擁有虛擬對象。
當 Python 的 Actor handle 被釋放時,這會減少虛擬對象的本地引用計數。當在 Actor handle 上提交任務時,這會增加虛擬對象的已提交任務計數。當一個 Actor handle 被傳遞給另一個進程時,接收進程被算作虛擬對象的借用者。一旦引用計數達到 0,所有者就通知 GCS 服務可以安全地銷燬 Actor。
Actor 是不會被 Ray 自動回收的,需要顯式刪除。
與 Python GC
當對象是 Python 中引用循環的一部分時,Python 不能保證這些對象會被及時地垃圾回收。所有 ObjectRef 可以在分佈式對象存儲中惡意地保持 Ray 對象的活動狀態,當對象存儲接近容量時,Ray 會週期性地在所有 Python worker 中觸發 “gc.collect()”。這確保 Python 引用循環不會導致虛假的對象存儲已滿狀態。
Object Failure
在發生系統故障時,Ray 將嘗試恢復任何丟失的對象,如果無法恢復,並且 worker 試圖獲取對象的值,則會引發應用程序級異常。
在更高層次上,Ray 保證如果所有者仍然活着,將嘗試恢復對象。如果恢復失敗,所有者將在異常中填寫原因。如果對象的所有者已經死亡,任何試圖獲取該值的 worker 都會收到一個關於所有者死亡的異常,即使對象副本仍然存在於集羣中。
小對象
小對象存儲在所有者的進程中對象存儲中,因此如果所有者死亡,小對象將丟失。任何試圖在未來獲取對象值的 worker 都將收到所有者已死亡的異常,並將錯誤存儲在本地進程內對象存儲中。
大對象和譜系重建
如果不存在其它副本,Ray 將嘗試通過恢復對象。這是指通過重新執行創建對象的任務來恢復丟失的對象。如果任務的依賴關係也丟失,或者以前由於垃圾收集而被逐出,那麼這些對象將被遞歸地重建。
譜系重建通過在每個對象旁邊保持額外的 “譜系引用計數” 來工作。這是指依賴於對象本身可能被重新執行的任務數。如果任務或下游任務返回的任何對象仍在範圍內,則可以重新執行任務。一旦譜系引用計數達到 0,Ray 將垃圾回收創建對象的 task specification。請注意,這是一個獨立於對象值的垃圾收集機制:如果對象的直接引用計數達到 0,則即使其譜系計數保持在範圍內,其值也將從 Ray 的對象存儲中進行垃圾收集。
請注意,譜系重建可能會導致比通常更高的 Driver 內存使用率。如果總大小超過系統範圍閾值(默認值爲 1GB),每個 Ray 工作程序將嘗試回收其本地緩存的譜系計數。
譜系重建目前有以下限制。如果應用程序不滿足這些要求,那麼它將收到一個重建失敗的異常:
-
對象及其任何傳遞依賴項必須由任務(actor or non-actor)生成。這意味着 ray.put 創建的對象不可恢復。請注意,由 “ray.put” 創建的對象始終與其所有者存儲在同一節點上,所有者將最終與該節點共享;因此,在 “ray.put” 對象的主副本丟失的情況下,應用程序將收到一個通用的 “OwnerDiedError”。
-
任務被假定爲確定性和冪等性的。因此,默認情況下,由 Actor 任務創建的對象是不可重構的。如果用戶將參與者的 “max_task_retrys” 和 “max_restarts” 設置爲非零值,則可以作爲譜系的一部分重新執行參與者任務。
-
任務將僅重新執行其最大重試次數。默認情況下非 Actor 最多重試 3 次,Actor 不能重試。你可以通過 “max_retrys” 和 “max_task_retry” 參數修改。
-
對象的所有者必須仍然活着。
如果存儲在分佈式內存中的對象的所有者丟失:在對象解析期間,raylet 將嘗試定位對象的副本。同時,raylet 將定期與所有者聯繫,以檢查所有者是否還活着。如果所有者已死亡,raylet 將存儲一個系統級錯誤,該錯誤將在對象解析期間拋出給引用持有者。
任務管理
任務執行
任務調用方在從分佈式調度程序請求資源之前等待創建所有任務參數可用。在許多情況下,任務的調用者也是任務參數的所有者。任務的調用方可能借用了任務參數,即它從所有者處收到了參數 “ObjectRef” 的反序列化副本。在這種情況下,任務調用方必須通過與參數所有者執行協議來確定參數是否已創建。借用進程將在反序列化 “ObjectRef” 時與所有者聯繫。一旦創建了對象,所有者就會做出響應,借用者會將對象標記爲就緒。如果所有者失敗,借用者也會將該對象標記爲已準備好,因爲對象的命運與所有者共享。
任務可以有三種類型的參數:plain values, inlined objects, 和 non-inlined objects.
plain values 不需要依賴關係解析。
inlined objects 是足夠小的對象,可以存儲在進程內存儲中(默認閾值爲 100KB)。調用者可以將這些直接複製到 task specification 中。
non-inlined objects 是存儲在分佈式對象存儲中的對象。其中包括大對象和已被所有者以外的進程借用的對象。在這種情況下,調用者將要求 raylet 在調度決策期間說明這些依賴關係。raylet 將等待這些對象成爲其節點的本地對象,然後再給予依賴這個任務的 worker 。這確保了正在執行的工作程序在接收任務時不會阻塞(等待對象變爲本地對象)。
資源調度實現
任務調用程序通過首先向請求的首選 raylet 發送資源請求來調度任務。可選擇以下任一項:
-
按數據位置:如果任務的對象參數存儲在共享內存中,則調用方選擇本地對象參數最多的節點。該信息通過調用方的本地對象目錄檢索,可能是過時的(例如,如果同時發生對象傳輸或逐出)。
-
按節點關聯:如果目標 raylet 使用了 NodeAffinitySchedulengStrategy 指定。
-
默認情況下使用本地的 raylet。
首選 raylet 對請求進行排隊,如果它要給予資源,則使用當前租給調用者的本地 worker 的地址來響應調用者。只要主動請求方和租用的 worker 還活着,租約就保持活動狀態,並且 raylet 確保在租約處於活動狀態時,其他客戶端都不能使用該 worker。爲了確保公平性,如果沒有剩餘的任務或已經過了足夠的時間(例如,幾百毫秒),調用方將返回空閒的工作程序。
請求方可以將任意數量的任務調度到租用的 worker 上,只要這些任務與授權的資源請求兼容即可。因此,租用可以被認爲是一種以避免與調度器進行類似調度請求的通信優化方案。調度請求可以重用租用的工作人員,如果它具有以下相同的條件:
-
資源規模,如 CPU:1。
-
共享內存任務參數,因爲這些參數必須在任務執行之前在節點上設置爲本地。注意,小任務參數不需要匹配,因爲這些參數被內聯到任務參數中。此外,在數據結構內部傳遞的 ObjectRef 不需要匹配,因爲 Ray 不會在任務開始之前將這些 ObjectRef 設置爲本地。
-
運行時環境,因爲租用的工作程序在此環境中啓動。
調用方可以持有多個 worker 租約以提高並行性。worker 租約在多個任務之間緩存,可以以減少調度程序的負載。
如果首選 raylet 選擇不在本地給予資源,它還可以使用遠程 raylet 的地址來響應調用者,調用者應該在該地址重試資源請求。這就是所謂的溢出調度。遠程 raylet 可以根據其本地資源的當前可用性同意或拒絕資源請求。如果資源請求被拒絕,則調用者將再次從首選 raylet 請求,並且重複相同的過程,直到某個 raylet 授予資源請求。
資源管理和調度
Ray 中的資源是一個鍵 - 值對,其中鍵表示資源名稱,值是一個浮點數。爲了方便起見,Ray 調度器具有對 C PU、GPU 和內存資源類型默認支持。Ray 的資源是邏輯上的資源,不需要與物理資源進行 1 對 1 映射,默認情況下,Ray 將每個節點上的邏輯資源數量設置爲 Ray 自動檢測到的物理數量。
用戶還可以使用自定義資源需求,例如,指定資源需求{“custom_resource”:0.01}。可以在啓動時向節點添加自定義資源。
分佈式調度程序會嘗試匹配集羣中合適的資源,如一個任務要求 {“CPU”:1.0,“GPU”:1.0,那麼此任務只能在 CPU >=1 和 GPU >=1 的節點上調度。默認每個@ray.remote
函數一定會需要 1 個 CPU 來運行。對於 actor 來說,默認是 0 個 CPU。這樣一來,單個節點可以承載比其核心更多的 actor,從而將 CPU 儘可能留給操作系統。
有一些資源需要特殊處理:
-
CPU、GPU 和 “內存” 的數量在 Ray 啓動期間自動檢測。
-
將 GPU 資源分配給任務將自動設置到 worker 的 CUDA_VISIBLE_DEVICES 系統變量,通過 ID 的方式限制使用的 GPU。
注意,因爲資源請求是邏輯上的,所以 Ray 不會強制執行物理資源限制。用戶可以指定準確的資源需求,例如,爲具有 n 個線程的任務指定 “num_cpus=n” 。
分佈式調度
資源統計
每個 raylet 跟蹤所在節點的資源。當授予資源請求時,raylet 會相應地減少可用的本地資源。一旦資源被釋放(或請求者死亡),raylet 就會相應地增加本地資源的可用性。因此,raylet 始終具有本地資源可用性的高度一致的視圖。
每個 raylet 還從 GCS 接收關於集羣中其他節點上資源可用性的信息。這用於分佈式調度,例如,跨集羣中的節點進行負載平衡。爲了減少收集和傳播的開銷,這些信息是最終一致性的,可能會存在過時的問題。信息通過定期廣播發送。GCS 定期(默認情況下爲 100ms)從每個 Ray 節點獲取可用的資源,然後將其聚合並廣播回每個 Ray 節點。
調度狀態機
當 raylet 接收到資源請求(即 RequestWorkerLease PRC)時,它將通過上述狀態機並以下面三種狀態之一結束:
-
Granted:客戶端現在可以使用 被授權的資源和 worker 來執行任務或 Actor。
-
Reschedule:根據當前節點對集羣的觀察,有一個比當前節點更好的節點。客戶應重新安排請求。有兩種可能性觸發這個情況:
-
如果當前節點是客戶端的首選 raylet(即客戶端聯繫的第一個 raylet),則這是一個 spillback 請求。客戶端應在第一個 raylet 指定的 raylet 處重試請求。
-
否則,當前節點是客戶端首選 raylet 選擇的節點。客戶端應該再次在首選的 raylet 上重試請求。
-
Canceled:無法調度運行所需的資源。
-
被選中的已經失效。
-
無法爲任務創建合適的運行環境,無法啓動工作京城。
調度策略
Ray 有幾個調度策略來控制在哪裏運行任務或參與者。當提交任務或參與者時,用戶可以選擇指定要使用的調度策略 / 策略。
默認混合策略
當沒有指定其它策略時,這是默認策略。此策略首先嚐試將任務打包到本地節點,直到節點的關鍵資源利用率超過配置的閾值(默認情況下爲 50%)。關鍵資源利用率是該節點上任何資源的最大利用率,例如,如果節點使用 8/10 個 CPU 和 70/100GB RAM,則其關鍵資源利用是 80%。
在本地節點上超過閾值後,策略將任務打包到第一個遠程節點(按節點 id 排序),然後打包到第二個遠程節點,依此類推,直到所有節點上的關鍵資源利用率超過閾值。之後,它將選擇關鍵資源利用率最低的節點。
該策略的目的是在 bin-packing 和負載均衡之間實現平衡。當節點處於臨界資源利用率時,策略傾向於 bin-packing 。按節點 ID 排序可確保所有節點在 bin-packing 時使用相同的順序。當節點超過臨界資源利用率時,策略支持負載平衡,選擇負載最少的節點。
差價策略
此策略是循環在具有可用資源的節點之間分配任務。這個循環是本地的,並不是全局的。
節點相關性策略
使用此策略,用戶可以明確指定任務或 Actor 應運行的目標節點。如果目標節點處於活動狀態,則任務或 Actor 僅在那裏運行。如果目標節點已死亡,則看是否能被調度到其它節點,不行的話就不會調度。
數據位置策略
Ray 通過讓每個任務調用方基於調用方關於任務參數位置的本地信息選擇首選 raylet。raylets 實現的單獨的調度策略不考慮數據位置,這是爲了避免向 raylet 添加用於發現哪些任務參數存儲在哪些其他節點上的額外的 RPC 和複雜性。
預佔用組策略
此策略將任務或 Actor 運行在指定的預佔用組。
預佔用組
Ray 支持預佔用組這個功能,從多個節點中自動保留一組資源。它可以用於普通任務或者 Actor 的調度。通常用於 gang - scheduling actors
由於資源組可能涉及跨多個節點的資源,Ray 使用跨 raylets 來確保原子性。該協議由 GCS 協調。如果有任何 raylet 在協議執行過程中死亡,預佔用組的創建將回滾,GCS 將再次對請求進行排隊。如果 GCS 請求失效,且 GCS 容錯功能啓用,則重啓後會 ping 所有參與者以重新啓動協議。
與 Ray 中的其它東西不同,預佔用組沒有引用計數,由創建它們的 worker 和獨立的 Actor 所有。在所有者死亡時自動銷燬。你也可以顯式銷燬,銷燬後使用這些保留的資源的任務和 Actor 都會被殺死,資源被釋放。
當一個預佔用組被創建時,它會請求保留了多個節點的資源。當其中一個節點故障時,確缺失的資源會重新被安排,優於其它還沒分配完成的預佔用組。在這些缺失的資源包被重新創建之前,該預佔用組仍然處於部分分配狀態。
Actor 管理
Actor 創建
當在 Python 中創建 Actor 時,創建工作人員首先向 GCS 註冊 Actor 。對於獨立 Actor ,註冊是以同步的方式進行的,以避免同名 Actor 註冊。對於非獨立的 Actor(默認), 使用異步註冊。
註冊後,一旦解決了 Actor 創建任務的所有輸入依賴關係,創建者就將 task specification 發送給 GCS 服務。然後,GCS 服務通過與正常任務相同的分佈式調度協議來調度 Actor 創建任務,就好像 GCS 是 Actor 創建任務的調用者一樣。
Actor handle 的原始創建者可以在 Actor handle 上提交任務,甚至在 GCS 安排 Actor 創建任務之前將其作爲參數傳遞給其它任務 / Actor。
異步註冊時,在 Actor 向 GCS 註冊之前,創建者不會將 Actor handle 傳遞給其它任務 / Actor。這是爲了防止創建者在註冊完成之前死亡;通過阻止任務提交,我們可以確保引用 Actor 的其它 worker 可以發現註冊失敗。在這種情況下,任務提交仍然是異步的,因爲創建者只是緩衝遠程任務,直到 Actor 註冊完成。
一旦創建 Actor 完成,GCS 將通過 pub-sub 通知任何擁有這個 Actor 的 handle 的 worker。每個 handle 緩存新創建的 Actor 的運行時元數據(例如 RPC 地址)。
然後,在 Actor handle 上提交的任何未處理的任務都可以發送給 Actor 執行。
與任務定義類似,Actor 定義通過 GCS 下載到 worker 上。
Actor 執行
Actor 可以有無限數量的調用者。一個 Actor handle 表示單個調用者,它包含其引用的 Actor 的 RPC 地址。需要調用的 worker 將連接到這個地址並且提交任務。
一旦創建, Actor 任務就轉換爲對 Actor 進程的直接 gRPC 調用。一個 Actor 可以處理多個併發調用,儘管這裏只顯示了一個。
Actor 死亡
Actor 可以使獨立或者非獨立的,默認是非獨立的。當所有 handle 超出範圍或執行結束時,Ray 會自動垃圾回收它們。獨立的 Actor 的生命週期與他們的原始創建者無關,一旦不再需要他們,應用程序必須手動刪除他們。
對於非獨立的 Actor,當 Actor 的所有等待的任務都已完成,所有的 Actor handle 都已超出範圍(通過引用計數進行跟蹤)時,Actor 的原始創建者通知 GCS 服務。GCS 服務然後向參與者發送 KillActor RPC 殺死 Actor。
如果 GCS 檢測到創建者已退出(通過心跳),GCS 也會終止 Actor。在這個 Actor 上提交的所有未處理的任務和後續任務都將失敗,並出現 RayActorError。
Actor 也可能在運行時意外崩潰,默認情況下,提交給崩潰了的 Actor 的所有任務都將失敗,並出現 RayActorError,就像 Actor 正常退出一樣。
Ray 還提供了一個(max_restarts)來自動重新啓動 Actor,可以指定最多重啓次數。如果啓用了此選項,並且 Actor 的所有者仍然活着,GCS 服務將嘗試通過重新提交其創建任務來重新啓動崩潰的 Actor。所有具有 Actor handle 的客戶端都會將任何未處理的任務緩存到 Actor,直到 Actor 重新啓動。如果 Actor 無法重新啓動或已達到最大重啓次數,則客戶端將使所有等待任務失敗。
第二個選項(max_task_retrys)可用於在 Actor 重新啓動後自動重試失敗的 Actor 任務。這對於冪等任務和用戶不需要自定義處理 RayActorError 的情況非常有用。
全局控制服務(Global Control Service,GCS)
全局控制服務,也稱爲 GCS,是 Ray 的控制平臺。它管理 Ray 集羣,並充當協調 raylets 和發現其他節點進程的集中平臺。GCS 還作爲外部服務(如自動縮放器和儀表盤)與 Ray 集羣通信的入口。GCS 目前是單線程的,心跳檢查和資源輪詢除外;
相關功能有:
-
節點管理,管理集羣節點的添加和刪除,並廣播給所有 raylet。
-
資源管理,廣播所有 raylet 並確認每個的資源可用性。
-
Actor 管理,處理 Actor 的創建和銷燬請求、監測他們是否活躍,在出現問題時嘗試重新創建。
-
預佔用組管理,處理 Ray 的預佔用組的創建和刪除。
-
元數據存儲,提供所有 worker 都能訪問的鍵值存儲,僅適用於小的元數據,任務和對象的元數據存儲在擁有者的 worker 中。
-
worker 管理,處理 raylet 的故障報告。
-
運行環境管理,用於管理運行環境包,包括包的使用次數和垃圾回收次數。
GCS 還提供了幾個 gRPC 端點用來幫助獲取當前 Ray 集羣的狀態,如 Actor
worker 、節點信息等。
GCS 默認使用簡單的哈希 map 內存存儲,可以將它放到 Redis 中。
節點管理
當 raylet 啓動時會向 GCS 註冊,GCS 會將 raylet 的信息存入存儲中,註冊成功後廣播其它所有的 raylet 節點。
節點註冊後,GCS 通過定期進行健康檢查來監控 raylet 的活躍度。GCS 還獲取 raylet 的資源情況,並將其廣播給其它 raylet 節點。如果 raylet 檢查失敗,GCS 還會向集羣廣播 raylet 的死亡。一旦 raylet 接收到信息,它們就會清除相關的記錄。
Raylets 還向 GCS 報告任何 worker 進程的死亡,以便將其廣播給其它 Raylets。方便提前終止向該 worker 提交任務之類的場景。
資源管理
GCS 負責確保 raylet 擁有集羣中資源使用情況的最新記錄。如果記錄不是最新的,raylet 可能會錯誤地將任務調度到另一個沒有資源的節點運行任務。
默認情況下,GCS 將每 100ms 從註冊的 raylet 中提取資源使用量。它還每 100 毫秒向所有 raylet 廣播全局資源情況。
GCS 也是自動縮放器獲取當前集羣負載的入口點。自動縮放器使用它來分配或刪除集羣中的節點。
Actor 管理
GCS 在 Actor 管理中發揮着重要作用。所有 Actor 都需要先在 GCS 登記,然後才能調度。GCS 也是獨立 Actor 的所有者。
預佔用組管理
GCS 還管理預佔用組的生命週期。GCS 通過兩階段提交協議來創建預佔用組。
元數據存儲
-
集羣儀表盤地址。
-
遠程功能的定義,在開發項目中定義遠程運行的函數時,Ray 會檢查有沒有註冊過,沒有的話會添加。被指派的 worker 會從 GCS 中獲取定義。
-
運行環境的數據,默認情況下運行環境目錄存儲在 GCS 中,GCS 通過計算獨立 Actor 和 job 使用情況來進行垃圾回收。
-
一些 Ray 的其它組件的數據也會存儲在這,如 Ray Serve 會將部署的元數據存儲在這。
容錯性
GCS 是 Ray 中非常關鍵的組件,故障的話整個集羣都會故障。
在 2.0 中 GCS 在故障中會嘗試恢復,恢復之前可能集羣工作會不正常。
默認情況下,GCS 將所有數據存儲到內存存儲中,一旦發生故障,該存儲將丟失。爲了使 GCS 能夠容錯,它必須將數據寫入持久存儲。Ray 支持 Redis 作爲外部存儲系統。爲了支持 GCS 容錯,GCS 應該有一個高可用 Redis 實例作爲支持。然後,當 GCS 重新啓動時,它首先從 Redis 存儲中加載信息,包括髮生故障時集羣中運行的所有 Raylet、actor 和預佔用組。然後,GCS 將恢復健康檢查和資源管理等常規功能。
GCS 故障時下列功能都無法使用:
-
Actor 的管理。
-
預佔用組的管理。
-
資源管理。
-
raylet 管理。
-
worker 的管理。
由於這些組件不需要讀取或寫入 GCS,因此任何正在運行的 Ray 的任務和 Actor 都將保持活動狀態。同樣,任何現有對象都將繼續可用。
作者介紹:大家好!我是 Andy.Qin,一個想創造哆啦 A 夢的 Maker,連續創業者。我最熟悉的領域是分佈式應用開發、高併發架構設計,其次是機器學習、自然語言處理和理解方向。我對開源社區和開源項目的建設也有極大的熱忱,期望能與大家多多交流討論!本次利用業務時間,花了一個月翻譯了將近 60 多頁的 Ray2.0 架構白皮書,期望對大家能夠有所幫助!大家有任何問題或者疑問(當然也包括鼓勵,請點擊閱讀原文,在論壇中留下你的寶貴意見!
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/qmoAuWaw5gmo5qP48WVqZw