從 GFS 失敗的架構設計來看一致性的重要性

(本文最初發於https://mp.weixin.qq.com/s/GuJ6VqZJy3ONaVOWvQT9kg,現轉回到自己的博客。)

GFS(Google File System)是Google公司開發的一款分佈式文件系統。在2003年,Google發表一篇論文詳細描述了GFS的架構。GFS,MapReduce,Bigtable並稱爲Google的三架馬車,推動了Google的高速發展。其他互聯公司和開源領域紛紛模仿,構建自己的系統。可以這麼說,GFS,MapReduce,Bigtable引領了互聯網公司的分佈式技術的發展。但GFS架構設計並不是一個完美的架構設計,它有諸多方面的問題,一致性的欠缺就是其中的一個問題。本文探討一下GFS架構設計、分析其在一致性方面的設計不足,並且看一下一致性對分佈式系統的重要性。

我們從GFS的接口設計說起。
接口
GFS採用了人們非常熟悉的接口,但是被沒有實現如POSIX的標準接口。通常的一些操作包括:create, delete, open, close, read, write, record append等。create,delete,open,close和POSIX的接口類似,這裏就不強調說了。

這裏詳細講述一下write, record append提供的語意。

write操作可以將任意長度len的數據寫入到任意指定文件的位置offset.
record append操作可以原子的將len<=16MB的數據寫入到指定文件的末尾,之所以GFS設計這個接口,是因爲record append不是簡單的offset等於文件末尾的write操作。record append是具有原子特性的操作,本文後面會詳細解釋這個原子特性。
write和record append操作都允許多個客戶端併發操作。
架構
GFS架構如下圖。(摘自GFS的論文)

在這裏插入圖片描述

主要的架構部件有GFS client, GFS master, GFS chunkserver。一個GFS集羣包括:一個master,多個chunkserver,集羣可以被多個GFS client訪問。

GFS客戶端(GFS client)是運行在應用(Application)進程裏的代碼,通常以SDK形式存在。
GFS中的文件被分割成固定大小的塊(chunk),每個塊的長度是固定64MB。GFS chunkserver把這些chunk存儲在本地的Linux文件系統中,也就是本地磁盤中。通常每個chunck會被保存3個副本(replica)。一個chunkserver會保存多個塊,每個塊都會有一個標識叫做塊柄(chunk handle)
GFS 主(master)維護文件系統的元數據(metadata),包括名字空間(namespace,也就是常規文件系統的中的文件樹),訪問控制信息,每個文件有哪些chunk構成,chunk存儲在哪個chunkserver上,也就是位置(location)。

在這樣的架構下,文件的讀寫基本過程簡化、抽象成如下的過程:

寫流程:
1.client向master發送create請求,請求包含文件路徑和文件名。master根據文件路徑和文件名,在名字空間裏創建一個對象代表這個文件。
2.client向3個chunkserver發送要寫入到文件中的數據,每個chunkserver收到數據後,將數據寫入到本地的文件系統中,寫入成功後,發請求給master告知master一個chunk寫入成功,與此同時告知client寫入成功。
3.master收到chunkserver寫入成功後,記錄這個chunk與機器之間的對應關係,也就是chunk 的位置。
4.client確認3個chunkserver都寫成功後,本次寫入成功。

這個寫流程是一個高度簡化抽象的流程,實際的寫流程是一個非常複雜的流程,要考慮到寫入類型(即,是隨機寫還是尾部追加),還要考慮併發寫入,後面我們繼續詳細的描述寫流程,解釋GFS是如何處理不同的寫入類型和併發寫入的。

讀流程:
1.應用發起讀操作,指定文件路徑,偏移量(offset)
2.client根據固定的chunk大小(也即64MB),計算出數據在第幾個chunk上,也就是chunk索引號(index)
3.client向master發送一個請求,包括文件名和索引號,master返回3個副本在哪3臺機器上,也就是副本位置(location of replica)。
4.client向其中一個副本的機器發送請求,請求包換塊柄和字節的讀取範圍。
5.chunkserver根據塊柄和讀取範圍從本地的文件系統讀取數據返回給client

這個讀流程未做太多的簡化和抽象,但實際的讀流程還會做一些優化,這些優化和本文主題關係不大就不展開了。

寫流程詳述
我們詳細的講一下寫入流程的幾個細節。

1.名字空間管理(namespace management)和鎖保護(locking)
寫入流程需要向主發送create這樣請求,來操作保存在主上的名字空間。
如果有多個客戶端同時進行寫入操作,那麼這些客戶端也會同時操作向主發送create請求。主在同一時間收到多個請求,通過加鎖的方式,防止多個客戶端同時修改同一個文件的元數據。

2.租約(Lease)
client需要向3個副本寫入數據,在併發的情況下會有多個client同時向3個副本寫入數據。GFS需要一個規則來管理這些數據的寫入。
這個規則簡單來講,每個chunk都只有一個副本來管理多個client的併發寫入。也就是說,對於一個chunk,master會授予其中一個副本一個租約(lease),具有租約的副本來管理所有要寫入到這個chunk的數據寫入,這個具有租約的副本稱之爲首要副本(primary)。其他的副本稱之爲二級副本(secondary)

3.變更次序(Mutation Order)
將對文件的寫入(不管是在任意位置的寫入,還是在末尾追加)稱之爲變更(Mutation)。Primary管理所有client的併發請求,讓所有的請求按照一定順序(Order)應用到chunk上。

4.基本變更流程
1).client詢問master哪個chunkserver持有這個chunk的lease,以及其他的副本的位置。如果沒有副本持有lease,master挑選一個副本,通知這副本它持有lease。
2).master回覆client,告述客戶端首要副本的位置,和二級副本的位置。客戶端聯繫首要副本,如果首要副本無響應或者回復客戶端它不是首要副本,則客戶端重新聯繫主。
3).客戶端向所有的副本推送數據。客戶端可以以任意的順序推送。每個chunkserver會緩存這些數據在緩衝區中。
4).當所有的副本都回復說已經收到數據後,客戶端發送一個寫入請求(write request)給首要副本,這個請求裏標識着之前寫入的數據。首要副本收到請求後,會給寫入分配一個連續的編號,首要副本會按照這個編號的順序,將數據寫入到本地磁盤。
5).首要副本將這個帶有編號寫入請求轉發給其他二級副本,二級副本也會按照編號的順序,將數據寫入本地,並且回覆首要副本數據寫入成功。
6).當首要副本收到所有二級副本的回覆時,說明這次寫入操作成功。
7).首要副本回復客戶端寫入成功。在任意一個副本上遇到的任意錯誤,都會報告給客戶端失敗。

