共計 4392 個字符,預計需要花費 11 分鐘才能閱讀完成。
這期內容當中丸趣 TV 小編將會給大家帶來有關如何設計文件系統 LFS,文章內容豐富且以專業的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。
LFS 超快的文件系統,可以同時存儲海量大文件和小文件。并且不單單是一個文件系統,我還用作了數據庫。
我的測試數據是:和 C 直接讀寫文件速度幾乎一樣。
項目地址:github git@osc
注:這是一個開源項目,你可以自由使用,但對于開發者,我們需要審核,確認是否能承擔開發工作。所以,需要 @ME.
在設計之前,需要明確兩個目標:高并發,超快讀寫。
為了實現高并發,那么必須將每個并發進行分離,各自做自己的工作,互不影響。意味著 A 和 B 可以同時讀相同的或者不同的文件,或者寫不同的文件,但是,不能寫同一個文件。
為了實現超快讀寫,在高并發的前提下已經可以很快的進行文件的操作了,并且對文件的定位也非常重要,事實上,我是基于對文件定位的設計來實現的高并發。
那么我的工作就變得清晰簡單了,我需要實現一個非常棒的文件定位就好了。
為了更快的速度,我沒有使用文件名的方案(在應用場景,這根本就不必用到),而是將文件名設計為一個文件 ID。我使用一個 int 來實現,意味著最大可以提供 2^32 -1 個文件,這已經很大了,幾乎可以存儲一個大型服務的所有文件了。(事實上,為了實現海量存儲,實現分布式,我們可以非常簡單的組織這個文件 ID,來實現一個宇宙唯一的文件 ID,為何這么說?因為文件 ID 是自增長的,所以雖然有 int 的限制,但我們可以無視它,就是說文件 ID 也可以是變長的。)
文件 ID 是自增長的,所以對于上層應用來講,文件名是由文件系統生成的。這樣的好處是,文件名足夠小,并且因為是設計好的,所以文件系統知道該怎么分配一個文件 ID 給上層應用。并且可以根據這個文件 ID,實現理想的文件定位策略。在我的實現中,這個文件 ID 沒有任何神秘之處,所以不需要任何算法來計算出這個文件 ID,它只是自增長的而已,只不過它自己增長的非常恰到好處。
暫時忘記文件 ID,先 Mark 一下,稍后再講,我們先來看一下如何能快速定位到文件內容。最好的方式莫過于能直接知道文件內容的存儲位置了(起始位置和結束位置),ok,那么我們就用兩個 int 來存儲這兩個信息,然后我們就可以直接根據這個存儲位置找到實際的文件內容了。嗯,沒錯,這種方法不錯。那么問題來了,我們有許多許多的文件,每個都需要記錄有位置信息才行,所以我們得弄一個文件出來,專門存儲這個位置信息來對應文件的實際內容。因此我做一個索引文件出來,用來記錄文件內容的存儲位置(人們稱之為元數據,但我不這么理解,在 LFS 中,它就是一個索引文件 Index,稍后你也會理解為何如此命名)。這樣一來我可以用這個索引文件記錄許多個文件的位置信息了,在索引文件里查一下,就能知道存儲位置在什么地方,很簡單。不過,當文件變得多起來的時候,和索引文件對應的存儲實際內容的文件會增長的比索引文件快得多,當其增長到一定上限時,我們無法對其寫入新的內容(受限于操作系統的文件格式,我們可能無法對一個單一文件寫入大量內容,并且由于我們前面使用兩個 int 來記錄位置信息,這就決定了,文件內容不能大于 4G)。所以我們得對存儲實際內容的文件進行分塊,用許多許多的塊文件來存儲超過容量限制的內容,在 LFS 里稱之為 Block。
這樣一來我們可以存儲許多數據了,但是 Index 和 Block 的對應關系被破壞掉了(事實上,我很樂意看到如此情況,因為由此,才可以進入高并發的第一步)。為何這么說,如果依然保留這種對應關系也是可以的,就是說每個 Index 都有一個 Block 與之對應。但是這樣一來,Index 會有許多個,并且如果一個 Block 只存儲一個文件的話,那么 Index 會變得非常小(只有 8 字節),這會造成 Index 的嚴重碎片化,而一旦 Index 變得碎片化了,那么我們根據其進行定位文件內容也相應變得更復雜了。所以,我們打破 Index 和 Block 的對應關系,使用另外一種方式,令 Block 對應到 Index。使 Index 中記錄每個文件對應的 Block ID,來實現新的對應關系,這就增加了一個 int 來記錄 Block ID。這樣,Index 就可以記錄許多個 Block 了,并且也不會產生碎片化,可以平穩的進行增長了。
這樣一來,我們就可以實現一種并發了。因為每個文件都會記錄自己的 Block ID,那么當對不同文件進行操作時,意味著,是對不同 Block 進行操作,因為每個 Block 具有獨立性,所以,只要同一時刻,處理的不是同一個 Block,那么許多個 Block 可以自由的進行處理,互不影響。至此,我們已經可以實現部分并發了。
對于 Index 來講,已經記錄的位置信息也是可以進行并發處理的。因為已經記錄過的內容不會再次發生改變,所以讀取索引時,可以實現高并發,從而實現讀取的高并發。
不過,這里有一個問題,被前文忽略了,就是 Index 是怎么寫的呢?
當 LFS 收到寫文件請求時,需要找到一個空閑的 Block,并且將 Block ID 和該 Block 內的位置信息記錄到 Index 中,即 Index 中的每個記錄有 12 個字節。單線程處理時沒有任何問題,不停的對這個 Index 進行追加寫。但是當并發產生時,我們就遇到了麻煩,Index 的內容會被哪個線程進行寫操作呢?可能會被寫亂。所以,我們的要求高并發之路,在這里被擋了下來,這里會變成單線程操作。不過,幸運的是,Index 寫的內容很少,每次只有 12 個字節,所以會很快,從而降低了對并發的影響。
恰好,和 Block 類似,我們也可以用許多個 Index 來實現高并發。就是說,每一個 Index 只有一個線程在寫,這樣一來,高并發又提高了一個量級。
至此,我們來描述下 Index 的格式:BlockID:int, start:int, end:int,共 12 個字節。每一個文件都有這 12 個字節。但是?額……我們的文件 ID 在哪里呢?
答案是:沒有文件 ID。
接下來講一下,我們前面 Mark 的文件 ID。如我所說,沒有文件 ID 會存儲,那么是如何找到對應的文件內容的呢,就是說如何找到 Index 內的位置信息呢?
事實上,我很討厭文件 ID 這個東西,所以有意避開它了,因為確實沒有必要來存儲這個文件 ID,如果是文件名的話,那就不得不存儲了,但是幸好在 LFS 設計之初,就使用的是文件 ID。并且我為什么要浪費字節來存儲文件 ID 呢?所以,由于我們之前的設計,我們可以不用存儲文件 ID 了,這就是說,LFS 內不會有任何的查找過程,即使是 Index 內也不會。
LFS 使用的方案是通過計算找到具體內容,并且只有計算。由于 Index 內每個文件都是相同的 12 個字節的記錄,所以,LFS 通過應用層傳來的文件 ID 乘以 12 個字節,就得到了,該文件 ID 在 Index 內的位置,然后取出這 12 個字節就能到對應的 Block 進行操作了。索引一定需要哈希或者排序?看樣子不是。所以這也是我稱之為索引的原因,因為 Index 發揮的索引的作用比元數據的作用高得多。
不過,因為 Index 文件也有多個,那么首先我們得知道文件 ID,所在的 Index 才行。幸好,每個 Index 的額定大小是相同的,即每個 Index 都可以記錄相同數量的文件位置信息。并且每個 Index 都是有編號的,即我們理解為:編號為 0 的 Index 存儲文件 ID [0 – 99] 的位置信息,編號為 1 的 Index 存儲文件 ID [100 – 199] 的位置信息;現在需要讀取文件 ID 為 109 的內容,那么使用 Math.floor(109 / 100),得到 1 即為編號為 1 的 Index;然后我們還需要獲得在該 Index 內的文件 ID,即:109 – (1 * 100) = 9;最后,因為每個文件記錄 12 個字節的信息,所以:9 * 12 = 108,即為索引內的偏移,然后取出后面的 12 個字節,就是對應 Block 的信息了。從而根據讀取到的 Block 信息對 Block 進行操作。
因為文件 ID 與 Index 是隱式關聯的,所以,在并發時分配空閑 Index 時,即意味著,分配了一個恰到好處的文件 ID,這樣就實現了文件 ID 的自增長,并且確實恰到好處。
這就是 LFS 的核心原理。 有什么理由會不快呢?
核心原理,看似簡單,但是,LFS 實現的更多,支持更新和刪除,尤其是更新,實現起來確實復雜。
LFS 會像其他的文件系統一樣在更新時使用擴展塊嗎?不會,為什么需要呢?我們已經有了 Block,那么直接用 Block 進行更新就好了。LFS 為更新提供了多種操作方式,暫且以實現起來最簡單,并且描述起來也最簡單的方式來做一個說明:
操作方式之一是,先刪除舊的內容,然后重新寫文件。即重新進行一次寫文件的流程,只不過,此時,文件 ID 不需要增長,直接向指定的文件 ID 覆蓋寫入內容即可。這也就意味著,即使是在第一次寫文件的時候(LFS 還沒有自增到該文件 ID),也可以使用指定的文件 ID 來寫入內容,并不是一定要 LFS 先生成該文件 ID,然后才能進行寫入(但是我個人不建議這么做)。
其他的操作方式是在原數據上進行修改(處理方式不同),不刪除舊的內容。如果新內容大于原來的內容的話,會分配一個新的 Block 用來寫入溢出的內容。這個過程可能會產生數據遷移,幸好,LFS 的多種更新方式中存在避免數據遷移的方式,并且也提供分配更少 Block 的方式來使更新效率最大化。(我很喜歡這些處理方式,非常符合我的工作需求,對于更新頻繁,或者不斷增長的內容,非常棒。)
由于更新方式有多種以應對各種需求,所以更新的功能實現起來很復雜,但對外 API 依然很簡單。事實上,LFS 沒有更新的 API,寫文件的 API 同時實現了更新,這樣對外層應用來講會更簡單(為什么非要弄一個更新 API 呢?NO!)。
LFS 把許多文件都進行分塊處理,就是說,幾乎每個部分都是并發的。每個文件的大小默認是 64M(為什么?不是因為流行,而是我認為 64M 可以非常快的一次性載入內存,即使是配置不怎么樣的機器。事實上,如你所見,幾乎沒有多少 CPU 計算,所以,LFS 更適合部署在成本低但 IO 優秀的機器上,從而減少成本。成本也是 LFS 的要求之一,我希望任何人都能用得起 LFS。),可以用來存儲海量大文件和小文件,由于前面介紹的原理,這意味著,可以同時存儲海量大文件和小文件。
并且,我不單把 LFS 只作為一個文件系統來用,事實上,我個人也使用它的數據庫特性,相信這一點大家都能理解。
另外,LFS 在計劃中,提供碎片整理,用來將碎片進行合并處理等,希望有意向的伙伴可以加入進來。事實上,我已經通過一種方式,來將這種操作變得更簡單,但還有部分尚未實現。
上述就是丸趣 TV 小編為大家分享的如何設計文件系統 LFS 了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關知識,歡迎關注丸趣 TV 行業資訊頻道。