天天看點

Google檔案系統1. 介紹2 設計概覽3 系統互動4. master操作5 故障容忍和診斷6. 測量

GFS這三個字母無需過多修飾,《Google File System》的論文也早有譯版。但是這不妨礙我們加點批注、重溫經典,并結合上篇Haystack的文章,将GFS、TFS、Haystack進行一次全方位的對比,一窺各巨頭的架構師們是如何權衡利弊、各取所需。

1. 介紹

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

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

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

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

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

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

2 設計概覽

2.1 假設

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

•系統是建構在很多廉價的、普通的元件上,元件會經常發生故障。它必須不間斷監控自己、偵測錯誤,能夠容錯和快速恢複。

•系統存儲了适當數量的大型檔案,我們預期幾百萬個,每個通常是100MB或者更大,即使是GB級别的檔案也需要高效管理。也支援小檔案,但是不需要着重優化。

•系統主要面對兩種讀操作:大型流式讀和小型随機讀。在大型流式讀中,單個操作會讀取幾百KB,也可以達到1MB或更多。相同用戶端發起的連續操作通常是在一個檔案讀取一個連續的範圍。小型随機讀通常在特定的偏移位置上讀取幾KB。重視性能的應用程式通常會将它們的小型讀批量打包、組織排序,能顯著的提升性能。

•也會面對大型的、連續的寫,将資料append到檔案。append資料的大小與一次讀操作差不多。一旦寫入,幾乎不會被修改。不過在檔案特定位置的小型寫也是支援的,但沒有着重優化。

•系統必須保證多用戶端對相同檔案并發append的高效和原子性。我們的檔案通常用于制造者消費者隊列或者多路合并。幾百個機器運作的制造者,将并發的append到一個檔案。用最小的同步代價實作原子性是關鍵所在。檔案被append時也可能出現并發的讀。

•持久穩定的帶寬比低延遲更重要。我們更注重能夠持續的、大批量的、高速度的處理海量資料,對某一次讀寫操作的回複時間要求沒那麼嚴格。

2.2 接口

  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讨論。

2.3 架構

  一個GFS叢集包含單個master和多個chunkserver,被多個用戶端通路,如圖1所示。圖1中各元件都是某台普通Linux機器上運作在使用者級别的一個程序。在同一台機器上一起運作chunkserver和用戶端也很容易,隻要機器資源允許。

Google檔案系統1. 介紹2 設計概覽3 系統互動4. master操作5 故障容忍和診斷6. 測量

  檔案被劃分為固定大小的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緩存已經儲存了頻繁通路的檔案。

2.4 單一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互動,幾乎不會導緻額外的花費。

2.5 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模式~)。

2.6 中繼資料

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

2.6.1 記憶體中資料結構

  因為中繼資料存儲在記憶體中,master可以很快執行中繼資料操作。而且可以簡單高效的在背景周期性掃描整個中繼資料狀态。周期性的掃描作用很多,有些用于實作chunk垃圾回收,有些用于chunkserver故障導緻的重新複制,以及為了均衡各機器負載與磁盤使用率而執行的chunk遷移。章節4.3和4.4将讨論其細節。

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

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

2.6.2 Chunk位置

  master不會持久化的儲存哪個chunkserver有哪些chunk副本。它隻是在自己啟動時拉取chunkserver上的資訊(随後也會周期性的執行拉取)。master能保證它自己的資訊時刻都是最新的,因為它控制了所有的chunk布置操作,并用正常心跳消息監控chunkserver狀态。

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

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

2.6.3 記錄檔

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

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

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

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

  恢複中繼資料時,僅僅需要最後完成的存檔和其後産生的日志。老的存檔和日志檔案能被自由删除,不過我們保險起見不會随意删除。在存檔期間如果發生故障(存檔檔案爛尾了)也不會影響正确性,因為恢複代碼能偵測和跳過未完成的存檔。

Google檔案系統1. 介紹2 設計概覽3 系統互動4. master操作5 故障容忍和診斷6. 測量

2.7 一緻性模型

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

2.7.1 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的對策是版本偵測機制,利用版本偵測踢除異常的副本。

