谷歌文件系統(The Google File System譯)(1~2章)

摘要

我們設計並實現了一個面向大規模數據密集型應用的可擴展的分佈式文件系統,即谷歌文件系統。當運行在廉價硬件上時,它能提供一種容錯機制,在大量客戶端連接時提供了高內聚的性能

儘管與之前的分佈式文件系統有着相同的目標,但我們的設計是考量於我們應用的當前和未來的工作負載以及技術環境,這反映了它與一些早期的文件系統預設有着明顯的不同,也讓我們重新審視傳統的設計選擇,並探索有根本性差別的設計觀點

這個文件系統已經成功滿足我們的存儲需要,已經作爲存儲平臺在谷歌內部廣泛部署,用於生成並處理我們用於需要大量數據集的搜索和研發服務的數據。迄今爲止最大的集羣,在超過一千臺機器上的的數千個磁盤上提供了數百TB的存儲服務,同時能夠響應數百臺客戶端的併發訪問

在本篇論文中,我們將展示用於支持分佈式應用的文件系統的接口擴展,討論我們的許多設計切面,以及微觀標準下和現實世界中的一些測量報告

分類和主題描述

D [4]: 3— 分佈式文件系統

通用詞彙

設計,可靠性,性能,測量

關鍵詞

容錯,可擴展性,數據存儲,集羣存儲

1. 介紹

我們設計並實現了谷歌文件系統(GFS)來滿足快速增長的谷歌數據處理需求的需要。GFS和之前的分佈式文件系統有着許多相同的目標,比如性能,可擴展性,可靠性,以及可用性。然而,谷歌文件系統的設計是受到當前乃至未來我們應用的工作負載和技術環境的觀察而推動的,反映了與早期的一些文件系統的設計理念有着根本性的不同。我們重新審視了傳統的設計選擇,並在設計空間中探索出了一些根本不同的觀點

第一,組件失效是一種常態而不是異常。文件系統由數百乃至數千個通過廉價商品零件組裝起來的存儲服務器組成,能夠被相當數量的客戶端訪問。這些組件的數量和質量在本質上決定了一部分機器在任何給定的時間均無法使用,一部分機器將不會從故障中恢復。我們觀察到了很多故障,包括由於應用程序的bug,操作系統的bug,人爲的錯誤,磁盤、內存、連接器、網絡,乃至電力系統的故障。因此,常態化的監控,故障發現,容錯機制,以及自動恢復機制必須集成到系統中

第二,以傳統的標準來看,文件是龐大的,數GB的文件是很常見的。每一個文件一般都由許多類似網頁文檔等應用程序對象組成,當我們經常處理快速增長的包含數十億個對象的數TB大小的數據集時,即使文件系統可以支持,管理數十億個KB左右大小的文件也是不可取的。所以,不得不重新考量設計中的假設和參數,比如I/O操作和塊大小的設計

第三,大多數文件是通過追加新數據發生內容改變的,而不是覆寫已存在的數據,文件內容的隨機寫幾乎是不存在的。一旦寫入,文件就是隻讀的,而且都是順序讀取的。大量的數據都有這種特點,有些可能是構成數據分析程序掃描的大型存儲庫,有些可能是運行中的應用持續生成的數據流,有些可能是檔案數據,有些可能是在一臺機器上生成的用於另一臺機器處理的中間結果,不管是同時還是稍後。給定大文件的訪問模式, 當緩存數據塊在客戶端丟失引用時,追加操作就成了性能優化和保證原子性的關鍵

第四,共同設計應用程序和文件系統的服務接口可以提高靈活性,這對於整個系統是有利的。比如,我們放寬了GFS的一致性模型從而大大簡化了文件系統,而沒有給應用程序帶來繁重的負擔。我們還引入了一個原子性的追加操作,使得多個客戶端可以同時對文件執行追加操作而不需要在它們之間執行額外的同步操作。這些都將會在接下來進行更詳細的討論。

目前我們部署了多個出於不同目的的GFS集羣,其中最大的存儲節點擁有超過1000個存儲節點,以及超過300TB的磁盤存儲,並且被不同機器上的數百個客戶端頻繁大量地訪問

2. 設計概述

2.1 假設

