JuiceFS 目錄配額功能設計詳解
JuiceFS 在最近 v1.1 版本中加入了社區(qū)中呼聲已久的目錄配額功能。已發(fā)布的命令支持為目錄設置配額、獲取目錄配額信息、列出所有目錄配額等。完整的詳細信息,請查閱文檔。
在設計此功能時,對于它的統(tǒng)計準確性,實效性以及對性能的影響,團隊內部經歷過多次討論和權衡。在本文中,我們會詳述一些在設計關鍵功能時的不同抉擇及其優(yōu)缺點,并分享最終的實現方案,為想深入了解目錄配額或有相似開發(fā)需求的用戶提供參考。
01 需求分析
配額的設計首先需考慮以下三個要素:
統(tǒng)計的維度:常見的是基于目錄來統(tǒng)計用量和實現限制,其他還有基于用戶和用戶組的統(tǒng)計
統(tǒng)計的資源:一般包括文件總容量和文件總數量
限制的方式:最簡單的就是當使用量達到預定值時,就不讓應用繼續(xù)寫入。這種預定值一般稱為硬閾值(Hard Limit)。還有一種常見的限制叫做軟閾值(Soft Limit),在使用量達到這個值時,僅觸發(fā)告警通知但不立即限制寫入,而是在達到硬閾值或者經過一定的寬限時間(Grace Period)后再實施限制。
其次,也應考慮對配額統(tǒng)計實效性和準確性的要求。在分布式系統(tǒng)中,往往會有多個客戶端同時訪問,若要保證他們在同一時間點對配額的視圖始終一致,勢必會對性能有比較大的影響。最后,還應考慮是否支持復雜的配置,如配額嵌套、為非空目錄設置配額等。
開發(fā)原則
我們的主要考量是盡量簡單和便于管理。在實現時避免大規(guī)模代碼重構,減少對關鍵讀寫路徑的侵入,以期在實現新特性的同時,不會對現有系統(tǒng)的穩(wěn)定性和性能造成較大影響?;诖耍覀冋沓隽巳缦卤硭镜拇_發(fā)功能:

值得一提的是表中標紅的三項。一開始我們并不打算支持這些,因為它們的復雜性對配額功能的整體實現構成了挑戰(zhàn),而且也不在我們定義的核心功能之列。但在與多方用戶溝通后,我們意識到缺少這些功能會導致配額功能的實用性大打折扣,許多用戶確實需要這些功能來滿足他們的實際需求。因此,最終我們還是決定在 v1.1 版本中就帶入這些功能。
02 基礎功能
1 用戶接口
在設計配額功能時,首先要考慮的是用戶如何設置和管理配額。這一般有兩種方式:
1.使用特定的命令行工具,如?GlusterFS?使用以下命令為指定目錄設置硬閾值:
$ gluster volume quota <VOLNAME> limit-usage <DIR> <HARD_LIMIT>
2.借助已有的 Linux 工具,但使用特定的字段;如?CephFS?將配額作為一項特殊的擴展屬性來管理:
$ setfattr -n ceph.quota.max_bytes -v 100000000 /some/dir ? ? # 100 MB
JuiceFS 采用了第一種方式,命令形式為:
$ juicefs quota set <METAURL> --path <PATH> --capacity <LIMIT> --inodes <LIMIT>
做出這個選擇主要有以下三點理由:
JuiceFS 已有現成的 CLI 工具,要添加配額管理功能只需新加一個子命令即可,非常方便。
配額通常應由管理員來進行配置,普通用戶不能隨意更改;自定義的命令中可要求提供 METAURL 來保證權限。
第二種方式需要提前將文件系統(tǒng)掛載到本地。配額設置常需對接管控平臺,將目錄路徑作為參數直接包含在命令中可以避免此步驟,使用起來更加方便。
2 元數據結構
JuiceFS 支持三大類元數據引擎,包括 Redis,SQL 類(MySQL、PostgreSQL、SQLite 等)和 TKV 類(TiKV、FoundationDB、BadgerDB 等)。每類引擎根據其支持的數據結構有不同的具體實現,但管理的信息大體上是一致的。在上一小節(jié)我們已決定使用獨立的?juicefs quota
?命令來管理配額,那么元數據引擎中也同樣使用獨立的字段來存儲相關信息。以較簡單的 SQL 類為例:
// SQL tabletype dirQuota struct {
? ?Inode ? ? ?Ino ? `xorm:”pk”`
? ?MaxSpace ? int64 `xorm:”notnull”`
? ?MaxInodes ?int64 `xorm:”notnull”`
? ?UsedSpace ?int64 `xorm:”notnull”`
? ?UsedInodes int64 `xorm:”notnull”`}
可見,JuiceFS 為目錄配額新建了一張表,以目錄索引號(Inode)為主鍵,保存了配額中容量和文件數的閾值以及已使用值。
3 配額更新/檢查
接下來考慮配額信息的維護,主要是兩個任務:更新和檢查。
更新配額通常牽涉到新建和刪除文件或目錄,這些操作都會對文件個數產生影響。此外,文件的寫入操作會對配額的使用容量產生影響。實現上最直接的方式是在每個請求完成更新后,同時將更改提交到數據庫。這可以確保統(tǒng)計信息的實時性和準確性,但很容易造成嚴重的元數據事務沖突。
究其原因,是因為在 JuiceFS 的架構中,沒有獨立的元數據服務進程,而是由多個客戶端以樂觀事務的形式并發(fā)將修改提交到元數據引擎。一旦它們在短時間內嘗試更改同一個字段(比如配額的使用量),就會引發(fā)嚴重的沖突。
因此,JuiceFS 的做法是在每個客戶端內存中同步維護配額相關的緩存,并將本地更新每隔 3 秒異步地提交到數據庫。這樣做犧牲了一定的實時性,但可以有效減少請求個數和事務沖突。此外,客戶端在每個心跳周期(默認 12 秒)從元數據引擎加載最新信息,包括配額閾值和使用量,以了解文件系統(tǒng)全局的情況。
配額檢查與更新類似,但更為簡單。在執(zhí)行操作之前,如有必要客戶端可直接在內存中進行同步檢查,并在檢查通過后才繼續(xù)后面的流程。
03 復雜功能設計
本章討論目錄配額中相對復雜的兩個功能(即第一章需求表中標紅項)的設計思路。
功能1:配額嵌套
在與用戶進行溝通時,我們經常面臨這樣的需求:某個部門設置了一個大型的配額,但在該部門內部可能還有小組或個人,而這些個體也需要各自的配額。
這里就需要對配額增加嵌套結構。如果不考慮嵌套,每個目錄只有兩種狀態(tài):沒有配額或者只受一個配額限制,整體維護比較簡單。一旦引入嵌套結構,情況就會變得相對復雜。例如,在更新文件時,我們需要找到所有受影響的配額并對其進行檢查或更改。那么在給定目錄后,如何快速找到其所有受影響的配額呢?
方案一:緩存 Quota 樹以及目錄到最近 Quota 的映射

這個方案比較簡單直接,即維護配額間相互的嵌套結構,以及每個目錄到最近配額的映射信息。針對上圖的數據結構如下:
// quotaTree map[quotaID]quotaID{q1: 0, q2:0, q3: q1}// dirQuotas map[Inode]quotaID{d1: q1, d3: q1, d4: q1, d6: q3, d2: q2, d5: q2}
有了這些信息,在配額更新或查找時,我們可以根據操作的目錄 Inode 快速找到最近的配額 ID,再根據 quotaTree 逐級找到所有受影響的配額。這個方案能實現高效的查找,從靜態(tài)角度來看,是有優(yōu)勢的。然而,某些動態(tài)變化會難以處理。考慮如下圖所示場景:

現在需要將目錄 d4 從原來的 d1 移動到 d2 下。這個操作中 q3 的父配額從 q1 變成了 q2,但由于 q3 被配置在 d6 上,這個變化很難被感知到(我們可以在移動 d4 的同時遍歷其下所有目錄看它們是否有配額,但顯然這會是個大工程)。鑒于此,這個方案并不可取。
方案二:緩存目錄到父目錄的映射關系