前面講的write接口就是按照這個基本流程進行的。

下圖描述了這個基本過程。(摘自GFS的論文)

在這裏插入圖片描述

5.跨邊界變更
如果一次寫入的數據量跨過了一個塊的邊界,那麼客戶端會把這次寫入分解成向多個chunk的多個寫入。

6.原子記錄追加(Atomic Record Appends)
Record Appends在論文中被稱之爲原子記錄追加(Atomic Record Appends),這個接口也遵循基本的變更流程,有一些附加的邏輯:客戶端把數據推送給所有的副本後,客戶端發請求給首要副本,首要副本收到寫入請求後,會檢查如果把這個record附加在尾部,會不會超出塊的邊界,如果超過了邊界,它把塊中剩餘的空間填充滿(這裏填充什麼並不重要,後面我們會解釋這塊),並且讓其他的二級副本做相同的事,再告述客戶端這次寫入應該在下一塊重試。如果記錄適合塊剩餘的空間,則首要副本把記錄追加尾部,並且告述其他二級副本寫入數據在同樣的位置,最後通知客戶端操作成功。

原子性
講完架構和讀寫流程,我們開始分析GFS的一致性,首先從原子性開始分析。

Write和Atomic Record Append的區別
前面講過,如果一次寫入的數量跨越了塊的邊界,那麼會分解成多個操作,write和record append在處理數據跨越邊界時的行爲是不同的。

我們舉例2個例子來說明一下。
例子1,文件目前有2個chunk,分別是chunk1, chunk2。
Client1要在54MB的位置寫入20MB數據。這寫入跨越了邊界,要分解成2個操作,第一個操作寫入chunk1最後10MB,第二個操作寫入chunk2的開頭10MB。
Client2也要在54MB的位置寫入20MB的數據。這個寫入也跨越邊界,也要分解爲2個操作,作爲第三個操作寫入chunk1最後10MB,作爲第四個操作寫入chunk2的開頭10MB。