在設計一個滿足我們自己需要的文件系統時,我們以充滿着機遇與挑戰的假設爲導向。之前曾提到了一些關鍵的觀察結果,現在我們把我們的假設在這裏更詳細地羅列出來

  • 系統是由許多經常會故障的廉價商品組件組成,所以必須要經常對它進行監控,以及在例行基礎上進行檢測、容錯,以及故障組件的恢復
  • 系統存儲了一定量的大文件。我們預計有數百萬個文件,每個一般在100MB或更大。數GB的文件是很常見的情況,應該被有效的管理。小文件必須得到支持,但我們並不需要爲它們進行優化
  • 工作負載主要包括兩種讀取情況:大型流式讀取和小型隨機讀取。在大型流式讀取中,單個操作一般會讀取數百KB的內容,更常見的是1MB或更多。同一個客戶端連續的操作通常會讀取文件中的相鄰區域。小型隨機讀取則一般會在隨機偏移位置讀取幾KB的內容。注重性能的應用程序通常會對小量讀取進行批處理和排序,以便穩步地向前讀取文件而不是來回移動
  • 工作負載還有很多追加數據到文件的大型的順序寫入。一般操作的大小和讀取的大小是相似的。一旦寫入結束,文件就幾乎不再進行改變。文件隨機位置的少量寫入是被支持的,但是效率不一定高
  • 系統必須爲同時對同一文件執行追加操作的客戶端有效地實現定義良好的語義。我們的文件通常用作生產者消費者隊列或是多方合併。每臺機器運行數百個生產者同時爲文件執行追加操作,具有最小同步開銷的原子操作是必不可少的。文件可以被稍後閱讀,而消費者可以同時閱讀文件
  • 持續的高帶寬比低延遲更重要。我們大多數的應用程序都非常重視以高速率批量處理數據,而幾乎不會對單次讀寫有着嚴格的響應時間要求

2.2 接口

GFS提供了我們熟悉的文件系統的接口,雖然沒有實現例如POSIX的標準API。文件在目錄中按層次結構組織,並通過路徑名標識。我們也支持諸如創建、刪除、打開、關閉、讀取,以及寫入文件等常規操作

除此之外,GFS還提供了快照和記錄追加操作。快照可以以較低的成本創建文件或目錄樹的一個副本,記錄追加操作允許多個客戶端同時追加數據到同一個文件中,同時保證每個客戶端追加操作的原子性。許多客戶端在不需要額外加鎖的條件下同時進行追加操作,這一點對於實現多路合併和生產者消費者隊列是很有用的,我們還發現這些文件的類型在構建大規模分佈式應用中是很有價值的。快照和記錄追加將會分別在3.4和3.3節中進一步討論

2.3 架構

一個GFS集羣由單個master和數個chunkserver組成,可以被多個客戶端訪問,如圖1所示。每個客戶端通常都是由一臺商用Linux服務器來運行用戶級的服務器進程。只要機器的資源允許,並且能夠接受由於運行可能的碎片應用代碼導致的較低的可靠性,那麼在同一臺機器上運行chunkserver和客戶端是很容易的

文件分爲固定大小的塊,每個塊都是由塊創建時被master分配的一個全局且不可變的唯一64位塊句柄標識,chunkserver將塊Linux文件存儲在本地磁盤上,塊數據的讀寫由塊句柄和字節範圍來指定。出於可靠性的考慮,每個塊都在多個chunkserver上進行復制。默認情況下,我們存儲三個副本,但是用戶可以爲文件命名空間的不同區域指定不同的拷貝級別

master維護文件系統中包括命名空間,訪問控制信息,文件到塊的映射關係,以及塊的當前位置在內的所有的元數據,同樣也控制着系統範圍內的活動,比如塊的租約管理,孤立塊的垃圾收集,還有chunkserver之前的塊遷移。master通過心跳消息和每個chunserver進行週期性的通信,以發送指令並收集它們的狀態

連接到每個應用程序的GFS客戶端代碼實現了文件系統API,並代替應用程序與master和chunkserver進行通信,從而完成數據的讀取和寫入。客戶端通過與master交互可以進行元數據的操作,但是所有承載數據的通信都必須直接進入chunkserver,我們不提供POSIX API,因此也不需要掛載到Linux的vnode層

客戶端和chunkserver都不緩存文件數據。客戶端的緩存沒有意義,因爲大多數的應用程序使用大型文件流工作,或是工作集過大導致難以緩存。沒有這些地方的緩存就消除了緩存一致性,從而簡化了客戶端和整個系統(但是客戶端會緩存元數據)。chunkserver不需要緩存是因爲塊作爲本地文件存儲,Linux的緩存已經將頻繁訪問的數據保存在內存中了

圖1

2.4 單個Master

