大數據理論篇HDFS的基石——Google File System
Google File System
但凡是要開始講大數據的,都繞不開最初的Google三駕馬車:Google File System(GFS), MapReduce,BigTable。
為這一切的基礎的Google File System,不但沒有任何倒台的跡象,還在不斷的演化,事實上支撐着Google這個龐大的互聯網公司的一切計算。
以下是原文內容,內容較長,建議詳細閱讀。
摘要
我們設計並實現了 Google GFS 文件系統,一個面向大規模數據密集型應用的、可伸縮的分佈式文件系統。
GFS 雖然運行在廉價的普遍硬件設備上,但是它依然了提供災難冗餘的能力,為大量客戶機提供了高性能的服務。
雖然 GFS 的設計目標與許多傳統的分佈式文件系統有很多相同之處,但是,我們的設計還是以我們對自己的應用的負載情況和技術環境的分析為基礎的,不管現在還是將來,GFS 和早期的分佈式文件系統的設想都有明顯的不同。所以我們重新審視了傳統文件系統在設計上的折衷選擇,衍生出了完全不同的設計思路。
GFS 完全滿足了我們對存儲的需求。GFS 作為存儲平台已經被廣泛的部署在 Google 內部,存儲我們的服務產生和處理的數據,同時還用於那些需要大規模數據集的研究和開發工作。目前為止,最大的一個集群利用數千台機器的數千個硬盤,提供了數百 TB 的存儲空間,同時為數百個客戶機服務。
在本論文中,我們展示了能夠支持分佈式應用的文件系統接口的擴展,討論我們設計的許多方面,最後列出了小規模性能測試以及真實生產系統中性能相關數據。
1 簡介
為了滿足 Google 迅速增長的數據處理需求,我們設計並實現了 Google 文件系統(Google File System – GFS)。GFS 與傳統的分佈式文件系統有着很多相同的設計目標,比如,性能、可伸縮性、可靠性以及可用性。
但是,我們的設計還基於我們對我們自己的應用的負載情況和技術環境的觀察的影響,不管現在還是將來,GFS 和早期文件系統的假設都有明顯的不同。所以我們重新審視了傳統文件系統在設計上的折衷選擇,衍生出了完全不同的設計思路。
-
首先,組件失效被認為是常態事件,而不是意外事件。GFS 包括幾百甚至幾千台普通的廉價設備組裝的存儲機器,同時被相當數量的客戶機訪問。GFS 組件的數量和質量導致在事實上,任何給定時間內都有可能發生某些組件無法工作,某些組件無法從它們目前的失效狀態中恢復。我們遇到過各種各樣的問題,比如應用程序 bug、操作系統的 bug、人為失誤,甚至還有硬盤、內存、連接器、網絡以及電源失效等造成的問題。所以,持續的監控、錯誤偵測、災難冗餘以及自動恢復的機制必須集成在 GFS 中。
-
其次,以通常的標準衡量,我們的文件非常巨大。數 GB 的文件非常普遍。每個文件通常都包含許多應用程序對象,比如 web 文檔。當我們經常需要處理快速增長的、並且由數億個對象構成的、數以 TB 的數據集時,採用管理數億個 KB 大小的小文件的方式是非常不明智的,儘管有些文件系統支持這樣的管理方式。因此,設計的假設條件和參數,比如 I/O 操作和 Block 的尺寸都需要重新考慮。
-
第三,絕大部分文件的修改是採用在文件尾部追加數據,而不是覆蓋原有數據的方式。對文件的隨機寫入操作在實際中幾乎不存在。一旦寫完之後,對文件的操作就只有讀,而且通常是按順序讀。大量的數據符合這些特性,比如:數據分析程序掃描的超大的數據集;正在運行的應用程序生成的連續的數據流;存檔的數據;由一台機器生成、另外一台機器處理的中間數據,這些中間數據的處理可能是同時進行的、也可能是後續才處理的。對於這種針對海量文件的訪問模式,客戶端對數據塊緩存是沒有意義的,數據的追加操作是性能優化和原子性保證的主要考量因素。
-
第四,應用程序和文件系統 API 的協同設計提高了整個系統的靈活性。比如,我們放鬆了對 GFS 一致性模型的要求,這樣就減輕了文件系統對應用程序的苛刻要求,大大簡化了 GFS 的設計。我們引入了原子性的記錄追加操作,從而保證多個客戶端能夠同時進行追加操作,不需要額外的同步操作來保證數據的一致性。本文後面還有對這些問題的細節的詳細討論。
Google 已經針對不同的應用部署了多套 GFS 集群。最大的一個集群擁有超過 1000 個存儲節點,超過300TB 的硬盤空間,被不同機器上的數百個客戶端連續不斷的頻繁訪問。
2 設計概述
2.1 設計預期
在設計滿足我們需求的文件系統時候,我們的設計目標既有機會、又有挑戰。之前我們已經提到了一些需要關注的關鍵點,這裡我們將設計的預期目標的細節展開討論。
系統由許多廉價的普通組件組成,組件失效是一種常態。系統必須持續監控自身的狀態,它必須將組件失效作為一種常態,能夠迅速地偵測、冗餘並恢復失效的組件。系統存儲一定數量的大文件。我們預期會有幾百萬文件,文件的大小通常在 100MB 或者以上。數個 GB大小的文件也是普遍存在,並且要能夠被有效的管理。系統也必須支持小文件,但是不需要針對小文件做專門的優化。
系統的工作負載主要由兩種讀操作組成: 大規模的流式讀取和小規模的隨機讀取。大規模的流式讀取通常一次讀取數百 KB 的數據,更常見的是一次讀取 1MB 甚至更多的數據。來自同一個客戶機的連續操作通常是讀取同一個文件中連續的一個區域。小規模的隨機讀取通常是在文件某個隨機的位置讀取幾個 KB 數據。如果應用程序對性能非常關注,通常的做法是把小規模的隨機讀取操作合併並排序,之後按順序批量讀取,這樣就避免了在文件中前後來回的移動讀取位置。系統的工作負載還包括許多大規模的、順序的、數據追加方式的寫操作。一般情況下,每次寫入的數據的大小和大規模讀類似。數據一旦被寫入後,文件就很少會被修改了。系統支持小規模的隨機位置寫入操作,但是可能效率不彰。系統必須高效的、行為定義明確的 2 實現多客戶端並行追加數據到同一個文件里的語意。我們的文件通常被用於「生產者-消費者」隊列,或者其它多路文件合併操作。通常會有數百個生產者,每個生產者進程運行在一台機器上,同時對一個文件進行追加操作。使用最小的同步開銷來實現的原子的多路追加數據操作是必不可少的。文件可以在稍後讀取,或者是消費者在追加的操作的同時讀取文件。高性能的穩定網絡帶寬遠比低延遲重要。我們的目標程序絕大部分要求能夠高速率的、大批量的處理數據,極少有程序對單一的讀寫操作有嚴格的響應時間要求。
2.2 接口
GFS 提供了一套類似傳統文件系統的 API 接口函數,雖然並不是嚴格按照 POSIX 等標準 API 的形式實現的。文件以分層目錄的形式組織,用路徑名來標識。我們支持常用的操作,如創建新文件、刪除文件、打開
文件、關閉文件、讀和寫文件。
另外,GFS 提供了快照和記錄追加操作。快照以很低的成本創建一個文件或者目錄樹的拷貝。記錄追加操作允許多個客戶端同時對一個文件進行數據追加操作,同時保證每個客戶端的追加操作都是原子性的。這對於實現多路結果合併,以及「生產者-消費者」隊列非常有用,多個客戶端可以在不需要額外的同步鎖定的情況下,同時對一個文件追加數據。我們發現這些類型的文件對於構建大型分佈應用是非常重要的。快照和記錄追加操作將在 3.4 和 3.3 節分別討論。
2.3 架構
一個 GFS 集群包含一個單獨的 Master 節點 3 、多台 Chunk 服務器,並且同時被多個客戶端訪問,如圖 1所示。所有的這些機器通常都是普通的 Linux 機器,運行着用戶級別(user-level)的服務進程。我們可以很容易的把 Chunk 服務器和客戶端都放在同一台機器上,前提是機器資源允許,並且我們能夠接受不可靠的應用程序代碼帶來的穩定性降低的風險。
GFS 存儲的文件都被分割成固定大小的 Chunk。在 Chunk 創建的時候,Master 服務器會給每個 Chunk 分配一個不變的、全球唯一的 64 位的 Chunk 標識。Chunk 服務器把 Chunk 以 Linux 文件的形式保存在本地硬盤上,並且根據指定的 Chunk 標識和位元組範圍來讀寫塊數據。出於可靠性的考慮,每個塊都會複製到多個塊服務器上。缺省情況下,我們使用 3 個存儲複製節點,不過用戶可以為不同的文件命名空間設定不同的複製級別。
Master 節點管理所有的文件系統元數據。這些元數據包括名字空間、訪問控制信息、文件和 Chunk 的映射信息、以及當前 Chunk 的位置信息。Master 節點還管理着系統範圍內的活動,比如,Chunk 租用管理 、孤兒 Chunk 的回收、以及 Chunk 在 Chunk 服務器之間的遷移。Master 節點使用心跳信息周期地和每個 Chunk服務器通訊,發送指令到各個 Chunk 服務器並接收 Chunk 服務器的狀態信息。
GFS 客戶端代碼以庫的形式被鏈接到客戶程序里。客戶端代碼實現了 GFS 文件系統的 API 接口函數、應用程序與 Master 節點和 Chunk 服務器通訊、以及對數據進行讀寫操作。客戶端和 Master 節點的通信只獲取元數據,所有的數據操作都是由客戶端直接和 Chunk 服務器進行交互的。我們不提供 POSIX 標準的 API 的功能,因此,GFS API 調用不需要深入到 Linux vnode 級別。
無論是客戶端還是 Chunk 服務器都不需要緩存文件數據。客戶端緩存數據幾乎沒有什麼用處,因為大部分程序要麼以流的方式讀取一個巨大文件,要麼工作集太大根本無法被緩存。無需考慮緩存相關的問題也簡化了客戶端和整個系統的設計和實現 。Chunk 服務器不需要緩存文件數據的原因是,Chunk 以本地文件的方式保存,Linux 操作系統的文件系統緩存會把經常訪問的數據緩存在內存中。
2.4 單一 Master 節點
單一的 Master 節點的策略大大簡化了我們的設計。單一的 Master 節點可以通過全局的信息精確定位Chunk 的位置以及進行複製決策。另外,我們必須減少對 Master 節點的讀寫,避免 Master 節點成為系統的瓶頸。客戶端並不通過 Master 節點讀寫文件數據。反之,客戶端向 Master 節點詢問它應該聯繫的 Chunk 服務器。
客戶端將這些元數據信息緩存一段時間,後續的操作將直接和 Chunk 服務器進行數據讀寫操作。
我們利用圖 1 解釋一下一次簡單讀取的流程。首先,客戶端把文件名和程序指定的位元組偏移,根據固定的 Chunk 大小,轉換成文件的 Chunk 索引。然後,它把文件名和 Chunk 索引發送給 Master 節點。Master 節點將相應的 Chunk 標識和副本的位置信息發還給客戶端。客戶端用文件名和 Chunk 索引作為 key 緩存這些信息。之後客戶端發送請求到其中的一個副本處,一般會選擇最近的。請求信息包含了 Chunk 的標識和位元組範圍。在對這個 Chunk 的後續讀取操作中,客戶端不必再和 Master 節點通訊了,除非緩存的元數據信息過期或者文件被重新打開。實際上,客戶端通常會在一次請求中查詢多個 Chunk 信息,Master 節點的回應也可能包含了緊跟着這些被請求的 Chunk 後面的 Chunk 的信息。在實際應用中,這些額外的信息在沒有任何代價的情況下,避免了客戶端和 Master 節點未來可能會發生的幾次通訊。
2.5 Chunk 尺寸
Chunk 的大小是關鍵的設計參數之一。我們選擇了 64MB,這個尺寸遠遠大於一般文件系統的 Block size。
每個 Chunk 的副本都以普通 Linux 文件的形式保存在 Chunk 服務器上,只有在需要的時候才擴大。惰性空間
分配策略避免了因內部碎片造成的空間浪費,內部碎片或許是對選擇這麼大的 Chunk 尺寸最具爭議一點。
選擇較大的 Chunk 尺寸有幾個重要的優點。首先,它減少了客戶端和 Master 節點通訊的需求,因為只需要一次和 Mater 節點的通信就可以獲取 Chunk 的位置信息,之後就可以對同一個 Chunk 進行多次的讀寫操作。這種方式對降低我們的工作負載來說效果顯著,因為我們的應用程序通常是連續讀寫大文件。即使是小規模的隨機讀取,採用較大的 Chunk 尺寸也帶來明顯的好處,客戶端可以輕鬆的緩存一個數 TB 的工作數據集所有的 Chunk 位置信息。其次,採用較大的 Chunk 尺寸,客戶端能夠對一個塊進行多次操作,這樣就可以通過與 Chunk 服務器保持較長時間的 TCP 連接來減少網絡負載。第三,選用較大的 Chunk 尺寸減少了 Master節點需要保存的元數據的數量。這就允許我們把元數據全部放在內存中,在 2.6.1 節我們會討論元數據全部放在內存中帶來的額外的好處。
另一方面,即使配合惰性空間分配,採用較大的 Chunk 尺寸也有其缺陷。小文件包含較少的 Chunk,甚至只有一個 Chunk。當有許多的客戶端對同一個小文件進行多次的訪問時,存儲這些 Chunk 的 Chunk 服務器就會變成熱點。在實際應用中,由於我們的程序通常是連續的讀取包含多個 Chunk 的大文件,熱點還不是主要的問題。
然而,當我們第一次把 GFS 用於批處理隊列系統的時候,熱點的問題還是產生了:一個可執行文件在GFS 上保存為 single-chunk 文件,之後這個可執行文件在數百台機器上同時啟動。存放這個可執行文件的幾個 Chunk 服務器被數百個客戶端的並發請求訪問導致系統局部過載。我們通過使用更大的複製參數來保存可執行文件,以及錯開批處理隊列系統程序的啟動時間的方法解決了這個問題。一個可能的長效解決方案是,在這種的情況下,允許客戶端從其它客戶端讀取數據。
2.6 元數據
Master 服務器存儲 3 種主要類型的元數據,包括:文件和 Chunk 的命名空間、文件和 Chunk 的對應關係、
每個 Chunk 副本的存放地點。所有的元數據都保存在 Master 服務器的內存中。前兩種類型的元數據同時也會以記錄變更日誌的方式記錄在操作系統的系統日誌文件中,日誌文件存儲在本地磁盤上,同時日誌會被複制到其它的遠程Master服務器上。採用保存變更日誌的方式,我們能夠簡單可靠的更新Master服務器的狀態,並且不用擔心 Master 服務器崩潰導致數據不一致的風險。Master 服務器不會持久保存 Chunk 位置信息。Master服務器在啟動時,或者有新的 Chunk 服務器加入時,向各個 Chunk 服務器輪詢它們所存儲的 Chunk 的信息。
2.6.1 內存中的數據結構
因為元數據保存在內存中,所以 Master 服務器的操作速度非常快。並且,Master 服務器可以在後台簡單
而高效的周期性掃描自己保存的全部狀態信息。這種周期性的狀態掃描也用於實現 Chunk 垃圾收集、在 Chunk
服務器失效的時重新複製數據、通過 Chunk 的遷移實現跨 Chunk 服務器的負載均衡以及磁盤使用狀況統計等
功能。4.3 和 4.4 章節將深入討論這些行為。
將元數據全部保存在內存中的方法有潛在問題:Chunk 的數量以及整個系統的承載能力都受限於 Master
服務器所擁有的內存大小。但是在實際應用中,這並不是一個嚴重的問題。Master 服務器只需要不到 64 個字
節的元數據就能夠管理一個 64MB 的 Chunk。由於大多數文件都包含多個 Chunk,因此絕大多數 Chunk 都是
滿的,除了文件的最後一個 Chunk 是部分填充的。同樣的,每個文件的在命名空間中的數據大小通常在 64
位元組以下,因為保存的文件名是用前綴壓縮算法壓縮過的。
即便是需要支持更大的文件系統,為 Master 服務器增加額外內存的費用是很少的,而通過增加有限的
費用,我們就能夠把元數據全部保存在內存里,增強了系統的簡潔性、可靠性、高性能和靈活性。
2.6.2 Chunk 位置信息
Master 服務器並不保存持久化保存哪個 Chunk 服務器存有指定 Chunk 的副本的信息。Master 服務器只是
在啟動的時候輪詢 Chunk 服務器以獲取這些信息。Master 服務器能夠保證它持有的信息始終是最新的,因為
它控制了所有的 Chunk 位置的分配,而且通過周期性的心跳信息監控 Chunk 服務器的狀態。
最初設計時,我們試圖把 Chunk 的位置信息持久的保存在 Master 服務器上,但是後來我們發現在啟動的
時候輪詢 Chunk 服務器,之後定期輪詢更新的方式更簡單。這種設計簡化了在有 Chunk 服務器加入集群、離
開集群、更名、失效、以及重啟的時候,Master 服務器和 Chunk 服務器數據同步的問題。在一個擁有數百台
服務器的集群中,這類事件會頻繁的發生。
可以從另外一個角度去理解這個設計決策:只有 Chunk 服務器才能最終確定一個 Chunk 是否在它的硬盤
上。我們從沒有考慮過在 Master 服務器上維護一個這些信息的全局視圖,因為 Chunk 服務器的錯誤可能會導
致 Chunk 自動消失(比如,硬盤損壞了或者無法訪問了),亦或者操作人員可能會重命名一個 Chunk 服務器。
2.6.3 操作日誌
操作日誌包含了關鍵的元數據變更歷史記錄。這對 GFS 非常重要。這不僅僅是因為操作日誌是元數據唯
一的持久化存儲記錄,它也作為判斷同步操作順序的邏輯時間基線。文件和 Chunk,連同它們的版本(參考
4.5 節),都由它們創建的邏輯時間唯一的、永久的標識。
操作日誌非常重要,我們必須確保日誌文件的完整,確保只有在元數據的變化被持久化後,日誌才對客
戶端是可見的。否則,即使 Chunk 本身沒有出現任何問題,我們仍有可能丟失整個文件系統,或者丟失客戶
端最近的操作。所以,我們會把日誌複製到多台遠程機器,並且只有把相應的日誌記錄寫入到本地以及遠程
機器的硬盤後,才會響應客戶端的操作請求。Master 服務器會收集多個日誌記錄後批量處理,以減少寫入磁
盤和複製對系統整體性能的影響。
Master 服務器在災難恢復時,通過重演操作日誌把文件系統恢復到最近的狀態。為了縮短 Master 啟動的
時間,我們必須使日誌足夠小。Master 服務器在日誌增長到一定量時對系統狀態做一次 Checkpoint ,將所
有的狀態數據寫入一個 Checkpoint 文件。在災難恢復的時候,Master 服務器就通過從磁盤上讀取這Checkpoint 文件,以及重演 Checkpoint 之後的有限個日誌文件就能夠恢復系統。Checkpoint 文件以壓縮 B-樹形勢的數據結構存儲,可以直接映射到內存,在用於命名空間查詢時無需額外的解析。這大大提高了恢復速度,增強了可用性。
由於創建一個 Checkpoint 文件需要一定的時間,所以 Master 服務器的內部狀態被組織為一種格式,這種格式要確保在 Checkpoint 過程中不會阻塞正在進行的修改操作。Master 服務器使用獨立的線程切換到新的日誌文件和創建新的 Checkpoint 文件。新的 Checkpoint 文件包括切換前所有的修改。對於一個包含數百萬個文件的集群,創建一個 Checkpoint 文件需要 1 分鐘左右的時間。創建完成後,Checkpoint 文件會被寫入在本地和遠程的硬盤裡。
Master 服務器恢復只需要最新的 Checkpoint 文件和後續的日誌文件。舊的 Checkpoint 文件和日誌文件可
以被刪除,但是為了應對災難性的故障 ,我們通常會多保存一些歷史文件。Checkpoint 失敗不會對正確性產
生任何影響,因為恢復功能的代碼可以檢測並跳過沒有完成的 Checkpoint 文件。
2.7 一致性模型
GFS 支持一個寬鬆的一致性模型,這個模型能夠很好的支撐我們的高度分佈的應用,同時還保持了相對簡單且容易實現的優點。本節我們討論 GFS 的一致性的保障機制,以及對應用程序的意義。我們也着重描述了 GFS 如何管理這些一致性保障機制,但是實現的細節將在本論文的其它部分討論。
2.7.1 GFS 一致性保障機制
文件命名空間的修改(例如,文件創建)是原子性的。它們僅由 Master 節點的控制:命名空間鎖提供了
原子性和正確性(4.1 章)的保障;Master 節點的操作日誌定義了這些操作在全局的順序(2.6.3 章)。
數據修改後文件 region 的狀態取決於操作的類型、成功與否、以及是否同步修改。表 1 總結了各種操作
的結果。
如果所有客戶端,無論從哪個副本讀取,讀到的數據都一樣,那麼我們認為文件 region 是「一致的」;
如果對文件的數據修改之後,region 是一致的,並且客戶端能夠看到寫入操作全部的內容,那麼這個 region
是「已定義的」。
當一個數據修改操作成功執行,並且沒有受到同時執行的其它寫入操作的干擾,那麼影響的 region 就是
已定義的(隱含了一致性):所有的客戶端都可以看到寫入的內容。並行修改操作成功完成之後,region 處於一致的、未定義的狀態:所有的客戶端看到同樣的數據,但是無法讀到任何一次寫入操作寫入的數據。通常情況下,文件 region 內包含了來自多個修改操作的、混雜的數據片段。失敗的修改操作導致一個 region 處於不一致狀態(同時也是未定義的):不同的客戶在不同的時間會看到不同的數據。後面我們將描述應用如何區分已定義和未定義的 region。應用程序沒有必要再去細分未定義 region 的不同類型。
數據修改操作分為寫入或者記錄追加兩種。寫入操作把數據寫在應用程序指定的文件偏移位置上。即使
有多個修改操作並行執行時,記錄追加操作至少可以把數據原子性的追加到文件中一次,但是偏移位置是由
GFS 選擇的(3.3 章) 。GFS 返回給客戶端一個偏移量,表示了包含了寫入記錄的、已定義的 region 的起點。
另外,GFS 可能會在文件中間插入填充數據或者重複記錄。這些數據佔據的文件 region 被認定是不一致的,
這些數據通常比用戶數據小的多。經過了一系列的成功的修改操作之後,GFS 確保被修改的文件 region 是已定義的,並且包含最後一次修改操作寫入的數據。GFS 通過以下措施確保上述行為:
(a) 對 Chunk 的所有副本的修改操作順序一致(3.1章),
(b)使用 Chunk 的版本號來檢測副本是否因為它所在的 Chunk 服務器宕機(4.5 章)而錯過了修改操作
而導致其失效。
失效的副本不會再進行任何修改操作,Master 服務器也不再返回這個 Chunk 副本的位置信息給客戶端。它們會被垃圾收集系統儘快回收。由於 Chunk 位置信息會被客戶端緩存,所以在信息刷新前,客戶端有可能從一個失效的副本讀取了數據。在緩存的超時時間和文件下一次被打開的時間之間存在一個時間窗,文件再次被打開後會清除緩存中與該文件有關的所有 Chunk 位置信息。而且,由於我們的文件大多數都是只進行追加操作的,所以,一個失效的副本通常返回一個提前結束的 Chunk 而不是過期的數據。當一個 Reader 16 重新嘗試並聯絡 Master 服務器時,它就會立刻得到最新的 Chunk 位置信息。
即使在修改操作成功執行很長時間之後,組件的失效也可能損壞或者刪除數據。GFS 通過 Master 服務器和所有 Chunk 服務器的定期「握手」來找到失效的 Chunk 服務器,並且使用 Checksum 來校驗數據是否損壞(5.2 章)。一旦發現問題,數據要儘快利用有效的副本進行恢復(4.3 章)。只有當一個 Chunk 的所有副本在 GFS 檢測到錯誤並採取應對措施之前全部丟失,這個 Chunk 才會不可逆轉的丟失。在一般情況下 GFS 的反應時間是幾分鐘。即使在這種情況下,Chunk 也只是不可用了,而不是損壞了:應用程序會收到明確的錯誤信息而不是損壞的數據。
2.7.2 程序的實現
使用 GFS 的應用程序可以利用一些簡單技術實現這個寬鬆的一致性模型,這些技術也用來實現一些其它的目標功能,包括:盡量採用追加寫入而不是覆蓋,Checkpoint,自驗證的寫入操作,自標識的記錄。
在實際應用中,我們所有的應用程序對文件的寫入操作都是盡量採用數據追加方式,而不是覆蓋方式。
一種典型的應用,應用程序從頭到尾寫入數據,生成了一個文件。寫入所有數據之後,應用程序自動將文件改名為一個永久保存的文件名,或者周期性的作 Checkpoint,記錄成功寫入了多少數據。Checkpoint 文件可以包含程序級別的校驗和。Readers 僅校驗並處理上個 Checkpoint 之後產生的文件 region,這些文件 region 的狀態一定是已定義的。這個方法滿足了我們一致性和並發處理的要求。追加寫入比隨機位置寫入更加有效率,對應用程序的失敗處理更具有彈性。Checkpoint 可以讓 Writer 以漸進的方式重新開始,並且可以防止 Reader處理已經被成功寫入,但是從應用程序的角度來看還並未完成的數據。
我們再來分析另一種典型的應用。許多應用程序並行的追加數據到同一個文件,比如進行結果的合併或者是一個生產者-消費者隊列。記錄追加方式的「至少一次追加」的特性保證了 Writer 的輸出。Readers 使用下面的方法來處理偶然性的填充數據和重複內容。Writers 在每條寫入的記錄中都包含了額外的信息,例如Checksum,用來驗證它的有效性。Reader 可以利用 Checksum 識別和拋棄額外的填充數據和記錄片段。如果應用不能容忍偶爾的重複內容(比如,如果這些重複數據觸發了非冪等操作),可以用記錄的唯一標識符來過濾它們,這些唯一標識符通常用於命名程序中處理的實體對象,例如 web 文檔。這些記錄 I/O 功能都包含在我們的程序共享的庫中,並且適用於 Google 內部的其它的文件接口實現。所以,相同序列的記錄,加上一些偶爾出現的重複數據,都被分發到 Reader 了。
3 系統交互
我們在設計這個系統時,一個重要的原則是最小化所有操作和 Master 節點的交互。帶着這樣的設計理念,我們現在描述一下客戶機、Master 服務器和 Chunk 服務器如何進行交互,以實現數據修改操作、原子的記錄追加操作以及快照功能。
3.1 租約(lease) 和變更順序
變更是一個會改變 Chunk 內容或者元數據的操作,比如寫入操作或者記錄追加操作。變更操作會在 Chunk
的所有副本上執行。我們使用租約(lease)機制來保持多個副本間變更順序的一致性。Master 節點為 Chunk的一個副本建立一個租約,我們把這個副本叫做主 Chunk。主 Chunk 對 Chunk 的所有更改操作進行序列化。
所有的副本都遵從這個序列進行修改操作。因此,修改操作全局的順序首先由 Master 節點選擇的租約的順序
決定,然後由租約中主 Chunk 分配的序列號決定。
設計租約機制的目的是為了最小化 Master 節點的管理負擔。租約的初始超時設置為 60 秒。不過,只要
Chunk 被修改了,主 Chunk 就可以申請更長的租期,通常會得到 Master 節點的確認並收到租約延長的時間。
這些租約延長請求和批准的信息通常都是附加在 Master 節點和 Chunk 服務器之間的心跳消息中來傳遞。有時
Master 節點會試圖提前取消租約(例如,Master 節點想取消在一個已經被改名的文件上的修改操作)。即使
Master節點和主Chunk失去聯繫,它仍然可以安全地在舊的租約到期後和另外一個Chunk副本簽訂新的租約。
在圖 2 中,我們依據步驟編號,展現寫入操作的控制流程。
客戶機向 Master 節點詢問哪一個 Chunk 服務器持有當前的租約,以及其它副本的位置。如果沒有一個
Chunk 持有租約,Master 節點就選擇其中一個副本建立一個租約(這個步驟在圖上沒有顯示)。
Master 節點將主 Chunk 的標識符以及其它副本(又稱為 secondary 副本、二級副本)的位置返回給客戶機。客戶機緩存這些數據以便後續的操作。只有在主 Chunk 不可用,或者主 Chunk 回複信息表明它已不再持有租約的時候,客戶機才需要重新跟 Master 節點聯繫。
客戶機把數據推送到所有的副本上。客戶機可以以任意的順序推送數據。Chunk 服務器接收到數據並保存在它的內部 LRU 緩存中,一直到數據被使用或者過期交換出去。由於數據流的網絡傳輸負載非常高,通過分離數據流和控制流,我們可以基於網絡拓撲情況對數據流進行規劃,提高系統性能,而不用去理會哪個Chunk 服務器保存了主 Chunk。3.2 章節會進一步討論這點。
當所有的副本都確認接收到了數據,客戶機發送寫請求到主 Chunk 服務器。這個請求標識了早前推送到所有副本的數據。主 Chunk 為接收到的所有操作分配連續的序列號,這些操作可能來自不同的客戶機,序列號保證了操作順序執行。它以序列號的順序把操作應用到它自己的本地狀態中(alex 註:也就是在本地執行這些操作,這句話按字面翻譯有點費解,也許應該翻譯為「它順序執行這些操作,並更新自己的狀態」)。主 Chunk 把寫請求傳遞到所有的二級副本。每個二級副本依照主 Chunk 分配的序列號以相同的順序執行這些操作。
所有的二級副本回復主 Chunk,它們已經完成了操作。
主 Chunk 服務器 20 回復客戶機。任何副本產生的任何錯誤都會返回給客戶機。在出現錯誤的情況下,寫
入操作可能在主 Chunk 和一些二級副本執行成功。(如果操作在主 Chunk 上失敗了,操作就不會被分配序列
號,也不會被傳遞。)客戶端的請求被確認為失敗,被修改的 region 處於不一致的狀態。我們的客戶機代碼通
過重複執行失敗的操作來處理這樣的錯誤。在從頭開始重複執行之前,客戶機會先從步驟(3)到步驟(7)做幾次嘗試。
如果應用程序一次寫入的數據量很大,或者數據跨越了多個 Chunk,GFS 客戶機代碼會把它們分成多個寫操作。這些操作都遵循前面描述的控制流程,但是可能會被其它客戶機上同時進行的操作打斷或者覆蓋。因此,共享的文件 region 的尾部可能包含來自不同客戶機的數據片段,儘管如此,由於這些分解後的寫入操作在所有的副本上都以相同的順序執行完成,Chunk 的所有副本都是一致的。這使文件 region 處於 2.7 節描述的一致的、但是未定義的狀態。
3.2 數據流
為了提高網絡效率,我們採取了把數據流和控制流分開的措施。在控制流從客戶機到主 Chunk、然後再到所有二級副本的同時,數據以管道的方式,順序的沿着一個精心選擇的 Chunk 服務器鏈推送。我們的目標是充分利用每台機器的帶寬,避免網絡瓶頸和高延時的連接,最小化推送所有數據的延時。
為了充分利用每台機器的帶寬,數據沿着一個 Chunk 服務器鏈順序的推送,而不是以其它拓撲形式分散推送(例如,樹型拓撲結構)。線性推送模式下,每台機器所有的出口帶寬都用於以最快的速度傳輸數據,而不是在多個接受者之間分配帶寬。
為了儘可能的避免出現網絡瓶頸和高延遲的鏈接(eg,inter-switch 最有可能出現類似問題),每台機器都盡量的在網絡拓撲中選擇一台還沒有接收到數據的、離自己最近的機器作為目標推送數據。假設客戶機把數據從 Chunk 服務器 S1 推送到 S4。它把數據推送到最近的 Chunk 服務器 S1。S1 把數據推送到 S2,因為 S2和 S4 中最接近的機器是 S2。同樣的,S2 把數據傳遞給 S3 和 S4 之間更近的機器,依次類推推送下去。我們的網絡拓撲非常簡單,通過 IP 地址就可以計算出節點的「距離」。
最後,我們利用基於 TCP 連接的、管道式數據推送方式來最小化延遲。Chunk 服務器接收到數據後,馬上開始向前推送。管道方式的數據推送對我們幫助很大,因為我們採用全雙工的交換網絡。接收到數據後立刻向前推送不會降低接收的速度。在沒有網絡擁塞的情況下,傳送 B 位元組的數據到 R 個副本的理想時間是B/T+RL ,T 是網絡的吞吐量,L 是在兩台機器數據傳輸的延遲。通常情況下,我們的網絡連接速度是 100Mbps(T),L 將遠小於 1ms。因此,1MB 的數據在理想情況下 80ms 左右就能分發出去。
3.3 原子的記錄追加
GFS 提供了一種原子的數據追加操作–記錄追加。傳統方式的寫入操作,客戶程序會指定數據寫入的偏移量。對同一個 region 的並行寫入操作不是串行的:region 尾部可能會包含多個不同客戶機寫入的數據片段。使用記錄追加,客戶機只需要指定要寫入的數據。GFS 保證至少有一次原子的寫入操作成功執行(即寫入一個順序的 byte 流),寫入的數據追加到 GFS 指定的偏移位置上,之後 GFS 返回這個偏移量給客戶機。這類似於在 Unix 操作系統編程環境中,對以 O_APPEND 模式打開的文件,多個並發寫操作在沒有競態條件時的行為。
記錄追加在我們的分佈應用中非常頻繁的使用,在這些分佈式應用中,通常有很多的客戶機並行地對同一個文件追加寫入數據。如果我們採用傳統方式的文件寫入操作,客戶機需要額外的複雜、昂貴的同步機制,例如使用一個分佈式的鎖管理器。在我們的工作中,這樣的文件通常用於多個生產者/單一消費者的隊列系統,或者是合併了來自多個客戶機的數據的結果文件。
記錄追加是一種修改操作,它也遵循 3.1 節描述的控制流程,除了在主 Chunk 有些額外的控制邏輯。客戶機把數據推送給文件最後一個 Chunk 的所有副本,之後發送請求給主 Chunk。主 Chunk 會檢查這次記錄追加操作是否會使 Chunk 超過最大尺寸(64MB)。如果超過了最大尺寸,主 Chunk 首先將當前 Chunk 填充到最大尺寸,之後通知所有二級副本做同樣的操作,然後回復客戶機要求其對下一個 Chunk 重新進行記錄追加操作。(記錄追加的數據大小嚴格控制在 Chunk 最大尺寸的 1/4,這樣即使在最壞情況下,數據碎片的數量仍然在可控的範圍。)通常情況下追加的記錄不超過 Chunk 的最大尺寸,主 Chunk 把數據追加到自己的副本內,然後通知二級副本把數據寫在跟主 Chunk 一樣的位置上,最後回復客戶機操作成功。
如果記錄追加操作在任何一個副本上失敗了,客戶端就需要重新進行操作。重新進行記錄追加的結果是,同一個Chunk的不同副本可能包含不同的數據–重複包含一個記錄全部或者部分的數據。GFS並不保證Chunk的所有副本在位元組級別是完全一致的。它只保證數據作為一個整體原子的被至少寫入一次。這個特性可以通過簡單觀察推導出來:如果操作成功執行,數據一定已經寫入到 Chunk 的所有副本的相同偏移位置上。這之後,所有的副本至少都到了記錄尾部的長度,任何後續的記錄都會追加到更大的偏移地址,或者是不同的Chunk 上,即使其它的 Chunk 副本被 Master 節點選為了主 Chunk。就我們的一致性保障模型而言,記錄追加操作成功寫入數據的region 是已定義的(因此也是一致的),反之則是不一致的(因此也就是未定義的)。正如我們在 2.7.2 節討論的,我們的程序可以處理不一致的區域。
3.4 快照
快照操作幾乎可以瞬間完成對一個文件或者目錄樹(「源」)做一個拷貝,並且幾乎不會對正在進行的其它操作造成任何干擾。我們的用戶可以使用快照迅速的創建一個巨大的數據集的分支拷貝(而且經常是遞歸的拷貝拷貝),或者是在做實驗性的數據操作之前,使用快照操作備份當前狀態,這樣之後就可以輕鬆的提交或者回滾到備份時的狀態。
就像 AFS(alex 註:AFS,即 Andrew File System,一種分佈式文件系統),我們用標準的 copy-on-write技術實現快照。當 Master 節點收到一個快照請求,它首先取消作快照的文件的所有 Chunk 的租約。這個措施保證了後續對這些 Chunk 的寫操作都必須與 Master 交互以找到租約持有者。這就給 Master 節點一個率先創建
Chunk 的新拷貝的機會。
租約取消或者過期之後,Master 節點把這個操作以日誌的方式記錄到硬盤上。然後,Master 節點通過複製源文件或者目錄的元數據的方式,把這條日誌記錄的變化反映到保存在內存的狀態中。新創建的快照文件和源文件指向完全相同的 Chunk 地址。
在快照操作之後,當客戶機第一次想寫入數據到 Chunk C,它首先會發送一個請求到 Master 節點查詢當前的租約持有者。Master節點注意到Chunk C的引用計數超過了1 22 。Master節點不會馬上回復客戶機的請求,
而是選擇一個新的 Chunk 句柄 C。之後,Master 節點要求每個擁有 Chunk C 當前副本的 Chunk 服務器創建一 個叫做 C
的新 Chunk。通過在源 Chunk 所在 Chunk 服務器上創建新的 Chunk,我們確保數據在本地而不是通
過網絡複製(我們的硬盤比我們的 100Mb 以太網大約快 3 倍)。從這點來講,請求的處理方式和任何其它 Chunk沒什麼不同:Master 節點確保新 Chunk C`的一個副本擁有租約,之後回復客戶機,客戶機得到回復後就可以正常的寫這個 Chunk,而不必理會它是從一個已存在的 Chunk 克隆出來的。
4 Master 節點的操作
Master 節點執行所有的名稱空間操作。此外,它還管理着整個系統里所有 Chunk 的副本:它決定 Chunk的存儲位置,創建新 Chunk 和它的副本,協調各種各樣的系統活動以保證 Chunk 被完全複製,在所有的 Chunk服務器之間的進行負載均衡,回收不再使用的存儲空間。本節我們討論上述的主題。
4.1 名稱空間管理和鎖
Master 節點的很多操作會花費很長的時間:比如,快照操作必須取消 Chunk 服務器上快照所涉及的所有的 Chunk 的租約。我們不希望在這些操作的運行時,延緩了其它的 Master 節點的操作。因此,我們允許多個
操作同時進行,使用名稱空間的 region 上的鎖來保證執行的正確順序。
不同於許多傳統文件系統,GFS 沒有針對每個目錄實現能夠列出目錄下所有文件的數據結構。GFS 也不
支持文件或者目錄的鏈接(即 Unix 術語中的硬鏈接或者符號鏈接)。在邏輯上,GFS 的名稱空間就是一個全
路徑和元數據映射關係的查找表。利用前綴壓縮,這個表可以高效的存儲在內存中。在存儲名稱空間的樹型
結構上,每個節點(絕對路徑的文件名或絕對路徑的目錄名)都有一個關聯的讀寫鎖。
每個 Master 節點的操作在開始之前都要獲得一系列的鎖。通常情況下,如果一個操作涉及/d1/d2/…
/dn/leaf,那麼操作首先要獲得目錄/d1,/d1/d2,…,/d1/d2/…/dn 的讀鎖,以及/d1/d2/…/dn/leaf 的讀寫鎖。注意,根據操作的不同,leaf 可以是一個文件,也可以是一個目錄。
現在,我們演示一下在/home/user 被快照到/save/user 的時候,鎖機制如何防止創建文件/home/user/foo。
快照操作獲取/home 和/save 的讀取鎖,以及/home/user 和/save/user 的寫入鎖。文件創建操作獲得/home 和/home/user 的讀取鎖,以及/home/user/foo 的寫入鎖。這兩個操作要順序執行,因為它們試圖獲取的/home/user的鎖是相互衝突。文件創建操作不需要獲取父目錄的寫入鎖,因為這裡沒有「目錄」,或者類似 inode 等用來禁止修改的數據結構。文件名的讀取鎖足以防止父目錄被刪除。
採用這種鎖方案的優點是支持對同一目錄的並行操作。比如,可以再同一個目錄下同時創建多個文件:
每一個操作都獲取一個目錄名的上的讀取鎖和文件名上的寫入鎖。目錄名的讀取鎖足以的防止目錄被刪除、
改名以及被快照。文件名的寫入鎖序列化文件創建操作,確保不會多次創建同名的文件。
因為名稱空間可能有很多節點,讀寫鎖採用惰性分配策略,在不再使用的時候立刻被刪除。同樣,鎖的
獲取也要依據一個全局一致的順序來避免死鎖:首先按名稱空間的層次排序,在同一個層次內按字典順序排
序。
4.2 副本的位置
GFS 集群是高度分佈的多層布局結構,而不是平面結構。典型的拓撲結構是有數百個 Chunk 服務器安裝
在許多機架上。Chunk 服務器被來自同一或者不同機架上的數百個客戶機輪流訪問。不同機架上的兩台機器
間的通訊可能跨越一個或多個網絡交換機。另外,機架的出入帶寬可能比機架內所有機器加和在一起的帶寬
要小。多層分佈架構對數據的靈活性、可靠性以及可用性方面提出特有的挑戰。
Chunk 副本位置選擇的策略服務兩大目標:最大化數據可靠性和可用性,最大化網絡帶寬利用率。為了
實現這兩個目的,僅僅是在多台機器上分別存儲這些副本是不夠的,這隻能預防硬盤損壞或者機器失效帶來的影響,以及最大化每台機器的網絡帶寬利用率。我們必須在多個機架間分佈儲存Chunk的副本。這保證Chunk
的一些副本在整個機架被破壞或掉線(比如,共享資源,如電源或者網絡交換機造成的問題)的情況下依然
存在且保持可用狀態。這還意味着在網絡流量方面,尤其是針對 Chunk 的讀操作,能夠有效利用多個機架的
整合帶寬。另一方面,寫操作必須和多個機架上的設備進行網絡通信,但是這個代價是我們願意付出的。
4.3 創建,重新複製,重新負載均衡
Chunk 的副本有三個用途:Chunk 創建,重新複製和重新負載均衡。
當 Master 節點創建一個 Chunk 時,它會選擇在哪裡放置初始的空的副本。Master 節點會考慮幾個因素
(1)我們希望在低於平均硬盤使用率的 Chunk 服務器上存儲新的副本。這樣的做法最終能夠平衡 Chunk
服務器之間的硬盤使用率。
(2)我們希望限制在每個 Chunk 服務器上「最近」的 Chunk 創建操作的次數。雖然創建操作本身是廉
價的,但是創建操作也意味着隨之會有大量的寫入數據的操作,因為 Chunk 在 Writer 真正寫入數據的時候才
被創建,而在我們的「追加一次,讀取多次」的工作模式下,Chunk 一旦寫入成功之後就會變為只讀的了。
(3)如上所述,我們希望把 Chunk 的副本分佈在多個機架之間。
當 Chunk 的有效副本數量少於用戶指定的複製因數的時候,Master 節點會重新複製它。這可能是由幾個
原因引起的:一個 Chunk 服務器不可用了,Chunk 服務器報告它所存儲的一個副本損壞了,Chunk 服務器的
一個磁盤因為錯誤不可用了,或者 Chunk 副本的複製因數提高了。每個需要被重新複製的 Chunk 都會根據幾
個因素進行排序。一個因素是 Chunk 現有副本數量和複製因數相差多少。例如,丟失兩個副本的 Chunk 比丟
失一個副本的 Chunk 有更高的優先級。另外,我們優先重新複製活躍(live)文件的 Chunk 而不是最近剛被
刪除的文件的 Chunk(查看 4.4 節)。最後,為了最小化失效的 Chunk 對正在運行的應用程序的影響,我們提
高會阻塞客戶機程序處理流程的 Chunk 的優先級。
Master 節點選擇優先級最高的 Chunk,然後命令某個 Chunk 服務器直接從可用的副本」克隆」一個副本
出來。選擇新副本的位置的策略和創建時類似:平衡硬盤使用率、限制同一台 Chunk 服務器上的正在進行的
克隆操作的數量、在機架間分佈副本。為了防止克隆產生的網絡流量大大超過客戶機的流量,Master 節點對
整個集群和每個 Chunk 服務器上的同時進行的克隆操作的數量都進行了限制。另外,Chunk 服務器通過調節
它對源 Chunk 服務器讀請求的頻率來限制它用於克隆操作的帶寬。
最後,Master 服務器周期性地對副本進行重新負載均衡:它檢查當前的副本分佈情況,然後移動副本以便更好的利用硬盤空間、更有效的進行負載均衡。而且在這個過程中,Master服務器逐漸的填滿一個新的Chunk服務器,而不是在短時間內用新的 Chunk 填滿它,以至於過載。新副本的存儲位置選擇策略和上面討論的相同。另外,Master 節點必須選擇哪個副本要被移走。通常情況,Master 節點移走那些剩餘空間低於平均值的Chunk 服務器上的副本,從而平衡系統整體的硬盤使用率。
4.4 垃圾回收
GFS 在文件刪除後不會立刻回收可用的物理空間。GFS 空間回收採用惰性的策略,只在文件和 Chunk 級
的常規垃圾收集時進行。我們發現這個方法使系統更簡單、更可靠。
4.4.1 機制
當一個文件被應用程序刪除時,Master 節點象對待其它修改操作一樣,立刻把刪除操作以日誌的方式記
錄下來。但是,Master 節點並不馬上回收資源,而是把文件名改為一個包含刪除時間戳的、隱藏的名字。當
Master 節點對文件系統命名空間做常規掃描的時候,它會刪除所有三天前的隱藏文件(這個時間間隔是可以
設置的)。直到文件被真正刪除,它們仍舊可以用新的特殊的名字讀取,也可以通過把隱藏文件改名為正常顯
示的文件名的方式「反刪除」。當隱藏文件被從名稱空間中刪除,Master 服務器內存中保存的這個文件的相關
元數據才會被刪除。這也有效的切斷了文件和它包含的所有 Chunk 的連接。
在對 Chunk 名字空間做類似的常規掃描時,Master 節點找到孤兒 Chunk(不被任何文件包含的 Chunk)
並刪除它們的元數據。Chunk 服務器在和 Master 節點交互的心跳信息中,報告它擁有的 Chunk 子集的信息,
Master 節點回復 Chunk 服務器哪些 Chunk 在 Master 節點保存的元數據中已經不存在了。Chunk 服務器可以任
意刪除這些 Chunk 的副本。
4.4.2 討論
雖然分佈式垃圾回收在編程語言領域是一個需要複雜的方案才能解決的難題,但是在 GFS 系統中是非常
簡單的。我們可以輕易的得到 Chunk 的所有引用:它們都只存儲在 Master 服務器上的文件到塊的映射表中。
我們也可以很輕易的得到所有Chunk的副本:它們都以Linux文件的形式存儲在Chunk服務器的指定目錄下。
所有 Master 節點不能識別的副本都是「垃圾」。
垃圾回收在空間回收方面相比直接刪除有幾個優勢。首先,對於組件失效是常態的大規模分佈式系統,
垃圾回收方式簡單可靠。Chunk 可能在某些 Chunk 服務器創建成功,某些 Chunk 服務器上創建失敗,失敗的
副本處於無法被 Master 節點識別的狀態。副本刪除消息可能丟失,Master 節點必須重新發送失敗的刪除消息,
包括自身的和 Chunk 服務器的 24 。垃圾回收提供了一致的、可靠的清除無用副本的方法。第二,垃圾回收把
存儲空間的回收操作合併到 Master 節點規律性的後台活動中,比如,例行掃描和與 Chunk 服務器握手等。因
此,操作被批量的執行,開銷會被分散。另外,垃圾回收在 Master 節點相對空閑的時候完成。這樣 Master節點就可以給那些需要快速反應的客戶機請求提供更快捷的響應。第三,延緩存儲空間回收為意外的、不可
逆轉的刪除操作提供了安全保障。
根據我們的使用經驗,延遲回收空間的主要問題是,延遲回收會阻礙用戶調優存儲空間的使用,特別是
當存儲空間比較緊缺的時候。當應用程序重複創建和刪除臨時文件時,釋放的存儲空間不能馬上重用。我們
通過顯式的再次刪除一個已經被刪除的文件的方式加速空間回收的速度。我們允許用戶為命名空間的不同部
分設定不同的複製和回收策略。例如,用戶可以指定某些目錄樹下面的文件不做複製,刪除的文件被即時的、
不可恢復的從文件系統移除。
4.5 過期失效的副本檢測
當 Chunk 服務器失效時,Chunk 的副本有可能因錯失了一些修改操作而過期失效。Master 節點保存了每
個 Chunk 的版本號,用來區分當前的副本和過期副本。
無論何時,只要 Master 節點和 Chunk 簽訂一個新的租約,它就增加 Chunk 的版本號,然後通知最新的
副本。Master 節點和這些副本都把新的版本號記錄在它們持久化存儲的狀態信息中。這個動作發生在任何客
戶機得到通知以前,因此也是對這個 Chunk 開始寫之前。如果某個副本所在的 Chunk 服務器正好處於失效狀
態,那麼副本的版本號就不會被增加。Master 節點在這個 Chunk 服務器重新啟動,並且向 Master 節點報告它
擁有的 Chunk 的集合以及相應的版本號的時候,就會檢測出它包含過期的 Chunk。如果 Master 節點看到一個
比它記錄的版本號更高的版本號,Master 節點會認為它和 Chunk 服務器簽訂租約的操作失敗了,因此會選擇
更高的版本號作為當前的版本號。
Master 節點在例行的垃圾回收過程中移除所有的過期失效副本。在此之前,Master 節點在回復客戶機的
Chunk 信息請求的時候,簡單的認為那些過期的塊根本就不存在。另外一重保障措施是,Master 節點在通知
客戶機哪個 Chunk 服務器持有租約、或者指示 Chunk 服務器從哪個 Chunk 服務器進行克隆時,消息中都附帶
了 Chunk 的版本號。客戶機或者 Chunk 服務器在執行操作時都會驗證版本號以確保總是訪問當前版本的數據。
5 容錯和診斷
我們在設計 GFS 時遇到的最大挑戰之一是如何處理頻繁發生的組件失效。組件的數量和質量讓這些問題
出現的頻率遠遠超過一般系統意外發生的頻率:我們不能完全依賴機器的穩定性,也不能完全相信硬盤的可
靠性。組件的失效可能造成系統不可用,更糟糕的是,還可能產生不完整的數據。我們討論我們如何面對這
些挑戰,以及當組件失效不可避免的發生時,用 GFS 自帶工具診斷系統故障。
5.1 高可用性
在 GFS 集群的數百個服務器之中,在任何給定的時間必定會有些服務器是不可用的。我們使用兩條簡單但是有效的策略保證整個系統的高可用性:快速恢復和複製。
5.1.1 快速恢復
不管 Master 服務器和 Chunk 服務器是如何關閉的,它們都被設計為可以在數秒鐘內恢復它們的狀態並重
新啟動。事實上,我們並不區分正常關閉和異常關閉;通常,我們通過直接 kill 掉進程來關閉服務器。客戶
機和其它的服務器會感覺到系統有點顛簸 25 ,正在發出的請求會超時,需要重新連接到重啟後的服務器,然後
重試這個請求。6.6.2 章節記錄了實測的啟動時間。
5.1.2 Chunk 複製
正如之前討論的,每個 Chunk 都被複制到不同機架上的不同的 Chunk 服務器上。用戶可以為文件命名空
間的不同部分設定不同的複製級別。缺省是 3。當有 Chunk 服務器離線了,或者通過 Chksum 校驗(參考 5.2
節)發現了已經損壞的數據,Master 節點通過克隆已有的副本保證每個 Chunk 都被完整複製 26 。雖然 Chunk
複製策略對我們非常有效,但是我們也在尋找其它形式的跨服務器的冗餘解決方案,比如使用奇偶校驗、或
者 Erasure codes 27 來解決我們日益增長的只讀存儲需求。我們的系統主要的工作負載是追加方式的寫入和讀取
操作,很少有隨機的寫入操作,因此,我們認為在我們這個高度解耦合的系統架構下實現這些複雜的冗餘方
案很有挑戰性,但並非不可實現。
5.1.3 Master 服務器的複製
為了保證 Master 服務器的可靠性,Master 服務器的狀態也要複製。Master 服務器所有的操作日誌和
checkpoint 文件都被複制到多台機器上。對 Master 服務器狀態的修改操作能夠提交成功的前提是,操作日誌
寫入到 Master 服務器的備節點和本機的磁盤。簡單說來,一個 Master 服務進程負責所有的修改操作,包括後
台的服務,比如垃圾回收等改變系統內部狀態活動。當它失效的時,幾乎可以立刻重新啟動。如果 Master 進
程所在的機器或者磁盤失效了,處於 GFS 系統外部的監控進程會在其它的存有完整操作日誌的機器上啟動一
個新的 Master 進程。客戶端使用規範的名字訪問 Master(比如 gfs-test)節點,這個名字類似 DNS 別名,因
此也就可以在 Master 進程轉到別的機器上執行時,通過更改別名的實際指向訪問新的 Master 節點。
此外,GFS 中還有些「影子」Master 服務器,這些「影子」服務器在「主」Master 服務器宕機的時候提
供文件系統的只讀訪問。它們是影子,而不是鏡像,所以它們的數據可能比「主」Master 服務器更新要慢,
通常是不到 1 秒。對於那些不經常改變的文件、或者那些允許獲取的數據有少量過期的應用程序,「影子」Master 服務器能夠提高讀取的效率。事實上,因為文件內容是從 Chunk 服務器上讀取的,因此,應用程序不
會發現過期的文件內容。在這個短暫的時間窗內,過期的可能是文件的元數據,比如目錄的內容或者訪問控
制信息。
「影子」Master 服務器為了保持自身狀態是最新的,它會讀取一份當前正在進行的操作的日誌副本,並
且依照和主 Master 服務器完全相同的順序來更改內部的數據結構。和主 Master 服務器一樣,「影子」Master
服務器在啟動的時候也會從 Chunk 服務器輪詢數據(之後定期拉數據),數據中包括了 Chunk 副本的位置信
息;「影子」Master 服務器也會定期和 Chunk 服務器「握手」來確定它們的狀態。在主 Master 服務器因創建
和刪除副本導致副本位置信息更新時,「影子」Master 服務器才和主 Master 服務器通信來更新自身狀態。
5.2 數據完整性
每個 Chunk 服務器都使用 Checksum 來檢查保存的數據是否損壞。考慮到一個 GFS 集群通常都有好幾百
台機器、幾千塊硬盤,磁盤損壞導致數據在讀寫過程中損壞或者丟失是非常常見的(第 7 節講了一個原因)。
我們可以通過別的 Chunk 副本來解決數據損壞問題,但是跨越 Chunk 服務器比較副本來檢查數據是否損壞很
不實際。另外,GFS 允許有歧義的副本存在:GFS 修改操作的語義,特別是早先討論過的原子紀錄追加的操
作,並不保證副本完全相同(alex 註:副本不是 byte-wise 完全一致的)。因此,每個 Chunk 服務器必須獨立維
護 Checksum 來校驗自己的副本的完整性。
我們把每個 Chunk 都分成 64KB 大小的塊。每個塊都對應一個 32 位的 Checksum。和其它元數據一樣,
Checksum 與其它的用戶數據是分開的,並且保存在內存和硬盤上,同時也記錄操作日誌。
對於讀操作來說,在把數據返回給客戶端或者其它的 Chunk 服務器之前,Chunk 服務器會校驗讀取操作
涉及的範圍內的塊的 Checksum。因此 Chunk 服務器不會把錯誤數據傳遞到其它的機器上。如果發生某個塊的
Checksum 不正確,Chunk 服務器返回給請求者一個錯誤信息,並且通知 Master 服務器這個錯誤。作為回應,
請求者應當從其它副本讀取數據,Master 服務器也會從其它副本克隆數據進行恢復。當一個新的副本就緒後,
Master 服務器通知副本錯誤的 Chunk 服務器刪掉錯誤的副本。
Checksum 對讀操作的性能影響很小,可以基於幾個原因來分析一下。因為大部分的讀操作都至少要讀取
幾個塊,而我們只需要讀取一小部分額外的相關數據進行校驗。GFS 客戶端代碼通過每次把讀取操作都對齊
在 Checksum block 的邊界上,進一步減少了這些額外的讀取操作的負面影響。另外,在 Chunk 服務器上,
Checksum 的查找和比較不需要 I/O 操作,Checksum 的計算可以和 I/O 操作同時進行。
Checksum 的計算針對在 Chunk 尾部的追加寫入操作做了高度優化(與之對應的是覆蓋現有數據的寫入操
作),因為這類操作在我們的工作中佔了很大比例。我們只增量更新最後一個不完整的塊的 Checksum,並且
用所有的追加來的新 Checksum塊來計算新的 Checksum。即使是最後一個不完整的 Checksum 塊已經損壞了,而且我們不能夠馬上檢查出來,由於新的 Checksum 和已有數據不吻合,在下次對這個塊進行讀取操作的時候,
會檢查出數據已經損壞了。
相比之下,如果寫操作覆蓋已經存在的一個範圍內的 Chunk,我們必須讀取和校驗被覆蓋的第一個和最
後一個塊,然後再執行寫操作;操作完成之後再重新計算和寫入新的 Checksum。如果我們不校驗第一個和最
後一個被寫的塊,那麼新的 Checksum 可能會隱藏沒有被覆蓋區域內的數據錯誤。
在 Chunk 服務器空閑的時候,它會掃描和校驗每個不活動的 Chunk 的內容。這使得我們能夠發現很少被
讀取的 Chunk 是否完整。一旦發現有 Chunk 的數據損壞,Master 可以創建一個新的、正確的副本,然後把損
壞的副本刪除掉。這個機制也避免了非活動的、已損壞的 Chunk 欺騙 Master 節點,使 Master 節點認為它們已
經有了足夠多的副本了。
5.3 診斷工具
詳盡的、深入細節的診斷日誌,在問題隔離、調試、以及性能分析等方面給我們帶來無法估量的幫助,
同時也只需要很小的開銷。沒有日誌的幫助,我們很難理解短暫的、不重複的機器之間的消息交互。GFS 的
服務器會產生大量的日誌,記錄了大量關鍵的事件(比如,Chunk 服務器啟動和關閉)以及所有的 RPC 的請
求和回復。這些診斷日誌可以隨意刪除,對系統的正確運行不造成任何影響。然而,我們在存儲空間允許的
情況下會盡量的保存這些日誌。
RPC 日誌包含了網絡上發生的所有請求和響應的詳細記錄,但是不包括讀寫的文件數據。通過匹配請求
與回應,以及收集不同機器上的 RPC 日誌記錄,我們可以重演所有的消息交互來診斷問題。日誌還用來跟蹤
負載測試和性能分析。
日誌對性能的影響很小(遠小於它帶來的好處),因為這些日誌的寫入方式是順序的、異步的。最近發生
的事件日誌保存在內存中,可用於持續不斷的在線監控。
6 度量
本節中,我們將使用一些小規模基準測試來展現 GFS 系統架構和實現上的一些固有瓶頸,還有些來自
Google 內部使用的真實的 GFS 集群的基準數據。
6.1 小規模基準測試
我們在一個包含 1 台 Master 服務器,2 台 Master 服務器複製節點,16 台 Chunk 服務器和 16 個客戶機組
成的 GFS 集群上測量性能。注意,採用這樣的集群配置方案只是為了易於測試。典型的 GFS 集群有數百個
Chunk 服務器和數百個客戶機。
所有機器的配置都一樣:兩個 PIII 1.4GHz 處理器,2GB 內存,兩個 80G/5400rpm 的硬盤,以及 100Mbps全雙工以太網連接到一個 HP2524 交換機。GFS 集群中所有的 19 台服務器都連接在一個交換機,所有 16 台
客戶機連接到另一個交換機上。兩個交換機之間使用 1Gbps 的線路連接。
6.1.1 讀取
N 個客戶機從 GFS 文件系統同步讀取數據。每個客戶機從 320GB 的文件集合中隨機讀取 4MB region 的
內容。讀取操作重複執行 256 次,因此,每個客戶機最終都讀取 1GB 的數據。所有的 Chunk 服務器加起來總
共只有 32GB 的內存,因此,我們預期只有最多 10%的讀取請求命中 Linux 的文件系統緩衝。我們的測試結
果應該和一個在沒有文件系統緩存的情況下讀取測試的結果接近。
上邊的曲線顯示了我們網絡拓撲下的合計理論吞吐量上限。下邊的曲線顯示了觀測到的吞吐量。這個曲
線有着 95%的可靠性,因為有時候測量會不夠精確。
圖 3(a)顯示了 N 個客戶機整體的讀取速度以及這個速度的理論極限。當連接兩個交換機的 1Gbps 的鏈
路飽和時,整體讀取速度達到理論的極限值是125MB/S,或者說每個客戶機配置的100Mbps網卡達到飽和時,
每個客戶機讀取速度的理論極限值是 12.5MB/s。實測結果是,當一個客戶機讀取的時候,讀取的速度是 10MB/s,
也就是說達到客戶機理論讀取速度極限值的 80%。對於 16 個客戶機,整體的讀取速度達到了 94MB/s,大約
是理論整體讀取速度極限值的75%,也就是說每個客戶機的讀取速度是6MB/s。讀取效率從80%降低到了75%,
主要的原因是當讀取的客戶機增加時,多個客戶機同時讀取一個 Chunk 服務器的幾率也增加了,導致整體的
讀取效率下降。
6.1.2 寫入
N 個客戶機同時向 N 個不同的文件中寫入數據。每個客戶機以每次 1MB 的速度連續寫入 1GB 的數據。
圖 3(b)顯示了整體的寫入速度和它們理論上的極限值。理論上的極限值是 67MB/s,因為我們需要把每一
byte 寫入到 16 個 Chunk 服務器中的 3 個上,而每個 Chunk 服務器的輸入連接速度是 12.5MB/s。
一個客戶機的寫入速度是 6.3MB,大概是理論極限值的一半。導致這個結果的主要原因是我們的網絡協
議棧。它與我們推送數據到 Chunk 服務器時採用的管道模式不相適應。從一個副本到另一個副本的數據傳輸
延遲降低了整個的寫入速度。
16 個客戶機整體的寫入速度達到了 35MB/s(即每個客戶機 2.2MB/s),大約只是理論極限值的一半。和
多個客戶機讀取的情形很類型,隨着客戶機數量的增加,多個客戶機同時寫入同一個 Chunk 服務器的幾率也
增加了。而且,16 個客戶機並行寫入可能引起的衝突比 16 個客戶機並行讀取要大得多,因為每個寫入都會
涉及三個不同的副本。
寫入的速度比我們想像的要慢。在實際應用中,這沒有成為我們的主要問題,因為即使在單個客戶機上
能夠感受到延時,它也不會在有大量客戶機的時候對整體的寫入帶寬造成顯著的影響。
6.1.3 記錄追加
圖 3(c)顯示了記錄追加操作的性能。N 個客戶機同時追加數據到一個文件。記錄追加操作的性能受限
於保存文件最後一個 Chunk 的 Chunk 服務器的帶寬,而與客戶機的數量無關。記錄追加的速度由一個客戶機
的 6.0MB/s 開始,下降到 16 個客戶機的 4.8MB/s 為止,速度的下降主要是由於不同客戶端的網絡擁塞以及網
絡傳輸速度的不同而導致的。
我們的程序傾向於同時處理多個這樣的文件。換句話說,即N個客戶機同時追加數據到M個共享文件中,
這裡 N 和 M 都是數十或者數百以上。所以,在我們的實際應用中,Chunk 服務器的網絡擁塞並沒有成為一個
嚴重問題,如果 Chunk 服務器的某個文件正在寫入,客戶機會去寫另外一個文件。
6.2 實際應用中的集群
我們現在來仔細評估一下 Google 內部正在使用的兩個集群,它們具有一定的代表性。集群 A 通常被上百
個工程師用於研究和開發。典型的任務是被人工初始化後連續運行數個小時。它通常讀取數 MB 到數 TB 的
數據,之後進行轉化或者分析,最後把結果寫回到集群中。集群 B 主要用於處理當前的生產數據。集群 B 的
任務持續的時間更長,在很少人工干預的情況下,持續的生成和處理數 TB 的數據集。在這兩個案例中,一
個單獨的「任務」都是指運行在多個機器上的多個進程,它們同時讀取和寫入多個文件。
6.2.1 存儲
如上表前五行所描述的,兩個集群都由上百台 Chunk 服務器組成,支持數 TB 的硬盤空間;兩個集群雖
然都存儲了大量的數據,但是還有剩餘的空間。「已用空間」包含了所有的 Chunk 副本。實際上所有的文件都
複製了三份。因此,集群實際上各存儲了 18TB 和 52TB 的文件數據。
兩個集群存儲的文件數量都差不多,但是集群 B 上有大量的死文件。所謂「死文件」是指文件被刪除了
或者是被新版本的文件替換了,但是存儲空間還沒有來得及被回收。由於集群 B 存儲的文件較大,因此它的
Chunk 數量也比較多。
6.2.2 元數據
Chunk 服務器總共保存了十幾 GB 的元數據,大多數是來自用戶數據的、64KB 大小的塊的 Checksum。
保存在 Chunk 服務器上其它的元數據是 Chunk 的版本號信息,我們在 4.5 節描述過。
在 Master 服務器上保存的元數據就小的多了,大約只有數十 MB,或者說平均每個文件 100 位元組的元數
據。這和我們設想的是一樣的,Master 服務器的內存大小在實際應用中並不會成為 GFS 系統容量的瓶頸。大
多數文件的元數據都是以前綴壓縮模式存放的文件名。Master 服務器上存放的其它元數據包括了文件的所有
者和權限、文件到 Chunk 的映射關係,以及每一個 Chunk 的當前版本號。此外,針對每一個 Chunk,我們都
保存了當前的副本位置以及對它的引用計數,這個引用計數用於實現寫時拷貝(即 COW,copy-on-write)。
對於每一個單獨的服務器,無論是 Chunk 服務器還是 Master 服務器,都只保存了 50MB 到 100MB 的元
數據。因此,恢復服務器是非常快速的:在服務器響應客戶請求之前,只需要花幾秒鐘時間從磁盤上讀取這
些數據就可以了。不過,Master服務器會持續顛簸一段時間–通常是30到60秒–直到它完成輪詢所有的Chunk服務器,並獲取到所有 Chunk 的位置信息。
6.2.3 讀寫速率
表三顯示了不同時段的讀寫速率。在測試的時候,這兩個集群都運行了一周左右的時間。(這兩個集群最
近都因為升級新版本的 GFS 重新啟動過了)。
集群重新啟動後,平均寫入速率小於 30MB/s。當我們提取性能數據的時候,集群 B 正進行大量的寫入操
作,寫入速度達到了 100MB/s,並且因為每個 Chunk 都有三個副本的原因,網絡負載達到了 300MB/s。
讀取速率要比寫入速率高的多。正如我們設想的那樣,總的工作負載中,讀取的比例遠遠高於寫入的比
例。兩個集群都進行着繁重的讀取操作。特別是,集群 A 在一周時間內都維持了 580MB/s 的讀取速度。集群
A的網絡配置可以支持750MB/s的速度,顯然,它有效的利用了資源。集群B支持的峰值讀取速度是1300MB/s,
但是它的應用只用到了 380MB/s。
6.2.4 Master 服務器的負載
表 3 的數據顯示了發送到 Master 服務器的操作請求大概是每秒鐘 200 到 500 個。Master 服務器可以輕鬆
的應付這個請求速度,所以 Master 服務器的處理能力不是系統的瓶頸。
在早期版本的 GFS 中,Master 服務器偶爾會成為瓶頸。它大多數時間裏都在順序掃描某個很大的目錄
(包含數萬個文件)去查找某個特定的文件。因此我們修改了 Master 服務器的數據結構,通過對名字空間進
行二分查找來提高效率。現在 Master 服務器可以輕鬆的每秒鐘進行數千次文件訪問。如果有需要的話,我們
可以通過在名稱空間數據結構之前設置名稱查詢緩衝的方式進一步提高速度。
6.2.5 恢復時間
當某個 Chunk 服務器失效了,一些 Chunk 副本的數量可能會低於複製因子指定的數量,我們必須通過克
隆副本使 Chunk 副本數量達到複製因子指定的數量。恢復所有 Chunk 副本所花費的時間取決於資源的數量。
在我們的試驗中,我們把集群 B 上的一個 Chunk 服務器 Kill 掉。這個 Chunk 服務器上大約有 15000 個 Chunk,
共計 600GB 的數據。為了減小克隆操作對正在運行的應用程序的影響,以及為 GFS 調度決策提供修正空間,
我們缺省的把集群中並發克隆操作的數量設置為 91 個(Chunk 服務器的數量的 40%),每個克隆操作最多允
許使用的帶寬是 6.25MB/s(50mbps)。所有的 Chunk 在 23.2 分鐘內恢復了,複製的速度高達 440MB/s。
在另外一個測試中,我們 Kill 掉了兩個 Chunk 服務器,每個 Chunk 服務器大約有 16000 個 Chunk,共
計 660GB 的數據。這兩個故障導致了 266 個 Chunk 只有單個副本。這 266 個 Chunk 被 GFS 優先調度進行復
制,在 2 分鐘內恢復到至少有兩個副本;現在集群被帶入到另外一個狀態,在這個狀態下,系統可以容忍另
外一個 Chunk 服務器失效而不丟失數據。
6.3 工作負荷分析(Workload Breakdown)
本節中,我們展示了對兩個 GFS 集群工作負載情況的詳細分析,這兩個集群和 6.2 節中的類似,但是不
完全相同。集群 X 用於研究和開發,集群 Y 用於生產數據處理。
6.3.1 方法論和注意事項
本章節列出的這些結果數據只包括客戶機發起的原始請求,因此,這些結果能夠反映我們的應用程序對
GFS 文件系統產生的全部工作負載。它們不包含那些為了實現客戶端請求而在服務器間交互的請求,也不包
含 GFS 內部的後台活動相關的請求,比如前向轉發的寫操作,或者重新負載均衡等操作。
我們從 GFS 服務器記錄的真實的 RPC 請求日誌中推導重建出關於 IO 操作的統計信息。例如,GFS 客
戶程序可能會把一個讀操作分成幾個 RPC 請求來提高並行度,我們可以通過這些 RPC 請求推導出原始的讀
操作。因為我們的訪問模式是高度程式化,所以我們認為任何不符合的數據都是誤差 28 。應用程序如果能夠記
錄更詳盡的日誌,就有可能提供更準確的診斷數據;但是為了這個目的去重新編譯和重新啟動數千個正在運
行的客戶機是不現實的,而且從那麼多客戶機上收集結果也是個繁重的工作。
應該避免從我們的工作負荷數據中過度的歸納出普遍的結論 29 。因為Google完全控制着GFS和使用GFS
的應用程序,所以,應用程序都針對 GFS 做了優化,同時,GFS 也是為了這些應用程序而設計的。這樣的相
互作用也可能存在於一般程序和文件系統中,但是在我們的案例中這樣的作用影響可能更顯著。
表 4 顯示了操作按涉及的數據量大小的分佈情況。讀取操作按操作涉及的數據量大小呈現了雙峰分佈。
小的讀取操作(小於 64KB)一般是由查找操作的客戶端發起的,目的在於從巨大的文件中查找小塊的數據。
大的讀取操作(大於 512KB)一般是從頭到尾順序的讀取整個文件。
在集群 Y 上,有相當數量的讀操作沒有返回任何的數據。在我們的應用中,尤其是在生產系統中,經常
使用文件作為生產者-消費者隊列。生產者並行的向文件中追加數據,同時,消費者從文件的尾部讀取數據。
某些情況下,消費者讀取的速度超過了生產者寫入的速度,這就會導致沒有讀到任何數據的情況。集群 X 通
常用於短暫的數據分析任務,而不是長時間運行的分佈式應用,因此,集群 X 很少出現這種情況。
寫操作按數據量大小也同樣呈現為雙峰分佈。大的寫操作(超過 256KB)通常是由於 Writer 使用了緩存
機制導致的。Writer 緩存較小的數據,通過頻繁的 Checkpoint 或者同步操作,或者只是簡單的統計小的寫入
(小於 64KB)的數據量(alex 註:即彙集多次小的寫入操作,當數據量達到一個閾值,一次寫入),之後批量
寫入。
再來觀察一下記錄追加操作。我們可以看到集群 Y 中大的記錄追加操作所佔比例比集群 X 多的多,這是
因為集群 Y 用於我們的生產系統,針對 GFS 做了更全面的調優。
表 5 顯示了按操作涉及的數據量的大小統計出來的總數據傳輸量。在所有的操作中,大的操作(超過
256KB)佔據了主要的傳輸量。小的讀取(小於 64KB)雖然傳輸的數據量比較少,但是在讀取的數據量中仍
佔了相當的比例,這是因為在文件中隨機 Seek 的工作負荷而導致的。
6.3.3 記錄追加 vs. 寫操作
記錄追加操作在我們的生產系統中大量使用。對於集群 X,記錄追加操作和普通寫操作的比例按照位元組
比是108:1,按照操作次數比是8:1。對於作為我們的生產系統的集群Y來說,這兩個比例分別是 3.7:1和 2.5:1。
更進一步,這一組數據說明在我們的兩個集群上,記錄追加操作所佔比例都要比寫操作要大。對於集群 X,
在整個測量過程中,記錄追加操作所佔比率都比較低,因此結果會受到一兩個使用某些特定大小的 buffer 的
應用程序的影響。
如同我們所預期的,我們的數據修改操作主要是記錄追加操作而不是覆蓋方式的寫操作。我們測量了第
一個副本的數據覆蓋寫的情況。這近似於一個客戶機故意覆蓋剛剛寫入的數據,而不是增加新的數據。對於
集群 X,覆蓋寫操作在寫操作所佔據位元組上的比例小於 0.0001%,在所佔據操作數量上的比例小於 0.0003%。
對於集群 Y,這兩個比率都是 0.05%。雖然這只是某一片斷的情況,但是仍然高於我們的預期。這是由於這
些覆蓋寫的操作,大部分是由於客戶端在發生錯誤或者超時以後重試的情況。這在本質上應該不算作工作負
荷的一部分,而是重試機制產生的結果。
6.3.4 Master 的工作負荷
表 6 顯示了 Master 服務器上的請求按類型區分的明細表。大部分的請求都是讀取操作查詢 Chunk 位置信
息(FindLocation)、以及修改操作查詢 lease 持有者的信息(FindLease-Locker)。
集群 X 和 Y 在刪除請求的數量上有着明顯的不同,因為集群 Y 存儲了生產數據,一般會重新生成數據
以及用新版本的數據替換舊有的數據。數量上的差異也被隱藏在了 Open 請求中,因為舊版本的文件可能在以
重新寫入的模式打開時,隱式的被刪除了(類似 UNIX 的 open 函數中的「w」模式)。
FindMatchingFiles 是一個模式匹配請求,支持「ls」以及其它類似的文件系統操作。不同於 Master 服務
器的其它請求,它可能會檢索 namespace 的大部分內容,因此是非常昂貴的操作。集群 Y 的這類請求要多一
些,因為自動化數據處理的任務進程需要檢查文件系統的各個部分,以便從全局上了解應用程序的狀態。與
之不同的是,集群 X 的應用程序更加傾向於由單獨的用戶控制,通常預先知道自己所需要使用的全部文件的
名稱。
7 經驗
在建造和部署 GFS 的過程中,我們經歷了各種各樣的問題,有些是操作上的,有些是技術上的。
起初,GFS 被設想為我們的生產系統的後端文件系統。隨着時間推移,在 GFS 的使用中逐步的增加了對
研究和開發任務的支持。我們開始增加一些小的功能,比如權限和配額,到了現在,GFS 已經初步支持了這
些功能。雖然我們生產系統是嚴格受控的,但是用戶層卻不總是這樣的。需要更多的基礎架構來防止用戶間
的相互干擾。
我們最大的問題是磁盤以及和 Linux 相關的問題。很多磁盤都聲稱它們支持某個範圍內的 Linux IDE 硬盤
驅動程序,但是實際應用中反映出來的情況卻不是這樣,它們只支持最新的驅動。因為協議版本很接近,所以
大部分磁盤都可以用,但是偶爾也會有由於協議不匹配,導致驅動和內核對於驅動器的狀態判斷失誤。這會導致數據因為內核中的問題意外的被破壞了。這個問題促使我們使用 Checksum 來校驗數據,同時我們也修改內核來處理這些因為協議不匹配帶來的問題。
較早的時候,我們在使用 Linux 2.2 內核時遇到了些問題,主要是 fsync()的效率問題。它的效率與文件的大小而不是文件修改部分的大小有關。這在我們的操作日誌文件過大時給出了難題,尤其是在我們尚未實現 Checkpoint 的時候。我們費了很大的力氣用同步寫來解決這個問題,但是最後還是移植到了 Linux2.4 內核上。
另一個和 Linux 相關的問題是單個讀寫鎖的問題,也就是說,在某一個地址空間的任意一個線程都必須在從磁盤 page in(讀鎖)的時候先 hold 住,或者在 mmap()調用(寫鎖)的時候改寫地址空間。我們發現即使我們的系統負載很輕的情況下也會有偶爾的超時,我們花費了很多的精力去查找資源的瓶頸或者硬件的問題。最後我們終於發現這個單個鎖在磁盤線程交換以前映射的數據到磁盤的時候,鎖住了當前的網絡線程,阻止它把新數據映射到內存。由於我們的性能主要受限於網絡接口,而不是內存 copy 的帶寬,因此,我們用pread()替代 mmap(),用了一個額外的 copy 動作來解決這個問題。
儘管偶爾還是有其它的問題,Linux 的開放源代碼還是使我們能夠快速探究和理解系統的行為。在適當的
時候,我們會改進內核並且和公開源碼組織共享這些改動。
8 相關工作
和其它的大型分佈式文件系統,比如 AFS[5]類似,GFS 提供了一個與位置無關的名字空間,這使得數據
可以為了負載均衡或者災難冗餘等目的在不同位置透明的遷移。不同於 AFS 的是,GFS 把文件分佈存儲到不
同的服務器上,這種方式更類似 Xfs[1]和 Swift[3],這是為了提高整體性能以及災難冗餘的能力。由於磁盤相對來說比較便宜,並且複製的方式比 RAID[9]方法簡單的多,GFS 目前只使用複製的方式來進行冗餘,因此要比 xFS 或者 Swift 佔用更多的裸存儲空間(alex 註:Raw storage,裸盤的空間)。
與 AFS、xFS、Frangipani[12]以及 Intermezzo[6]等文件系統不同的是,GFS 並沒有在文件系統層面提供
任何 Cache 機制。我們主要的工作在單個應用程序執行的時候幾乎不會重複讀取數據,因為它們的工作方式
要麼是流式的讀取一個大型的數據集,要麼是在大型的數據集中隨機 Seek 到某個位置,之後每次讀取少量的
數據。
某些分佈式文件系統,比如 Frangipani、xFS、Minnesota』s GFS[11]、GPFS[10],去掉了中心服務器,只
依賴於分佈式算法來保證一致性和可管理性。我們選擇了中心服務器的方法,目的是為了簡化設計,增加可
靠性,能夠靈活擴展。特別值得一提的是,由於處於中心位置的 Master 服務器保存有幾乎所有的 Chunk 相關
信息,並且控制着 Chunk 的所有變更,因此,它極大地簡化了原本非常複雜的 Chunk 分配和複製策略的實現
方法。我們通過減少 Master 服務器保存的狀態信息的數量,以及將 Master 服務器的狀態複製到其它節點來保證系統的災難冗餘能力。擴展能力和高可用性(對於讀取)目前是通過我們的影子 Master 服務器機制來保證
的。對 Master 服務器狀態更改是通過預寫日誌的方式實現持久化。為此,我們可以調整為使用類似 Harp[7]
中的 primary-copy 方案,從而提供比我們現在的方案更嚴格的一致性保證。
我們解決了一個難題,這個難題類似 Lustre[8]在如何在有大量客戶端時保障系統整體性能遇到的問題。
不過,我們通過只關注我們的應用程序的需求,而不是提供一個兼容 POSIX 的文件系統,從而達到了簡化問
題的目的。此外,GFS 設計預期是使用大量的不可靠節點組建集群,因此,災難冗餘方案是我們設計的核心。
GFS 很類似 NASD 架構[4]。NASD 架構是基於網絡磁盤的,而 GFS 使用的是普通計算機作為 Chunk 服
務器,就像 NASD 原形中方案一樣。所不同的是,我們的 Chunk 服務器採用惰性分配固定大小的 Chunk 的方
式,而不是分配變長的對象存儲空間。此外,GFS 實現了諸如重新負載均衡、複製、恢復機制等等在生產環
境中需要的特性。
不同於與 Minnesota』s GFS 和 NASD,我們並不改變存儲設備的 Model 30 。我們只關注用普通的設備來解
決非常複雜的分佈式系統日常的數據處理。
我們通過原子的記錄追加操作實現了生產者-消費者隊列,這個問題類似 River[2]中的分佈式隊列。River
使用的是跨主機的、基於內存的分佈式隊列,為了實現這個隊列,必須仔細控制數據流;而 GFS 採用可以被
生產者並發追加記錄的持久化的文件的方式實現。River 模式支持 m-到-n 的分佈式隊列,但是缺少由持久化
存儲提供的容錯機制,GFS 只支持 m-到-1 的隊列。多個消費者可以同時讀取一個文件,但是它們輸入流的區
間必須是對齊的。
9 結束語
Google 文件系統展示了一個使用普通硬件支持大規模數據處理的系統的特質。雖然一些設計要點都是針
對我們的特殊的需要定製的,但是還是有很多特性適用於類似規模的和成本的數據處理任務。
首先,我們根據我們當前的和可預期的將來的應用規模和技術環境來評估傳統的文件系統的特性。我們
的評估結果將我們引導到一個使用完全不同於傳統的設計思路上。根據我們的設計思路,我們認為組件失效
是常態而不是異常,針對採用追加方式(有可能是並發追加)寫入、然後再讀取(通常序列化讀取)的大文
件進行優化,以及擴展標準文件系統接口、放鬆接口限制來改進整個系統。
我們系統通過持續監控,複製關鍵數據,快速和自動恢復提供災難冗餘。Chunk 複製使得我們可以對
Chunk 服務器的失效進行容錯。高頻率的組件失效要求系統具備在線修復機制,能夠周期性的、透明的修復
損壞的數據,也能夠第一時間重新建立丟失的副本。此外,我們使用 Checksum 在磁盤或者 IDE 子系統級別
檢測數據損壞,在這樣磁盤數量驚人的大系統中,損壞率是相當高的。我們的設計保證了在有大量的並發讀寫操作時能夠提供很高的合計吞吐量。我們通過分離控制流和數據流來實現這個目標,控制流在 Master 服務器處理,而數據流在 Chunk 服務器和客戶端處理。當一般的操作涉及到 Master 服務器時,由於 GFS 選擇的 Chunk 尺寸較大(alex 註:從而減小了元數據的大小),以及通過 ChunkLease 將控制權限移交給主副本,這些措施將 Master 服務器的負擔降到最低。這使得一個簡單、中心的 Master不會成為成為瓶頸。我們相信我們對網絡協議棧的優化可以提升當前對於每客戶端的寫入吞吐量限制。GFS 成功的實現了我們對存儲的需求,在 Google 內部,無論是作為研究和開發的存儲平台,還是作為生產系統的數據處理平台,都得到了廣泛的應用。它是我們持續創新和處理整個 WEB 範圍內的難題的一個重要工具。
更多Flink,Kafka,Spark等相關技術博文,科技資訊,歡迎關注實時流式計算 公眾號後台回復 「電子書」 下載300頁Flink實戰電子書