
2025 年 2 月 28 日,DeepSeek 在其開源周最後一天壓軸釋出了自研的並行檔案系統 Fire-Flyer File System,簡稱 3FS。該系統支撐了 DeepSeek V3&R1 模型訓練、推理的全流程,在資料預處理、資料集載入、CheckPoint、KVCache 等場景發揮了重要作用。
專案一經發布,就獲得了儲存領域的廣泛關注。大家迫切地想一探究竟,看看 3FS 到底有哪些壓箱底的獨門秘籍。火山引擎檔案儲存團隊閱讀和分析了 3FS 的設計文件和原始碼,總結出這篇文章,在介紹了 3FS 關鍵設計的同時,嘗試從儲存專業的視角挖掘出 3FS 團隊在這些設計背後的考量。

與業界很多分散式檔案系統架構類似,3FS 整個系統由四個部分組成,分別是 Cluster Manager、Client、Meta Service、Storage Service。所有元件均接入 RDMA 網路實現高速互聯,DeepSeek 內部實際使用的是 InfiniBand。
Cluster Manager 是整個叢集的中控,承擔節點管理的職責:
-
Cluster Manager 採用多節點熱備的方式解決自身的高可用問題,選主機制複用 Meta Service 依賴的 FoundationDB 實現;
-
Meta Service 和 Storage Service 的所有節點,均透過週期性心跳機制維持線上狀態,一旦這些節點狀態有變化,由 Cluster Manager 負責通知到整個叢集;
-
Client 同樣透過心跳向 Cluster Manager 彙報線上狀態,如果失聯,由 Cluster Manager 幫助回收該 Client 上的檔案寫開啟狀態。
Client 提供兩種客戶端接入方案:
-
FUSE 客戶端 hf3fs_fuse 方便易用,提供了對常見 POSIX 介面的支援,可快速對接各類應用,但效能不是最優的;
-
原生客戶端 USRBIO 提供的是 SDK 接入方式,應用需要改造程式碼才能使用,但效能相比 FUSE 客戶端可提升 3-5 倍。
Meta Service 提供元資料服務,採用存算分離設計:
-
元資料持久化儲存到 FoundationDB 中,FoundationDB 同時提供事務機制支撐上層實現檔案系統目錄樹語義;
-
Meta Service 的節點本身是無狀態、可橫向擴充套件的,負責將 POSIX 定義的目錄樹操作翻譯成 FoundationDB 的讀寫事務來執行。
Storage Service 提供資料儲存服務,採用存算一體設計:
-
每個儲存節點管理本地 SSD 儲存資源,提供讀寫能力;
-
每份資料 3 副本儲存,採用的鏈式複製協議 CRAQ(Chain Replication with Apportioned Queries)提供 write-all-read-any 語義,對讀更友好;
-
系統將資料進行分塊,儘可能打散到多個節點的 SSD 上進行資料和負載均攤。
一個 3FS 叢集可以部署單個或多個管理服務節點 mgmtd。這些 mgmtd 中只有一個主節點,承接所有的叢集管理響應訴求,其它均為備節點僅對外提供查詢主的響應。其它角色節點都需要定期向主 mgmtd 彙報心跳保持線上狀態才能提供服務。

每個節點啟動後,需要向主 mgmtd 上報必要的資訊建立租約。mgmtd 將收到的節點資訊持久化到 FoundationDB 中,以保證切主後這些資訊不會丟失。節點資訊包括節點 ID、主機名、服務地址、節點類別、節點狀態、最後心跳的時間戳、配置資訊、標籤、軟體版本等。
租約建立之後,節點需要向主 mgmtd 週期傳送心跳對租約進行續租。租約雙方根據以下規則判斷租約是否有效:
-
如果節點超過 T 秒(可配置,預設 60s)沒有上報心跳,主 mgmtd 判斷節點租約失效;
-
如果節點與主 mgmtd 超過 T/2 秒未能續上租約,本節點自動退出。
對於元資料節點和客戶端,租約有效意味著服務是可用的。但對於儲存服務節點,情況要複雜一些。一個儲存節點上會有多個 CRAQ 的 Target,每個 Target 是否可服務的狀態是不一致的,節點可服務不能代表一個 Target 可服務。因此,Target 的服務狀態會進一步細分為以下幾種:

元資料和儲存節點(包括其上的 Target)的資訊,以及下文會描述的 CRAQ 複製連結串列資訊,共同組成了叢集的路由資訊(RoutingInfo)。路由資訊由主 mgmtd 廣播到所有的節點,每個節點在需要的時候透過它找到其它節點。
mgmtd 的選主機制基於租約和 FoundationDB 讀寫事務實現。租約資訊 LeaseInfo 記錄在 FoundationDB 中,包括節點 ID、租約失效時間、軟體版本資訊。如果租約有效,節點 ID 記錄的節點即是當前的主。每個 mgmtd 每 10s 執行一次 FoundationDB 讀寫事務進行租約檢查,具體流程如下圖所示。

上述流程透過以下幾點保證了選主機制的正確性:
-
LeaseInfo 的讀取和寫入在同一個 FoundationDB 讀寫事務裡完成,FoundationDB 讀寫事務確保了即使多個 mgmtd 併發進行租約檢查,執行過程也是序列一個一個的,避免了多個 mgmtd 交織處理分別認為自己成為主的情況。
-
發生切主之後新主會靜默 120s 才提供服務,遠大於租約有效時長 60s,這個時間差可以保證老主上的在飛任務有充足的時間處理完,避免出現新主、老主併發處理的情況。

3FS 提供了兩種形態的客戶端,FUSE 客戶端 hf3fs_fuse 和原生客戶端 USRBIO:
-
FUSE 客戶端適配門檻較低,開箱即用。在 FUSE 客戶端中,使用者程序每個請求都需要經過核心 VFS、FUSE 轉發給使用者態 FUSE Daemon 進行處理,存在 4 次“核心 – 使用者態”上下文切換,資料經過 1-2 次複製。這些上下文切換和資料複製開銷導致 FUSE 客戶端的效能存在瓶頸;
-
USRBIO 是一套使用者態、非同步、零複製 API,使用時需要業務修改原始碼來適配,使用門檻高。每個讀寫請求直接從使用者程序傳送給 FUSE Daemon,消除了上下文切換和資料複製開銷,從而實現了極致的效能。
FUSE 客戶端基於 libfuse lowlevel api 實現,要求 libfuse 3.16.1 及以上版本。和其它業界實現相比,最大的特色是使用了 C++20 協程,其它方面大同小異。本文僅列舉一些實現上值得注意的點:

基於共享記憶體 RingBuffer 的通訊機制被廣泛應用在高效能儲存、網路領域,在 DPDK、io_uring 中均有相關實現,一般採用無鎖、零複製設計,相比其它通訊的機制有明顯的效能提升。3FS 借鑑了這個思路實現了 USRBIO,和原有的 FUSE 實現相比,有以下特點:
-
整個執行路徑非常精簡,完全在使用者態實現,不再需要陷入核心經過 VFS、FUSE 核心模組的處理
-
讀寫資料的 buffer 和 RDMA 打通,整個處理過程沒有複製開銷
-
只加速最關鍵的讀寫操作,其它操作複用 FUSE 現有邏輯,在效率和相容性之間取得了極佳的平衡。這一點和 GPU Direct Storage 的設計思路有異曲同工之處
USRBIO 的使用說明可以參考 3FS 程式碼庫 USRBIO API Reference 文件:https://github.com/deepseek-ai/3FS/blob/main/src/lib/api/UsrbIo.md