只擁有一個master可以極大簡化我們的設計,同時保證master能夠應用全局信息來針對塊的放置和備份作出複雜的決策。然而,我們必須將其在讀寫操作中的參與度降到最小,以避免成爲瓶頸。客戶端永遠不會通過master進行文件的讀寫,相反的,客戶端只是詢問master它應該與哪個chunkserver建立通信。master在有限的時間內會緩存此信息,並與chunkserver直接進行交互來執行許多後續操作

我們參考圖1來解釋一個簡單的讀取教交互過程。首先,使用固定的塊大小,客戶端將應用程序中指定的文件名和字節偏移量轉化爲塊索引。然後,客戶端向master發送包含有文件名和塊索引的請求,master會回覆對應的塊句柄和副本的位置。客戶端會使用文件名和塊索引作爲鍵值來緩存這些信息

接下來客戶端會向其中一個副本發送請求,通常是最近的那一個。請求中指定了塊句柄和塊中的字節範圍。在緩存過期或是文件重新打開之前,對同一個塊的後續讀取操作不需要與master再進行交互。實際上,客戶端通常會在一次請求中請求多個塊,master也可以將這些請求的塊信息包裹在一起返回。這些額外的信息幾乎不需要什麼開銷就避免接下來的一些客戶端和master的交互

2.5 塊大小

塊大小是一個關鍵的設計參數。我們選擇了64MB作爲塊大小,這比一般的文件系統的塊大小要大得多。每個塊副本都作爲一個普通的Linux文件在chunkserver上存儲,並在需要時進行擴展。惰性空間分配避免了由於內部碎片導致的空間浪費,可能出現的最大碎片要比我們設定的這麼大的塊大小還要大

大的塊有一些重要的優點。首先,減少了客戶端與master的交互需要,因爲同一個塊上的讀寫只需要給master發送一個初始化請求來獲得塊定位信息。這些工作量的減少對我們的工作負載尤其重要,因爲應用程序大多數情況下都是按序讀寫大文件,即使對於少量隨機讀寫,客戶端也可以方便地緩存一個數TB工作集的所有的塊定位信息。其次,因爲塊很大,客戶端更可能會在給定的塊上執行大量操作,因此可以通過在較長時間內維持與chunkserver的TCP長連接來減少網絡開銷。最後,減少了master需要存儲的元數據的大小,所以允許我們將元數據保存在內存中,這又給我們帶來了將在2.6.1節中討論的其他優點

另一方面,即使採用了惰性空間分配,大的塊也有其缺點。一個小文件由一些或一個小塊組成,如果許多客戶端都來訪問相同的文件,存儲這些塊的chunkserver可能會成爲熱點。在實際情況中,熱點並不是主要的問題,因爲我們的應用程序主要是按序讀取多個大塊

然而,當批處理隊列系統首次使用GFS時,確實產生了熱點:一個可執行文件作爲單獨的塊文件寫入GFS,緊接着在數百臺機器上同時開始執行。存儲此文件的少部分chunkserver因爲數百個併發請求而導致過載。我們的解決辦法是使用更高的備份級別來存儲這樣的可執行文件,以及使用批處理隊列來錯開應用程序的啓動時間。一個潛在的長遠的解決方案是允許客戶端在這樣的情況下從其他的客戶端讀取數據

2.6 元數據

master存儲了3種主要類型的元數據:文件和塊的命名空間,文件到塊的映射,以及每個塊副本的位置。所有元數據都保存在master的內存中。前兩種類型(命名空間和文件到塊的映射)通過將更新操作記錄到存儲在master本地磁盤的操作日誌上來保證持久化,這份日誌也會在遠程服務器上進行備份。使用日誌可以讓我們簡便可靠地更新master的狀態,同時也避免了由於master故障導致不一致的風險。master並不會永久存儲塊的位置信息,相反地,會在master啓動或是一個chunkserver加入集羣時,來詢問每一臺chunkserver的塊信息

2.6.1 內存數據結構

由於元數據存儲在內存中,所有master的操作很快。此外,master可以簡單高效地在後臺對其整個狀態進行定期掃描。這個定期掃描被用來實現塊的垃圾收集,當出現故障時進行重備份,以及在chunkserver之間進行塊遷移時平衡負載和磁盤空間。我們將在4.3和4.4節進一步討論這些行爲

這種侷限於內存的方式有一個潛在的限制,就是塊的數量,因此整個系統的容量受到master的內存大小限制,不過在實際場景中並不是很嚴重的限制。master爲每個64MB的塊維護了至少64字節的元數據。大多數塊都是滿的,因爲大部分文件都包含了許多塊。類似地,對於每個文件而言,因爲使用了前綴壓縮算法來壓縮存儲,文件命名空間數據一般需要的字節數要少於64

