在早期階段,vivo AI 計算平臺使用 GlusterFS 作為底層儲存基座。隨著資料規模的擴大和多種業務場景的接入,開始出現效能、維護等問題。為此,vivo 轉而採用了自研的軒轅檔案系統,該系統是基於 JuiceFS 開源版本開發的一款分散式檔案儲存方案。
本文將介紹 vivo 軒轅檔案系統在 JuiceFS 基礎之上開發的新特性。以及 vivo 針對一些關鍵場景,如樣本資料讀取速度慢和檢查點寫入環節的最佳化措施。此外,文章還將介紹 vivo 的技術規劃包括 FUSE、 後設資料引擎及 RDMA 通訊等方面,希望能為在大規模 AI 場景使用 JuiceFS 的使用者提供參考與啟發。01 計算平臺引入軒轅檔案儲存的背景
01 計算平臺引入軒轅檔案儲存的背景
最初,vivo 的 AI 計算平臺 使用 GlusterFS ,並由該團隊自行維護。在使用過程中,團隊遇到了一些問題。一是處理小檔案時速度變得非常緩慢;二是當需要對 GlusterFS 進行機器擴容和資料平衡時,對業務產生了較大的影響。
隨後,由於早期叢集容量已滿且未進行擴容,計算團隊選擇搭建了新的叢集。然而,這導致了多個叢集需要維護,從而增加了管理的複雜度。此外,作為平臺方,他們在儲存方面的投入人力有限,因此難以進行新特性開發。
他們瞭解到我們網際網路部門正在研發檔案儲存解決方案,經過深入交流和測試。最終,他們決定將其資料儲存遷移至我們的軒轅檔案儲存系統。
軒轅檔案系統基於 JuiceFS 開源版,進行了二次開發,支援多種標準訪問協議,包括 POSIX、HDFS 以及 Windows 上的 CIFS 協議。此外,我們還提供了檔案恢復功能,該功能參考了商用解決方案,能夠按照原路徑進行資料恢復。
同時,我們的系統支援客戶端熱升級,這一功能在開源版本中也已經實現。另外,我們還支援使用者名稱許可權管理,預設使用本地 uid/gid 進行鑑權。在此基礎上,我們還參考 JuiceFS 企業版實現了使用者名稱鑑權功能。
下圖是軒轅檔案系統的架構圖,與 JuiceFS 類似。在底層基座方面,我們使用 TikV 儲存後設資料,而資料則儲存在我們自研的物件儲存系統中。特別值得一提的是,在 Windows 場景下,我們在 Samba 中開發了一個外掛,該外掛直接呼叫 JuiceFS API,從而為使用者提供了一個在 Windows 上訪問我們檔案儲存的通道。
目前的 AI 計算平臺儲存流程如下:首先獲取原始資料並透過一個包含 4 萬個批處理任務的系統進行處理,生成樣本庫。這些樣本庫隨後在 GPU 上訓練,產生模型檔案,這些模型檔案被傳輸至線上系統用於推理。原始資料及處理後的樣本庫直接儲存在軒轅檔案系統中,由於其相容 HDFS API,Spark 可以直接處理這些資料。模型檔案也儲存在軒轅中,並透過其提供的CSI外掛,使線上推理系統能直接掛載並讀取這些檔案。
02 儲存效能最佳化
訓練階段涉及儲存的主要有兩個重要方面:樣本讀和訓練過程中的檢查點( checkpoint) 儲存。
環節1:加速樣本讀
為了提升樣本載入的速度,我們開發了一個分散式讀快取層。在訓練模型前,我們藉助JuiceFS 提供的 warm up 功能,優先將本次訓練所需的資料預載入至讀快取層。透過這種方式,訓練資料可以直接從讀快取層獲取,而無需從物件儲存系統中拉取。通常情況下,直接從物件儲存中讀取資料需要花費十幾至幾十毫秒,但透過讀快取層則可將讀取時間縮短至 10 毫秒以內,從而進顯著提高了資料載入到 GPU的 速度。
環節2:檢查點 (Checkpoint) 寫入
在檢查點寫入方面,我們參考了百度的方案。具體而言,檢查點資料首先被寫入一個臨時快取區域(我們稱之為“協管”區域,但此處可能指的是某種形式的中間快取或暫存區),然後再逐步重新整理到物件儲存中。在這個過程中,我們也採用了單副本模式,因為檢查點本身就是每隔一段時間儲存的,即使某個時間段的檢查點丟失,對整體訓練的影響也是有限的。當然,我們也制定了一些策略來確保關鍵資料的安全性,並非所有資料都會進入這個中間快取區域。通常,只有檢查點檔案和訓練階段的日誌檔案會被寫入。如果訓練中斷,檢查點檔案可以從這個中間快取區域中讀取。
此外,當資料被寫入並重新整理到物件儲存中時,我們並不會立即從檢查點快取中清除這些資料。因為訓練過程中隨時可能中斷,如果此時檢查點快取中的資料被清除,而需要從物件儲存中重新拉取,將會耗費較長時間。因此,我們設定了一個 TTL(生存時間)機制。例如,如果檢查點資料每小時重新整理一次到物件儲存中,我們可以將 TTL 設定為 1.5 小時。這樣,即使訓練中斷,我們也能確保檢查點快取中有一個最新的備份可供使用。
在開發寫快取的過程中,我們遇到了一個挑戰。由於我們的客戶端與寫快取之間的通訊採用 gRPC 協議,該協議在資料反序列化時會重新申請記憶體以儲存解析後的資料。在特定時間段內,如果寫操作非常集中(例如在幾十秒內),會導致大量的記憶體申請和釋放。由於我們使用的是 Go 語言開發,其垃圾回收(GC)機制在這種情況下表現較慢,可能會導致寫快取的記憶體耗盡。
為了解決這個問題,我們調研了其他資料反序列化的方案。最終,我們採用了 Facebook 的 flatterbuffer 方案。與 gRPC 的 Pb 反序列化不同,flatterbuffer 在反序列化後可以直接使用資料,無需額外的解析步驟。透過這種方式,我們減少了記憶體的使用,與 Pb 相比,記憶體節省達到了 50%。同時,我們也對寫效能進行了測試,發現使用 flatterbuffer 後,寫效能提升了20%
環節3:線上推理,模型載入流量大
在使用者進行線上推理時,我們注意到模型下載產生的流量極大,有時甚至會佔滿物件儲存閘道器的頻寬。深入分析這個場景後,我們發現存在眾多例項,每個例項都會獨立地將完整模型載入到記憶體中,並且這些例項幾乎是同時開始載入模型的,這一行為造成了巨大的流量壓力。
為解決此問題,我們借鑑了商業解決方案,採用了在 Pod 中實施邏輯分組的方法。在這種策略下,每個分組僅從底層儲存讀取一份完整模型,而分組內的各個節點則讀取模型的部分檔案,並透過節點間的資料共享(類似於 P2P 方式)來減少總體流量需求。這種方法顯著降低了對底層物件儲存頻寬的佔用,有效緩解了流量壓力。
03 技術規劃
libc 呼叫繞過 FUSE 核心,提升讀寫效能 下面這份圖表來源於 ACM 期刊中的一篇論文。文中指出,在使用 FUSE 掛載時,請求的處理流程會先從使用者態轉移到核心態,然後再返回使用者態。在這個流程中,上下文切換所帶來的消耗是相當巨大的。
柱狀圖較高的部分代表原生的 FUSE,而柱狀圖較低的部分則代表經過最佳化的方案。
- 小檔案場景:原生的 FUSE 相較於最佳化方案,其上下文次數切換的數量差距達到了 1000 倍;
- 大檔案場景:原生的 FUSE 與最佳化方案之間的上下文次數切換的數量差距約為 100 倍;
- 混合負載場景:同樣顯示出了巨大的上下文次數切換的數量差異。
在論文中提到,鏈路消耗的主要來源是上下文切換。因此,我們計劃在 FUSE 這一層進行最佳化,主要針對後設資料和小檔案場景。目前,我們正在進行方案選型工作。
自研後設資料引擎,檔案語義下沉
我們還計劃開發一個自己的後設資料引擎。當前,我們使用的後設資料引擎是基於 TiKV 的,但 TiKV 並不具備檔案語義,所有的檔案語義都是在客戶端實現的。這給我們的特性開發工作帶來了極大的不便。
同時,當多個節點同時寫入一個 key 時,事務衝突也會非常頻繁。近期,我們還遇到了程序會突然卡住的問題,持續時間從幾分鐘到十幾分鐘不等。這個問題一直未能得到解決。
另外,TiKV PD 元件為主節點 Active 模式,請求上 10 萬後,時延上升明顯,PD 節點(112核)CPU 使用率接近飽和。因此,我們正在嘗試一些方案來降低主節點的 CPU 利用率,以觀察是否能改善耗時問題。我們參考了一些論文,如百度的 CFS 論文,將所有的後設資料操作儘量變成單機事務,以減少分散式事務的開銷。
快取層實現 RDMA
通訊關於我們機房的 GPU 節點,它們目前使用的是 RDMA 網路。與快取層的通訊仍然使用 TCP 協議。我們有規劃開發一個基於 RDMA 的通訊方式,以實現客戶端與快取之間的低延遲、低 CPU 消耗的通訊。
透過觀察客戶端的火焰圖,我們發現 RPC 通訊的耗時仍然非常明顯。雖然寫快取的處理資料只需要一兩毫秒,但客戶端將資料上傳到整個鏈路的耗時可能達到五六毫秒,甚至十毫秒。在客戶端 CPU 非常繁忙的情況下,這個時間可能會達到二三十毫秒。而 RDMA 本身並不怎麼消耗 CPU,記憶體消耗也比較少,因此我們認為這是一個值得嘗試的解決方案。