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實戰電子書