1. 程式人生 > >The Google File System

The Google File System

介紹

我們設計和實現了GFS來滿足Google與日俱增的資料處理需求。與傳統的分散式檔案系統一樣,GFS著眼在幾個重要的目標,比如效能、可伸縮性、可靠性和可用性。不過它也會優先考慮我們自身應用場景的特徵和技術環境,所以與早先一些檔案系統的設計思想還是有諸多不同。我們取傳統方案之精華、根據自身需求做了大膽的設計創新。在我們的場景中:

  • 首先,元件故障是常態而不是異常。檔案系統包含成百上千的儲存機器,而且是廉價的普通機器,被大量的客戶端機器訪問。這樣的機器質量和數量導致任何時間點都可能有一些機器不可用,甚至無法從當前故障中恢復。導致故障的原因很多,比如應用bug、作業系統bug、人為錯誤,以及磁碟、記憶體、聯結器、網路等硬體故障,甚至是電力供應。因此,持續監控、錯誤偵測、故障容忍和自動恢復必須全面覆蓋整個系統。

  • 其次,用傳統視角來看,我們要處理的檔案很多都是巨型的,好幾GB的檔案也很常見。通常情況下每個檔案中包含了多個應用物件,比如web文件。面對快速增長、TB級別、包含數十億物件的資料集合,如果按數十億個KB級別的小檔案來管理,即使檔案系統能支援,也是非常不明智的。因此,一些設計上的假設和引數,比如I/O操作和塊大小,需要被重新審視。

  • 第三,大部分檔案發生變化是通過append新資料,而不是覆蓋、重寫已有的資料,隨機寫幾乎不存在。被寫入時,檔案變成只讀,而且通常只能是順序讀。很多資料場景都符合這些特徵。比如檔案組成大型的庫,使用資料分析程式對其掃描。比如由執行中的程式持續生成的資料流。比如歸檔資料。還可能是分散式計算的中間結果,在一臺機器上產生、然後在另一臺處理。這些資料場景都是由製造者持續增量的產生新資料,再由消費者讀取處理。在這種模式下append是效能優化和保證原子性的焦點。然而在客戶端快取資料塊沒有太大意義。

  • 第四,嚮應用提供類似檔案系統API,增加了我們的靈活性。鬆弛的一致性模型設計也極大的簡化了API,不會給應用程式強加繁重負擔。我們將介紹一個原子的append操作,多客戶端能併發的對一個檔案執行append,不需考慮任何同步。

當前我們部署了多個GFS叢集,服務不同的應用。最大的擁有超過1000個儲存節點,提供超過300TB的磁碟儲存,被成百上千個客戶端機器大量訪問。

設計概覽

假設

設計GFS過程中我們做了很多的設計假設,它們既意味著挑戰,也帶來了機遇。現在我們詳細描述下這些假設。

  • 系統是構建在很多廉價的、普通的元件上,元件會經常發生故障。它必須不間斷監控自己、偵測錯誤,能夠容錯和快速恢復。
  • 系統儲存了適當數量的大型檔案,我們預期幾百萬個,每個通常是100MB或者更大,即使是GB級別的檔案也需要高效管理。也支援小檔案,但是不需要著重優化。
  • 系統主要面對兩種讀操作:大型流式讀和小型隨機讀。在大型流式讀中,單個操作會讀取幾百KB,也可以達到1MB或更多。相同客戶端發起的連續操作通常是在一個檔案讀取一個連續的範圍。小型隨機讀通常在特定的偏移位置上讀取幾KB。重視效能的應用程式通常會將它們的小型讀批量打包、組織排序,能顯著的提升效能。
  • 也會面對大型的、連續的寫,將資料append到檔案。append資料的大小與一次讀操作差不多。一旦寫入,幾乎不會被修改。不過在檔案特定位置的小型寫也是支援的,但沒有著重優化。
  • 系統必須保證多客戶端對相同檔案併發append的高效和原子性。我們的檔案通常用於製造者消費者佇列或者多路合併。幾百個機器執行的製造者,將併發的append到一個檔案。用最小的同步代價實現原子性是關鍵所在。檔案被append時也可能出現併發的讀。
  • 持久穩定的頻寬比低延遲更重要。我們更注重能夠持續的、大批量的、高速度的處理海量資料,對某一次讀寫操作的回覆時間要求沒那麼嚴格。

介面

GFS提供了一個非常親切的檔案系統介面,儘管它沒有全量實現標準的POSIX API。像在本地磁碟中一樣,GFS按層級目錄來組織檔案,一個檔案路徑(path)能作為一個檔案的唯一ID。我們支援常規檔案操作,比如create、delete、open、close、read和write。

除了常規操作,GFS還提供快照和record append操作。快照可以用很低的花費為一個檔案或者整個目錄樹建立一個副本。record append允許多個客戶端併發的append資料到同一個檔案,而且保證它們的原子性。這對於實現多路合併、製造消費者佇列非常有用,大量的客戶端能同時的append,也不用要考慮鎖等同步問題。這些特性對於構建大型分散式應用是無價之寶。快照和record append將在章節3.4、3.3討論。

架構

一個GFS叢集包含單個master和多個chunkserver,被多個客戶端訪問,如圖1所示。圖1中各元件都是某臺普通Linux機器上執行在使用者級別的一個程序。在同一臺機器上一起執行chunkserver和客戶端也很容易,只要機器資源允許。

GFS Architecture

檔案被劃分為固定大小的chunk。每個chunk在建立時會被分配一個chunk控制代碼,chunk控制代碼是一個不變的、全域性唯一的64位的ID。chunkserver在本地磁碟上將chunk儲存為Linux檔案,按照chunk控制代碼和位元組範圍來讀寫chunk資料。為了可靠性,每個chunk被複制到多個chunkserver上,預設是3份,使用者能為不同名稱空間的檔案配置不同的複製級別。

master維護所有的檔案系統元資料。包括名稱空間,訪問控制資訊,從檔案到chunk的對映,和chunk位置。它也負責主導一些影響整個系統的活動,比如chunk租賃管理、孤兒chunk的垃圾回收,以及chunkserver之間的chunk遷移。master會週期性的與每臺chunkserver通訊,使用心跳訊息,以發號施令或者收集chunkserver狀態。

每個應用程式會引用GFS的客戶端API,此API與正規檔案系統API相似,並且負責與master和chunkserver通訊,基於應用的行為來讀寫資料。客戶端只在獲取元資料時與master互動,真實的資料操作會直接發至chunkserver。我們不需提供嚴格完整的POSIX API,因此不需要hook到Linux的vnode層面。

客戶端和chunkserver都不會快取檔案資料。客戶端快取檔案資料收益很小,因為大部分應用通常會順序掃描大型檔案,快取重用率不高,要麼就是工作集合太大快取很困難。沒有快取簡化了客戶端和整個系統,排除快取一致性問題。(但是客戶端會快取元資料。)chunkserver不需要快取檔案資料因為chunk被儲存為本地檔案,Linux提供的OS層面的buffer快取已經儲存了頻繁訪問的檔案。

單一Master

單一master大大的簡化了我們的設計,單一master能夠放心使用全域性策略執行復雜的chunk佈置、制定複製決策等。然而,我們必須在讀寫過程中儘量減少對它的依賴,它才不會成為一個瓶頸。客戶端從不通過master讀寫檔案,它只會詢問master自己應該訪問哪個chunkserver。客戶端會快取這個資訊一段時間,隨後的很多操作即可以複用此快取,與chunkserver直接互動。