在實現上,USRBIO 使用了很多共享記憶體檔案:
-
每個 USRBIO 例項使用一個 Iov 檔案和一個 Ior 檔案
-
Iov 檔案用來作為讀寫資料的 buffer
-
使用者提前規劃好需要使用的總容量
-
檔案建立之後 FUSE Daemon 將該其註冊成 RDMA memory buffer,進而實現整個鏈路的零複製
-
Ior 檔案用來實現 IoRing
-
使用者提前規劃好併發度
-
在整個檔案上抽象出了提交佇列和完成佇列,具體佈局參考上圖
-
檔案的尾部是提交完成佇列的訊號量,FUSE Daemon 在處理完 IO 後透過這個訊號量通知到使用者程序
-
一個掛載點的所有 USRBIO 共享 3 個 submit sem 檔案
-
這三個檔案作為 IO 提交事件的訊號量(submit sem),每一個檔案代表一個優先順序
-
一旦某個 USRBIO 例項有 IO 需要提交,會透過該訊號量通知到 FUSE Daemon
-
所有的共享記憶體檔案在掛載點 3fs-virt/iovs/ 目錄下均建有 symlink,指向 /dev/shm 下的對應檔案
Iov、Ior 共享記憶體檔案透過 symlink 註冊給 FUSE Daemon,這也是 3FS FUSE 實現上有意思的一個點,下一章節還會有進一步的描述。
通常一個檔案系統如果想實現一些非標能力,在 ioctl 介面上整合是一個相對標準的做法。3FS 裡除了使用了這種方式外,對於 USRBIO、遞迴刪除目錄、停用回收站的 rename、修改 conf 等功能,採用了整合到 symlink 介面的非常規做法。
3FS 採用這種做法可能基於兩個原因:
-
ioctl 需要提供專門的工具或寫程式碼來使用,但 symlink 只要有掛載點就可以直接用。
-
和其它介面相比,symlink 相對低頻、可傳遞的引數更多。
symlink 的完整處理邏輯如下:
-
當目標目錄為掛載點 3fs-virt 下的 rm-rf、iovs、set-conf 目錄時:
-
rm-rf:將 link 路徑遞迴刪除,請求傳送給元資料服務處理;
-
iovs:建 Iov 或者 Ior,根據 target 檔案字尾判定是否 ior;
-
set-conf:設定 config 為 target 檔案中的配置。
-
當 link 路徑以 mv: 開頭,rename 緊跟其後的 link 檔案路徑到 target 路徑,停用回收站。
-
其它 symlink 請求 Meta Service 進行處理。
3FS 沒有對小檔案做調優,直接存取大量小檔案效能會比較差。為了彌補這個短板,3FS 專門設計了 FFRecord (Fire Flyer Record)檔案格式來充分發揮系統的大 IO 讀寫能力。
FFRecord 檔案格式具有以下特點:
-
合併多個小檔案,減少了訓練時開啟大量小檔案的開銷;
-
支援隨機批次讀取,提升讀取速度;
-
包含資料校驗,保證讀取的資料完整可靠。
以下是 FFRecord 檔案格式的儲存 layout:

圖片在 FFRecord 檔案格式中,每一條樣本的資料會做序列化按順序寫入,同時檔案頭部包含了每一條樣本在檔案中的偏移量和 crc32 校驗和,方便做隨機讀取和資料校驗。
3FS 面向高吞吐能力而設計,系統吞吐能力跟隨 SSD 和網路頻寬線性擴充套件,即使發生個別 SSD 介質故障,也能依然提供很高的吞吐能力。3FS 採用分攤查詢的鏈式複製 CRAQ 來保證資料可靠性,CRAQ 的 write-all-read-any 特性對重讀場景非常友好。