2.7.2 對應用的啟示

  在使用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雖然沒有直接在系統層面解決上述難以避免的一緻性問題,但是上面提到的解決方案都會作為共享代碼庫供大家使用。

3 系統互動

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

3.1 租賃和變異順序

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

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

Google檔案系統1. 介紹2 設計概覽3 系統互動4. master操作5 故障容忍和診斷6. 測量

  圖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描述的)。

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

3.2 資料流

  我們将資料流和控制流解耦來更高效的利用網絡。從上述控制流的分析中可以看出,從用戶端到首要副本然後到所有次級副本,請求是沿着一個小心謹慎的鍊路、像管道一樣,在各個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。

3.3 原子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讨論過)。

3.4 快照

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

  與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及其副本執行正常的寫入操作。

4. master操作

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

4.1 命名空間管理和鎖

  很多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配置設定、用完即删。而且鎖申請不是随意的,為了防止死鎖,一個操作必須按特定的順序來申請鎖:首先按命名空間樹的層級排序,在相同層級再按字典序。

4.2 副本布置

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

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

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

  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的空閑空間較低,優先為其遷移和删除。

4.4 垃圾回收

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

4.4.1 機制

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

  在另一個針對chunk命名空間的正常掃描中,master會識别出孤兒chunk(也就是那些任何檔案都不會引用的chunk),并删除它們的中繼資料。在與master的心跳消息交換中,每個chunkserver都會報告它的一個chunk子集,master會回複哪些chunk已經不在其中繼資料中了,chunkserver于是删除這些chunk的副本。

4.4.2 讨論

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

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

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

4.5 舊副本偵測

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

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

  master會在正常垃圾回收活動時删除舊副本。在那之前,它隻需保證回複給用戶端的資訊中不包含舊副本。不僅如此,master會在各種與用戶端、與chunkserver的其他互動中都附帶上版本号資訊,盡可能避免任何操作、活動通路到舊的副本。

5 故障容忍和診斷

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

5.1 高可用性

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

5.1.1 快速恢複

  master和chunkserver都可以在幾秒内重新開機并恢複它們的狀态。恢複的時間非常短,甚至隻會影響到那些正在執行中的未能回複的請求,用戶端很快就能重連到已恢複的伺服器。

5.1.2 chunk複制

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

5.1.3 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副本時才需要和陰影互動(陰影需要從它的副本裡抓日志重放)。

5.2 資料完整性

  每個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無法察覺它的副本異常,此機制可以有效的避免這個風險。

5.3 診斷工具

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

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

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

6. 測量

  這一章節,我們用一個小型測試來展現GFS系統架構和實作的固有瓶頸,還有些來自Google使用的真實的叢集的數字。

6.1 小規模測試

  我們用一個包含1個主伺服器,2個主伺服器副本,16個塊伺服器,和16個客戶機組成的GFS叢集測量GFS的性能。注意設定這個配置隻是為了易于測試。典型的叢集有數百個塊伺服器,數百個客戶機。

  所有機器的配置是,雙PIII 1.4GHz處理器,2GB記憶體,兩個80G,5400rpm硬碟,以及100Mbps全雙工以太網連接配接到HP2524交換機。所有19個GFS伺服器連接配接在一個交換機,所有16個用戶端連接配接在另一個上。兩個交換機用1Gbps的線路連接配接。

6.1.1 讀取

  N個客戶機從檔案系統同步讀取資料。每個客戶機從320GB的檔案集讀取随機選擇的4MB區域的内容。這操作重複256次,這樣每個客戶機最後會讀取1GB的資料。所有的塊伺服器一共僅有32GB的記憶體,是以我們期望至少10%的請求命中Linux的緩沖。我們的結果應該接近一個幾乎無緩沖的結果。

  