我們利用圖1來展示一個簡單讀操作的互動過程。首先,使用固定的chunk size,客戶端將應用程式指定的檔名和位元組偏移量翻譯為一個GFS檔案及內部chunk序號,隨後將它們作為引數,傳送請求到master。master找到對應的chunk控制代碼和副本位置,回覆給客戶端。客戶端快取這些資訊,使用GFS檔名+chunk序號作為key。

客戶端然後傳送一個讀請求到其中一個副本,很可能是最近的那個。請求中指定了chunk控制代碼以及在此chunk中讀取的位元組範圍。後面對相同chunk的讀不再與master互動,直到客戶端快取資訊過期或者檔案被重新開啟。事實上,客戶端通常會在一個與master的請求中順帶多索要一些其他chunk的資訊,而且master也可能將客戶端索要chunk後面緊跟的其他chunk資訊主動回覆回去。這些額外的資訊避免了未來可能發生的一些client-master互動,幾乎不會導致額外的花費。

chunk size

chunk size是其中一個關鍵的設計引數。我們選擇了64MB,這是比典型的檔案系統的塊大多了。每個chunk副本在chunkserver上被儲存為一個普通的Linux檔案,只在必要的時候才去擴充套件。懶惰的空間分配避免了內部碎片導致的空間浪費,chunk size越大,碎片的威脅就越大。

chunk size較大時可以提供幾種重要的優勢:

  • 首先,它減少了客戶端與master的互動,因為對同一個chunk的讀寫僅需要對master執行一次初始請求以獲取chunk位置資訊。在我們的應用場景中大部分應用會順序的讀寫大型檔案,chunk size較大(chunk數量就較少)能有效的降低與master的互動次數。對於小型的隨機讀,即使整個資料集合達到TB級別,客戶端也能舒服的快取所有的chunk位置資訊(因為chunk size大,chunk數量小)。
  • 其次,既然使用者面對的是較大的chunk,它更可能願意在同一個大chunk上執行很多的操作(而不是操作非常多的小chunk),這樣就可以對同一個chunkserver保持長期的TCP連線以降低網路負載。
  • 第三,它減少了master上元資料的大小,這允許我們放心的在記憶體快取元資料,章節2.6.1會討論繼而帶來的各種好處。

不過chunk size如果很大,即使使用懶惰的空間分配,也有它的缺點。一個小檔案包含chunk數量較少,可能只有一個。在chunkserver上這些chunk可能變成熱點,因為很多客戶端會訪問相同的檔案(如果chunk size較小,那小檔案也會包含很多chunk,資源競爭可以分擔到各個小chunk上,就可以緩解熱點)。不過實際上熱點沒有導致太多問題,因為我們的應用大部分都是連續的讀取很大的檔案,包含很多chunk(即使chunk size較大)。

然而,熱點確實曾經導致過問題,當GFS最初被用在批量佇列系統時:使用者將一個可執行程式寫入GFS,它只佔一個chunk,然後幾百臺機器同時啟動,請求此可執行程式。儲存此可執行檔案的chunkserver在過多的併發請求下負載較重。我們通過提高它的複製級別解決了這個問題(更多冗餘,分擔壓力),並且建議該系統交錯安排啟動時間。一個潛在的長期解決方案是允許客戶端從其他客戶端讀取資料(P2P模式~)。

元資料

master主要儲存三種類型的元資料:檔案和chunk的名稱空間,從檔案到chunk的對映,每個chunk副本的位置。所有的元資料被儲存在master的記憶體中。前兩種也會持久化儲存,通過記錄操作日誌,儲存在master的本地磁碟並且複製到遠端機器。使用操作日誌允許我們更簡單可靠的更新master狀態,不會因為master的當機導致資料不一致。master不會持久化儲存chunk位置,相反,master會在啟動時詢問每個chunkserver以獲取它們各自的chunk位置資訊,新chunkserver加入叢集時也是如此。

記憶體中資料結構

  • 因為元資料儲存在記憶體中,master可以很快執行元資料操作。而且可以簡單高效的在後臺週期性掃描整個元資料狀態。週期性的掃描作用很多,有些用於實現chunk垃圾回收,有些用於chunkserver故障導致的重新複製,以及為了均衡各機器負載與磁碟使用率而執行的chunk遷移。章節4.3和4.4將討論其細節。

  • 這麼依賴記憶體不免讓人有些顧慮,隨著chunk的數量和今後整體容量的增長,整個系統將受限於master有多少記憶體。不過實際上這不是一個很嚴重的限制。每個64MB的chunk,master為其維護少於64byte的元資料。大部分chunk是填充滿資料的,因為大部分檔案包含很多chunk,只有少數可能只填充了部分。同樣的,對於檔案名稱空間資料,每個檔案只能佔用少於64byte,檔名稱會使用字首壓縮緊密的儲存。

  • 如果整個檔案系統真的擴充套件到非常大的規模,給master添點記憶體條、換臺好機器scale up一下也是值得的。為了單一master+記憶體中資料結構所帶來的簡化、可靠性、效能和彈性,咱豁出去了。

Chunk位置

  • master不會持久化的儲存哪個chunkserver有哪些chunk副本。它只是在自己啟動時拉取chunkserver上的資訊(隨後也會週期性的執行拉取)。master能保證它自己的資訊時刻都是最新的,因為它控制了所有的chunk佈置操作,並用常規心跳訊息監控chunkserver狀態。

  • 我們最初嘗試在master持久化儲存chunk位置資訊,但是後來發現這樣太麻煩,每當chunkserver加入或者離開叢集、改變名稱、故障、重啟等等時候就要保持master資訊的同步。一般叢集都會有幾百臺伺服器,這些事件經常發生。

  • 話說回來,只有chunkserver自己才對它磁碟上存了哪些chunk有最終話語權。沒理由在master上費盡心機的維護一個一致性檢視,chunkserver上發生的一個錯誤就可能導致chunk莫名消失(比如一個磁碟可能失效)或者運維人員可能重新命名一個chunkserver等等。

操作日誌

  • 操作日誌是對重要元資料變更的歷史記錄。它是GFS的核心之一。不僅因為它是元資料唯一的持久化記錄,而且它還要承擔一個邏輯上的時間標準,為併發的操作定義順序。各檔案、chunk、以及它們的版本(見章節4.5),都會根據它們建立時的邏輯時間被唯一的、永恆的標識。

  • 既然操作日誌這麼重要,我們必須可靠的儲存它,而且直至元資料更新被持久化完成(記錄操作日誌)之後,才能讓變化對客戶端可見。否則,我們有可能失去整個檔案系統或者最近的客戶端操作,即使chunkserver沒有任何問題(元資料丟了或錯了,chunkserver沒問題也變得有問題了)。因此,我們將它複製到多個遠端機器,直到日誌記錄被flush到本地磁碟以及遠端機器之後才會回覆客戶端。master會捆綁多個日誌記錄,一起flush,以減少flush和複製對整個系統吞吐量的衝擊。

  • master可以通過重放操作日誌來恢復它的元資料狀態。為了最小化master的啟動時間,日誌不能太多(多了重放就需要很久)。所以master會在適當的時候執行“存檔”,每當日誌增長超過一個特定的大小就會執行存檔。所以它不需要從零開始回放日誌,僅需要從本地磁碟裝載最近的存檔,並回放存檔之後發生的有限數量的日誌。存檔是一個緊密的類B樹結構,它能直接對映到記憶體,不用額外的解析。通過這些手段可以加速恢復和改進可用性。

  • 因為構建一個存檔會消耗點時間,master的內部狀態做了比較精細的結構化設計,建立一個新的存檔不會延緩持續到來的請求。master可以快速切換到一個新的日誌檔案,在另一個後臺執行緒中建立存檔。這個新存檔能體現切換之前所有的變異結果。即使一個有幾百萬檔案的叢集,建立存檔也可以在短時間完成。結束時,它也會寫入本地和遠端的磁碟。

  • 恢復元資料時,僅僅需要最後完成的存檔和其後產生的日誌。老的存檔和日誌檔案能被自由刪除,不過我們保險起見不會隨意刪除。在存檔期間如果發生故障(存檔檔案爛尾了)也不會影響正確性,因為恢復程式碼能偵測和跳過未完成的存檔。