2個客戶端併發寫入數據,那麼第一個操作和第三個操作在chunk1上就是併發執行的,第二個操作和第四個操作在chunk2上併發執行,如果chunk1的先執行第一操作再執行第三個操作,chunk2先執行第四個操作再執行第二個操作,那麼最後,在chunk1上會保留client1的寫入的數據,在chunk2上保留了client2的寫入的數據。雖然client1和client2的寫入都成功了,但最後既不是client1想要的結果,也不是client2想要的結果。最後的結果是client1和client2寫入的混合。對於client1和client2來說,他們操作都不是原子的。

例子2,文件目前有2個chunk,分別是chunk1, chunk2。
Client要在54MB的位置寫入20MB數據。這寫入跨越了邊界,要分解成2個操作,第一個操作寫入chunk1最後10MB,第二個操作寫入chunk2的開頭10MB。chunk1執行第一個操作成功了,chunk2執行第二個操作失敗了,也就是寫入的這部分數據,一部分是成功的,一部分是失敗的,這也不是原子操作。

接下來看record append。由於record append最多能寫入16MB的數據,並且當寫入的數據量超過塊的剩餘空間時,剩餘的空間會被填充,這次寫入操作會在下個塊重試,這2點保證了record append操作只會在一個塊上生效。這樣就避免了跨越邊界分解成多個操作,從而也就避免了,寫入的數據一部分成功一部分失敗,也避免了併發寫入數據混合在一起,這2種非原子性的行爲。

GFS原子性的含義
所以,GFS的原子性不是指多副本之間原子性,而是指發生在多chunk上的多個操作的的原子性。

可以得出這樣的推論,如果Write操作不跨越邊界,那麼沒有跨越邊界的write操作也滿足GFS所說的原子性。

GFS多副本不具有原子性
GFS一個chunk的副本之間是不具有原子性的,不具有原子性的副本複製,它的行爲是:
一個寫入操作,如果成功,他在所有的副本上都成功,如果失敗,則有可能是一部分副本成功,而另外一部分失敗。

在這樣的行爲如下,失敗是這樣處理的:
如果是write失敗,那麼客戶端可以重試,直到write成功,達到一致的狀態。但是如果在重試達到成功以前出現宕機,那麼就變成了永久的不一致了。
Record Append在寫入失敗後,也會重試,但是與write的重試不同,不是在原有的offset上重試,而是接在失敗的記錄後面重試,這樣Record Append留下的不一致是永久的不一致,並且還會有重複問題,如果一條記錄在一部分副本上成功,在另外一部分副本上失敗,那麼這次Record Append就會報告給客戶端失敗,並且讓客戶端重試,如果重試後成功,那麼在某些副本上,這條記錄就會成功的寫入2次。
我們可以得出,Record Append保證是至少一次的原子操作(at least once atomic)。
一致性
GFS把自己的一致性稱爲鬆弛的一致性模型(relaxed consistency model)。這個模型分析元數據的一致性和文件數據的一致性,鬆弛主要是指文件數據具有鬆弛的一致性。

元數據的一致性
元數據的操作都是由單一的master處理的,並且操作通過鎖保護,所以是保證原子的,也保證正確性的。

文件數據的一致性
在說明鬆弛一致性模型之前,我們先看看這個模型中的2個概念。對於一個文件中的區域:
如果無論從哪個副本讀取,所有的客戶端都能總是看到相同的數據,那麼就叫一致的(consistent);
在一次數據變更後,這個文件的區域是一致的,並且客戶端可以看到這次數據變更寫入的所有數據,那麼就叫界定的(defined)。

GFS論文中,用下面的這個表格總結了鬆弛一致性:
分別說明表中的幾種情況:
1.在沒有併發的情況下,寫入不會有相互干擾,成功的寫入是界定的,那麼必然也就是一致的
2.在有併發的情況下,成功的寫入是一致的,但不是界定的,也就是我們前面所舉的例子2。
3.如果寫入失敗,那麼副本之間就會出現不一致。
4.Record Append能夠保證是界定的,但是在界定的區域之間夾雜着一些不一致的區域。Record Append會填充數據,不管每個副本是否填充相同的數據,這部分區域都會認爲是inconsistent的。
如何適應鬆弛的一致性模型