Google檔案系統1. 介紹2 設計概覽3 系統互動4. master操作5 故障容忍和診斷6. 測量

  圖3(a)顯示了N個客戶機的整合讀取速度以及它的理論極限。當兩個交換機之間使用1Gbps的連接配接的時候整合讀取的理論極限的頂峰是125MB/s,或者是當客戶機的100Mbps網絡飽和的時候每個客戶機12.5MB/s。實測結果是當一個客戶機讀的時候,讀取速度10M/s,或者說客戶機極限的80%。16個客戶機讀取的時候,整合的讀取速度達到了94MB/s,大約是連接配接極限的75%,或者說每個客戶機6MB/s。效率從80%降低到75%,主要原因是讀取者增加了,多個讀取者同時讀取一個塊伺服器的幾率也提高了。

6.1.2 寫入

  N個客戶機同步寫入N個不同的檔案。每個客戶機用一次1MB的速度寫入1GB的資料。整合的寫入速度和它們的理論極限顯示在圖3(b)。理論極限的頂峰是67MB/s,因為我們需要把每一個位寫入到16個塊伺服器中的3個裡面,每個擁有12.5MB/s的輸入連接配接。

  一個客戶機的寫入速度是6.3MB,大概是極限的一半。主要的原因是我們的網絡結構。它和我們推送資料到塊伺服器使用的管道模式不相适應。從一個副本到另一個副本之間傳輸資料的延遲降低了整個寫入速度。

  16個客戶機并行寫入的速度達到了35MB/s(每個客戶機2.2MB/s),大約是理論值的一半。跟讀取的案例很相似,一旦客戶機的數量增加,就會有更多的客戶機同時寫入同一個塊伺服器。而且,16個寫入者的并行沖突比16個讀取者要大得多,因為每個寫入都與三個不同的副本相關。

  寫入比我們預想的要慢。但是在實踐中,這沒有成為我們的主要問題,因為即使多個客戶機會增加寫入的延遲,但是在我們擁有大量客戶機的系統中,它沒有對整合的寫入帶寬造成顯著的影響。

6.1.3 記錄追加

  圖3(c)顯示了記錄追加的性能。N個客戶機同步追加到一個檔案。性能受限于儲存最後一個塊的塊伺服器的帶寬,與客戶機數量無關。由一個客戶機6.0MB/s開始,直到6個客戶機4.8MB/s結束,主要原因是不同客戶機看到的擁擠和混亂造成的。

  我們的程式傾向于同時處理多個這樣的檔案。換句話說,N個客戶機同時追加到M個共享檔案,N和M都是數十或者數百以上。是以,塊伺服器的網絡擁擠在我們經驗中沒有成為實踐中的一個顯著問題,因為客戶機在塊伺服器某個檔案繁忙的時候,優先寫入另外的一個檔案。

6.2 真實的叢集

  現在我們審視Google正在使用的兩個叢集,它們具有一定的代表性。叢集A通常用于研究和開發,被上百工程師使用。典型的任務是被人工初始化,然後運作數個小時。它讀取從數MB到數TB的資料,轉化或者分析資料,然後把結果寫回到叢集中。叢集B主要用于産品的資料處理。任務持續更長的時間,持續生成和處理數TB的資料集,隻有非常偶然的人工參與。在兩個案例中,一個”任務”都包含了運作在多個機器上的多個程序,同時讀取和寫入多個檔案。

6.2.1 存儲

  正如表格中前五個條目顯示的,這兩個叢集都有數百個塊伺服器,提供數TB的硬碟空間,有相當的資料,但是都沒完全塞滿。”已用空間”包括所有的塊副本。幾乎所有的檔案都被複制了三次。它們分别存儲了18TB和52TB的檔案資料。

  這兩個叢集有類似數目的檔案數,雖然叢集B有大量的死檔案,改名的檔案,已經删除或者被新版本替換的檔案,但是它們仍舊被存儲沒有被回收。它有更多的塊,因為它的檔案更大。