一致性模型

GFS鬆弛的一致性模型能很好的支援我們高度分散式的應用,而且實現起來非常簡單高效。我們現在討論GFS的一致性保證。

GFS的一致性保證

  • 檔案名稱空間變化(比如檔案建立)是原子的,只有master能處理此種操作:master中提供了名稱空間的鎖機制,保證了原子性的和正確性(章節4.1);master的操作日誌為這些操作定義了一個全域性統一的順序(章節2.6.3)

  • 各種資料變異在不斷髮生,被它們改變的檔案區域處於什麼狀態?這取決於變異是否成功了、有沒有併發變異等各種因素。表1列出了所有可能的結果。對於檔案區域A,如果所有客戶端從任何副本上讀到的資料都是相同的,那A就是一致的。如果A是一致的,並且客戶端可以看到變異寫入的完整資料,那A就是defined。當一個變異成功了、沒有受到併發寫的干擾,它寫入的區域將會是defined(也是一致的):所有客戶端都能看到這個變異寫入的完整資料。對同個區域的多個併發變異成功寫入,此區域是一致的,但可能是undefined:所有客戶端看到相同的資料,但是它可能不會反應任何一個變異寫的東西,可能是多個變異混雜的碎片。一個失敗的變異導致區域不一致(也是undefined):不同客戶端可能看到不同的資料在不同的時間點。下面描述我們的應用程式如何區分defined區域和undefined區域。

  • 資料變異可能是寫操作或者record append。寫操作導致資料被寫入一個使用者指定的檔案偏移。而record append導致資料(record)被原子的寫入GFS選擇的某個偏移(正常情況下是檔案末尾,見章節3.3),GFS選擇的偏移被返回給客戶端,其代表了此record所在的defined區域的起始偏移量。另外,某些異常情況可能會導致GFS在區域之間插入了padding或者重複的record。他們佔據的區域可認為是不一致的,不過資料量不大。

  • 如果一系列變異都成功寫入了,GFS保證發生變異的檔案區域是defined的,並完整的包含最後一個變異。GFS通過兩點來實現:(a)chunk的所有副本按相同的順序來實施變異(章節3.1);(b)使用chunk版本數來偵測任何舊副本,副本變舊可能是因為它發生過故障、錯過了變異(章節4.5)。執行變異過程時將跳過舊的副本,客戶端呼叫master獲取chunk位置時也不會返回舊副本。GFS會盡早的通過垃圾回收處理掉舊的副本。

  • 因為客戶端快取了chunk位置,所以它們可能向舊副本發起讀請求。不過快取項有超時機制,檔案重新開啟時也會更新。而且,我們大部分的檔案是append-only的,這種情況下舊副本最壞只是無法返回資料(append-only意味著只增不減也不改,版本舊只意味著會丟資料、少資料),而不會返回過期的、錯誤的資料。一旦客戶端與master聯絡,它將立刻得到最新的chunk位置(不包含舊副本)。

  • 在一個變異成功寫入很久之後,元件的故障仍然可能腐化、破壞資料。GFS中,master和所有chunkserver之間會持續handshake通訊並交換資訊,藉此master可以識別故障的chunkserver並且通過檢查checksum偵測資料腐化(章節5.2)。一旦發現此問題,會盡快執行一個restore,從合法的副本複製合法資料替代腐化副本(章節4.3)。一個chunk也可能發生不可逆的丟失,那就是在GFS反應過來採取措施之前,所有副本都被丟失。通常GFS在分鐘內就能反應。即使出現這種天災,chunk也只是變得不可用,而不會腐化:應用收到清晰的錯誤而不是錯誤的資料。

【譯者注】一致性的問題介紹起來難免晦澀枯燥,下面譯者用一些比較淺顯的例子來解釋GFS中的一致、不一致、defined、undefined四種狀態。

讀者可以想象這樣一個場景,某人和他老婆共用同一個Facebook賬號,同時登陸,同時看到某張照片,他希望將其順時針旋轉90度,他老婆希望將其逆時針旋轉90度。兩人同時點了修改按鈕,Facebook應該聽誰的?俗話說意見相同聽老公的,意見不同聽老婆的。但是Facebook不懂這個演算法,當他們重新開啟頁面時可能會:1 都看到圖片順時針旋轉了90度;2 都看到圖片逆時針旋轉了90度;3 其他情況。對於1、2兩種情況,都是可以接受的,小夫妻若來投訴那隻能如實相告讓他們自己回去猜拳,不關Facebook的事兒。因為1、2既滿足一致性(兩人在併發修改發生後都一直看到一致相同的內容),又滿足defined(內容是其中一人寫入的完整資料)。對於3會有哪些其他情況呢?如果這事兒發生在單臺電腦的本地硬碟(相當於兩人同時開啟一個圖片軟體、編輯同一個圖片、然後併發提交儲存),若不加鎖讓其序列,則可能導致資料碎片,以簡單的程式碼為例:

File file = new File(“D:/temp.txt”);
FileOutputStream fos1 = new FileOutputStream(file);
FileOutputStream fos2 = new FileOutputStream(file);
fos1.write(‘1’);
fos1.write(‘2’);
fos1.write(‘3’);
fos2.write(‘a’);
fos2.write(‘b’);
fos2.write(‘c’);
fos1.close();
fos2.close();

這樣一段程式碼可以保證temp.txt的內容是“abc”(fos2寫入的位元組流完全覆蓋了fos1),fos2寫入是完全的,也就是defined。而寫入位元組流是一個持續過程,不是原子的,如果在多執行緒環境下則可能因為執行緒排程、I/O中斷等因素導致程式碼的執行順序交錯,形成這樣的效果:

File file = new File(“D:/temp.txt”);
FileOutputStream fos1 = new FileOutputStream(file);
FileOutputStream fos2 = new FileOutputStream(file);
fos1.write(‘1’);
fos2.write(‘a’);
fos2.write(‘b’);
fos1.write(‘2’);
fos1.write(‘3’);
fos2.write(‘c’);
fos1.close();
fos2.close();