每個資料節點透過 Ext4 或者 XFS 檔案系統管理其上的多塊 NVME DISK,對內部模組提供標準的 POSIX 檔案介面。資料節點包含幾個關鍵模組:Chunk Engine 提供 chunk 分配管理;MetaStore 負責記錄分配管理資訊,並持久化到 RocksDB 中;主 IO handle 提供正常的讀寫操作。各個資料節點間組成不同的鏈式複製組,節點之間有複製鏈間寫 IO、資料恢復 sync 寫 IO。
鏈式複製是將多個數據節點組成一條鏈 chain,寫從鏈首開始,傳播到鏈尾,鏈尾寫完後,逐級向前傳送確認資訊。標準 CRAQ 的讀全部由鏈尾處理,因為尾部才是完全寫完的資料。
多條鏈組成 chain table,存放在元資料節點,Client 和資料節點透過心跳,從元資料節點獲取 chain table 並快取。一個叢集可有多個 chain table,用於隔離故障域,以及隔離不同型別(例如離線或線上)的任務。
3FS 的寫採用全鏈路 RDMA,鏈的後繼節點採用單邊 RDMA 從前序節點讀取資料,相比前序節點透過 RDMA 傳送資料,少了資料切包等操作,效能更高。而 3FS 的讀,可以向多個數據節點同時傳送讀請求,資料節點透過比較 commit version 和 update version 來讀取已經提交資料,多節點的讀相比標準 CRAQ 的尾節點讀,顯著提高吞吐。
資料打散
傳統的鏈式複製以固定的節點形成 chain table。如圖所示節點 NodeA 只與 NodeB、C 節點形成 chain。若 NodeA 故障,只能 NodeB 和 C 分擔讀壓力。
3FS 採用了分攤式的打散方法,一個 Node 承擔多個 chain,多個 chain 的資料在叢集內多個節點進行資料均攤。如圖所示,節點 NodeA 可與 Node B-F 節點組成多個 chain。若 NodeA 產生故障,NodeB-F 更多節點分擔讀壓力,從而可以避免 NodeA 節點故障的情況下,產生節點讀瓶頸。

檔案建立流程
-
步驟 1:分配 FoundationDB 讀寫事務;
-
步驟 2:事務內寫目標檔案的 dentry、inode;建立檔案是繼承父目錄 layout,根據 stripe size 選取多條 chain,並記錄在 inode 中;寫開啟建立場景,還會寫入對應 file session;
-
步驟 3:事務內將父目錄 inode、 目標 dentry 加入讀衝突列表。保證父目錄未被刪除,及檢查目標檔案已存在場景;
-
步驟 4:提交讀寫事務。
讀寫流程
寫資料流程:
-
步驟 1:Client 獲取資料的目標 chain,並向 chain 首節點 NodeA 傳送寫請求;
-
步驟 2:NodeA 檢查 chain version 並鎖住 chunk,保證對同一 chunk 的序列寫,再用單邊 RDMA 從 client 讀取資料,寫入本地 chunk,記錄 updateVer;
-
步驟 3:NodeA 將寫請求傳播到 NodeB 和 NodeC,NodeB 和 NodeC 處理邏輯和 NodeA 相同;
-
步驟 4:chain 尾節點 NodeC 寫完資料後,將回復傳播到 NodeB,NodeB 更新 commitVer 為 updateVer;
-
步驟 5:NodeB 將回復傳播到 NodeA,NodeA 處理同 NodeB;
-
步驟 6:NodeA 回覆 Client 寫完成。

讀資料流程:
-
步驟 1:Client 獲取資料所在的 chain,並向 chain 某個節點 NodeX 發讀請求;
-
步驟 2:NodeX 檢查本地 commitVer 和 updateVer 是否相等;
-
步驟 2.1:如果不等,說明有其它 flying 的寫請求,通知 Client 重試;
-
步驟 2.2:如果相等,則從本地 chunk 讀取資料,並透過 RDMA 寫給 Client;

一個檔案在建立時,會按照父目錄配置的 layout 規則,包括 chain table 以及 stripe size,從對應的 chain table 中選擇多個 chain 來儲存和並行寫入檔案資料。chain range 的資訊會記錄到 inode 元資料中,包括起始 chain id 以及 seed 資訊(用來做隨機打散)等。在這個基礎之上,檔案資料被進一步按照父目錄 layout 中配置的 chunk size 均分成固定大小的 chunk(官方推薦 64KB、512KB、4MB 3 個設定,預設 512KB),每個 chunk 根據 index 被分配到檔案的一個 chain 上,chunk id 由 inode id + track + chunk index 構成。當前 track 始終為 0,猜測是預留給未來實現 chain 動態擴充套件用的。