6.2.2 中繼資料

  所有塊伺服器一共儲存了數十GB的中繼資料,大多數是使用者資料的64KB的校驗和。另外的,儲存在塊伺服器僅有的中繼資料是塊版本号,在4.5章節我們曾經讨論過。

  儲存在主伺服器的中繼資料更小,隻有數十MB,平均一個檔案100個位元組。這驗證了我們的論斷,那就是在實際中主伺服器的記憶體大小不是局限系統能力的主要因素。每個檔案中繼資料最大的部分是以字首壓縮形式儲存的檔案名。中繼資料的其他部分包括,檔案的擁有者和權限,檔案到塊的映射,每個塊的目前版本。另外,我們儲存每個塊的目前副本位置,以及一個用來實作copy-on-write技術的引用計數。

  每個分别的伺服器,不管塊伺服器還是主伺服器,都隻有50到100MB的中繼資料。是以恢複會很快:在伺服器可以回複請求之前,隻需要幾秒鐘去從硬碟讀取這些中繼資料。然而,伺服器可能會不穩定一會兒–通常是30-60秒–知道它從所有塊伺服器獲得塊位置資訊。

6.2.3 讀寫速度

  表3展示不同時間階段的讀寫速度。在測量之前,兩個叢集都運作了一個星期以上。(最近叢集都重新開機過,更新了最新版本的GFS。)

Google檔案系統1. 介紹2 設計概覽3 系統互動4. master操作5 故障容忍和診斷6. 測量

  一重新開機動平均寫入速度就低于30MB。進行測試時,叢集B正處于100MB/s資料帶來的寫入高峰之中,産生了300MB/s的網絡負擔,因為寫入要傳送到三個副本。

  讀取速度比寫入速度高許多。正像我們預期的那樣,總的負載中包括的讀取比寫入要多。兩個叢集都處在沉重的讀取負擔中。實踐中,之前的一個星期叢集A都保持了580MB/s的讀取速度。它的網絡設定可以支援750MB,是以它的資源使用效率很高。叢集B可以支援1300MB的峰值讀取速度,但是它的程式隻使用了380MB。

6.2.4 主伺服器負載

  表3顯示了發送操作的速度大概是每秒鐘200到500個操作。主伺服器可以輕松的保持這個速度,是以這不是負載中的瓶頸。

  在早期版本的GFS中,主伺服器偶爾會成為負載中的瓶頸。它花費了大量的時間順序掃描大目錄(包含數萬個檔案)尋找某個特點的檔案。我們改變了主伺服器資料結構提高名稱空間二進制搜尋的效率。現在可以輕松的支援每秒鐘數千次檔案通路。如果需要的話,我們可以通過在名稱空間資料結構之前假設名稱查詢緩沖的方式進一步提高速度。

6.2.5 恢複時間

  一個塊伺服器失效後,有些塊的副本數量可能過低,必須被克隆以恢複它們的複制水準。恢複所有這樣的塊需要的時間取決于資源的總量。在我們的實驗中,我們殺死叢集B裡面的一台塊伺服器。這個塊伺服器有15000個塊,包含600GB的資料。為了限制對正在運作的程式的幹擾,為一些定期任務提供餘地,我們的預設參數限制叢集中最多有91個并行的克隆操作(塊伺服器數量的40%),每個克隆操作的速度可以是6.25MB(50Mbps)。所有的塊會在23.2分鐘内恢複,複制的速度是440MB/s。

  在另外的實作中,我們殺死兩個塊伺服器,每個大概有16000個塊和660GB資料。這個雙倍的失效,造成266個塊隻有一個副本。這266個塊被優先複制,在2分鐘内所有都恢複到至少兩個副本,這樣就把叢集推到一個狀态,可以容忍另外的塊伺服器失效,不會造成資料丢失。

6.3 負載分析

  在這個章節,我們分析兩個GFS叢集負載的構成,這不同于6.2章節讨論的内容。叢集X用于研究和開發,叢集Y用于産品的資料處理。

6.3.1 方法論和警告

  這些結果隻包括客戶機發出的請求,它們可以反映出我們程式在整個檔案系統産生的工作負擔。它們不包括喲那股承載客戶機請求的伺服器間請求,也不包括内部的背景活動,例如轉發的寫操作,以及負載均衡的操作。

  I/O操作的統計資訊基于那些利用真實的GFS伺服器記錄的RPC請求日志重建的資訊。例如,GFS客戶機代碼可能把一個讀操作分成幾個RPC請求來提高并行性。因為我們的通路高度程式化,我們希望任何錯誤都包含在噪聲内。更詳盡的日志可能提高資料的準确性,但是不太可能為了這個去重新編譯和重新啟動數千個正在運作的客戶機,收集那麼多機器的結果太過麻煩。

  小心不要太過于概括我們的工作負載。因為Google完全控制GFS和應用程式,是以應用程式都為GFS優化,而同時GFS也是為這些應用程式設計的。這樣的互相作用可能也存在于一般程式和檔案系統中,但是它們的影響顯然沒有我們的案例那麼嚴重。