這段程式碼導致temp.txt的內容變成了“a2c”,它不是fos1的寫入也不是fos2的寫入,它是碎片的組合,這就是undefined狀態。還有更糟的情況,這種情況在單臺電腦本地硬碟不會出現,而會在分散式檔案系統上出現:分散式檔案系統都有冗餘備份,fos1和fos2的寫入需要在每個副本上都執行,而在每個副本上會因為各自的執行緒排程、I/O中斷導致交錯的情況不一、順序不一,於是出現了副本資料不一致的情況(不僅有a2c,還可能是12c、1b3等等),在查詢時由於會隨機選擇副本,於是導致多個查詢可能看到各種不一致的資料。這就是既不一致又undefined的情況。在分散式檔案系統上還有另一種情況,在各副本上fos1和fos2都沒有交錯產生碎片,但是它們整體順序不一致,一個副本產出了123,另一個產出了abc,這種也是不一致的異常情況。

如何解決上述問題呢?比較可行的方案就是序列化,按順序執行,fos1寫完了才輪到fos2。不過即使如此也不能完全避免一些令人不悅的現象:比如fos1要寫入的是“12345”,fos2要寫入的是“abc”,即使序列,最後也會產出“abc45”。不過對於這種現象,只能認為是外界需求使然,不是檔案系統能解決的,GFS也不會把它當做碎片,而認為它是defined。在分散式環境下,不僅要保證每個副本序列執行變異,還要保證序列的順序是一致的,GFS的對策就是後文中的租賃機制。這樣還不夠,還要謹防某個副本因為機器故障而執行異常,GFS的對策是版本偵測機制,利用版本偵測踢除異常的副本。

對應用的啟示

  • 在使用GFS時,應用如果希望達到良好的一致性效果,需要稍作考慮以配合GFS的鬆弛一致性模型。但GFS的要求並不高,而且它要求的事情一般你都會去做(為了某些其他的目的):比如GFS希望應用使用append寫而不是覆蓋重寫,以及一些自我檢查、鑑定和驗證的能力。

  • 無論你面對GFS還是普通的本地檔案API(比如FileInputStream、FileOutputStream),有些一致性問題你都要去考慮。當一個檔案正在被寫時,它依然可以被另一個執行緒讀,寫入磁碟不是一瞬間的事情,當然有可能讀到沒有寫入完全的資料(可以理解為上述的undefined情況,你只看到了碎片沒有看到完整寫入的內容),這種情況GFS不會幫你解決(它是按照標準檔案API來要求自己的,標準檔案API也沒有幫你解決這種問題)。比較嚴謹的程式會使用各種方法來避免此問題,比如先寫入臨時檔案,寫入結束時才原子的重新命名檔案,此時才對讀執行緒可見。或者在寫入過程中不斷記錄寫入完成量,稱之為checkpoint,讀執行緒不會隨意讀檔案的任何位置,它會判斷checkpoint(哪個偏移之前的資料是寫入完全的,也就是defined),checkpoint甚至也可以像GFS一樣做應用級別的checksum。這些措施都可以幫助reader只讀取到defined區域。

  • 還有這種情況:你正在寫入一個檔案,將新資料append到檔案末尾,還沒結束時程式異常或者機器故障了,於是你必須重試,但是之前那次append可能已經寫入了部分資料,這部分資料也是undefined,也不希望讓reader讀到。無論在本地磁碟還是在GFS上都要面臨這種問題。不過這一點上GFS提供了一些有效的幫助。在GFS裡,剛才那種情況可能會導致兩種異常,一是沒有寫入完全的padding,二是重複的資料(GFS有冗餘副本,寫入資料時任一副本故障會導致所有副本都重試,這就可能導致正常的副本上不止寫入一次)。對於padding,GFS提供了checksum機制,讀取時通過簡單的核查即可跳過不合法的padding。不過對於重複,應用如果不能容忍的話最好能加強自身的冪等性檢查,比如當你將大量應用實體寫入檔案時,實體可以包含ID,讀取實體進行業務處理時能通過ID的冪等性檢查避免重複處理。

  • GFS雖然沒有直接在系統層面解決上述難以避免的一致性問題,但是上面提到的解決方案都會作為共享程式碼庫供大家使用。

系統互動

在GFS的架構設計中,我們會竭盡所能的減少所有操作對master的依賴(因為架構上的犧牲權衡,master是個理論上的單點)。在這個背景下,下面將描述客戶端、master、chunkserver之間是如何互動,最終實現了各種資料變異、原子的record append、快照等特性。

租賃和變異順序

變異可以理解為一種操作,此操作會改變chunk的資料內容或者元資料,比如一個寫操作或者一個append操作。對chunk的任何變異都需要實施到此chunk的各個副本上。我們提出了一種“租賃”機制,來維護一個跨副本的一致性變異順序。master會在chunk各副本中選擇一個,授予其租賃權,此副本稱之為首要副本,其他的稱之為次級副本。首要副本負責為chunk的所有變異排出一個嚴格的順序。所有副本在實施變異時都必須遵循此順序。因此,全域性統一的變異過程可以理解為:首先由master選出首要和次級副本;首要副本為這些變異制定實施序號;首要和次級副本內嚴格按首要副本制定的序號實施變異。

租賃機制需要儘量減少對master產生的負載。一個租賃初始的超時時間為60秒。然而只要chunk正在實施變異,首要副本能向master申請連任,一般都會成功。master和所有chunkserver之間會持續的交換心跳訊息,租賃的授予、請求連任等請求都是在這個過程中完成。master有時候會嘗試撤回一個還沒過期的租賃(比如要重新命名一個檔案,master希望暫停所有對其實施的變異)。即使master與首要副本失去通訊,它也能保證在老租賃過期後安全的選出一個新的首要副本。

這裡寫圖片描述

圖2描述了具體的控制流程,其中步驟的解釋如下:

  1. 客戶端要對某chunk執行操作,它詢問master哪個chunkserver持有其租賃以及各副本的位置資訊。如果沒有任何人拿到租賃,master選擇一個副本授予其租賃(此時不會去通知這個副本)。
  2. master將首要者、副本位置資訊回覆到客戶端。客戶端快取這些資料以便未來重用,這樣它僅需要在當前首要副本無法訪問或者卸任時去再次聯絡master。
  3. 客戶端推送資料到所有的副本。只是推送,不會實施,只是在各chunkserver上將資料準備好,推送的順序也與控制流無關。每個副本所在的chunkserver將資料儲存在一個內部的LRU的緩衝中,直到資料被使用或者過期。通過將資料流和控制流解耦,我們能有效的改進效能,實現基於網路拓撲的演算法來排程“昂貴”的資料流,而不需要關心控制流中哪個chunkserver是首要的還是次要的。章節3.2將討論此演算法的細節。
  4. 一旦所有副本都確認收到了資料,客戶端正式傳送一個寫請求到首要副本。寫請求無真實資料,只有一個身份標識,對應第三步中發給各個副本的資料包。在首要副本中會持續的收到來自各個客戶端的各種變異請求,本次寫請求只是其中一個而已。在持續接收請求的過程中,首要副本會為每個請求分配唯一的遞增序號,它也會嚴格按照此順序來在本地狀態中實施變異。
  5. 首要將寫請求推送到所有次級副本(請求中已帶有分配的序號),每個次級副本都會嚴格按順序依次實施變異。
  6. 次級副本回復給首要的,確認他們已完成操作
  7. 首要副本回復客戶端。在任何副本遭遇的任何錯誤,都被彙報給客戶端。在錯誤發生時,此寫操作可能已經在首要和某些次級副本中實施成功。(如果它首要就失敗,就不會分配序號也不會往後推進。)客戶端則認為此次請求失敗,請求所修改的區域變成了不一致狀態。對於失敗變異,客戶端會重試,它首先會做一些嘗試在步驟3到步驟7,實在不行就重試整個流程。

