TiFS 能存數據,爲什麼不能存文件?

本篇文章的作者爲龍姐姐說的都隊的李晨曦,他們團隊在本次 Hackathon 比賽中構建了一個基於 TiKV 的分佈式 POSIX 文件系統 TiFS,繼承了 TiKV 強大的分區容錯和嚴格一致性特性,爲 TiKV 生態開闢了一個新的領域。

起源

一次給朋友安利 TiDB 時,得知他只有一臺機器,那跑 TiDB 集羣有什麼優勢呢?我告訴他可以每塊盤跑一個 TiKV 實例,這樣實現了多磁盤容災,就不需要組 RAID 了。

當然最後一句只是玩笑話,畢竟 TiDB 是個數據庫,只能做到數據容災。但轉念一想,如果把文件系統的數據也存進 TiKV,不就能做到文件系統容災了嗎? 於是我們花了幾天寫出了 TiFS 的雛形 —— 一個充滿 bug 經常死鎖的 POSIX 文件系統,但令人興奮的是這個想法確實可行。

雛形出來後,我們需要考慮更多的問題。比如文件系統基於 TiKV 的優勢是什麼?又比如 CAP 應該如何取捨?相比於常見的分佈式文件系統存儲後端,我認爲 TiKV 最大的優勢是天然支持分佈式事務,基於此我們可以保證文件系統的嚴格一致性。如果我們要保證嚴格一致性,即我們要構建一個 CP 系統,那適用場景應當是通用 POSIX 文件系統,完全覆蓋本地文件系統的需求,另外還能實現跨機器的文件協作或滿足其它分佈式應用的文件存儲需求。 更酷的是,支持多實例協作的單機應用運行在 TiFS 上就可能變成分佈式應用,比如 SQLite on TiFS 就是另一個分佈式關係型數據庫

設計細節

TiFS 一共需要在 TiKV 中存儲系統元數據(Meta)、文件元數據(Inode)、文件塊(Block)、文件句柄(FileHandler)、符號鏈接(SymbolicLink)、目錄(Directory)和文件索引(FileIndex)共七種值。其中文件塊是用戶寫入的透明數據,符號鏈接只存儲目標路徑,而另外五種都是序列化的結構數據。

系統元數據

系統元數據僅有一個用來生成文件序列號(inode number)的整數,其結構如下:

struct Meta {
    inode_next: u64,
}

整個文件系統只有一份系統元數據,且僅在 mknodmkdir 的過程中被更新。

文件元數據

每個文件都有一份對應的文件元數據,其結構如下:

struct Inode {
    file_attr: FileAttr,
    lock_state: LockState,
    inline_data: Option<Vec<u8>>,
    next_fh: u64,
    opened_fh: u64,
}

其中 file_attr 字段中存儲了 POSIX 文件系統所必要的元數據,比如文件序列號、文件大小、塊數量等,詳細結構可參考文檔lock_state 字段存儲了當前的上鎖狀態和持鎖人,用於實現 flock;inline_data 字段可能會存儲少量文件內容,用於提升小文件的讀寫性能;next_fn 字段是一個自增整數,用於生成文件句柄;opened_fn 字段用於記錄打開狀態的文件句柄數量。

文件句柄

文件系統對用戶的每次 open 調用生成一個文件句柄,僅用於存儲句柄的讀寫限制,其結構如下:

struct FileHandler {
    flags: i32,
}

目錄

每個目錄都需要存儲一份子文件列表以實現 readdir,列表中的每一項都存儲了一個子文件的文件序列號、文件名和文件類型,其結構如下:

type Directory = Vec<DirItem>;

struct DirItem {
    ino: u64,
    name: String,
    typ: FileType,
}

文件索引

我們可以直接遍歷目錄來實現文件查找,但爲每個文件鏈接創建索引顯然是更高效的解決方案。每個文件索引僅含有目標文件的序列號,其結構如下:

struct Index {
    ino: u64,
}

TiKV 本身只提供簡單的鍵值對存儲,鍵和值都是不定長的字節數組,所以設計系統之前需要給鍵分邏輯區域。TiFS 一共有系統元數據、文件元數據、文件塊、文件句柄和文件索引五種鍵,其中文件塊類的鍵可以用來存儲文件塊數據、符號鏈接和目錄,另外四種鍵都只用於存儲前文提到的同名值。