訪問資料時使用者只需要訪問 Meta Service 一次獲得 chain 資訊和檔案長度,之後根據讀寫的位元組範圍就可以計算出由哪些 chain 進行處理。
假設一個檔案的 chunk size 是 512KB,stripe size 是 200,對應的會從 chain table 裡分配 200 個 chain 用來儲存這個檔案的所有 chunk。在檔案寫滿 100MB(512KB * 200)之前,其實並不是所有的 chain 都會有 chunk 儲存。在一些需要和 Storage Service 互動的操作中,比如計算檔案長度(需要獲得所有 chain 上最後一個 chunk 的長度)、或者 Trucate 操作,需要向所有潛在可能存放 chunk 的 Storage Service 發起請求。但是對不滿 100MB(不滿 stripe size 個 chunk)的小檔案來說,向 200 個 chain 的 Storage Service 都發起網路請求無疑帶來無謂的延時增加。
為了最佳化這種場景,3FS 引入了 Dynamic Stripe Size 的機制。這個的作用就是維護了一個可能存放有 chunk 的 chain 數量,這個值類似 C++ vector 的擴容策略,每次 x2 來擴容,在達到 stripe size 之後就不再擴了。這個值的作用是針對小檔案,縮小存放有這個檔案資料的 chain 範圍,減少需要和 Storage Service 通訊的數量。
透過固定切分 chunk 的方式,能夠有效的規避資料讀寫過程中與 Meta Service 的互動次數,降低元資料服務的壓力,但是也引入另外一個弊端,即對寫容錯不夠友好,當前寫入過程中,如果一個 chunk 寫失敗,是不支援切下一個 chunk 繼續寫入的,只能在失敗的 chunk 上反覆重試直到成功或者超時失敗。
Chunk Engine 由 chunk data file、Allocator、LevelDB/RocksDB 組成。其中 chunk data file 為資料檔案;Allocator 負責 chunk 分配;LevelDB/RocksDB 主要記錄本地元資料資訊,預設使用 LevelDB。
為確保查詢效能高效,記憶體中全量保留一份元資料,同時提供執行緒級安全的訪問機制,API 包括:

Chunk 大小範圍 64KiB-64MiB,按照 2 的冪次遞增,共 11 種,Allocator 會選擇最接近實際空間大小的物理塊進行分配。
對於每種物理塊大小,以 256 個物理塊組成一個 Resource Pool,透過 Bitmap 標識空間狀態,為 0 代表空閒可回收狀態,分配的時候優先分配空閒可回收的物理塊。
-
修改寫:採用 COW 的方式,Allocator 優先分配新的物理塊,系統讀取已經存在的 Chunk Data 到記憶體,然後填充 update 資料,拼裝完成後寫入新分配的物理塊;
-
尾部 Append 寫:資料直接寫入已存在 block,會新生成一份元資料包括新寫入的 location 資訊和已經存在的 chunk meta 資訊,原子性寫入到 LevelDB 或 RocksDB 中,以避免覆蓋寫帶來的寫放大。
儲存服務崩潰、重啟、介質故障,對應的儲存 Target 不參與資料寫操作,會被移動到 chain 的末尾。當服務重新啟動的時候,offline 節點上對應儲存 Target 的資料為老資料,需要與正常節點的資料進行補齊,才能保證資料一致性。offline 的節點週期性的從 cluster manager 拉取最新的 chain table 資訊,直到該節點上所有的儲存 Target 在 chain table 中都被標記為 offline 以後,才開始傳送心跳。這樣可以保證該節點上的所有儲存 Target 各自獨立進入恢復流程。資料恢復採用了一種 full-chunk-replace 寫的方式,支援邊寫邊恢復,即上游節點發現下游的 offline 節點恢復,開始透過鏈式複製把寫請求轉發給下游節點,此時,哪怕 Client 只是寫了部分資料,也會直接把完整的 chunk 複製給下游,實現 chunk 資料的恢復。
資料恢復過程整體分成為兩個大步驟:Fetch Remote Meta、Sync Data。其中 Local node 代表前繼正常節點,Remote node 為恢復節點。
資料恢復流程
-
步驟 1:Local Node 向 Remote Node 發起 meta 獲取,Remote Node 讀取本地 meta;
-
步驟 2:Remote Node 向 Local Node 返回元資料資訊,Local Node 比對資料差異;
-
步驟 3:若資料有差異,Local Node 讀取本地 chunk 資料到記憶體;
-
步驟 4:Remote Node 單邊讀取 Local Node chunk 記憶體資料;
-
步驟 5:Remote Node 申請新 chunk,並把資料寫入新 chunk。