一個寫請求(非append)可能很大,跨越了chunk邊界,GFS客戶端程式碼會將其拆分為對多個chunk的多個寫操作。各個寫操作都遵從上述控制流,但是也可能因為來自其他客戶端的併發寫導致某幾個子操作的檔案區域產生資料碎片。不過即使如此,各副本的資料是相同的,因為此控制流保證了所有副本執行的變異順序是完全一致的。所以即使某些區域產生了碎片,還是滿足一致性的,但是會處於undefined狀態(章節2.7描述的)。

【譯者注】上述流程中多次提到要按順序、依次、序列等詞彙,來避免併發導致的一致性問題。這些會不會導致效能問題?畢竟這是一個I/O密集型系統,請求序列化不是一個值得驕傲的解決方案。文章末尾對此疑問會嘗試解答。

資料流

我們將資料流和控制流解耦來更高效的利用網路。從上述控制流的分析中可以看出,從客戶端到首要副本然後到所有次級副本,請求是沿著一個小心謹慎的鏈路、像管道一樣,在各個chunkserver之間推送。我們不能容忍真實資料的流程被此嚴謹的控制流綁架,我們的目標是最大化利用每個機器的網路頻寬,避免網路瓶頸和高延遲連線,最小化推送延遲。

為了最大化利用每臺機器的網路頻寬,我們讓資料沿著一個線性鏈路推送(chunkserver就是鏈路中的一個個節點),而不是零亂的分佈於其他拓撲結構中(比如樹狀)。我們希望每臺機器都會使用全量頻寬儘快傳輸一整批資料,而不是頻繁收發零亂的小批資料。

為了儘可能的避免網路瓶頸和高延遲連線(內聯交換機經常遇到此問題),每個機器都會嘗試推送資料到網路拓撲中最近的其他目標機器。假設客戶端希望推送資料到chunkserver S1、S2、S3、S4。不管網路拓撲結構如何,我們假設S1離客戶端最近,S2離S1最近。首先客戶端會發送資料到最近的S1;S1收到資料,傳輸目標減少為[S2、S3、 S4],繼而推送到離S1最近的S2,傳輸目標減少為[S3、S4]。相似的,S2繼續推送到S3或者S4(看誰離S2更近),如此繼續。我們的網路拓撲並不複雜,可以用IP地址準確的預估出“距離”。

最後,我們使用TCP流式傳輸資料,以最小化延遲。一旦chunkserver收到資料,它立刻開始推送。TCP管道流式傳輸的效果顯著,因為我們使用的是 switched network with full-duplex links。立刻傳送資料並不會影響接收速度。沒有網路擁擠的情況下,傳輸B個位元組到R個副本的理想耗時是B/T+RL,T是網路吞吐量,L是在機器間傳輸位元組的延遲。我們網路連線是典型的100Mbps(T),L小於1ms,因此1MB的資料流大約耗時80ms。

原子append

GFS提供了原子append能力,稱之為record append。在傳統的寫操作中,客戶端指明偏移量,寫入時seek到此偏移,然後順序的寫入新資料。而record append操作中,客戶端僅需要指明資料。GFS可以選擇一個偏移量(一般是檔案末尾),原子的將資料append到此偏移量,至少一次(沒有資料碎片,是一個連續序列的位元組)偏移量被返回到客戶端。類似的,UNIX中多個writer併發寫入O_APPEND模式開啟的檔案時也沒有競爭條件。

record append在我們分散式應用中被大量的使用,其中很多機器上的大量客戶端會併發的append到相同的檔案。如果用傳統的寫模式,將嚴重增加客戶端的複雜度,實施昂貴的同步,比如通過一個分散式鎖管理器。我們的實際應用場景中,record append經常用於多個製造者、單個消費者佇列情景,或者用於儲存多客戶端的合併結果。

record append也是一種變異,遵從控制流(章節3.1),但是會需要首要副本執行一點點額外的邏輯。客戶端將資料推送到檔案末尾對應的chunk的所有副本上。然後傳送寫請求到首要副本。首要副本需要檢查append到此chunk是否會導致chunk超過最大的size(64MB)。如果超過,它將此chunk填補到最大size,並告訴次級副本也這麼做,隨後回覆客戶端這個操作需要重試,並使用下一個chunk(上一個chunk剛剛已經被填滿,檔案末尾會對應到一個新chunk)。record append的資料大小被限制為小於等於chunk maxsize的四分之一,這樣可以避免填補導致的過多碎片。如果不需要填補(通常都不需要),首要副本append資料到它的副本,得出其偏移量,並告訴次級副本將資料準確的寫入此偏移,最終回覆客戶端操作已成功。

如果一個record append在任何副本失敗了,客戶端需要重試。因此,同一個chunk的各個副本可能包含不同的資料,各自都可能包含重複的record。GFS不保證所有副本是位元組上相同的。它僅僅保證record apend能原子執行,寫入至少一次。不過有一點可以保證,record append最終成功後,所有副本寫入此有效record的偏移量是相同的。另外,所有副本至少和此record的結尾是一樣長的,因此任何未來的record將被分配到更高的偏移或者不同的chunk,即使首要副本換人。依據我們的一致性保證,成功的record append操作寫入的區域是defined(因此也是一致的),若操作最終失敗,則此區域是不一致的(因此undefined的)。我們的應用能處理這種不一致區域(2.7.2討論過)。

快照

快照操作能非常快的對一個檔案或者一個目錄樹(稱之為源)執行一次拷貝,期間收到的新變異請求也只會受到很小的影響。我們的使用者經常使用快照功能快速的為大型的資料集合建立分支拷貝(經常拷貝再拷貝,遞迴的),或者存檔當前狀態,以便安全的實驗一些變異,隨後可以非常簡單提交或回滾。

與AFS類似,我們使用標準的copy-on-write技術來實現快照。當master收到一個快照請求,它找出此快照涉及的檔案對應的所有chunk,撤回這些chunk上任何未償還的租賃。這樣即可保證隨後對這些chunk的寫請求將需要一個與master的互動來找到租賃擁有者。master利用此機會暗地裡對此chunk建立一個新拷貝。

在撤回租賃完成後,master將此快照操作日誌記錄到磁碟。實施快照操作時,它會在記憶體狀態中快速複製一份原始檔、源目錄樹的元資料,複製出來的元資料對映到相同的chunk(和JVM中物件的引用計數相似,此chunk的引用計數為2,源元資料和快照元資料兩份引用)。

假設快照操作涉及的某個檔案包含一個chunk(稱之為C),在快照操作後,某個客戶端需要寫入chunk C,它傳送一個請求到master來找到當前租賃持有者。master注意到C的引用計數大於1(源元資料和快照元資料,2個引用)。它不著急給客戶端回覆,而是選擇一個新的chunk控制代碼(稱之為C’),然後要求包含C的副本的chunkserver都為C’建立一個新副本。新老副本在同一個chunkserver,資料都是本地複製,不需要網路傳輸(磁碟比100Mb的乙太網快三倍)。master確認C’的副本都建立完畢後才會回覆客戶端,客戶端只是略微感到了一點延遲,隨後它會對C及其副本執行正常的寫入操作。

Master操作