我們根據第一個字節區分不同類的鍵,這個字節可稱爲鍵的域(scope)。鍵的字節數組通用佈局如下:

系統元數據

系統元數據域只有唯一鍵值對,其鍵的字節數組佈局如下:

文件元數據

文件元數據域的鍵僅含有大端序編碼的文件序列號,這樣所有的文件元數據都順序地存儲在 TiKV 上,可以在 statfs 操作時直接用 TiKV 的 scan 接口掃描出所有文件的元數據。

文件元數據鍵的字節數組佈局如下:

文件塊

文件塊域的鍵由文件序列號和塊序列號的大端序編碼構成,這樣同一文件的所有的文件塊都順序地存儲在 TiKV 上,可以在讀取大段數據時直接使用 TiKV 的 scan 接口一次掃描出所需的文件塊。

文件塊鍵的字節數組佈局如下:

文件句柄

文件句柄域的鍵由文件序列號和句柄號的大端序編碼構成,其字節數組佈局如下:

文件索引

文件索引的鍵由大端序編碼的目錄文件序列號和 utf-8 編碼的文件名構成,其字節數組佈局如下:

一致性

TiKV 同時支持樂觀事務和悲觀事務,但由於 Rust 版客戶端只提供了實驗性的悲觀事務支持,以及無衝突情況下悲觀事務性能較差,目前TiFS 只使用了樂觀事務

應用場景

TiFS 可以用於大文件存儲,但它相比於現有的大文件存儲方案沒有特別的性能或存儲效率上的優勢,它的主要使用場景是小文件讀寫和複雜的文件系統操作。git 遠程倉庫可以直接使用 TiFS 存儲項目並運行 git 任務,比如 rebase 或 cherry-pick,而無需先轉存到本地文件系統;多節點應用讀寫同一文件時可以直接使用 flock 來解決衝突。它的空間管理無需複雜的 SDK 或 API 接入,只需要調用簡單的文件系統 API 或 shell 腳本。

另外,像文章開頭所說的,支持多實例協作的單機應用運行在 TiFS 上就可能變成分佈式應用,如運行在 TiFS 上的 SQLite 就成了分佈式關係型數據庫。當然這種場景對單機應用本身的要求比較嚴苛,首先應用本身得支持單機多實例,另外儘量不依賴 page cache 或其它緩存以避免寫入不可見。

測試與性能

目前我們還沒有給 TiFS 寫測試,開發過程中我們一直以 pjdfstest 爲正確性基準並最終通過了它。但 pjdfstest 並不能覆蓋讀寫正確性和併發下正確性,後面需要再跟進其它的測試。

從理論上來說 TiFS 的讀寫性能的影響因素主要有三個:文件系統塊大小、網絡帶寬延遲和負載塊大小。下面我們給出了一份 benchmark結果,並針對讀寫 $IOPS$ 和讀寫速度作出了四張圖表。

我們首先來討論 $IOPS$ 的變化規律,如下兩張圖分別是順序寫 $IOPS$ 和順序讀 $IOPS$ 隨負載塊大小的變化,四條折線代表不同的文件系統塊大小和數據副本數。

由於順序讀寫的 IO 操作是線性的,每次 IO 操作都是一次 TiKV 事務。如果我們忽略每次 IO 操作之間的細微區別,那一次 IO 操作的耗時 $T$ 就是 $IOPS$ 的倒數,且 $T$ 由 FUSE 的 IO 耗時 $T_f$,TiFS 本身的邏輯耗時 $T_c$ 、 網絡傳輸耗時 $T_n$ 和 TiKV 的邏輯耗時 $T_s$ 疊加而成。如果不考慮流式處理,耗時就是線性疊加,即 $T=T_f+T_c+T_n+T_s$, $IOPS=1/T_f+T_c+T_n+T_s$。

只考慮讀的情況,$T_f$ 與負載塊大小正相關; $T_n$ 和 $T_s$ 跟負載塊和文件系統塊中的較大者正相關(因爲 TiFS 每次 IO 操作必須讀寫文件塊整數倍的數據),而更大的流量可能會造成更多的網絡和磁盤 IO 耗時;$T_c$ 的理論變化規律未知。