如果需要支持更大的文件系統,與我們將元數據存儲在內存中獲得的簡便性、可靠性、高性能,以及靈活性相比,爲master增加額外的內存的開銷幾乎算不了什麼

2.6.2 塊的位置

master不會保留哪些chunkserver擁有給定塊的副本這樣的持久化信息,而只會在啓動時對chunkserver進行輪詢來獲取這些信息。master可以讓自己保持最新狀態,因爲它控制着所有的塊的放置,並通過定期的心跳消息來監控chunkserver的狀態

我們最初嘗試將塊的位置信息持久化保存在master上,但我們認爲在啓動時以及之後定期的從chunkserver請求這些數據要簡單得多。這種方式避免了chunkserver加入和退出集羣,更改名稱,失效和重啓等等情況下需要保持master和chunkserver的同步的問題。當集羣中有數百臺服務器時,這些情況會頻繁地發生

要想理解這樣的設計決策,我們要知道,只有chunkserver它本身才能夠確定一個塊是否存於它的磁盤中,在master中維護這個一致性視圖信息是沒有意義的,因爲chunkserver上的錯誤可能導致文件塊自行消失(例如,磁盤損壞導致不可用),或是管理員可能對chunkserver進行重命名

2.6.3 操作日誌

操作日誌包含了關鍵元數據更改的歷史記錄,它是GFS的核心。操作日誌不僅是元數據的唯一持久記錄,而且還充當了定義併發操作順序的邏輯時間線。文件和塊,以及它們的版本(參見4.5節),都由它們創建的邏輯時間進行唯一永久的標識

由於操作日誌至關重要,所以我們必須要進行可靠的存儲,並且在元數據的變更被持久化之前應當對客戶端不可見。否則,即使塊處於存活狀態,我們也會在根本上丟失整個文件系統或是最近的客戶端操作信息。因此,我們對操作日誌在多臺機器上進行備份,並且只有當本地和遠程均刷新了磁盤上對應的日誌記錄後,再響應客戶端的操作。master在刷新之前對多個日誌記錄執行批處理,從而減少了刷新和備份對系統整體的影響

master通過重新執行操作日誌來恢復文件系統的狀態。爲了最大限度縮短啓動時間,我們必須讓日誌儘量小。只要日誌增長超過了一定大小,master就會給當前狀態設置檢查點,以便可以在這之後通過從本地磁盤加載最近的檢查點,並重放有限數量的日誌來實現系統的恢復。檢查點採用壓縮的類似B樹的結構,不需要額外的解析就可以直接映射到內存中,並使用命名空間來進行查找,這進一步的提高了恢復的速度和系統的可用性

因爲構建檢查點需要一段時間,所以master的內部狀態被構建爲可以使得新的檢查點在無需對到來的改變進行延時就能夠創建的形式。master會使用另一個線程來切換新的日誌文件,並創建新的檢查點,新檢查點包括切換前的所有變更。對於有着數百萬文件的集羣,可以在一分鐘左右被創建。當創建完成後,會寫入本地和遠程的磁盤。

只需要最新的檢查點和其後的日誌文件就能夠恢復系統狀態。更早的檢查點和日誌文件可以自由刪除,但我們也保存了一部分來防止意外情況發生。在檢查點生成期間的錯誤不會影響系統的正確性,因爲恢復代碼會檢測並跳過不完整的檢查點。

2.7 一致性模型

GFS具有寬鬆的一致性模型,可以很好地支持我們的高度分佈式應用程序,而且實現起來仍是相對簡單和高效的。我們現在討論GFS提供的保證和其對於應用程序的意義,我們還會重點介紹GFS是如何實現這些保證,具體的細節會在論文的其他部分呈現

2.7.1 GFS提供的保證

文件命名空間的更改(例如文件創建)是原子性的,僅由master負責處理:命名空間鎖保證了原子性和正確性(4.1節);master的操作日誌定義了所有這些操作的全局順序

數據變更後,文件區域的狀態取決於變更的類型,即變更是否成功,以及是否存在併發更新。表1是對結果的一個概述。如果所有的客戶端無論從哪個副本讀取,都能看到同樣的數據,那我們就說文件區域是一致的。如果區域是一致的,那麼我們稱區域塊在文件數據更新後是已定義的,所有的客戶端都能從整體上看到變更的結果。如果一個變更成功執行,且沒有被其他併發的寫入干擾,那麼被影響的區域就是已定義的(意味着一致性):所有的客戶端都能夠看到更新寫入的結果。同時成功執行的變更操作會讓該區域具有不確定性,但是依然是一致的:所有客戶端都能夠看到相同的數據,但是它可能無法反映其中任何一個變更寫入的結果。通常,這部分數據由來自多個變更操作的混合片段組成。一個失敗的更新可能會導致區域變得不一致(也因此導致不確定性):不同的客戶端可能會在不同的時間看到不同的數據。我們會在下面來描述應用程序如何區分已定義和未定義的區域。應用程序不需要對未定義區域做進一步的區分。