所有的名稱空間操作都由master執行。而且,它還負責管理所有chunk副本,貫穿整個系統始終:它需要做出佈置決策、建立新chunk及其副本,協調控制各種系統級別的活動,比如保持chunk的複製級別、均衡所有chunkserver的負載,以及回收無用儲存。下面我們就各個主題展開討論。

名稱空間管理和鎖

很多master操作會花費較長時間:比如一個快照操作需要撤回很多chunkserver的租賃。因此master操作必須能夠同時併發的執行以提高效率,但是又要避免它們產生的衝突。為此我們提供了名稱空間的區域鎖機制,來保證在某些點的序列,避免衝突。

不像傳統的檔案系統,GFS沒有目錄的listFiles功能。也不支援檔案或者目錄的別名(也就是軟連結、硬連結、快捷方式)。master中的名稱空間邏輯上可以理解為一個lookup table,其中包含完整的路徑名到元資料的對映。並且利用字首壓縮提高其效率。名稱空間樹的每個節點(無論一個絕對檔名或者一個絕對目錄名)都有一個對應的讀寫鎖。

每個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上的鎖衝突,快照和建立新檔案操作會序列執行。GFS中的目錄比標準檔案API要弱化(不支援listFiles等),沒有類似的inode資訊需要維護,所以在建立、刪除檔案時不會修改此檔案上級目錄的結構資料,建立/home/user/foo時也不需要申請父目錄/home/user的寫鎖。上述例子中申請/home/user的讀鎖可以保護此目錄不被刪除。

通過名稱空間鎖可以允許在相同目錄發生併發的變化。比如多個檔案在同一個目錄被併發建立:每個建立會申請此目錄的讀鎖和各自檔案的寫鎖,不會導致衝突。目錄的讀鎖可以保護在建立時此目錄不會被刪除、重新命名或者執行快照。對相同檔案的建立請求,由於寫鎖的保護,也只會導致此檔案被序列的建立兩次。

因為名稱空間的節點不少,全量分配讀寫鎖有點浪費資源,所以它們都是lazy分配、用完即刪。而且鎖申請不是隨意的,為了防止死鎖,一個操作必須按特定的順序來申請鎖:首先按名稱空間樹的層級排序,在相同層級再按字典序。

副本佈置

GFS叢集是高度分散式的,而且有多個層級(層級是指:機房/機架/伺服器這樣的層級結構)。通常會在多個機架上部署幾百個chunkserver。這些chunkserver可能被各機架的幾百個客戶端訪問。不同機架之間的機器通訊可能跨一個或多個網路交換機。進出一個機架的頻寬可能會低於機架內所有機器的總頻寬。多級分散式要求我們更加合理的分佈資料,以提高可擴充套件性、可靠性和可用性。

chunk副本的佈置策略主要遵循兩個目標:最大化資料可靠性和可用性,最大化網路頻寬利用。僅僅跨機器的冗餘副本是不夠的,這僅僅能防禦磁碟或者機器故障,也只考慮到單臺機器的網路頻寬。我們必須跨機架的冗餘chunk副本。這能保證系統仍然可用即使整個機架損壞下線(比如網路交換機或者電力故障)。而且能按機架的頻寬來分攤讀操作的流量。不過這會導致寫流量被髮往多個機架,這一點犧牲我們可以接受。

建立、重複制、重負載均衡

chunk副本會在三個情況下被建立:chunk建立、restore、重負載均衡

當master建立一個chunk,它需要選擇在哪些chunkserver上佈置此chunk的初始化空副本。選擇過程主要會考慮幾個因素。1 儘量選擇那些磁碟空間利用率低於平均值的chunkserver。這樣長此以往可以均衡各chunkserver的磁碟使用率。2 我們不希望讓某臺chunkserver在短時間內建立過多副本。儘管建立本身是廉價的,但它預示著即將來臨的大量寫流量(客戶端請求建立chunk就是為了向其寫入),而且據我們觀察它還預示著緊隨其後的大量讀操作。3 上面論述過,我們想要跨機架的為chunk儲存副本。

master需要關注chunk的複製級別是否達標(每個chunk是否有足夠的有效副本),一旦不達標就要執行restore操作為其補充新副本。很多原因會導致不達標現象:比如某個chunkserver不可用了,某個副本可能腐化了,某個磁碟可能不可用了,或者是使用者提高了複製級別。restore時也要按優先順序考慮幾個因素。第一個因素是chunk低於複製標準的程度,比如有兩個chunk,一個缺兩份副本、另一個只缺一份,那必須先restore缺兩份的。第二,我們會降低已被刪除和曾被刪除檔案對應chunk的優先順序。最後,我們會提高可能阻塞客戶端程序的chunk的優先順序。

master選擇高優先順序的chunk執行restore時,只需指示某些chunkserver直接從一個已存在的合法副本上拷貝資料並建立新副本。選擇哪些chunkserver也是要考慮佈置策略的,其和建立時的佈置策略類似:儘量均衡的利用磁碟空間、避免在單臺chunkserver上建立過多活躍的chunk副本、以及跨機架。restore會導致整個chunk資料在網路上傳輸多次,為了儘量避免影響,master會限制整個叢集以及每臺chunkserver上同時執行的restore數量,不會在短時間執行大量的restore。而且每個chunkserver在拷貝源chunkserver的副本時也會採用限流等措施來避免佔用過多網路頻寬。

重負載均衡是指:master會檢查當前的副本分佈情況,為了更加均衡的磁碟空間利用率和負載,對必要的副本執行遷移(從負擔較重的chunkserver遷移到較輕的)。當新的chunkserver加入叢集時也是依靠這個活動來慢慢的填充它,而不是立刻讓它接收大量的寫流量。master重新佈置時不僅會考慮上述的3個標準,還要注意哪些chunkserver的空閒空間較低,優先為其遷移和刪除。

垃圾回收

在一個檔案被刪除後,GFS不會立刻回收物理儲存。它會在懶惰的、延遲的垃圾回收時才執行物理儲存的回收。我們發現這個方案讓系統更加簡單和可靠。

機制

  • 當一個檔案被應用刪除時,master立刻列印刪除操作的日誌,然而不會立刻回收資源,僅僅將檔案重新命名為一個隱藏的名字,包含刪除時間戳。在master對檔案系統名稱空間執行常規掃描時,它會刪除任何超過3天的隱藏檔案(週期可配)。在那之前此隱藏檔案仍然能夠被讀,而且只需將它重新命名回去就能恢復。當隱藏檔案被刪除時,它才在記憶體中元資料中被清除,高效的切斷它到自己所有chunk的引用。

  • 在另一個針對chunk名稱空間的常規掃描中,master會識別出孤兒chunk(也就是那些任何檔案都不會引用的chunk),並刪除它們的元資料。在與master的心跳訊息交換中,每個chunkserver都會報告它的一個chunk子集,master會回覆哪些chunk已經不在其元資料中了,chunkserver於是刪除這些chunk的副本。