這種鬆弛的一致性模型,實際上是一種不能保證一致性的模型,或者更準確的說是一致性的數據中間夾雜不一致數據。

這些夾雜其中的不一致數據,對應用來說是不可接受的。在這種一致性下,應該如何使用GFS那?GFS的論文中,給出了這樣幾條使用GFS的建議:依賴追加(append)而不是依賴覆蓋(overwrite),設立檢查點(checkpointing),寫入自校驗(write self-validating),自記錄標識(self-identifying records)。

使用方式1:只有單個寫入的情況下,按從頭到尾的方式生成文件。
方法1.1:先臨時寫入一個文件,再全部數據寫入成功後,將文件改名成一個永久的名字,文件的讀取方只能通過永久的文件名訪問到這個文件。
方法1.2:寫入方按一定的週期,寫入數據,寫入成功後,記錄一個寫入進度檢查點(checkpoint),這個檢查點包括應用級的校驗數(checksum)。讀取方只去校驗、處理檢查點之前的數據。即便寫入方出現宕機的情況,重啓後的寫入方或者新的寫入方,會從檢查點開始,繼續重新寫入數據,這樣就修復了不一致的數據。

使用方式2:多個寫入併發向一個文件尾部追加,這個使用方式就像是一個生產消費型的消息隊列,多個生產者向一個文件尾部追加消息,消費者從文件讀取消息
方法2.1:使用record append接口,保證數據至少被成功的寫入一次。但是應用需要應對不一致數據,和重複數據。
爲了校驗不一致數據,給每個記錄添加校驗數(checksum),讀取方通過校驗數識別出不一致的數據,並且丟棄不一致的數據。
如果應用讀取數據沒有冪等處理,那麼應用就需要過濾掉重複數據。寫入方寫入記錄時額外寫入一個唯一的標識(identifier),讀取方讀取數據後通過標識辨別之前是否已經處理過這個標識的數據。
GFS的設計哲學
可以看出基於GFS的應用需要通過一些特殊的手段來應對GFS鬆弛的一致性模型帶來的各種問題。GFS的一致性保證對於使用者是非常不友好的,很多人第一次看到這樣的一致性保證都是比較吃驚的。

那麼GFS爲什麼要設計這樣的一致性模型那?GFS在架構上選擇這樣的設計有它自己的設計哲學。GFS追求的是簡單、夠用的原則。GFS主要要解決的問題是如何使用廉價的服務器存儲海量的數據,並且達到非常高的吞吐量(GFS非常好的做到了這2點,但不是本文的主題,這裏就不展開了),並且文件系統本身的實現要簡單,能夠快速的實現出來(GFS的開發者在開發完GFS之後,很快就去開發BigTable了)。GFS很好的完成了完成了這樣的目標。但是留下了一致性問題,給使用者帶來的負擔。但是在GFS應用的前期,一致性不是問題,GFS的主要使用者(BigTable)就是GFS開發者,他們深知應該如何使用GFS,這種不一致在BigTable中被很好屏蔽掉(採用上面所說的方法),BigTable提供了很好的一致性保證。

但是隨着GFS使用的不斷深入,GFS簡單夠用的架構開始帶來很多問題,一致性問題僅僅是其中的一個。主導了GFS很長時間的Leader Sean Quinlan在一次採訪中,詳細說明了在GFS度過了應用的初期之後,這種簡單的架構帶來的各種問題[1]。

開源分佈式文件系統HDFS,清晰的看到了GFS的一致性模型給使用者帶來的不方便,堅定地摒棄了GFS的一致性模型,提供了很好的一致性保證。HDFS達到了怎樣的一致性,這裏就不在展開了,另外再做詳細的討論。

總之,保證一直性對於分佈式文件系統,甚至所有的分佈式系統都是很重要的。

作者簡介
陳東明,餓了麼北京技術中心架構組負責人,負責餓了麼的產品線架構設計以及餓了麼基礎架構研發工作。曾任百度架構師,負責百度即時通訊產品的架構設計。具有豐富的大規模系統構建和基礎架構的研發經驗,善於複雜業務需求下的大併發、分佈式系統設計和持續優化。個人微信公衆號dongming_cdm。

1.GFS: Evolution on Fast-forward, Sean Quinlan, 2009, https://queue.acm.org/detail.cfm?id=1594206

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章