寫操作或是記錄追加都會導致數據變更。寫操作在應用程序指定的偏移位置寫入數據,記錄追加操作即使存在併發的數據變更,也會原子性的執行至少一次追加數據(即“記錄”)操作,但是偏移量由GFS指定(3.3節)(相反地,一個“常規”的追加操作僅僅是一個偏移量是客戶端認爲的當前文件結尾的寫操作)。偏移位置會返回給客戶端,並標記了包含該記錄的已定義區域的起始位置。此外,GFS還可能在其間插入填充或者記錄的副本,它們會佔用那些被認爲是不一致的區域,通常這些數據和用戶的數據比起來要小得多

在一系列成功的變更後,更新後的文件區域能夠確保是已定義的,並且包含了最後一次更新寫入的數據。GFS通過以下方式來實現:(a) 對所有塊副本上以同樣的順序來對塊執行更新(3.1節),(b) 使用塊版本號來檢測那些因爲chunkserver宕機造成修改丟失的過期的副本(4.5節)。過期的副本將不會參與更新,也不會在客戶端向master詢問塊位置時被返回,它們會被儘早的執行垃圾回收

因爲客戶端緩存了塊的位置,因此在數據刷新之前,可能會讀到過期的副本。窗口的大小收到緩存條目的超時時間和下一次打開文件的限制,打開文件的操作會從緩存中清除文件的所有塊信息。此外,我們的大多數文件都是隻能執行追加操作的,一個過期的副本通常會提前返回塊的結束,而不是過時的數據。當讀取者進行重試並與master聯繫時,會立即獲得塊的當前位置

變更成功執行很久之後,很明顯組件故障依然能夠污染或損壞數據。GFS通過master和所有chunkserver之間的定期握手來標識那些失效的chunkserver,並通過校驗和來檢測被污染的數據(5.2節)。一旦出現問題,數據會儘快地從有效的副本中恢復(4.3節)。只有在GFS作出響應前(通常在幾分鐘之內)所有塊的副本均丟失,纔會導致真正不可逆的塊丟失。即使在這種情況下,也僅僅是變得不可用,而不是損壞:應用程序會接收到明確的報錯信息,而不是損壞的數據

2.7.2 對應用程序的影響

GFS應用程序可以通過一些用於其他目的的簡單的技術來適應這種弱一致性模型:依賴於追加而不是覆寫操作,檢查點技術,以及寫入自檢查和自認證的記錄

實際上我們所有的應用程序都是通過追加操作來更新文件,而不是覆寫操作。在一個典型的應用場景中,一個寫入者從頭到尾創建了一個文件,在寫完數據後自動將文件重命名爲一個永久的名稱,或是使用檢查點週期性地確認有多少數據被寫入。檢查點同樣包括應用程序級的校驗和。讀取者僅僅會驗證最後一個檢查點之前的區域,這些區域可以確認處於已定義的狀態。無論一致性和併發性如何要求,這種方法都對我們是很有幫助的。與隨機寫入相比,追加操作更爲高效,對於應用程序的故障的處理也更爲靈活。檢查點技術允許寫入者以增量的方式重啓,也讓讀取者避免處理成功寫入的數據,這在應用程序的角度來看仍然是不完整的

另一種典型的應用場景中,許多寫入者爲了合併結果或是作爲生產者-消費者隊列,同時對一個文件執行追加操作。記錄追加的append-at-least-once(至少一次追加)的語義保證了每一個寫入者的輸出。讀取者處理臨時的填充和副本,如下所示。寫入者的每條記錄都包含了像校驗和之類的額外信息,以便驗證其有效性。讀取者可以通過校驗和來識別並丟棄額外的填充和記錄段。如果不能容忍偶然的重複數據(例如,觸發了非冪等的操作),可以在記錄中使用唯一標識來對重複的部分進行過濾,通常不管怎樣都需要這些標識來命名對應的應用程序實體,比如網頁文檔。這些用於記錄的輸出輸出的功能函數(除了重複刪除)都在我們應用程序共享的代碼庫中,同時適用於Google中其他文件接口的實現。使用這些工具,同一序列的記錄,再加上一小部分的重複,總是會分發給記錄的閱讀者

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