討論

  • 儘管分散式垃圾回收是一個困難的問題,它需要複雜的解決方案,但是我們的做法卻很簡單。master的“檔案到chunk對映”中記錄了對各chunk引用資訊。我們也能輕易的識別所有chunk副本:他們是在某臺chunkserver上、某個指定的目錄下的一個Linux檔案。任何master沒有登記在冊的副本都可以認為是垃圾。

  • 我們的垃圾回收方案主要有三點優勢。首先,它保證了可靠性的同時也簡化了系統。chunk建立操作可能在一些chunkserver成功了、在另一些失敗了,失敗的也有可能是建立完副本之後才失敗,如果對其重試,就會留下垃圾。副本刪除訊息也可能丟失,master是否需要嚴謹的關注每個訊息並保證重試?垃圾回收提供了一個統一的可依靠的方式來清理沒有任何引用的副本,可以讓上述場景少一些顧慮,達到簡化系統的目的。其次,垃圾回收的邏輯被合併到master上各種例行的後臺活動中,比如名稱空間掃描,與chunkserver的握手等。所以它一般都是批處理的,花費也被大家分攤。而且它只在master相對空閒時執行,不影響高峰期master的快速響應。第三,延遲的回收有時可挽救偶然的不可逆的刪除(比如誤操作)。

  • 在我們的實驗中也遇到了延遲迴收機制的弊端。當應用重複的建立和刪除臨時檔案時,會產生大量不能被及時回收的垃圾。針對這種情況我們在刪除操作時會主動判斷此檔案是否是首次刪除,若不是則主動觸發一些回收動作。與複製級別類似,不同的名稱空間區域可配置各自的回收策略。

舊副本偵測

當chunkserver故障,錯過對chunk的變異時,它的版本就會變舊。master會為每個chunk維護一個版本號來區分最新的和舊的副本。

每當master授予一個新的租賃給某個chunk,都會增長chunk版本號並通知各副本。master和這些副本都持久化記錄新版本號。這些都是在寫操作被處理之前就完成了。如果某個副本當前不可用,它的chunk版本號不會被更新。master可以偵測到此chunkserver有舊的副本,因為chunkserver重啟時會彙報它的chunk及其版本號資訊。如果master看到一個比自己記錄的還要高的版本號,它會認為自己在授予租賃時發生了故障,繼而認為更高的版本才是最新的。

master會在常規垃圾回收活動時刪除舊副本。在那之前,它只需保證回覆給客戶端的資訊中不包含舊副本。不僅如此,master會在各種與客戶端、與chunkserver的其他互動中都附帶上版本號資訊,儘可能避免任何操作、活動訪問到舊的副本。

故障容忍和診斷

我們最大挑戰之一是頻繁的元件故障。GFS叢集中元件的質量(機器質量較低)和數量(機器數量很多)使得這些問題更加普遍:我們不能完全的信賴機器,也不能完全信賴磁碟。元件故障能導致系統不可用甚至是腐化的資料。下面討論我們如何應對這些挑戰,以及我們構建的幫助診斷問題的工具。

高可用性

  • GFS叢集中有幾百臺機器,任何機器任何時間都可能不可用。我們保持整體系統高度可用,只用兩個簡單但是高效的策略:快速恢復和複製。

快速恢復

  • master和chunkserver都可以在幾秒內重啟並恢復它們的狀態。恢復的時間非常短,甚至只會影響到那些正在執行中的未能回覆的請求,客戶端很快就能重連到已恢復的伺服器。

chunk複製

  • 早先討論過,每個chunk會複製到多個機架的chunkserver上。使用者能為不同的名稱空間區域指定不同的複製級別。預設是3份。master需要保持每個chunk是按複製級別完全複製的,當chunkserver下線、偵測到腐化副本時master都要補充新副本。儘管複製機制執行的挺好,我們仍然在開發其他創新的跨伺服器冗餘方案。

master複製

  • master儲存的元資料狀態尤其重要,它必須被冗餘複製。其操作日誌和存檔會被複制到多臺機器。只有當元資料操作的日誌已經成功flush到本地磁碟和所有master副本上才會認為其成功。所有的元資料變化都必須由master負責執行,包括垃圾回收之類的後臺活動。master故障時,它幾乎能在一瞬間完成重啟。如果它的機器或磁碟故障,GFS之外的監控設施會在另一臺冗餘機器上啟用一個新master程序(此機器儲存了全量的操作日誌和存檔)。客戶端是通過canonical域名(比如gfs-test)來訪問master的,這是一個DNS別名,對其做些手腳就能將客戶端引導到新master。

  • 此外我們還提供了陰影master,它能在master宕機時提供只讀訪問。他們是陰影,而不是完全映象,陰影會比主master狀態落後一秒左右。如果檔案不是正在發生改變,或者應用不介意拿到有點舊的結果,陰影確實增強了系統的可用性。而且應用不會讀取到舊的檔案內容,因為檔案內容是從chunkserver上讀取的,最多隻會從陰影讀到舊的檔案元資料,比如目錄內容或者訪問控制資訊。

  • 陰影master會持續的讀取某個master副本的操作日誌,並重放到自己的記憶體中資料結構。和主master一樣,它也是在啟動時拉取chunkserver上的chunk位置等資訊(不頻繁),也會頻繁與chunkserver交換握手訊息以監控它們的狀態。僅僅在master決定建立或刪除某個master副本時才需要和陰影互動(陰影需要從它的副本里抓日誌重放)。

資料完整性

每個chunkserver使用checksum來偵測腐化的儲存資料。一個GFS叢集經常包含幾百臺伺服器、幾千個磁碟,磁碟故障導致資料腐化或丟失是常有的事兒。我們能利用其他正常的chunk副本恢復腐化的資料,但是通過跨chunkserver對比副本之間的資料來偵測腐化是不切實際的。另外,各副本內的位元組資料出現差異也是正常的、合法的(原子的record append就可能導致這種情況,不會保證完全一致的副本,但是不影響客戶端使用)。因此,每個chunkserver必須靠自己來核實資料完整性,其對策就是維護checksum。

一個chunk被分解為多個64KB的塊。每個塊有對應32位的checksum。像其他元資料一樣,checksum被儲存在記憶體中,並用利用日誌持久化儲存,與使用者資料是隔離的。

在讀操作中,chunkserver會先核查讀取區域涉及的資料塊的checksum。因此chunkserver不會傳播腐化資料到客戶端(無論是使用者客戶端還是其他chunkserver)。如果一個塊不匹配checksum,chunkserver向請求者明確返回錯誤。請求者收到此錯誤後,將向其他副本重試讀請求,而master則會盡快從其他正常副本克隆資料建立新的chunk。當新克隆的副本準備就緒,master命令發生錯誤的chunkserver刪除異常副本。

checksum對讀效能影響不大。因為大部分讀只會跨幾個塊,checksum的資料量不大。GFS客戶端程式碼在讀操作中可以儘量避免跨越塊的邊界,進一步降低checksum的花費。而且chunkserver查詢和對比checksum是不需要任何I/O的,checksum的計算通常也在I/O 等待時被完成,不爭搶CPU資源。

checksum的計算是為append操作高度優化的,因為append是我們的主要應用場景。append時可能會修改最後的塊、也可能新增塊。對於修改的塊只需增量更新其checksum,對於新增塊不管它有沒有被填滿都可以計算其當前的checksum。對於最後修改的塊,即使它已經腐化了而且append時沒有檢測到,還對其checksum執行了增量更新,此塊的checksum匹配依然會失敗,在下次被讀取時即能偵測到。

