谷歌技術"三寶"之谷歌文件系統

題記:初學分佈式文件系統,寫篇博客加深點印象。GFS的特點是使用一堆廉價的商用計算機支撐大規模數據處理。

雖然"The Google File System " 是03年發表的老文章了,但現在仍被廣泛討論,其對後來的分佈式文件系統設計具有指導意義。然而,作者在設計GFS時,是基於過去很多實驗觀察的,並提出了很多假設作爲前提,這等於給出了一個GFS的應用場景。所以我們自己在設計分佈式系統時,一定要注意自己的應用場景是否和GFS相似,不能盲從GFS。

GFS的主要假設如下:

  1. GFS的服務器都是普通的商用計算機,並不那麼可靠,集羣出現結點故障是常態。因此必須時刻監控系統的結點狀態,當結點失效時,必須能檢測到,並恢復之。
  2. 系統存儲適當數量的大文件。理想的負載是幾百萬個文件,文件一般都超過100MB,GB級別以上的文件是很常見的,必須進行有效管理。支持小文件,但不對其進行優化。
  3. 負載通常包含兩種讀:大型的流式讀(順序讀),和小型的隨機讀。前者通常一次讀數百KB以上,後者通常在隨機位置讀幾個KB。
  4. 負載還包括很多連續的寫操作,往文件追加數據(append)。文件很少會被修改,支持隨機寫操作,但不必進行優化。
  5. 系統必須實現良好定義的語義,用於多客戶端併發寫同一個文件。同步的開銷必須保證最小。
  6. 高帶寬比低延遲更重要,GFS的應用大多需要快速處理大量的數據,很少會嚴格要求單一操作的響應時間。
從這些假設基本可以看出GFS期望的應用場景應該是大文件,連續讀,不修改,高併發。國內的淘寶文件系統(TFS)就不一樣,專門爲處理小文件進行了優化。

1 體系結構

GFS包括一個master結點(元數據服務器),多個chunkserver(數據服務器)和多個client(運行各種應用的客戶端)。在可靠性要求不高的場景,client和chunkserver可以位於一個結點。圖1是GFS的體系結構示意圖,每一結點都是普通的Linux服務器,GFS的工作就是協調成百上千的服務器爲各種應用提供服務。


  • chunkserver提供存儲。GFS會將文件劃分爲定長數據塊,每個數據塊都有一個全局唯一不可變的id(chunk_handle),數據塊以普通Linux文件的形式存儲在chunkserver上,出於可靠性考慮,每個數據塊會存儲多個副本,分佈在不同chunkserver。
  • GFS master就是GFS的元數據服務器,負責維護文件系統的元數據,包括命名空間、訪問控制、文件-塊映射、塊地址等,以及控制系統級活動,如垃圾回收、負載均衡等。
  • 應用需要鏈接client的代碼,然後client作爲代理與master和chunkserver交互。master會定期與chunkserver交流(心跳),以獲取chunkserver的狀態併發送指令。
圖1還描述了應用讀取數據的流程。1.應用指定讀取某個文件的某段數據,因爲數據塊是定長的,client可以計算出這段數據跨越了幾個數據塊,client將文件名和需要的數據塊索引發送給master;2.master根據文件名查找命名空間和文件-塊映射表,得到需要的數據塊副本所在的地址,將數據塊的id和其所有副本的地址反饋給client;3.client選擇一個副本,聯繫chunkserver索取需要的數據;4.chunkserver返回數據給client。

2 數據的佈局

GFS將文件條帶化,按照類似RAID0的形式進行存儲,可以提高聚合帶寬。事實上,大多數分佈式存儲系統都會採取這種策略。GFS將文件按固定長度切分爲數據塊,master在創建一個新數據塊時,會給每個數據塊分配一個全局唯一且不可變的64位id。每個數據塊以Linux文件的形式存儲在chunkserver的本地文件系統裏。

GFS爲數據塊設置了一個很大的長度,64MB,這比傳統文件系統的塊長要大多了。大塊長會帶來很多好處:1.減少client和master的交互次數,因爲讀寫同一個塊只需要一次交互,在GFS假設的順序讀寫負載的場景下特別有用;2.同樣也減少了client和chunkserver的交互次數,降低TCP/IP連接等網絡開銷;3.減少了元數據的規模,因此master可以將元數據完全放在內存,這對於集中式元數據模型的GFS尤爲重要。

大數據塊也有缺點。最大的缺點可能就是內部碎片了,不過考慮到文件一般都相當大,所以碎片也只存在於文件的最後一個數據塊。還有一個缺點不是那麼容易看出來,由於小文件可能只有少量數據塊,極端情況只有一個,那麼當這個小文件是熱點文件時,存儲該文件數據塊的chunkserver可能會負載過重。不過正如前面所說,小文件不在GFS的優化範圍。