6.3.2 塊伺服器負載

Google檔案系統1. 介紹2 設計概覽3 系統互動4. master操作5 故障容忍和診斷6. 測量

  表4 展現了資料大小和操作分布程度的關系。讀取的分布情況呈現出兩個高峰。小的讀取操作(64KB以下)來自位置查詢操作集中的客戶機,它們在巨大檔案中尋找小塊的資料。大的讀取(超過512KB)來自對整個檔案的長連續讀取。

  在叢集Y中有大量的讀取操作完全不傳回資料。我們的程式,尤其是在産品系統裡,經常使用檔案作為生産者-消費者對列。生産者持續的追加檔案而消費者讀取檔案的尾部。偶爾的,不傳回任何資料,因為消費者的速度超過了生産者。叢集X中很少出現這種情況,因為它通常用于短期存在的資料分析任務,而不是長期存活的分布應用程式。

  寫尺寸也呈現雙峰分布。大塊的寫(超過256KB)通常來自寫入者的緩沖。

  

【擴充閱讀】

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

  1. 願景和目标

  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。

  2 存儲資料結構

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

  首先,都有一個明确的邏輯存儲單元。在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都不會去考慮,它留給使用者自行解決(上面願景和目标裡讨論過了)。

  3 架構元件角色

  Haystack一文中的對比已經看到分布式檔案系統常用的架構範式就是“中繼資料總控+分布式協調排程+分區存儲”。在Haystack中Directory掌管所有應用中繼資料、為用戶端指引目标(指引到合适的目标Store機器做合适的讀寫操作,指引過程就是協調排程過程),單個Store機器則負責處理好自身的實體存儲、本地資料讀寫;TFS中NameServer控制所有應用中繼資料、指引用戶端,DataServer負責實體存儲結構、本地資料讀寫。可以看出這個範式裡的兩個角色——協調元件、存儲元件。協調元件負責了中繼資料總控+分布式協調排程,各存儲元件作為一個分區,負責實際的存儲結構和本地資料讀寫。架構上的相同點顯而易見——GFS也滿足此範式,作為協調元件的master、和作為存儲元件的chunkserver。在元件角色的一些關鍵設計上,三者也曾面臨了一些相似的技術難題,比如它們都竭力減少對協調元件的依賴、減少其互動次數,不希望給它造成過大壓力。協調元件為啥顯得這麼脆弱、這麼缺乏安全感呢?從兩篇論文都可以看出,GFS和Haystack的作者非常希望協調元件能夠簡化,其原因很簡單——人如果有兩個大腦那很多事會很麻煩。協調元件可認為是整個系統的大腦,它不停的派發指令、指點迷津;如果大腦有兩個,那用戶端聽誰的、兩個大腦資訊是否需要同步、兩個大腦在指定政策時是否有資源競争……就跟人一樣,這種核心總控的大腦有多個會帶來很多複雜度、一緻性問題,難以承受。即使各平台都有備用協調元件,比如GFS的陰影master,但都隻是作為容災備用,不會線上參與協調。

  4 中繼資料

  雖然Haystack的Directory、TFS的NameServer、GFS的master幹的是同樣的活,但是其在中繼資料問題上也有值得讨論的差異。上篇文章中曾重點介紹了譯者對于全量緩存應用中繼資料的疑惑(Haystack全量緩存所有圖檔的應用中繼資料,而TFS用了巧妙的命名規則),難道GFS不會遇到這個難題嗎?而且此篇文章還反複強調了GFS的master是單例的、純記憶體的,難道它真的不會遭遇單點瓶頸嗎?答案還是同一個:程式設計界面。GFS的程式設計界面決定了即使它整個叢集存了幾百TB的資料,也不會有太多的檔案。對于Haystack和TFS,它們面對的是billions的圖檔檔案,對于GFS,它可能面對的僅僅是一個超巨型檔案,此檔案裡有billions的圖檔資料。也就是說Haystack和TFS要儲存billions的應用中繼資料,而GFS隻需儲存一個。那GFS把工作量丢給誰了?對,丢給使用者了。是以GFS雖然很偉大、很通用、願景很酷,但是對特定應用場景的支援不一定友好,這也是Haystack最終自行定制的原因之一吧。

  同時,這也是GFS敢于設計單點、記憶體型master的原因,它認為一個叢集内的檔案數量不會超過單點master的能力上限。同樣的,GFS也沒有着重介紹檔案系統中繼資料的方案(Haystack文章中反複強調了檔案系統中繼資料牽扯的I/O問題),原因也是一樣:檔案系統中繼資料的I/O問題對于Haystack來說很難纏是因為Haystack要面對海量小檔案,而GFS并不需要。