TiKV 是個非常複雜的系統,其內部也可分爲邏輯耗時、磁盤 IO 耗時和網絡耗時。本文在性能分析時暫且將 TiKV 簡化,只考慮單副本的情況。

圖讀 $IOPS$ 隨負載大小變化 中,文件塊和負載塊均爲 $4K$ 時,隨着負載的增大 $T_f$, $T_n$ 和 $T_s$ 都在增加,故 $IOPS$ 減小。文件塊 $64K$ 和 $1M$ 的情況下,當負載塊小於文件塊,$T_n$ 和 $T_s$ 幾乎不變,$T_f$ 增大,$IOPS$ 減小;當負載塊大於文件塊,$T_f$, $T_n$ 和 $T_s$ 都在增加,故 $IOPS$ 持續減少。變化折線大致符合預期。

圖中橫座標爲對數距離,且取樣點過少,斜率僅供參考。

順序寫數據時,如果負載塊小於文件塊,則 TiFS 需要先讀一個髒文件塊,會造成額外的 $T_c$ 和 $T_n$。這一點在文件塊比較大時比較明顯。如圖寫 $IOPS$ 隨負載大小變化 中,文件塊 $1M$ 時 $IOPS$ 的極大值明顯處於文件塊與負載塊相等時。

另外我們可以發現,負載塊和文件塊爲 $4K$ 或 $64K$ 時的 $IOPS$ 的幾乎相等的。此時每秒最小流量 $4K \times 110=440K$,每秒最大流量 $64K \times 100=6.25M$,對網絡或者磁盤的壓力很小。在流量足夠小的情況下可以認爲 $IOPS$ 到達到了上限,此時 $T_n$ 的主要影響因素變成了網絡延遲(本機測試可以認爲是 $0ms$)。特別在寫 $IOPS$ 隨負載大小變化 圖中,文件塊和負載塊在 $4K$ 和 $64K$ 之間變化對 $IOPS$ 幾乎無影響。我們稱此時的 $T$ 爲 TiFS 在當前網絡環境下的固有操作延遲,它主要由 $T_c$ 和 $T_s$ 決定。TiFS 和 TiKV 的邏輯耗時造成了固有延遲,過高的固有延遲會造成小文件讀寫體驗很糟糕,但具體的優化思路還需要進一步的 perf。

讀寫速度等於 $IOPS$ 與負載塊的乘積,而 $IOPS$ 在負載塊 $4K$ 到 $1M$ 之間沒有劇烈變化,我們很容易就能看出負載塊 $1M$ 時讀寫速度達到最大值。下面兩張是負載塊 $1M$ 時不同集羣配置下的讀寫速度對比圖。

文件塊 $64K$ 下未開啓 Titan 的對比圖已有單副本數據,三副本數據僅供參考,未進行重複對比。

我們可以看到寫速度受文件塊大小和 Titan 的影響比較大,讀速度則幾乎不受影響。文件塊越小,TiKV 需要寫入的鍵值對越多,導致了額外耗時;但文件塊過大會導致 RocksDB 寫入性能不佳,開啓 Titan 可以減少不必要的值拷貝,明顯提升性能。

未來

TiFS 在架構上存在的一個問題是文件塊存儲成本比較高。TiKV 採用多副本冗餘,空間冗餘率(實際佔用空間/寫入數據量)一般 3 起步;而像 HDFS 或 CephFS,JuiceFS 等支持 EC 冗餘模式的分佈式文件系統冗餘率可降到 1.2 到 1.5 之間。EC 冗餘在寫入和重建數據時需要編解碼,均需要額外的計算資源。其在寫入時可以降低網絡開銷和存儲成本,但重建時一次需要讀取多個數據塊,有額外的網絡開銷,是一種犧牲部分讀性能以降低寫入時網絡開銷及存儲成本的冗餘策略

目前 TiKV 要支持 EC 冗餘還比較困難,後面 TiFS 會嘗試支持 EC 冗餘的對象存儲來存文件塊以降低存儲成本,但近期的工作還是集中在正確性驗證和性能調優。正確性驗證部分包括找其它的開源文件系統測試和自建測試。性能調優部分包括 TiFS 的本身的調優工作和 TiKV 的高性能使用,以降低固有延遲。如果你對這個項目感興趣,歡迎來試用或討論。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章