爲了提高數據的可靠性和併發性,每一個數據塊都有多個副本。當客戶端請求一個數據塊時,master會將所有副本的地址都通知客戶端,客戶端再擇優(距離最短等)選擇一個副本。一個典型的GFS集羣可能有數百臺服務器,跨越多個子網,因此在考慮副本的放置時,不僅要考慮機器級別的錯誤,還要考慮整個子網癱瘓了該怎麼辦。將副本分佈到多個子網去,還可以提高系統的聚合帶寬。因此創建一個數據塊時,主要考慮幾個因素:1.優先考慮存儲利用率低於平均水平的結點;2.限制單個結點同時創建副本的數量;3.副本儘量跨子網。

3 元數據服務

GFS是典型的集中式元數據服務,所有的元數據都存放在一個master結點內。元數據主要包括三種:文件和數據塊的命名空間,文件-數據塊映射表,數據塊的副本位置。所有的元數據都是放在內存裏的。

前兩種元數據會被持久化到本地磁盤中,以操作日誌的形式。操作日誌會記錄下這兩種元數據的每一次關鍵變化,因此當master宕機,就可以根據日誌恢復到某個時間點。日誌的意義還在於,它提供了一個時間線,用於定義操作的先後順序,文件、數據塊的版本都依賴於這個時間順序。

數據塊的副本位置則沒有持久化,因爲動輒數以百計的chunkserver是很容易出錯的,因此只有chunkserver對自己存儲的數據塊有絕對的話語權,而master上的位置信息很容易因爲結點失效等原因而過時。取而代之的方法是,master啓動時詢問每個chunkserver的數據塊情況,而且chunkserver在定期的心跳檢查中也會彙報自己存儲的部分數據塊情況。

GFS物理上沒有目錄結構,也不支持鏈接操作,使用一張表來映射文件路徑名和元數據。

4 緩存和預取

GFS的客戶端和chunkserver都不會緩存任何數據,這是因爲GFS的典型應用是順序訪問大文件,不存在時間局部性。空間局部性雖然存在,但是數據集一般很大,以致沒有足夠的空間緩存。

我們知道集中式元數據模型的元數據服務器容易成爲瓶頸,應該儘量減少客戶端與元數據服務器的交互。因此GFS設計了元數據緩存。client需要訪問數據時,先詢問master數據在哪兒,然後將這個數據地址信息緩存起來,之後client對該數據塊的操作都只需直接與chunkserver聯繫了,當然緩存的時間是有限的,過期作廢。

master還會元數據預取。因爲空間局部性是存在,master可以將邏輯上連續的幾個數據塊的地址信息一併發給客戶端,客戶端緩存這些元數據,以後需要時就可以不用找master的麻煩了。

5 出錯了腫麼辦

引用:“We treat component failures as the norm rather than the exception."

分佈式系統整體的可靠性是至關重要的。GFS集羣使用的都是普通的商用計算機,而且機器的數量衆多,設備故障經常出現,如何處理結點失效的問題是GFS最大的挑戰。

5.1 完整性

GFS使用數以千計的磁盤,磁盤出錯導致數據被破壞時有發生,我們可以用其它副本來恢復數據,但首先必須能檢測出錯誤。chunksever會使用校驗和來檢測錯誤數據。每一個塊(chunk)都被劃分爲64KB的單元(block),每個block對應一個32位的校驗和。校驗和與數據分開存儲,內存有一份,然後以日誌的形式在磁盤備一份。

chunkserver在發送數據之前會覈對數據的校驗和,防止錯誤的數據傳播出去。如果校驗和與數據不匹配,就返回錯誤,並且向master反映情況。master會開始克隆副本的操作,完成後就命令該chunkserver刪除非法副本。

5.2 一致性

一致性指的是master的元數據和chunkserver的數據是否一致,多個數據塊副本之間是否一致,多個客戶端看到的數據是否一致。

先來看看元數據一致性。GFS的命名空間操作是原子性的,並且用日誌記錄下操作順序。雖然GFS沒有目錄結構,但是仍然有一顆邏輯的目錄樹,樹的每個結點都有自己的讀寫鎖,每個元數據操作都需要獲得一系列的鎖,應該是寫鎖會阻塞其它的鎖,而讀鎖只阻塞寫鎖而不阻塞讀鎖。比如/home/user "目錄" 正在創建快照,需要獲得/home的讀鎖和/home/user的寫鎖,這時如果想創建文件/home/user/foo會被阻塞,因爲需要獲得/home、/home/user的讀鎖以及/home/user/foo的寫鎖,快照會阻塞創建操作獲取/home/user的讀鎖。如果是在一個有傳統目錄樹結構的文件系統裏,創建一個文件需要修改父目錄的數據,因此需要獲得父目錄的寫鎖。這種鎖機制允許在一個目錄裏併發修改數據(如併發創建文件等),這在傳統文件系統裏是不允許的。