Sync Data 原則:
-
如果 chunk Local Node 存在 Remote Node 不存在,需要同步;
-
如果 Remote Node 存在 Local Node 不存在,需要刪除;
-
如果 Local Node 的 chain version 大於 Remote Node,需要同步;
-
如果 Local Node 和 Remote Node chain version 一樣大,但是 commit version 不同,需要同步;
-
其他情況,包括完全相同的資料,或者正在寫入的請求資料,不需要同步。
業界基於分散式高效能 KV 儲存系統,構建大規模檔案系統元資料元件已成共識,如 Google Colossus、Microsoft ADLS 等。3FS 元資料服務使用相同設計思路,底層基於支援事務的分散式 KV 儲存系統,上層元資料代理負責對外提供 POSIX 語義介面。總體來說,支援了大部分 POSIX 介面,並提供通用元資料處理能力:inode、dentry 元資料管理,支援按目錄繼承 chain 策略、後臺資料 GC 等特性。

3FS 選擇使用 FoundationDB 作為底層的 KV 儲存系統。FoundationDB 是一個具有事務語義的分散式 KV 儲存,提供了 NoSQL 的高擴充套件,高可用和靈活性,同時保證了 serializable 的強 ACID 語義。該架構簡化了元資料整體設計,將可靠性、擴充套件性等分散式系統通用能力下沉到分散式 KV 儲存,Meta Service 節點只是充當檔案儲存元資料的 Proxy,負責語義解析。
利用 FoundationDB SSI 隔離級別的事務能力,目錄樹操作序列化,衝突處理、一致性問題等都交由 FoundationDB 解決。Meta Service 只用在事務內實現元資料操作語義到 KV 操作的轉換,降低了語義實現複雜度。
存算分離架構下,各 MetaData Service 節點無狀態,Client 請求可打到任意節點。但 Metadata Service 內部有透過 inode id hash,保證同目錄下建立、同一檔案更新等請求轉發到固定元資料節點上攢 Batch,以減少事務衝突,提升吞吐。計算、儲存具備獨立 scale-out 能力。
Metadata Service 採用 inode 和 dentry 分離的設計思路,兩種資料結構有不同的 schema 定義。具體實現時,採用了“將主鍵編碼成 key,並新增不同字首”的方式模擬出兩張邏輯表,除主鍵外的其它的欄位存放到 value 中。

在定義好的 inode、entry 結構之上,如何透過 FoundationDB 的讀寫事務正確實現各類 POSIX 元資料操作,是 Meta Service 中最重要的問題。但 POSIX 元資料操作有很多種,窮舉說明會導致文章篇幅過長。本章節我們從這些操作中抽取了幾種比較有代表性的常見操作來展開說明。

本文帶著讀者深入到了 3FS 系統內部去了解其各個組成部分的關鍵設計。在這個過程中,我們可以看到 3FS 的很多設計都經過了深思熟慮,不可否認這是一個設計優秀的作品。但是,我們也注意到這些設計和目前檔案儲存領域的一些主流做法存在差異。
本文是系列文章的上篇,在下篇文章中我們將進一步將 3FS 和業界的一些知名的檔案系統進行對比,希望能夠從整個檔案儲存領域的角度為讀者分析清楚 3FS 的優點和侷限性,並總結出我們從 3FS 得到的啟示,以及我們是如何看待這些啟示的。