有了這樣的前提,GFS的master确實可以解脫出來,做更多針對底層、面向偉大願景的努力。比如可以輕輕松松的在單機記憶體中全量掃描所有中繼資料、執行各種政策(如果master受中繼資料所累、有性能壓力、需要多個master分布式協作,那執行這種全局政策就會很麻煩了,各種一緻性問題會撲面而來)。

  不過GFS中會多出一種Haystack和TFS都沒有的中繼資料——檔案命名空間。當你把GFS當做本地磁盤一樣使用時,你需要考慮檔案的目錄結構,一個新檔案産生時是放到一個已有目錄還是建立新目錄、建立新檔案時會不會有另一個請求正在删除父目錄……這些内容是GFS特有的(Haystack和TFS的使用者不需要面對這些内容),是以它考慮了master的命名空間鎖、記錄檔順序等機制,來保護命名空間的更新。

  5 控制流、資料流(以及一緻性模型)

  GFS、Haystack、TFS的控制流大緻相同,其思路不外乎:1 用戶端需要發起一次新請求;2

用戶端詢問協調元件,自己應該通路哪個存儲元件;3 協調元件分析中繼資料,給出答複;4 用戶端拿到答複直接通路指定的存儲元件,送出請求;5

各存儲元件執行請求,傳回結果。

  但是細節上各自有差異,上篇文章提到Haystack是對等結構,用戶端直接面對各對等存儲元件(比如寫入時看到所有實體卷、分别向其寫入),而TFS是主備結構,用戶端隻面對主DataServer,主向備同步資料……GFS在這方面最大的差異在于:

5.1 租賃機制

  回想Haystack,用戶端直接将寫請求提給各個Store機器即可解決問題,GFS也是對等設計,為何要搗鼓出令人費解的租賃機制?不能直接由用戶端分别寫入各個對等chunkserver嗎?這個問題已經在一緻性模型章節讨論過,當時提到了租賃機制是為了保證各個副本按相同順序串行變異,那我們也可以反過來問一句:Haystack、TFS裡為啥沒有遇到這等問題?是它們對一緻性考慮太少了嗎?

這個答案如果要追本溯源,還是要牽扯到程式設計界面的問題:Haystack、TFS中使用者面對的是一個個小圖檔,使用者将圖檔存進去、拿到一個ID,将來隻要保證他拿着此ID依然能找到對應圖檔就行,即使各個副本執行存儲時順序有差别,也絲毫不影響使用者使用(比如Hasytack收到3個并發圖檔A、B、C的存儲請求,送出到3台Store機器,分别存成了ABC、ACB、BAC三種不同的順序,導緻副本内容不一緻,但是當使用者無論拿着A、B、C哪個圖檔ID來查詢、無論查的是哪個Store機器,都能檢索到正确的圖檔,沒有問題;Haystack和TFS的程式設計界面封裝的更進階,GFS在底層遭遇的難題影響不到它們)。而在GFS比較底層的程式設計界面中,使用者面對的是衆多圖檔組裝而成的一整個檔案,如果GFS在不同副本裡存儲的檔案内容不一緻,那就會影響使用者的檢索邏輯,那攤上大事兒了。另外,Haystack、TFS隻支援小檔案的append和remove,而GFS懷抱着偉大的願景,它需要支援對檔案的随機寫,隻有保證順序才能避免同個檔案區域并發随機寫導緻的undefined碎片問題。是以GFS花心思設計租賃機制也就合情合理了。

  談到串行就不得不考慮一下性能問題,串行絕不是大規模并發系統的目标,而隻是一種妥協——對于多核作業系統下的I/O密集型應用,當然是越并行越有益于性能。比如現在有10個并發的append請求,是逐個依次執行,每一個都等待上一個I/O處理完成嗎?這樣每個請求的I/O