普通的寫操作則比append複雜,它會覆蓋重寫檔案的某個區域,需要在寫之前檢查區域首尾塊的checksum。它不會建立新的塊,只會修改老的塊,而且不是增量更新。對於首尾之間的塊沒有關係,反正是被全量的覆蓋。而首尾塊可能只被覆蓋了一部分,又不能增量更新,只能重新計算整個塊的checksum,覆蓋老checksum,此時如果首尾塊已經腐化,就無法被識別了。所以必須先檢測後寫。

在系統較空閒時,chunkserver會去掃描和檢查不太活躍的chunk。這樣那些很少被讀的chunk也能被偵測到。一旦腐化被偵測到,master會為其建立一個新副本,並刪除腐化副本。GFS必須保證每個chunk都有足夠的有效副本以防不可逆的丟失,chunk不活躍可能會導致GFS無法察覺它的副本異常,此機制可以有效的避免這個風險。

診斷工具

大量詳細的診斷日誌對於問題隔離、除錯、和效能分析都能提供無法估量的價值,列印日誌卻只需要非常小的花費。如果沒有日誌,我們永遠捉摸不透那些短暫的、不可重現的機器間互動。GFS伺服器生成的診斷日誌儲存了很多重要的事件(比如chunkserver的啟動和關閉)以及所有RPC請求和回覆。這些診斷日誌能被自由的刪除而不影響系統正確性。然而我們會盡一切可能儘量儲存這些有價值的日誌。

RPC日誌包含了在線上每時每刻發生的請求和回覆,除了讀寫的真實檔案資料。通過在不同機器之間匹配請求和回覆、整理RPC記錄,我們能重現整個互動歷史,以便診斷問題。日誌也能服務於負載測試和效能分析的追蹤。

日誌造成的效能影響很小(與收益相比微不足道),可以用非同步緩衝等各種手段優化。有些場景會將大部分最近的事件日誌儲存在機器記憶體中以供更嚴格的線上監控。

擴充套件閱讀

“GFS……也支援小檔案,但是不需要著重優化”,這是論文中的一句原話,初讀此文時還很納悶,GFS不是據說解決了海量小檔案儲存的難題嗎,為何前後矛盾呢?逐漸深讀才發現這只是個小誤會。下面譯者嘗試在各個視角將GFS、TFS、Haystack進行對比分析,讀者可結合前文基礎,瞭解箇中究竟。

願景和目標

GFS的目標可以一言以蔽之:給使用者一個無限容量、放心使用的硬碟,快速的存取檔案。它並沒有把自己定位成某種特定場景的檔案儲存解決方案,比如小檔案儲存或圖片儲存,而是提供了標準的檔案系統API,讓使用者像使用本地檔案系統一樣去使用它。這與Haystack、TFS有所不同,GFS的目標更加通用、更加針對底層,它的程式設計介面也更加標準化。

比如使用者在使用Haystack存取圖片時,可想而知,程式設計介面中肯定會有類似create(photo)、read(photo_id)這樣的介面供使用者使用。而GFS給使用者提供的介面則更類似File、FileInputStream、FileOutputStream(以Java語言舉例)這樣的標準檔案系統介面。對比可見,Haystack的介面更加高層、更加抽象,更加貼近於應用,但可能只適合某些定製化的應用場景;GFS的介面則更加底層、更加通用,標準檔案系統能支援的它都能支援。舉個例子,有一萬張圖片,每張100KB左右,用Haystack、GFS儲存都可以,用Haystack更方便,直接有create(photo)這樣的介面可以用,呼叫即可;用GFS就比較麻煩,你需要自己考慮是存成一萬張小檔案還是組裝為一些大檔案、按什麼格式組裝、要不要壓縮……GFS不去管這些,你給它什麼它就存什麼。假如把一萬張圖片換成一部1GB的高清視訊AVI檔案,總大小差不多,一樣可以放心使用GFS來儲存,但是Haystack可能就望而卻步了(難道把一部電影拆散放入它的一個個needle?)。

這也回答了剛剛提到的那個小誤會,GFS並不是缺乏對小檔案的優化和支援,而是它壓根就沒有把自己定位成小檔案儲存系統,它是通用的標準檔案系統,它解決的是可靠性、可擴充套件性、存取效能等後顧之憂,至於你是用它來處理大檔案、儲存增量資料、打造一個NoSQL、還是解決海量小檔案,那不是它擔心的問題。只是說它這個檔案系統和標準檔案系統一樣,也不喜歡數量太多的小檔案,它也建議使用者能夠將資料合理的組織安排,放入有結構有格式的大檔案中,而不要將粒度很細的一條條小資料儲存為海量的小檔案。 相反,Haystack和TFS則更注重實用性,更貼近應用場景,並各自做了很多精細化的定製優化。在通用性和定製化之間如何抉擇,前文2.3中Haystack的架構師也糾結過,但是可以肯定的是,基於GFS,一樣可以設計needle結構、打造出Haystack。

儲存資料結構

這裡的資料結構僅針對真實檔案內容所涉及的儲存資料結構。三者在這種資料結構上有些明顯的相同點:

首先,都有一個明確的邏輯儲存單元。在GFS中就是一個Chunk,在Haystack、TFS中分別是邏輯卷和Block。三者都是靠各自的大量邏輯儲存單元組成了一個龐大的檔案系統。設計邏輯儲存單元的理由很簡單——保護真正的物理儲存結構,不被使用者左右。使用者給的資料太小,那就將多個使用者資料組裝進一個邏輯儲存單元(Haystack將多個圖片作為needle組裝到一個邏輯卷中);使用者給的資料太大,那就拆分成多個邏輯儲存單元(GFS將大型檔案拆分為多個chunk)。總之就是不管來者是大還是小,都要轉換為適應本系統的物理儲存格式,而不按照使用者給的格式。一個分散式檔案系統想要保證自身的效能,它首先要保證自己能基於真實的物理檔案系統打造出合格的效能指標(比如GFS對chunk size=64MB的深入考究),在普通Linux檔案系統上,固定大小的檔案+預分配空間+合理的檔案總數量+合理的目錄結構等等,往往是保證I/O效能的常用方案。所以必須有個明確的邏輯儲存單元。

另一個很明顯的相同點:邏輯儲存單元和物理機器的多對多關係。在GFS中一個邏輯儲存單元Chunk對應多個Chunk副本,副本分佈在多個物理儲存chunkserver上,一個chunkserver為多個chunk儲存副本(所以chunk和chunkserver是多對多關係)。Haystack中的邏輯卷與Store機器、TFS中的Block與DataServer都有這樣的關係。這種多對多的關係很好理解,一個物理儲存機器當然要儲存多個邏輯儲存單元,而一個邏輯儲存單元對應多個物理機器是為了冗餘備份。

三者在資料結構上也有明顯的區別,主要是其程式設計介面的差異導致的。比如在Haystack、TFS的儲存結構中明確有needle這種概念,但是GFS卻不見其蹤影。這是因為Haystack、TFS是為小檔案定製的,小檔案是它們的儲存粒度,是使用者視角下的儲存單元。比如在Haystack中,需要考慮needle在邏輯卷中如何組織檢索等問題,邏輯卷和needle是一對多的關係,一個邏輯卷下有多個needle,某個needle屬於一個邏輯卷。這些關係GFS都不會去考慮,它留給使用者自行解決(上面願景和目標裡討論過了)。

架構元件角色

Haystack一文中的對比已經看到分散式檔案系統常用的架構正規化就是“元資料總控+分散式協調排程+分割槽儲存”。在Haystack中Directory掌管所有應用元資