sina
Google File System,一個適用于大規模分布式數據處理相關應用的,可擴展的分布式文件系統。
GFS 提供了常見的文件系統的接口,文件是通過pathname 來通過目錄進行分層管理的。我們支持的一些常見操作:create,delete,open,close,read,write 等文件操作。另外,GFS 有snapshot,record append 等操作。snapshort 創建一個文件或者一個目錄樹的快照,這個快照的耗費比較低。record append 允許很多個客戶端同時對一個文件增加數據,同時保證每一個客戶端的添加操作的原子操作性。這個對于多路合并操作和多個客戶端同時操作的生產者/消費者隊列的實現非常有用,它不用額外的加鎖處理。
如上圖所示,GFS 集群由一個單個的master 和好多個chunkserver(塊服務器)組成,GFS 集群會有很多客戶端client 訪問。每一個節點都是一個普通的Linux 計算機,運行的是一個用戶級別(user-level)的服務器進程。只要機器資源允許,并且允許不穩定的應用代碼導致的低可靠性,我們就可以運行chunkserver 和client 可以運行在同一個機器上。
在 GFS 下,每一個文件都拆成固定大小的chunk(塊)。每一個塊都由master 根據塊創建的時間產生一個全局唯一的以后不會改變的64 位的chunk handle 標志。chunkservers 在本地磁盤上用Linux文件系統保存這些塊,并且根據chunk handle 和字節區間,通過Linux 文件系統讀寫這些塊的數據。出于可靠性的考慮,每一個塊都會在不同的chunkserver 上保存備份。缺省情況下,我們保存3 個備份,不過用戶對于不同的文件namespace 區域,指定不同的復制級別。
master 負責管理所有的文件系統的元數據。包括namespace,訪問控制信息,文件到chunk 的映射關系,當前chunk 的位置等等信息。master 也同樣控制系統級別的活動,比如chunk 的分配管理,孤點chunk 的垃圾回收機制,chunkserver 之間的chunk 鏡像管理。master 和這些chunkserver 之間會有定期的心跳線進行通訊,并且心跳線傳遞信息和chunckserver 的狀態。
這里我們簡單介紹一下上圖 中的讀取操作。首先,客戶端把應用要讀取的文件名和偏移量,根據固定的chunk 大小,轉換成為文件的chunk index。然后向master 發送這個包含了文件名和chunkindex的請求。master 返回相關的chunk handle 以及對應的位置。客戶端cache 這些信息,把文件名和chunkindex 作為cache 的關鍵索引字。
于是這個客戶端就像對應的位置的chunkserver 發起請求,通常這個會是離這個客戶端最近的一個。請求給定了chunk handle 以及一個在這個chunk 內需要讀取得字節區間。在這個chunk 內,再次操作數據將不用再通過客戶端-master 的交互,除非這個客戶端本身的cache 信息過期了,或者這個文件重新打開了。實際上,客戶端通常都會在請求中附加向master 詢問多個chunk 的信息,master于是接著會立刻給這個客戶端回應這些chunk 的信息。這個附加信息是通過幾個幾乎沒有任何代價的客戶端-master 的交互完成的。
chunk 的大小是一個設計的關鍵參數。選擇一個很大的chunk 大小提供了一些重要的好處,減少了客戶端和master 的交互,因為在同一個chunk 內的讀寫操作只需要客戶端初始詢問一次master 關于chunk 位置信息就可以了。可以通過維持一個到chunkserver 的TCP 長連接來減少網絡管理量,減少了元數據在master 上的大小,這個使得我們可以把元數據保存在內存。
元數據:
master 節點保存這樣三個主要類型的數據:文件和chunk 的namespace,文件到chunks 的映射關系,每一個chunk 的副本的位置。頭兩個類型(namepspaces 和文件到chunk 的映射)同時也是由在master 本地硬盤的記錄所有變化信息的operation log (操作記錄保存了關鍵的元數據變化歷史記錄。它是GFS 的核心。不僅僅因為這是唯一持久化的元數據記錄,而且也是因為操作記錄也是作為邏輯時間基線,定義了并行操作的順序)來持久化保存的,這個記錄也會在遠端機器上保存副本。通過log,在master 宕機的時候,我們可以簡單,可靠的恢復master 的狀態。master 并不持久化保存chunk 位置信息。相反,他在啟動地時候以及chunkserver 加入集群的時候,向每一個chunkserver 詢問他的chunk 信息。
操作記錄(operation log)
我們把這個日志文件保存在多個不同的主機上,并且只有當刷新這個相關的操作記錄到本地和遠程磁盤之后,才會給客戶端操作應答。master 可以每次刷新一批日志記錄,以減少刷新和復制這個日志導致的系統吞吐量。
為了減少啟動時間,我們必須盡量減少操作日志的大小。master 在日志增長超過某一個大小的時候,執行checkpoint 動作,卸出自己的狀態,這樣可以使下次啟動的時候從本地硬盤讀出這個最新的checkpoint,然后反演有限記錄數。
checkpoint 是一個類似B-樹的格式,可以直接映射到內存,而不需要額外的分析。這更進一步加快了恢復的速度,提高了可用性。因為建立一個checkpoint 可能會花一點時間,于是我們這樣設定master 的內部狀態,就是說新建立的checkpoint 可以不阻塞新的狀態變化。master 切換到一個新的log 文件,并且在一個獨立的線程中創建新的checkpoint。新的checkpoint 包含了在切換到新log 文件之前的狀態變化。當這個集群有數百萬文件的時候,創建新的checkpoint 會花上幾分鐘的時間。當checkpoint 建立完畢,會寫到本地和遠程的磁盤。
對于 master 的恢復,只需要最新的checkpoint 以及后續的log 文件。舊的checkpoint 及其log 文件可以刪掉了,雖然我們還是保存幾個checkpoint 以及log,用來防止比較大的故障產生。在checkpoint的時候得故障并不會導致正確性受到影響,因為恢復的代碼會檢查并且跳過不完整的checkpoint。
一致性模型
文件名字空間的改變(比如,文件的創建)是原子操作。他們是由master 來專門處理的。名字空間的鎖定保證了操作的原子性以及正確性;master 的操作日志定義了這些操作的全局順序。
namespace 管理及鎖定
GFS 從邏輯上是通過一個查找路徑名到元數據映射表的方式來體現namespace 的。通過前綴壓縮,這個表可以有效地在內存中存放。每一個namespace 樹種的節點(不論是絕對文件名還是絕對目錄名),都有一個相關的讀寫鎖。
副本位置
GFS 集群是在不止一個級別上的多級別的高度分布的系統。通常由好幾百臺分布在很多機架上的chunkserver 組成。這些chunkserver 可能被相同或者不同的機架的幾百臺客戶端輪流訪問。兩個機架上的兩臺機器可能會跨越不止一個網絡交換機。此外,機架的入口帶寬或者出口帶寬往往會比在機架內的機器聚合帶寬要小。多級別的分布凸現了一個獨特的要求,為了可擴展性,可靠性,以及可用性要把數據進行分布。
數據完整性
每一個chunkserver 都是用checksum 來檢查保存數據的完整性。通常一個GFS 集群都有好幾百臺機器以及幾千塊硬盤,磁盤損壞是很經常的事情,在數據的讀寫中經常出現數據損壞。我們可以通過別的chunk 副本來解決這個問題,但是如果跨越chunkserver 比較這個chunk的內容來決定是否損壞就很不實際。進一步說,允許不同副本的存在;在GFS 更改操作的語義上,特別是早先討論過的原子紀錄增加的情況下,并不保證byte 級別的副本相同。因此,每一個chunkserver上必須獨立的效驗自己的副本的完整性,并且自己管理checksum。
我們把一個chunk 分成64k 的塊。每一個都有相對應的32 位的checksum。就像其他的元數據一樣,checksum 是在內存中保存的,并且通過分別記錄用戶數據來持久化保存。對于讀操作來說,在給客戶端或者chunkserver 讀者返回數據之前,chunkserver 效驗要被讀取的數據所在塊的checksum。因此chunkserver 不會把錯誤帶給其他設備。如果一個塊的checksum 不正
確,chunkserver 會給請求者一個錯誤,并且告知master 這個錯誤。收到這個應答之后,請求者應當從其他副本讀取這個數據,master 也會安排從其他副本來做克隆。當一個新的副本就緒后,master會指揮剛才報錯的chunkserver 刪掉它剛才錯誤的副本。checksum 對于讀取性能來說,在幾方面有點影響。因為大部分的讀取操作都分布在好幾個block 上,我們只需要額外的多讀取一小部分相關數據進行checksum 檢查。GFS 客戶端代碼通過每次把讀取操作都對齊在block 邊界上,來進一步減少了這些額外的讀取。此外,在chunkserver 上的checksum的查找和比較不需要附加的I/O,checksum 的計算可以和I/O 操作同時進行。
checksum 的計算是針對添加到chunk 尾部的寫入操作作了高強度的優化(和改寫現有數據不同),因為它們顯然占據主要工作任務。我們增量更新關于最后block 的checksum 部分,并且計算添加操作導致的新的checksum block 部分。即使是某一個checksum 塊已經損壞了,但是我們在寫得時候并不立刻檢查,新的checksum 值也不會和已有數據吻合,下次對這個block 的讀取的時候,會檢查出這個損壞的block。
另一方面,如果寫操作基于一個已有的chunk(而不是在最后追加),我們必須讀取和效驗被寫得第一個和最后一個block,然后再作寫操作,最后計算和寫入新的checksum。如果我們不效驗第一個和最后一個被寫得block,那么新的checksum 可能會隱藏沒有改寫區域的損壞部分。
在 chunkserver 空閑的時候,他掃描和效驗每一個非活動的chunk 的內容。這使得我們能夠檢查不常用的chunk 塊的完整性。一旦發現這樣的塊有損壞,master 可以創建一個新的正確的副本,然后把這個損壞的副本刪除。這個機制防止了非活動的塊損壞的時候,master 還以為這些非活動塊都已經有了足夠多的副本。