Wait時間cpu不就空閑浪費嗎?根據譯者的推測,GFS所謂的串行僅僅應該是理論上的串行(即執行效果符合串行效果),但真正執行時并不會采用加鎖、同步互斥、按順序單線程依次執行等性能低劣的方案。以10個并發append為例,理論上隻需要一個AtomicLong對象,儲存了檔案長度,10個append線程可并發調用其addAndGet(),得到各自不沖突的合法偏移位置,繼而并發執行I/O寫入操作,互不幹擾。再比如對同個檔案的随機偏移寫,也不是要全部串行,隻有在影響了同一塊檔案區域時才需要串行,是以可以減小串行粒度,影響不同區域的請求可并行寫入。通過這樣的技巧,再結合在首要chunkserver上配置設定的唯一序号,GFS應該可以實作高性能的串行效果,尤其是append操作。

  TFS和Haystack的文章之是以沒有過多提及一緻性問題,并不是因為它們不保證,而是在它們的程式設計界面下(面向小檔案的append-only寫),一緻性問題不大,沒有什麼難纏的陷阱。

5.2 資料流分離

  Haystack沒法在資料流上做什麼文章,它的用戶端需要分别寫入各個Store機器;TFS可以利用其主備機制,合理安排master-slave的拓撲位置以優化網絡負載。而GFS則是完全将真實檔案的資料流和控制流解耦分離,在網絡拓撲、最短路徑、最小化鍊式傳輸上做足了文章。系統規模到一定程度時,機房、機架網絡拓撲結構對于整體性能的影響是不容忽略的,GFS在這種底層機制上考慮的确實更加到位。

6 可擴充性和容錯性

  在上篇文章中已經詳盡的讨論了Haystack、TFS是如何實作了優雅的高可擴充性。對于中繼資料不成難題的GFS,當然也具備同樣的實力,其原理就不重複叙述了。值得一提的是,GFS從中繼資料的負擔中解放出來後,它充分利用了自身的優勢,實作了各種全局政策、系統活動,極大的提高了系統的可擴充性及附屬能力,比如restore、重負載均衡等等。而且其并不滿足于簡單的機器層面,還非常深入的考慮了網絡拓撲、機架布置……GFS這些方面确實要領先于同類産品。

  容錯性的對比同樣在上篇文章中較長的描述過,在結構方面GFS的chunkserver也是對等結構(控制流的首次之分與容錯無關),其效果與Haystack的容錯機制類似,無論是機器故障還是機房故障。相對來說,GFS在故障的恢複、偵測、版本問題、心跳機制、協調元件主備等環節介紹的更加細緻,這些細節Haystack沒有怎麼提及。值得一提的差異是GFS在資料完整性上的深入考究,其checksum機制可有效的避免腐化的資料,這一點Haystack、TFS沒有怎麼提及。