再來看看GFS是如何併發寫(write)的,GFS必須將對數據塊的修改同步到每一個副本。考慮一下多個應用同時修改同一數據塊的情況,我們必須爲修改操作定義統一的時序,不然多個副本會出現不一致的情況,那麼定義時序由誰做呢?還記得前面提到的元數據緩存麼,爲了減少master的負擔,client在獲得副本位置後就不再和master交互,所以必然需要選出一個master代理來完成這個任務。事實上GFS採用了租約(lease)的機制,master會將租約授權給某個副本,稱爲primary,由這個primary來確定數據修改的順序,其它副本照做就是。

圖2是寫操作的控制流和數據流:

  1. client需要更新一個數據塊,詢問master誰擁有該數據塊的租約(誰是primary);
  2. master將持有租約的primary和其它副本的位置告知client,client緩存之;
  3. client向所有副本傳輸數據,這裏副本沒有先後順序,根據網絡拓撲情況找出最短路徑,數據從client出發沿着路徑流向各個chunkserver,這個過程採用流水線(網絡和存儲並行)。chunkserver將數據放到LRU緩存;
  4. 一旦所有的副本都確定接受數據,client向primary發送寫請求,primary爲這個前面接受到的數據分配序列號(primary爲所有的寫操作分配連續的序列號表示先後順序),並且按照順序執行數據更新;
  5. primary將寫請求發送給其它副本,每個副本都按照primary確定的順序執行更新;
  6. 其它副本向primary彙報操作情況;
  7. primary回覆client操作情況,任何副本錯誤都導致此次請求失敗,並且此時副本處於不一致狀態(寫操作完成情況不一樣)。client會嘗試幾次3到7的步驟,實在不行就只能重頭來過了。
如果一個寫請求太大了或者跨越了chunk,GFS的client會將其拆分爲多個寫請求,每個寫請求都遵循上述過程,但是可能和其它應用的寫操作交叉在一起。所以這些寫操作共享的數據區域就可能包含幾個寫請求的碎片(就是下文提到的undefined狀態)。

GFS還提供另一種寫操作append record,append只在文件的尾部以record爲單位(爲了避免內部碎片,record一般不會很大)寫入數據。append是原子性的,GFS保證將數據順序地寫到文件尾部至少一次。append record的流程和圖2類似,只是在primary有點區別,GFS必須保證一個record存儲在一個chunk內,所以當primary判斷當前chunk無足夠空間時,就通知所有副本將chunk填充,然後彙報失敗,client會申請創建新chunk並重新執行一次append record操作。如果該chunk大小合適,primary會將數據寫到數據塊的尾部,然後通知其它副本將數據寫到一樣的偏移。任何副本append失敗,各個副本會處於不一致狀態(完成或未完成),這時primary必然是成功寫入的(不然就沒有4以後的操作了)。客戶端重試append record操作時,因爲primary的chunk長度已經變化了,primary就必須在新的偏移寫入數據,而其它副本也是照做。這就導致上一個失敗的偏移位置,各個副本處於不一致狀態,應用必須自己區分record正確與否,我稱這爲無效數據區。


表1說明了GFS的一致性保證,明白write和append操作後就容易理解了。consistent指的是多個副本之間是完全一致的;defined指的是副本數據修改後不僅一致而且client能看到其修改的數據全貌。

  • 成功的連續write是已定義的,各個副本數據一致;
  • 成功的併發write能保證一致性性,即各個副本是一樣的,但數據並不一定如用戶所期望,如前所述,多個用戶的修改可能交錯在一起;
  • 失敗的write操作,使得副本之間不一致,而且數據undefined,不同client可能看到不同的數據(注意區別defined、undefined數據的方法);
  • 成功的append操作,不管是順序還是併發都是defined,因爲GFS保證了append是原子性的(atomically at least once)。有效數據區確實是defined的,但是失敗append操作留下的無效數據區可能會有不一致的情況,所以中間可能零散分佈着不一致的數據。

如上所述,在primary的協調下,能保證併發write的一致性。但還有一些可能會導致數據不一致,比如chunkserver宕機錯過了數據更新,這時就會出現新舊版本的數據,GFS爲每個數據塊分配版本來解決這個問題。master每次授權數據塊租約給primary之前,都會增加數據塊的版本號,並且通知所有副本更新版本號。客戶端需要讀數據時當然會根據這個版本號來判斷副本的數據是否最新。

