在高效能運算場景中,往往採用全快閃記憶體架構和核心態並行檔案系統,以滿足效能要求。隨著資料規模的增加和分散式系統叢集規模的增加,全快閃記憶體的高成本和核心客戶端的運維複雜性成為主要挑戰。
JuiceFS,是一款全使用者態的雲原生分散式檔案系統,透過分散式快取大幅提升 I/O 吞吐量,並使用成本較低的物件儲存來完成資料儲存,適用於大規模 AI 業務。
JuiceFS 資料讀取流程從客戶端的讀請求開始,然後再經過 FUSE 傳送給 JuiceFS 客戶端,透過預讀緩衝層,接著進入快取層,最終訪問物件儲存。為了提高讀取效率,JuiceFS 在其架構設計中採用了包括資料預讀、預取和快取在內的多種策略。
本文將詳細解析這些策略的工作原理,並分享我們在特定場景下的測試結果,以便讀者深入理解 JuiceFS 的效能優勢及一些相關的限制,從而更有效地應用於各種使用場景。
鑑於文章內容的深度和技術性,需要讀者有一定作業系統知識,建議收藏以便在需要時進行仔細閱讀。
01 JuiceFS 架構簡介
JuiceFS 社群版架構分為客戶端、資料儲存和後設資料三部分。 資料訪問支援多種介面,包括 POSIX 、HDFS API、S3 API,還有 Kubernetes CSI,以滿足不同的應用場景。在資料儲存方面,JuiceFS 支援幾十種物件儲存,包括公共雲服務和自託管解決方案,如 Ceph 和 MinIO。後設資料引擎支援多種常見的資料庫,包括 Redis、TiKV 和 MySQL 等。
企業版與社群版的主要區別在圖片左下角後設資料引擎和資料快取的處理。企業版包括一個自研的分散式後設資料引擎,並支援分散式快取,社群版只支援本地快取。
02 Linux 中關於“讀”的幾個概念
在 Linux 系統中,資料讀取主要透過幾種方式實現:
- Buffered I/O:這是標準的檔案讀取方式,資料會透過核心緩衝區,並且核心會進行預讀操作,以最佳化讀取效率;
- Direct I/O:允許繞過核心緩衝區直接進行檔案 I/O 操作,以減少資料複製和記憶體佔用,適合大量資料傳輸;
- Asynchronous I/O:通常與 Direct I/O 一起使用。它允許應用程式在單個執行緒中發出多個 I/O 請求,而不必等待每個請求完成,從而提高 I/O 併發效能;
- Memory Map:將檔案對映到程序的地址空間,可以透過指標直接訪問檔案內容。透過記憶體對映,應用程式可以像訪問普通記憶體一樣訪問對映的檔案區域,由核心自動處理資料的讀取和寫入。
幾種主要的讀取模式及它們對儲存系統帶來的挑戰:
- 隨機讀取,包括隨機大 I/O 讀和隨機小 I/O 讀,主要考驗儲存系統的延遲和 IOPS。
- 順序讀取,主要考驗儲存系統的頻寬。
- 大量小檔案讀取,主要考驗儲存系統的後設資料引擎的效能和系統整體的 IOPS 能力。
03 JuiceFS 讀流程原理解析
JuiceFS 採用了檔案分塊儲存的策略。 一個檔案首先邏輯上被分割成若干個 chunk,每個 chunk 固定大小為 64MB。每個 chunk 進一步被細分為若干個 4MB 的 block,block 是物件儲存中的實際儲存單元,JuiceFS 的設計中有不少效能最佳化措施與這個分塊策略緊密相關。(進一步瞭解 JuiceFS 儲存模式)。 為了最佳化讀取效能, JuiceFS 採取了預讀、預取與快取等多種最佳化方案。
預讀 readahead
預讀(readahead):透過預測使用者未來的讀請求,提前從物件儲存中載入資料到記憶體中,以降低訪問延遲,提高實際 I/O 併發。下面這張圖是一張簡化的讀取流程的示意圖。虛線以下代表應用層,虛線以上是核心層。
當使用者程序(左下角標藍色的應用層) 發起檔案讀寫的系統呼叫時,請求首先透過核心的 VFS,然後傳遞給核心的 FUSE 模組,經過 /dev/fuse
裝置與 JuiceFS 的客戶端程序通訊。
右下角所示的流程是後續在 JuiceFS 中進行的預讀最佳化。系統透過引入“session” 跟蹤一系列連續讀。每個 session 記錄了上一次讀取的偏移量、連續讀取的長度以及當前預讀視窗大小,這些資訊可用於判斷新來的讀請求是否命中這個 session,並自動調整/移動預讀視窗。 透過維護多個 session,JuiceFS 還能輕鬆支援高效能的併發連續讀。
為了提升連續讀的效能,在系統設計中,我們增加了提升併發的措施。具體來說,預讀視窗中的每一個 block (4MB) 都會啟動一個 goroutine 來讀資料。 這裡需要注意的是,併發數會受限於 buffer-size 引數。在預設 300MB 設定下,理論最大物件儲存併發數為 75(300MB 除以 4MB),這個設定在一些高效能場景下是不夠的,使用者需要根據自己的資源配置和場景需求去調整這個引數,下文中我們也對不同引數進行了測試。
以下圖第二行為例,當系統接收到連續的第二個讀請求時,實際上會發起一個包含預讀視窗和讀請求的連續三個資料塊的請求。按照預讀的設定,接下來的兩個請求都會直接命中預讀的 buffer 並被立即返回。
如果第一個和第二個請求沒有被預讀,而是直接訪問物件儲存,延遲會比較高(通常大於10ms)。而當延遲降低在 100 微秒以內,則說明這個 I/O 請求成功使用了預讀,即第三個和第四個請求,直接命中了記憶體中預讀的資料。
預取 prefetch
預取(prefetch):當隨機讀取檔案中的一小段資料時,我們假設這段資料附近的區域也可能會被讀取,因此客戶端會非同步將這一小段資料所在的整個 block 下載下來。
但是在有些場景,預取這個策略不適用,例如應用對大檔案進行大幅偏移的、稀疏的隨機讀取,預取會訪問到一些不必要的資料,導致讀放大。因此,使用者如果已經深入瞭解應用場景的讀取模式,並確認不需要預取,可以透過 --prefetch=0
禁用該行為。
快取 cache
在之前的一次分享中,我們的架構師高昌健詳細介紹了 JuiceFS的快取機制,或檢視快取文件。 在這篇文章中,對於快取的介紹會以基本概念為主。
頁快取 page cache
頁快取(page cache)是 Linux 核心提供的機制。它的核心功能之一就是預讀(readahead),它透過預先讀取資料到快取中,確保在實際請求資料時能夠快速響應。
進一步,頁快取(page cache)在特定場景下的應用也非常關鍵,例如在處理隨機讀操作時,如果使用者能策略性地使用頁快取,將檔案資料提前填充至進頁快取,如記憶體空閒的情況下提前完整連續讀一遍檔案,可以顯著提高後續隨機讀的效能,從而極大地提升業務整體效能。
本地快取 local cache
JuiceFS 的本地快取可以利用本地記憶體或本地磁碟儲存 block,從而在應用訪問這些資料時可以實現本地命中,降低網路時延並提升效能。我們通常推薦使用高效能 SSD。資料快取的預設單元是一個 block,大小為 4MB,該 block 會在首次從物件儲存中讀取後非同步寫入本地快取。
關於本地快取的配置,如 --cache-dir
和 --cache-size
等細節,企業版使用者可以檢視文件。
分散式快取 cache group
分散式快取是企業版的一個重要特性。與本地快取相比,分散式快取將多個節點的本地快取聚合成同一個快取池,提高快取的命中率。但由於分散式快取增加了一次網路請求,這導致其在時延上通常稍高於本地快取,分散式快取隨機讀延遲一般是 1-2ms,而本地快取隨機讀延遲一般是 0.2-0.5ms。關於分散式快取具體架構,可以檢視官網文件。
04 FUSE & 物件儲存的效能表現
JuiceFS 的讀請求都要經過 FUSE,資料要從物件儲存讀取,因此理解 FUSE 和物件儲存的效能表現是理解 JuiceFS 效能表現的基礎。
關於 FUSE 的效能
我們對 FUSE 效能進行了兩組測試。測試場景是當 I/O 請求到達 FUSE 掛載程序後,資料被直接填充到記憶體中並立即返回。測試主要評估 FUSE 在不同執行緒數量下的總頻寬,單個執行緒平均頻寬以及CPU使用情況。硬體方面,測試 1 是 Intel Xeon 架構,測試 2 則是 AMD EPYC 架構。
Threads | Bandwidth(GiB/s) | Bandwidth per Thread (GiB/s) | CPU Usage(cores) |
---|---|---|---|
1 | 7.95 | 7.95 | 0.9 |
2 | 15.4 | 7.7 | 1.8 |
3 | 20.9 | 6.9 | 2.7 |
4 | 27.6 | 6.9 | 3.6 |
6 | 43 | 7.2 | 5.3 |
8 | 55 | 6.9 | 7.1 |
10 | 69.6 | 6.96 | 8.6 |
15 | 90 | 6 | 13.6 |
20 | 104 | 5.2 | 18 |
25 | 102 | 4.08 | 22.6 |
30 | 98.5 | 3.28 | 27.4 |
FUSE 效能測試 1, 基於 Intel Xeon CPU 架構
- 在單執行緒測試中,最大頻寬達到 7.95GiB/s,同時 CPU 使用量不到一個核;
- 隨著執行緒數增加,頻寬基本實現線性擴充套件,當執行緒數增加到 20 時,總頻寬增加到 104 GiB/s;
此處,使用者需要特別注意的是,相同 CPU 架構下使用不同硬體型號、不同作業系統測得的 FUSE 頻寬表現都有可能不同。我們使用過多種機型進行測試,在其中一種機型上測得的最大單執行緒頻寬僅為 3.9GiB/s。
Threads | Bandwidth(GiB/s) | Bandwidth per Thread (GiB/s) | CPU Usage(cores) |
---|---|---|---|
1 | 3.5 | 3.5 | 1 |
2 | 6.3 | 3.15 | 1.9 |
3 | 9.5 | 3.16 | 2.8 |
4 | 9.7 | 2.43 | 3.8 |
6 | 14.0 | 2.33 | 5.7 |
8 | 17.0 | 2.13 | 7.6 |
10 | 18.6 | 1.9 | 9.4 |
15 | 21 | 1.4 | 13.7 |
FUSE 效能測試 2, 基於 AMD EPYC CPU 架構
- 在測試 2 中,頻寬不能線性擴充套件,特別是當併發數量達到 10 個時,每個併發的頻寬不足 2GiB/s;
在多併發情況下,測試 2( EPYC 架構)頻寬峰值約為 20GiBps;測試 1(Intel Xeon 架構)表現出更高的效能空間,峰值通常在 CPU 資源被完全佔用後出現,這時應用程序和 FUSE 程序的 CPU 都達到了資源極限。
在實際應用中,由於各個環節的時間開銷,實際的 I/O 效能往往會低於上述測試峰值 3.5GiB/s。例如,在模型載入的場景中,載入 pickle 格式的模型檔案,通常單執行緒頻寬只能達到 1.5 到 1.8GiB/s。這主要是因為讀取 pickle 檔案的同時,要進行資料反序列化,還會遇到 CPU 單核處理的瓶頸。即使是不經過 FUSE,直接從記憶體讀取的情況下,頻寬也最多隻能達到 2.8GiB/s。
關於物件儲存的效能
我們使用 juicefs objbench 工具進行測試,測試涵蓋了單併發、10 併發、200 併發以及 800 併發的不同負載。使用者需要注意的是,不同物件儲存的效能差距可能很大。
上傳頻寬 upload objects- MiB/s | 下載頻寬 download objects MiB/s | 上傳平均耗時 ms/object | 下載平均耗時 ms/object | |
---|---|---|---|---|
單併發 | 32.89 | 40.46 | 121.63 | 98.85ms |
10 併發 | 332.75 | 364.82 | 10.02 | 10.96 |
200 併發 | 5590.26 | 3551.65 | 067 | 1.13 |
800 併發 | 8270.28 | 4038.41 | 0.48 | 0.99 |
當我們增加物件儲存 GET 操作的併發數到 200 和 800 後,才能夠達到非常高的頻寬。這說明直接從物件儲存上讀資料時,單併發頻寬非常有限,提高併發對整體的頻寬效能至關重要。
05 連續讀與隨機讀測試
為了給大家提供一個直觀的基準參考,我們使用 fio 工具測試了 JuiceFS 企業版在連續讀取和隨機讀場景下的效能。
連續讀
從下圖可以看到 99% 的資料都小於 200 微秒。在連續讀場景下,預讀視窗總能很好地發揮作用,因此延遲很低。
同時,我們也能透過加大預讀視窗,以提高 I/O 併發,從而提升頻寬。當我們將 buffer-size 從預設 300MiB 調整為 2GiB 後,讀併發不再受限,讀頻寬從 674MiB/s 提升到了 1418 MiB/s,此時達到單執行緒 FUSE 的效能峰值,進一步提高頻寬需要提高業務程式碼中 I/O 併發度。
buffer-size | 頻寬 |
---|---|
300MiB | 674MiB/s |
2GiB | 1418MiB/s |
不同 buffer-size 頻寬效能測試(單執行緒)
當提高業務執行緒數到 4 執行緒時,頻寬能達到 3456MiB/s;16 執行緒時,頻寬達到了 5457MiB/s,此時網路頻寬已經達到飽和。
buffer-size | 頻寬 |
---|---|
1執行緒 | 1418MiB/s |
4執行緒 | 3456MiB/s |
16執行緒 | 5457MiB/s |
不同執行緒數量下頻寬效能測試 (buffer-size:2GiB)
隨機讀
對於小 I/O 隨機讀,其效能主要由延遲和 IOPS 決定,由於總 IOPS 能夠透過增加節點線性擴充套件,所以我們先關注單節點上的延遲資料。
“FUSE 資料頻寬”是指透過 FUSE 層傳輸的資料量,代表使用者應用實際可觀察和操作的資料傳輸速率;“底層資料頻寬”則指的儲存系統本身在物理層或作業系統層面處理資料的頻寬。
從表格中可以看到與穿透到物件儲存相比,命中本地快取和分散式快取的情況下,延遲都更低,當我們需要最佳化隨機讀延遲的時候就需要考慮提高資料的快取命中率。同時,我們也能看到使用非同步 I/O 介面及提高執行緒數可以大大提高 IOPS。
不同於小 I/O 的場景,大 I/O 隨機讀場景還要注意讀放大問題。如下表所示,底層資料頻寬高於 FUSE 資料頻寬,這是因為預讀的作用,實際的資料請求會比來自於應用的資料請求多1-3倍,此時可以嘗試關閉 prefetch 並調整最大預讀視窗來調優。
分類 | FUSE 資料頻寬 | 底層資料頻寬 |
---|---|---|
1MB buffered IO | 92MiB | 290MiB |
2MB buffered IO | 155MiB | 435MiB |
4MB buffered IO | 181MiB | 575MiB |
1MB direct IO | 306MiB | 306MiB |
2MB direct IO | 199MiB | 340MiB |
4MB direct IO | 245MiB | 735MiB |
JuiceFS(開啟分散式快取) 大 I/O 隨機讀測試結果