介紹
GFS的誕生來源于google日益增長的數據量的處理需求,它是一個可擴展的分布式文件系統,用于大型分布式數據密集型應用,在廉價的通用硬件上運行時提供容錯機制,并且可以為大量客戶端提供較高的聚合性能。
它的設計由當前和預期的應用負載(當時的)和技術環境驅動,與以前的文件系統的假設有著明顯不同,因此gfs在設計上有幾個不同的points:
- 組件故障是正常現象。文件系統包含成百上千臺由廉價組件構成的機器并且被大量客戶端訪問,某些組件可能會在任意時刻出現故障,這些問題可以來源于應用bugs、操作系統bugs、人的操作失誤、硬盤內存連接器網絡和電源故障等,因此系統監控、錯誤檢測、容錯和自動恢復功能都是系統不可或缺的。
- 傳統系統中的文件是非常大的,數GB的文件非常常見。每個文件通常包含很多應用的objects,當應用于快速增長的數據集中時,數TB的文件包含數十億的objects,這種管理方式將會很難控制。因此必須重新設計例如I/O操作和塊大小。
- 大多數文件的變動是通過append新的數據而不是重寫已存在的數據。隨機寫幾乎不存在。基于這個特征,appending成為性能優化和原子保證的關鍵,在客戶端緩存數據塊已經不再有吸引力。
- 共同設計應用程序和文件系統API,提高靈活性。比如gfs簡化了一致性模型,以極大簡化文件系統而又不會給應用程序帶來繁重的負擔,還引入了原子的append以使得多個客戶端可以并發地操作,它們不用增加額外的同步邏輯。
當前已部署多個集群用于不同目的,最大的擁有1000多個存儲節點,超過300TB的存儲服務,并且有數百個客戶端連續不斷地高負載請求。
設計概述
假設
前面提到一些對應用負載和技術環境的觀察,現在更詳細地進行闡述:
- 系統構建在許多廉價的組件之上,它們會經常出故障,因此系統必須不斷地自我監控、定期檢測、容錯并可以從故障中快速恢復。
- 系統存儲適量的大文件。預計有幾百個文件,每個文件的大小通常為100MB或者更大,數GB的文件也是常見的,需要有效地進行管理。小文件必須支持但不需要專門地優化。
- 讀負載主要包含兩種類型的請求:large stream read and small random read。數MB與數KB。性能敏感的應用程序通常會對小的讀請求做批量處理和排序。
- 寫負載通常是大的順序寫,通過追加寫的方式。操作的大小與讀請求相似,一旦寫后文件幾乎不會再次被修改。也支持小的隨機寫,但是效率很低。
- 多個客戶端可以高效地進行并發append操作很關鍵。例如文件經常用于生產者-消費者隊列或者多路歸并,數百個生產者并發地append數據到同一個文件,同步的代價將是不可忽視的。
- 高吞吐比低延遲更重要。大多數的目標應用都非常重視是否可以高速率地處理數據,很少會對讀寫的相應時間有較高的要求。
接口
雖然GFS不能提供像POSIX標準的API,但它提供一個相似的文件系統接口。文件在目錄中按層次結構組織,并以路徑名作為標識。支持create、delete、open、close、read and write files。
gfs支持快照和record append操作。快照以低代價創建文件副本或者目錄樹,record append支持多個客戶端并發地寫文件,保證每個獨立客戶端append的原子性。
架構
一個gfs集群包含一個master和多個chunkservers,chunkserver被多個客戶端訪問,如圖1所示。每一個都是普通linux機器上運行的用戶態服務進程。資源允許的情況下,客戶端可以和chunkserver部署在同一臺機器上。
文件被劃分為固定大小的塊。每個chunk由一個獨一無二的64位大小的chunk handle所標識,chunk handle在chunk被創建時由master分配。每個chunk的副本分布在多個機器上,系統默認為三副本模式,用戶也可以為不同namespace的文件指定不同級別的副本。
master包含文件系統的所有元信息。包含namespace、訪問控制權限信息、文件到chunks的映射、當前chunks的位置信息。也控制著全局的活動,像chunk租約管理、gc、chunk遷移等。master通過心跳的方式與每個chunkserver交流來發送它的指令和收集狀態。
客戶端與master的交互涉及元信息操作,所有數據操作直接與chunkserver交互。gfs不提供POSIX標準API,因此不需要掛接到linux的vnode層。
客戶端和chunkserver都不緩存文件數據。大多數應用傳輸大文件,客戶端緩存收益很低。chunks作為本地的文件存儲,linux系統有自己的buffer cache,chunkserver不需要再增加緩存。
單master
單master簡化了系統的設計,但是會有單點的瓶頸問題,這是必須要解決的。客戶端不會從master讀寫數據文件,客戶端請求master它需要的交互的chunkserver信息,并且將其緩存一段時間,后續的操作直接與chunkservers交互。
客戶端會發送請求給離它最近的一個副本。實際上,客戶端通常會向master請求多個chunk的信息,以減少未來與maser交互的代價。
Chunk Size
chunk size定為64MB,相比普通的文件系統的block size更大。每個chunk副本以linux文件的形式存在chunkserver上,僅根據需要來擴展。使用lazy space allocation的方式避免空間浪費。
large chunk size有以下幾個優點:
- 減少客戶端與master的交互。對于數TB的工作集,客戶端可以緩存住所有chunks的位置信息。
- 減少網絡overhead開銷。
- 減少了master所維護的元信息大小。
但是large chunk size with lazy space allocation也有其缺點:單個文件可能包含很少數量的chunks,或許只有一個,當許多客戶端訪問相同文件時這些chunks成為熱點。但由于目標應用大多是順序的讀多個large chunk文件,熱點并不是主要的問題。
然而GFS第一次用于批處理隊列系統時確實出現了熱點問題,數百個客戶端同時訪問一個單chunk文件,存儲這個文件的幾個chunkserver超負荷運轉,當時通過錯開應用的啟動時間避免了這個問題,一個潛在、長期的解決方法是允許客戶端從其它客戶端讀取數據。
元數據
master保存三種類型的元數據:
- the file and chunk namspaces
- 文件到chunks的映射
- 每個chunk副本的位置信息
所有元數據都保存在內存中。對于元數據的內存操作是很快的,后臺任務周期巡檢整個狀態也是比較簡單高效的。周期巡檢用于實現chunk gc、在chunkserver故障時重新構造副本、chunk遷移以平衡多個chunkserver的負載和disk usage。
雖然系統的容量受master內存大小的限制,但這并不是一個嚴重的問題,64MB的chunk只需要不到64byte大小的元信息,如果一定需要更大的文件系統,那么增加內存的代價相比為可靠性、性能和靈活性等付出的代價是較小的。
前兩種類型的元數據通過寫日志來保證持久化,并且會復制日志到遠程機器上。master不需要將chunks的位置信息持久化,而是在master啟動和新的chunkserver加入集群時向每個chunkserver詢問它的位置信息,之后通過心跳信息監控chunk位置變更信息。chunkserver作為最后一關是確切知道自己本地有沒有哪些chunk的,因此維護一個一致性的視圖是沒有必要的。
operation log包含元數據的變更記錄,它是GFS的核心,它不僅僅是唯一的元數據持久化記錄,也表明了并發操作的邏輯時間線。文件、chunks和它們的版本都是由邏輯時間線唯一標識。元數據變更記錄在持久化之前對客戶端是不可見的,而且日志被復制到多個遠程的機器,只有相應的記錄在本地和遠程都持久化到硬盤了才可以回復客戶端。master使用批處理log的方式提高系統的吞吐。
master通過回放日志來恢復文件系統的狀態,為提高恢復速度需要保持log量足夠小。當log增長超過特定大小時,master會checkpoint它的狀態,以加速恢復提高可用性。構建checkpoint可能需要花費一段時間,因此master以一種不delay后續變化的方式來組織內部狀態,先switch到一個新的日志文件,使用獨立的線程創建checkpoint,新的checkpoint包含了所有switch之前的變化。幾百萬個文件的集群在一分鐘內可以完成,完成后將同時被寫入本地和遠程。恢復只需要最新的checkpoint和之后的日志文件,舊的checkpoints和日志文件可以完全刪除。
一致性模型
GFS使用一個寬松的一致性模型,這種模型可以很好地支持分布式應用程序,而且實現起來簡單有效。
file namesapce變化(例如文件創建)是原子的,使用namespace鎖。
master的operation log定義了這些操作的全局順序。
數據變化后文件region的狀態取決于變化的類型,是否成功、失敗或者是并發的。Table1做了總結。如果所有客戶端都能看到相同的數據,無論它們讀的是哪個副本,則這個file region是一致的。
- region defined: 一致的并且客戶端可以看到變化的全貌。比如沒有并發的write并且是成功的。
- undefined but consistent: 并發且成功的變化使region是一致的,但是客戶端無法識別出哪一個mutation寫了什么。
- inconsistent: failed mutations。不同的客戶端可能在不同時間看到不同的數據。
數據變化有兩種:writes或者record appends。write是指從應用指定offset處開始寫數據,record append指即使存在并發沖突,數據也要被原子地append到文件至少一次,但offset是由GFS選定。
GFS保證在一系列成功的mutations后,file region是defined,通過下面兩點來保證:
- 以相同的order在所有包含數據的副本上apply這些mutations。
- 使用chunk version來檢測副本是否遺漏了mutations而過期(它的chunkserver下線)。
過期的副本將不會再涉及到任何mutation,master也不會將其位置信息回應給客戶端,不久后將會被gc。但客戶端緩存的信息可能包含過期的副本,緩存失效存在一個時間窗口,文件再次打開也會清除該文件的所有chunk信息。由于大多數文件是append-only,過期的副本通常返回的是過早的結尾???而不是過期的數據。
系統交互
介紹客戶端、master和chunkserver之間如何交互來實現數據變化、原子追加寫和快照的。
租約和Mutation Order
使用租約的方式維護多個副本間一致的mutation order。master授權租約給副本中的一個,稱之為primary。primary為chunk的mutaions選擇一個順序,所有副本都按照這個順序apply。
租約機制最小化了master的管理overhead。租約初始的超時時間是60s,如果chunk一直在變化過程中,primary可以申請續租。這些授權和續租請求由master和chunkserver之間的心跳信息攜帶。master也可以嘗試撤銷租約,即使它與primary失去了聯系,也可以等租約過期后安全地授權給另外一個副本。
在Figure2中,跟隨著寫入控制流展示了處理過程:
- 客戶端請求master持有當前租約的chunk和其它副本的位置信息,如果沒有人持有租約,master選擇一個授權。
- master回應客戶端primary和其它副本的位置信息,客戶端將其緩存,只有當primary連接不上或者不再持有租約時需要再次請求master。
- 客戶端將數據push到所有的副本,可以以任意的順序。每個chunkserver將數據存儲在LRU緩存中,直到數據被使用或者過期。通過將數據流和控制流分離,忽略哪個chunkserver是primary,數據流以網絡拓撲的方式傳輸提升性能。
- 當所有副本都確認收到了數據,客戶端發送寫請求給primary,primary為收到的mutations分配一個順序,這些mutations可能是來自多個客戶端,然后以這個順序將mutation應用到自己本地狀態。
- primary將寫請求發給所有的副本,每個副本按照相同的順序應用到本地。
- 二級副本完成操作后回應給primary。
- primary回應客戶端。在任何副本上遇到的任何錯誤都將報告給客戶端,一旦出現錯誤,write可能在primary和任意二級副本的子集中成功完成(如果primary失敗,則不會分配序列并轉發)。客戶端請求將被認為failed,修改的region處于不一致的狀態,通過客戶端重試來處理,將從步驟3開始到步驟7。
如果一個寫請求比較大或者超出了chunk邊界,GFS客戶端將它拆為多個寫操作,但是多個操作可能與其它客戶端并發交叉寫入,因此共享的fie region最終可能包含多個不同客戶端的碎片,這會造成一致性模型中所描述的file region處于consistent but undefined狀態。
數據流
數據以pipline的機制在chunkserver鏈上線性傳輸,而控制流是從客戶端到primary再到所有的其它副本。分離數據流和控制流可以更高效地使用網絡。可以帶來以下好處:
- 充分利用每個機器的網絡帶寬。數據push按照chunkserver鏈線性傳輸而非一些其它拓撲形式,每臺機器可以打滿自己outbound bandwidth。
- 避免網絡瓶頸和高延遲連接。每臺機器選擇網絡拓撲中沒有接收數據并且離他最近的一個進行傳輸數據。網絡拓撲的"距離"使用IP地址簡單估算。
- 最小化數據傳輸的延遲。通過pipline傳輸數據的方式,因為使用全雙向連接的網絡,發送數據不會減少接收的速率。
Atomic Record Appends
GFS提供原子的append operaton叫作record append。傳統的write中,客戶端指定offset,并發寫相同region時不是serializable,最終region可能包含多個客戶端的碎片數據。而對于record append,客戶端僅指定數據,GFS保證至少一次成功的原子append,offset由GFS選定,與Unix的O_APPEND模式相似。
多個客戶端并發操作相同文件是比較重的。如果處理傳統的write,客戶端需要額外復雜和昂貴的同步邏輯,像分布式鎖。而record append僅需要primary增加一點額外的邏輯:primary檢查是否并發append數據的chunk會超出max size,如果會超出則將chunk填充到max size,并且告訴所有二級副本同樣操作,然后回應客戶端指出這個操作應該選擇另一個chunk重試;大多數情況下記錄是在max size內的,primary將數據append到自己的副本,并告訴所有二級副本按照確切的offset寫數據,最后回應給客戶端。
如果中間出現錯誤,客戶端重試,相同chunk的副本可能包含不同的數據,可能包含相同的記錄或者一部分相同,GFS不保證bytewise identical,僅僅保證數據至少有一次被成功地原子寫入。從report success邏輯可以容易得出,數據必須是在某個chunk的所有副本上以相同的offset寫入。在此之后,所有副本都與記錄end一樣長,即使后面不同的副本成為primary,任何將來的記錄也將分配到更高的offset或者不同的chunk。根據上述的一致性保證,成功的record append的region是defined和一致的,而中間的region是不一致的(undefined)。GFS的應用可以處理這種不一致的region(2.7.2)。
snapshot
snapshot 操作拷貝一份文件或者目錄樹,幾乎是實時的,同時最大程度減少對正在進行中的mutation的干擾。
像AFS一樣,使用標準的COW技術實現snapshot。當master接收到一個snapshot請求,首先將所有涉及到chunks的租約撤銷,這保證了這些chunks后續的write將會先請求master查找租約持有者,master會創建一個新的副本來回應。
租約被撤銷或者過期后,master將這個操作記錄日志到disk。新創建的snapshot引用元數據相同的chunks。
當snapshot操作完成后,客戶端第一次要寫chunk C,發送請求給master查詢持有租約者,master察覺到chunk C的引用大于1,則讓每個含有當前chunk副本的chunkserver創建一個新的chunk叫作C',所有創建都使用本地的副本,相比100Mb的網絡本地速度大約是三倍速度。master授權租約給新的chunk C'中的一個并且回復給客戶端,之后正常地寫chunk。整個過程對客戶端是透明的。
MASTER OPERATION
master執行所有的namespace操作。另外,它管理整個系統的chunk副本:
- 放置決策
- 創建新chunk和副本
- 協調系統范圍內的各種活動以保持chunk的副本數量
- 平衡chunkservers的負載
- 回收空間
接下來,詳細探討這些細節。
Namespace Management and Locking
許多master操作可能花費較長一段時間,比如snapshot操作需要撤銷相關的所有chunks的租約。因此為了不delay其它master操作,在namesapce的regions上使用locks來確保串行化。
GFS沒有按目錄列出該目錄中所有文件的結構,也不支持文件和目錄的別名(unix中的硬鏈和軟鏈)。GFS將完整的路徑名到元數據的映射表作為它的邏輯namespace。使用前綴壓縮,這個表可以有效保存在內存中。namespace tree中的每個節點都有一個關聯的讀寫鎖。
每個master操作在運行前都會獲取一組鎖。如果涉及到/d1/d2/../dn/leaf,它將獲取目錄名稱/d1、/d1/d2、...、/d1/d2/.../dn上的讀鎖,完整路徑/d1/d2/../dn/leaf的讀鎖或者寫鎖。leaf可以是文件或者目錄。
創建文件不需要對父級目錄加鎖,因為沒有"目錄"的概念不會修改它,而加讀鎖是防止它被刪除、重命名或者snapshot。這種鎖機制的好處是允許相同目錄下并發的mutations。
副本放置
一個GFS集群通常具有分布在多個機架上的數百個chunkserver,這些chunkserver也會被相同或者不同機架的數百個客戶端訪問。不同機架上的兩臺計算機之間的通信可能會跨越一個或者多個網絡交換機。另外進出機架的帶寬可能小于機架內所有計算機的總帶寬。多級分布式對如何分發數據以實現可伸縮性、可靠性和可用性提出了獨特的挑戰。
副本放置策略有兩個目的:最大化數據可靠性和可用性,最大化網絡帶寬利用率。不僅要在多臺機器上放置,還要在多個racks上,即使整個racks損壞也可以確保部分副本保持可用。也可以利用多個racks的總帶寬。
Creation,Re-replication,Rebalancing
chunk副本創建有三個原因:
- creation
- re-replication
- rebalancing
當master創建新的chunk時,根據幾個因素考慮如何放置新的副本:
- 選擇空間利用率低的chunkserver。
- 限制一臺chunksever最近新創建的chunks數量。雖然創建的代價小,但是創建意味著寫請求,避免寫負載過重。
- 如上所述,希望在多個racks中分布。
當chunk可用副本的數量低于用戶指定時,master會重新復制。可能發生在幾種情況:
- chunkserver不可用。
- 副本損壞。
- disk error。
- 要求的副本數量增加。
需要重新復制的chunk根據以下幾個因素確定優先級:
- 剩余副本的數量。
- 文件的熱度。
- 提高block客戶端進度的chunk的優先級。
master限制集群和每一個chunkserver內的活躍的clone數量,另外chunkserver通過限制其對源chunkserver的讀請求來限制在每個clone操作上花費的帶寬。
master會定期重新平衡副本:檢查當前副本的分布,遷移副本以獲得更好的磁盤空間利用率和負載平衡。同樣通過此過程,master逐漸填充一個新的chunkserver。另外,master通常更傾向于移除具有低磁盤利用率chunkservers上的副本,以平衡空間使用。
GC
當文件被刪除時,master記錄日志,但不會立即回收資源,而是將文件重命名為包含刪除時間戳標記的隱藏名稱。如果這些文件存在時間超過三天(時間可配置),master巡檢時會將其刪除。在此之前,仍然可以用特殊名稱來讀取文件,并且可以重命名為正常名稱來取消刪除。當從namesapce中刪除隱藏文件時,其內存元數據將被刪除,這有效切斷了所有chunk的連接,在對chunk namespace的掃描中,master識別出孤立的chunk并清除元數據。在心跳信息中,每個chunkserver報告其擁有的chunks子集,而master將回應不在存在于master元數據中的所有的chunk的標識。chunkserver可以自由刪除此類chunk的副本。
這種gc機制相比立即刪除有以下幾個優點:
- simple and reliable。
- 將gc任務合并到master常規的后臺活動中,減少額外的開銷,僅在master相對空閑時執行。
- 提供可逆的刪除。
這種機制主要的缺點是當存儲空間緊張時,延遲有時會影響用戶的使用,重復創建和刪除臨時文件的應用可能無法立即重用存儲。如果刪除的文件再次被明確刪除,GFS將通過加快存儲回收來解決這些問題。還允許用戶將不同的復制和回收策略應用于不同的namespace的不同部分中。
過期副本刪除
如果一個chunkserver故障或者chunk丟失了mutations,這個chunk副本可能是過期的。對于每個chunk,master都維護了一個chunk版本號。
當master授權租約給一個chunk時,這個chunk的版本號增加1,如果一個副本當前不可用了,則其版本號將不會領先。當chunkserver重新啟動并報告其chunks集合和相關聯的版本號時,master將檢測到該chunkserver上具有過期的副本。如果master看到的版本號大于它記錄的版本號,則認為在授權租約時失敗了,因此將較高的版本號更新。
master在常規gc中刪除舊的副本。另一個保護措施,在master回應客戶端哪個chunk持有租約或者clone操作中chunkserver從另一個chunkserver讀取chunk時會包含chunk的最新版本號。客戶端或者chunkserver在執行操作時會驗證版本號。
容錯和診斷
這個系統最大的挑戰之一是處理經常故障的組件。組件的質量和數量造成的問題會超出預期,組件故障可能造成系統不可能,甚至數據錯誤。接下來討論GFS如何應對這些挑戰,還有系統如何診斷不可避免問題。
高可用
使用兩個簡單有效的方式保證系統的高可用:快速恢復和復制。
master和chunkserver的恢復都是秒級別的。
master維護每個chunk的副本數量,當chunkserver下線或者checksum檢測出錯誤副本時,master會通過已有副本來復制。盡管復制提供了很好的解決方式,但仍在探索其它形式的跨服務器冗余方案,例如奇偶校驗或者糾刪碼,以適應不斷增長的只讀存儲需求。在非常松耦合的系統中實現這些更復雜的冗余方案更具有挑戰性。
master的操作日志和checkpoint會被復制到多臺機器上,狀態的變化只有在本地和所有副本上都持久化以后才可以commit。master進程負責所有的mutations以及后臺任務,當它宕機時可以很快重啟,如果機器或者磁盤故障,GFS的外部監控將使用日志在其它節點重啟新的master進程。在master宕機時,master的備節點只提供只讀服務,它們不與master保持強一致,可能會落后于master,通常在1/4秒內。它們保證了那些不介意讀到過期數據的應用的高可用讀。類似于chunk的primary機制,master的備按照相同的序列應用日志。與master一樣,在啟動時從每個chunkserver拉取chunks的位置信息,與它們頻繁交換握手消息來監控其狀態。
數據完善
每個chunkserver使用checksum來檢測存儲數據的損壞。數據損壞的chunk可以通過其它的副本來恢復,但是通過副本間比較來檢驗數據是不切實際的。正常的副本也不是完全一樣的,如前文所講,原子的append并不能保證完全一樣的副本。因此每個chunkserver會維護自己的checksum。
每個chunk分為多個64kb的blocks,每個block包含一個32位的checksum,與其它元數據一樣,checksum保存在內存中,依靠log持久化,與用戶數據分離。
對于讀,chunkserver在返回數據給請求者前先檢測checksum,確保不會將出錯的數據傳輸給其它chunkservers或者客戶端。如果數據是壞的,chunkserver將錯誤返回給請求者并報告給master,請求者將會去讀其它副本, master將會根據其它副本重新克隆一份。當新的副本創建以后,master指示chunkserver將錯誤的副本刪除。checksum的計算不涉及I/O,對讀的影響比較小,客戶端通常嘗試使用對齊block邊界讀來減少overhead。
為append寫是做了checksum計算上的優化的,因為append寫是主要的負載(相比于overwrite)。GFS只增量地更新最后部分block的checksum,為新的block的計算新的checksum。這樣即使block已經損壞,新的checksum將與存儲的數據不會匹配,下次讀時將會與正常一樣被檢測出來。
如果一個寫請求要寫一個chunk中已存在的region,必要要先檢驗region的第一個和最后一個block的checksum,然后再重寫,最后計算新的checksums。因為第一個和最后一個block可能含有不被重寫的內容,如果這部分數據是損壞的,則新的checksum將包含錯誤的數據。
在idle時,checkserver可以掃描并檢查不活躍的chunks,可以檢測到冷chunks的錯誤,一旦錯誤被檢測到,master可以創建一個新的副本。
總結
GFS在設計上與傳統文件系統有很多不同,這些點是基于對當時應用負載和技術環境的觀察所重新設計,將組件故障看作平常的事件而非異常,為大文件的讀取和追加寫做優化,擴展和放寬了標準的文件系統接口以改善整個系統。通過監控、復制以及快速恢復能力提供容錯能力,使用checksum機制來校驗數據的正確性。通過將控制流和數據流分離,數據直接在chunkservers、客戶端之間傳輸,為許多并發的各種任務的讀取和寫入提供了高吞吐量。大chunk size和租約機制使得master的操作足夠輕量化,使得這樣一個簡單中心化的master不會成為瓶頸。
GFS成功地滿足了google的存儲需求,作為研究、開發和數據處理的存儲平臺廣泛地應用于google內部。