Frangipani 分布式文件系統深度解析:緩存一致性、分布式事務和分布式崩潰恢復
Frangipani 是一個誕生于 1997 年的分布式文件系統,由當時大名鼎鼎的 數字設備公司(Digital Equipment Corporation, DEC) 的研究員們打造。
我們為什么要在今天回顧這樣一個“古老”的系統?因為它極其清晰地展示了構建一個分布式系統時必須面對的三個核心挑戰—— 緩存一致性 (cache coherence) 、 分布式事務 (distributed transactions) 和 分布式崩潰恢復 (distributed crash recovery) ,以及這三者之間錯綜復雜的“三角關系”。Frangipani 的設計就像一本教科書,把這些復雜問題掰開揉碎了呈現給我們。
宏觀架構:聰明的“客戶端”與簡單的“存儲”
Frangipani 的設計目標非常明確:為一組在同一個辦公環境(比如一個實驗室)里協同工作的工程師,提供一個統一、高性能、且支持共享的文件系統。用戶可以在任何一臺工作站上登錄,都能看到完全一致的文件視圖,就像在操作本地文件一樣。
為了實現這個目標,Frangipani 采用了非常經典的兩層架構:
+---------------------------------------------------+
| User Programs |
+---------------------------------------------------+
| Workstations with Frangipani File System |
+---------------------------------------------------+
| Network |
+---------------------------------------------------+
| Petal Distributed Virtual Disk |
+---------------------------------------------------+
- 底層是 Petal :一個分布式的虛擬磁盤服務。你可以把它想象成一個容量巨大、高可用的網絡硬盤。它把文件系統的所有數據,包括目錄、i-node、文件內容塊、空閑塊位圖等,都以最原始的“塊”形式儲存起來。
- 上層是 Frangipani :它并不像傳統文件系統那樣有一個中心化的“文件服務器”。相反,文件系統的邏輯被分散到每一臺運行 Frangipani 的工作站上。這些工作站都直接與底層的 Petal 通信,共享同一份數據。
Frangipani 與 GFS 的核心區別
這里可以和另一個著名的分布式文件系統 GFS (Google File System) 做個對比。它們的設計哲學截然不同:
- 邏輯分布 :Frangipani 將文件系統的核心邏輯放在客戶端(工作站),而 GFS 的邏輯絕大部分集中在 Master 和 Chunkserver 這樣的服務器端。
- 緩存策略 :Frangipani 為了性能,在每個工作站都設計了復雜的 寫回緩存 (write-back cache) 。而 GFS 幾乎沒有客戶端緩存,因為它主要為一次性、順序讀寫TB級別的巨大文件而設計,緩存沒有意義。
- 一致性 :因為有緩存,Frangipani 必須實現一套復雜的 緩存一致性協議 來保證數據的一致性。而 GFS 因為沒有緩存,也就不需要這個協議。
- 接口 :Frangipani 對外呈現為一個標準的、與現有應用完全兼容的文件系統。而 GFS 需要應用專門通過其提供的庫函數來訪問。
為什么 Petal 提供的是“塊”接口?
你可能會問,既然最終需要的是文件和目錄,為什么不讓 Petal 直接提供文件服務(像 AFS 那樣)呢?
這背后主要有兩個原因。首先,Petal 這個項目本身是先于 Frangipani 開發的,它已經解決了存儲層的擴展性和容錯問題。直接復用 Petal,大大簡化了 Frangipani 的設計。其次,這種設計將計算密集型的文件系統邏輯從中心服務器轉移到了各個客戶端工作站,使得整個系統的性能可以隨著工作站數量的增加而線性擴展。
核心挑戰與 Frangipani 的解法
Frangipani 的去中心化架構帶來了高性能和高可擴展性,但同時也引出了三大嚴峻挑戰。
挑戰一:緩存一致性 (Cache Coherence)
由于每個工作站都有自己的寫回緩存,一個嚴峻的問題出現了:當工作站 A 修改了文件 foo
,它的修改只是暫存在自己的緩存里,并沒有立刻寫回 Petal 。這時,如果工作站 B 去讀取 foo
,它會讀到 Petal 上的舊數據嗎?
為了解決這個問題,Frangipani 引入了一個獨立的 分布式鎖服務 (lock service) 。
- 基本原則:一個工作站想讀寫某個文件,必須先從鎖服務那里獲取該文件的鎖。讀操作需要 讀鎖 (read lock) ,寫操作需要 寫鎖 (write lock) 。
- 協議流程:
請求 (Request) :工作站 A 向鎖服務請求文件 z
的寫鎖。
授予 (Grant) :鎖服務將 z
的所有權交給 A。A 從 Petal 讀取 z
的數據到本地緩存,然后進行修改。
撤銷 (Revoke) :此時,工作站 B 也想讀 z
,向鎖服務請求讀鎖。鎖服務發現鎖在 A 手里,于是向 A 發送一個“撤銷”請求。
釋放 (Release) :A 收到撤銷請求后,必須先將自己緩存中被修改過的(即“臟”的)z
數據寫回 Petal,然后再向鎖服務釋放鎖。
再次授予 :鎖服務收到 A 的釋放消息后,再將 z
的讀鎖授予 B。B 此時從 Petal 讀取的就是最新的數據了。
通過這套鎖機制,Frangipani 保證了任何讀操作都能讀到最近一次寫操作的結果,實現了 強一致性 。
什么是“偽共享 (false sharing)”問題?
在設計鎖的粒度時,一個常見的問題是 偽共享(false sharing) 。Frangipani 的讀寫單位是 512 字節的塊。如果兩個不相關的數據(比如兩個文件的 inode)被放在了同一個 512 字節的塊里,那么當兩個工作站分別想修改這兩個不相關的數據時,它們卻不得不爭搶同一個塊的鎖,來回傳遞數據塊,造成不必要的性能開銷。Frangipani 通過一個簡單的策略避免了這個問題: 讓每個 inode 單獨占用一個 512 字節的塊 。雖然有點浪費空間,但極大地簡化了設計,避免了偽共享。
挑戰二:原子性與分布式事務
當多個工作站同時操作同一個目錄時,比如同時在根目錄下創建文件 /a
和 /b
,如何保證目錄的最終狀態是正確的,而不會因為并發修改導致數據損壞?
Frangipani 巧妙地復用了它的鎖機制來實現 事務性操作 (transactional operations) 。一個文件系統操作(如創建文件)可能涉及多個步驟(分配 inode、初始化 inode、更新目錄數據塊)。Frangipani 的做法是:
- 在執行操作前,工作站必須 獲取所有 將要被修改的數據塊的鎖。
- 在持有所有鎖的情況下,完成所有的修改。
- 操作完成后,才釋放這些鎖。在持有鎖期間,它不會響應鎖服務的撤銷請求。
這樣一來,任何一個多步驟的操作,對于其他工作站來說都是 原子 (atomic) 的。它們要么看到操作完成后的最終狀態,要么什么都沒看到,絕不會看到一個只進行了一半的“中間狀態” 。
Frangipani vs. 兩階段提交 (Two-Phase Commit)
Frangipani 的這種方式和數據庫中經典的 兩階段提交(Two-Phase Commit, 2PC) 有相似之處,都是先加鎖,再執行,最后解鎖。但它們有本質區別:
- 在 Frangipani 中,鎖和數據是“可移動的”,執行操作的工作站會把所有需要的鎖和數據都“拉”到自己本地進行處理。
- 在典型的 2PC 系統中,數據是分片 (sharded) 存儲在不同服務器上的,工作也必須被拆分到各個服務器上執行。
- 更重要的是,Frangipani 對于節點崩潰的處理更優雅。如果一個持有鎖的工作站崩潰了,因為它的日志在共享的 Petal 上,其他工作站可以接管并完成恢復。而傳統的 2PC,如果一個參與者的日志在本地磁盤上且該機器宕機,整個事務就會被阻塞,直到它重啟。
挑戰三:崩潰恢復 (Crash Recovery)
最棘手的問題來了:如果一個工作站 A 在操作進行到一半時突然崩潰了,怎么辦?它可能已經將一部分修改寫回了 Petal,留下一個不一致的爛攤子。
Frangipani 的答案是: 預寫式日志 (Write-Ahead Logging, WAL) 。
- 獨立的共享日志 :Frangipani 的一個創新之處在于, 每個工作站都有自己獨立的日志 ,并且這些日志都存儲在共享的 Petal 虛擬磁盤上。
- 恢復流程 :
當工作站 A 崩潰后,它持有的鎖不會被立即釋放。
當另一個工作站 B 需要 A 持有的鎖時,鎖服務會發現 A 已經失聯(租約過期)。
鎖服務會授權 B 去恢復 A 的狀態。B 會讀取 A 在 Petal 上的日志,并根據日志內容,重放(replay)那些 A 已經開始但可能未完成的操作,將它們徹底完成。
恢復完成后,B 通知鎖服務,然后鎖服務才會釋放 A 的鎖。
用戶文件內容會丟失嗎?
這里需要強調,Frangipani 的日志 只記錄元數據 (metadata) 的變更 (比如目錄項、inode),而不記錄用戶文件的實際內容。這意味著,如果一個程序寫了一些數據到文件,然后工作站立刻崩潰,這些剛剛寫入的數據可能會丟失。
這聽起來很危險,但實際上,這和我們日常使用的標準 Unix/Linux 文件系統的行為是一樣的。操作系統為了性能,并不會在每次 write
調用后都立刻把數據刷到磁盤。那些需要強持久性保證的程序,比如數據庫或文本編輯器,會顯式調用 fsync()
系統調用來強制數據落盤。Frangipani 的設計哲學是:文件系統負責保護自己內部結構的完整性;而用戶數據的持久性,則交給應用程序自己來決定。
如何保證日志重放的正確性?
一個非常精妙的問題是:由于各個工作站的日志是獨立的,操作可能會交錯發生。比如下面這個場景:
WS1
:delete(d/f)
,然后崩潰。WS2
: 在WS1
崩潰后,create(d/f)
,操作成功。WS3
: 開始恢復WS1
的日志。
WS3
在重放 WS1
的日志時,會看到一條 delete(d/f)
的記錄。如果它無腦執行了這個刪除操作,那就會錯誤地刪除掉 WS2
剛剛創建的文件。
Frangipani 用一個叫做 版本號 (version number) 的機制完美解決了這個問題。
- 每個元數據塊(比如 inode)都有一個版本號。
- 當一個操作要修改某個塊時,它會在日志中記錄下這個塊的“新版本號”(通常是當前版本號 + 1)。
- 在恢復時,恢復進程只有在發現 日志中記錄的版本號 > 磁盤上塊的當前版本號 時,才會執行重放操作。
在上面的例子中,當 WS2
創建 d/f
時,它已經更新了 f
的 inode,使其版本號增加。當 WS3
恢復 WS1
的日志時,它會發現 WS1
日志中 delete
操作記錄的版本號,已經小于或等于磁盤上 inode 的當前版本號了。WS3
便知道這個 delete
操作已經被一個更新的操作(WS2
的 create
)所覆蓋,因此會安全地跳過它。
其他值得探討的問題
安全性問題
既然文件系統的邏輯都在客戶端,一個懷有惡意的用戶是不是可以修改自己工作站上的 Frangipani 軟件,然后為所欲為地讀寫 Petal 上的任何數據?
答案是 肯定的 。Frangipani 的設計基于一個核心假設:所有運行 Frangipani 的工作站都在一個可信的管理域內。因此,它不適合用戶彼此不信任的環境。不過,可以通過一種“客戶端/服務器”配置來解決這個問題:將 Frangipani 服務器部署在受保護的機器上,然后通過 NFS 等標準協議將文件系統導出給不受信任的客戶端使用。
性能考量
- 文件創建變慢 :文件創建在 Frangipani 中會更耗時。這是因為元數據的修改需要 寫兩次 :一次寫入日志,一次寫入元數據本身在磁盤上的位置。此外,如果日志空間被寫滿,系統必須暫停,等待緩存中的臟數據被刷回 Petal,才能重用日志空間,這也會引入延遲。
- 鎖的粒度 :Frangipani 使用的是 每個文件/目錄一把鎖 的粗粒度鎖定。如果換成更細的 每塊一把鎖 ,性能會更好嗎?答案是 可能更差 。對于工程類應用,常見的操作是整個文件的讀寫(如編譯器讀取源文件)。如果每讀一個塊都要申請一次鎖,那么鎖服務的開銷和網絡通信將是巨大的。
- 文件條帶化 (Striping) :這和 分片 (sharding) 很相似。它指將一個大文件的不同數據塊分散存儲到多個不同的物理服務器上。這樣做有兩個好處:一是當讀取這個大文件時,可以從多個服務器并行讀取,獲得極高的吞吐量;二是可以確保負載被均勻地分攤到各個服務器上。
Petal 的快照機制
Petal 提供了一個非常高效的 快照 (snapshot) 功能。它是如何做到的呢?Petal 內部維護著一個從“虛擬磁盤地址”到“物理磁盤地址”的映射表。這個映射表的索引不僅僅是虛擬塊號,而是 (虛擬塊號, 時間戳紀元號) 的一個組合對。
- 創建一個快照,操作極其簡單: 只需將全局的“當前紀元號”加一 。
- 當系統寫入一個虛擬塊時,它會檢查該塊現有映射的紀元號。如果小于當前的全局紀元號,Petal 會分配一個新的物理塊,并用當前的紀元號創建一個新的映射,而舊的物理塊和映射保持不變(這就是 copy-on-write)。
- 讀取數據時,系統會使用具有最高紀元號的那個映射。讀取一個歷史快照,則只需指定該快照的紀元號即可。
Frangipani 的現狀與啟示
Frangipani 已經有二十多年的歷史了。如今,分布式文件系統的版圖已經發生了巨大變化。
- 云存儲和大數據應用的興起,使得存儲系統的焦點發生了轉移。
- 對于 Web 服務,鍵值存儲(Key-Value Store)或數據庫通常是比文件系統更好的選擇。
- 對于大數據處理(如 MapReduce),系統更關心對巨大文件的并行吞吐能力,像 GFS 或 HDFS 這樣的系統更受歡迎,它們不需要 Frangipani 這種復雜的緩存和鎖機制。
- 盡管如此,像 SMB、NFS 這樣的傳統協議依然被廣泛使用,而更新的系統如 Ceph、Lustre 也占據了一席之地。
Frangipani 雖然沒有成為主流商業產品,但它所探索的“將智能分布在客戶端,后端提供簡單共享存儲”的設計模式,以及它在一致性、原子性和恢復性之間所做的精妙權衡,至今仍然對我們設計和理解分布式系統有著深刻的啟示。它是一座連接過去與未來的橋梁,展示了那些永恒的分布式計算難題和優雅的解決方案。