第二個方案是緩存所有目錄到其父目錄的映射關系,針對上圖的初始數據結構如下:
// dirParent map[Inode]Inode{d1: 1, d3: d1, d4: d1, d6: d4, d2: 1, d5: d2}
同樣的修改操作,這時僅需將 d4 的值由 d1 改成 d2 即可。此方案中,在查找某個目錄所有受影響的配額時,我們需要根據 dirParent 逐級往上直到根目錄,在過程中檢查每個路過的目錄是否設置了配額。顯然,這個方案的查找效率相比之前的方案略低。但好在這些信息都緩存在客戶端內存中,整體效率依然在可接受范圍內,因此我們最終采用了這個方案。
值得一提的是,這個目錄到父目錄的映射關系是常駐客戶端內存的,沒有設置特定的過期策略,這主要有兩個角度的考慮:
通常情況下,文件系統(tǒng)的目錄數量不會非常大,僅用少量內存即可將其全部緩存起來。
其他客戶端對目錄的更改,在本客戶端中并不需要立即感知;當本客戶端再次訪問相關目錄時,會通過內核下發(fā)的查找(Lookup)或讀取目錄(Readdir)請求更新緩存。
功能2:遞歸統(tǒng)計
在需求分析階段,除了嵌套配額外,還出現了兩個相關的問題:一是為非空目錄設置配額,二是目錄移動之后產生配額變化。這兩個問題其實本質上是同一個,那就是 “如何快速地獲取某個目錄樹的統(tǒng)計信息”。

方案一:默認為每個目錄添加遞歸統(tǒng)計信息
這個方案有點像前面的配額嵌套功能,只是現在需要為每個目錄都加上遞歸統(tǒng)計信息,數量上會比配額多不少。它的好處是使用時比較方便,僅需一次查詢就能立即知道指定目錄下整棵樹的大小。這個方案的代價是維護成本較高,在修改任一文件時,都需要逐級往上修改每個目錄的遞歸統(tǒng)計信息。這樣越靠近根節(jié)點的目錄被修改的越頻繁。JuiceFS 的元數據實現均采用樂觀鎖機制,即在發(fā)現沖突時通過重試來解決,在高壓力情況下,部分目錄的修改事務會沖突得非常嚴重。而且隨著集群規(guī)模的擴大,頻繁重試還會導致元數據引擎壓力急劇上升,容易導致崩潰。
方案二:平時不干預,只有在需要時,才對指定目錄樹進行臨時掃描
這是一個很簡單而直接的方案。其問題在于當目錄下的文件數量龐大時,臨時掃描可能會耗時非常久。同時,這也會對元數據引擎產生很高的爆發(fā)壓力。因此,這個方案也不適合拿來直接使用。
方案三:平時只維護每個目錄下一級子項的使用量,需要時掃描指定樹下所有目錄

這個方案結合了前兩個方案的優(yōu)點,并盡力避免了它們的缺點。在進一步說明前首先介紹兩個文件系統(tǒng)中的現象:
在處理大部分元數據請求時,其本身就帶有直接父目錄的信息,因此不需要額外的操作去獲取,也不會引入額外的事務沖突
通常情況下,文件系統(tǒng)中目錄數量會比普通文件少 2 ~ 3 個數量級
基于上述兩點觀察,JuiceFS 實現了稱之為目錄統(tǒng)計的功能,即在平時就維護好每一個目錄下一級子項的統(tǒng)計量。當配額功能需要使用遞歸統(tǒng)計信息時,無需遍歷所有文件,而只需統(tǒng)計所有子目錄的使用量即可。這也是 JuiceFS 最終采用的方案。
另外,在加入了目錄統(tǒng)計功能后,我們還發(fā)現了一些額外的好處。比如原本就有的?juicefs info -r
?命令,被用來代替?du
?統(tǒng)計指定目錄下的使用總量;現在這個命令的執(zhí)行速度又有了數量級上的提升。還有一個是新加的?juicefs summary
?命令,它可用來快速分析指定目錄下的具體使用情況,如執(zhí)行特定排序來找到已用容量最高的子目錄等。
04 其他功能 :配額修復
在上述的介紹中,我們已經知道 JuiceFS 在實現目錄配額時,為了追求穩(wěn)定性和減少對性能的影響,在一定程度上犧牲了準確性。當客戶端進程異常退出,或目錄被頻繁移動時,配額信息會有少量的丟失。隨著時間的推移,這可能導致存儲的配額統(tǒng)計值與實際情況出現較大的偏差。
因此,JuiceFS 還提供了?juicefs check
?這個修復功能。它被用來重新掃描統(tǒng)計整棵目錄樹,并將結果與配額中保存的值做比對。如果發(fā)現數據不匹配,系統(tǒng)會向您報告存在的問題,并提供可選的修復選項。
轉自:http://www.npqdlp.com/