5.3 可用性

爲了保證數據的可用性,GFS爲每個數據塊存儲了多個副本,在數據的佈局裏有介紹,這裏主要關注下元數據的可用性。

GFS是典型的集中式元數據模型,一個元數據服務器承擔了巨大的壓力,不僅有性能的問題,還有單點故障問題。master爲了能快速從故障中恢復過來,採用了log和checkpoint技術,log記錄了元數據的每一次變化。用咱們備份的話來說,checkpoint就相當於一次全量備份,log相當於連續數據保護,master宕機後,就先恢復到checkpoint,然後根據log恢復到最新狀態。每次創建一個新的checkpoint,log就可以清空,這有效控制了log的大小。

這還不夠,如果master完全壞了腫麼辦?GFS設置了“影子”服務器,master將日誌備份到影子上,影子按照日誌把元數據與master同步。如果master悲劇了,我們還有影子。平時正常工作時,影子可以分擔一部分元數據訪問工作,當然不提供直接的寫操作。

6 測試

6.1 模擬


實驗的網絡拓撲圖大概就是這樣。集羣包括一個master和它的兩個影子,16個chunkserver,16個client,和兩個HP交換機。兩個交換機之間是1Gbps的鏈路,結點與交換機的鏈路是100Mbps。聚合帶寬的理論上限是125MB/s,而單個client的理論帶寬上限是12.5MB/s。


實驗一:N個client同時隨機各自讀取1GB數據。

圖3(a),x軸是N,y軸是聚合帶寬,上面一條線是理論值,下面一條線是實際值。當N=1時,吞吐率是10MB/s,達到了理論值的80%。當N=16時,吞吐率是94MB/s,達到理論值的75%。此時瓶頸可能是在chunkserver,因爲client數量很多,同時讀一個chunkserver的概率很大。

實驗二:N個client同時寫N個不同文件,各自連續寫1GB。

圖3(b)。理論值上限是67MB/s(The limit plateaus at 67 MB/s because we need to write each byte to 3 of the 16 chunkservers,
each with a 12.5 MB/s input connection),這個理論值我沒有看明白是怎麼算的。當N=1,吞吐率是6.3MB/s,達到極限的一半,主要的瓶頸是數據在副本之間傳遞。N=16時,聚合帶寬爲35MB/s(每個client有2.2MB/s),達到極限的一半,這裏chunkserver同時接受多個請求的情況比讀更嚴重,因爲得寫3個副本。寫比期望的要慢。

實驗三:N個client同時向一個文件append record。

圖3(c)。理論上限值是一臺chunkserver的帶寬,即12.5MB/s。當N=1時,吞吐率有6.0MB/s。當N=16時,下降到4.8MB/s。這可能是因爲擁塞。

6.2 現實

文章裏還介紹了兩個真實集羣的使用情況,cluster A and B。


表2是兩個集羣的情況。A和B都有數百個chunkserver,存儲利用率很高,冗餘度是3,所以實際存儲的數據是18TB和52TB。文件數相當,dead file是需要刪除的文件,但還沒被垃圾回收(GFS會爲刪除的文件保留三天才回收空間)。B的塊數更多,意味着文件更大,不過A和B的文件平均都只有1到2個塊。chunkserver的元數據主要是block(64KB)的校驗和(4 Bytes),以及數據塊的版本信息。master的元數據(文件和數據塊的名字,文件-數據塊映射,塊位置等)相當小,平均每個文件只有100B元數據,大部分是用於存儲文件名。平均下來,每個服務器有50~100MB的元數據。

7 胡說八道

畫了個全分佈式元數據模型。


這估計是最簡單的,命名空間被劃分爲幾個區域,master各管各的。爲了可靠性,每個master做幾個副本。映射方法可以是文件名計算哈希取模,好的哈希函數可以使文件隨機分佈,負載比較均衡。當然擴展性不是很好,加入新的結點,所有文件得重新映射。而且冗餘度只能靠機器堆了,不能軟件控制。


又構思了個複雜點的。將文件映射到r(<=N)個master,利用參數r控制冗餘度,當r=N時就變成全對等元數據集羣。這個模型需要r個不同的哈希函數,爲了減少開銷,可以用兩個函數模擬多個函數。比如有隨機性很好的f(x)和g(x)函數,我們用式子f(x)+i*g(x)來模擬,其中i爲非負整數。當我們處理文件x時,就用前面式子求出r個不同的位置(有衝突的概率,實際可能不止計算r次)。

胡說八道,切勿當真;如有雷同,純屬巧合。

參考文獻:

[1] The Google File System.

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