7 删除和修改

  很多時候我們會重點關注一個系統是否能删除和修改資料,這是因為當今越來越多架構設計為了追求更高的主流可用性,而犧牲了其他次要的特性。比如你發表一篇微網誌時一瞬間就送出成功,速度快、使用者體驗非常好;結果你發現不小心帶了一句“大概八點二十分發”,傻眼了,到處找不着微網誌修改的按鈕;于是你隻能删除老微網誌,再發一條新的,老微網誌的評論、轉發都丢失了。當你不承認自己發過八點二十分的微網誌時,網友拿出了截圖,你說是PS的,新浪在旁冷笑,你那條八點二十分的微網誌一直在原處,從未被删除,估計要過好幾個小時之後才會真正從磁盤删掉。

  在學習Haystack、TFS、GFS的過程中我們可以看出大家都有這種傾向,原因可能有很多,其中有兩個是比較明顯的。首先,對于存儲的資料結構,增加新資料造成的破壞較小,而删除、修改造成的破壞較大。新增隻是往末尾附加一些新data,而不會影響已有的data;删除則導緻已有data中空出了一塊區域,這塊區域不能就這麼空放着,最低劣的做法是直接将已有data進行大規模位移來填補狹縫,比較婉轉的做法是先不着急等不忙的時候做一次碎片整理;修改會導緻某條資料size發生變化,size變小會導緻狹縫碎片,size變大則空間不足,要擠占後面資料的位置。其次,新增資料可以做到無競争(剛才5.1讨論了GFS的原子append,并發的新增操作影響的是不同的檔案區域,互不幹擾),而删除和修改則很難(并發的删除和修改操作可能影響相同的檔案區域,這就必須有條件競争)。有這兩個原因存在,大家都偏愛append、不直接實作修改和删除,就不難了解了。

  不過GFS依然支援直接的修改功能——随機偏移寫。這并不是因為GFS的架構設計更強大,而是因為它不需要承擔某些責任。比如在Haystack和TFS中,它們需要承擔圖檔在真實檔案中的存儲格式、檢索等責任,當将一個圖檔從100KB修改為101KB時,對存儲格式的破壞是難以承受的,是以它們不支援這種直接的修改,隻能采用删除+新增來模拟修改。而GFS并沒有維護一個檔案内部格式的責任,還是那句話,你交給它什麼它就存什麼。是以使用者會保護自己的檔案内部格式,他說可以寫那就可以寫,GFS并沒有什麼難題,隻需解決好一緻性、defined、統一變異順序等問題即可。

  同樣,GFS的删除與Haystack、TFS的删除,其意義也不同。GFS的删除指的是整個大檔案的删除。Haystack和TFS删除的是使用者視角下的一條資料,比如一張圖檔,它是邏輯存儲單元中的一個entry而已。而GFS的删除則是使用者視角下的一整個檔案,一個檔案就對應了多個邏輯存儲單元(chunk),裡面也包含了海量的應用實體(比如圖檔)。在這種差異的背景下,他們面臨的難題也完全不同。Haystack和TFS面臨的就是剛才提到的“删除會破壞存儲檔案已有資料的格式、造成狹縫碎片”問題,它們的對策就是懶惰軟删除、閑了再整理。而GFS則不會遭遇此難題,使用者在檔案内部删除幾條資料對于GFS來說和随機偏移寫沒啥差別,狹縫碎片也是使用者自己解決。GFS需要解決的是整個檔案被删除,遺留下了大量的chunk,如何回收的問題。相比Haystack和TFS,GFS的垃圾回收其實更加輕松,因為它要回收的是一個個邏輯存儲單元(chunk),一個chunk(副本)其實就是一個真實的Linux檔案,調用file.delete()删了就等于回收了,而不需要擔心檔案内部那些精細的組織格式、空間碎片。

8 其他細節和總結

  GFS還是有很多與衆不同的技巧,比如合理有效的利用記錄檔+存檔來實作中繼資料持久化存儲;比如細粒度、有條不紊的命名空間鎖機制;便捷的快照功能等等。從整體風格來說,GFS偏愛底層上的深入考究,追求标準化通用化的理想,它希望别人把它當成一個普通的檔案系統,無需華麗的産品包裝,而隻是務實的搭建好底層高可靠、高可用、高可擴充的基礎。但是美好的不一定是合适的,通用的不一定是最佳的,Haystack、TFS在追求各自目标的道路上一樣風雨無阻披荊斬棘。這兩篇論文的翻譯和對比,隻希望能将巨頭的架構師們糾結權衡的分岔路口擺到讀者面前,一起感同身受,知其痛,了解其抉擇背後的意義。

繼續閱讀