1. 程式人生 > >聊一聊分散式物件儲存

聊一聊分散式物件儲存

1. 前言

今天來聊聊我正在讀的一本分散式物件儲存的書籍。

前天11月10號,想著京東有滿200-100的活動,就買了一些書,準備沉澱一下。自己打算在分散式系統上搞幾年,所以買的書基本上都是關於分散式儲存的。本身也沒想著買一些分散式系統的經典教材,就隨便選了幾本京東上銷量比較高的,偏實用一些的書籍。權當心血來潮,未經過任何調研。

當前看的這本是《分散式物件儲存-原理,架構和go語言實現》。

2. 總體感覺

書今天上午開始看,還未看完,先說一下總體感覺,從半價角度上來說,這本書物超所值,如果是給有一定程式設計能力且打算入行分散式系統的同學看,那麼就更值得了。對於想自己動手實現一個分散式系統的同學,按照書中的程式碼,一點點的敲,完整實現一個分散式的物件儲存不成問題,應該會有不少的成就感。然而若是給架構師或者研究分散式的同學看,不是很建議,內容顯得過於寬泛且不夠深入(主觀感覺)。書中雖然知識點眾多,但各個知識點有效篇幅內容明顯不足,而且更大的困擾是各種實現層程式碼散佈全書,導致真正在講關於分散式的核心內容就更少了(不過對於一個想自己實現一個系統的同學來說,更多的程式碼反而更好,這就顯得仁者見仁智者見智了)。保守估計關於程式碼以及程式碼的函式流程的闡述佔據了全書的1/2,甚至更多。

  • 優點
    完整的講述了一個分散式物件儲存從0到1的過程,不斷改造和擴充套件新功能,循序漸進的方式來講解,是其優勢之處。書中涉及到知識點很多,有分散式系統的基本涉及,元資料管理,去重,糾刪碼,壓縮等,也就是說的常見的分散式系統的feature方面都有,能夠給一個不在這個領域的人一個巨集觀的感知。例如,本書能夠回答的是目前的分散式系統的架構是如何,有什麼主要特性。
  • 缺點
    大量的程式碼讓我遇到程式碼的時候都大量的跳躍,一上午的時間,讀了三章(其實很多程式碼都沒有細看,主要看了架構設計以及思路設計),收穫偏小。但是因為自己還沒有讀完,所以也沒有太大的發言權,只是自己一廂情願的看法而已了。

3. 內容總結

看書不總結書中內容,對於記憶力很差的我,等於白看,因此現在把我看到的內容寫下來,權當一記錄,也希望能夠給其他想買這本書的同學一些參考。

3.1. 物件儲存簡介

傳統的儲存系統有NAS和SAN,它們本質上是兩個點,NAS是相當於有一個檔案伺服器,可進行檔案上傳和下載等操作,而SAN更強調storage的概念,它將多個儲存裝置組成一個整體,對外只提供storage的功能,而沒有檔案等上層抽象概念,可以把它想象成一個大的網路硬碟。這些內容其實與物件儲存並不矛盾。物件儲存按照其名字來說,核心就是將物件進行儲存。物件有唯一標識,可認為是key,而物件的資料,可認為是value。

如果實現一個單機版本的物件儲存呢?
基於Go語言,使用REST請求(GET用於下載,PUT用於上傳)

  1. 啟動一個Listen Service,並且繫結handler(處理函式)
  2. 在處理函式判斷是GET還是PUT請求然後進入不同的處理
  3. 如果是GET請求,直接根據路徑,拿到物件的內容(其實就是一個本地的file檔案),返回給客戶端
  4. 如果是PUT請求,根據路徑,將post的body寫入到指定的檔案中
  5. 客戶端模擬使用curl,可用於演示put和get請求

這裡不得不說go實現真的好簡潔,寥寥幾十行,就實現了基本的HTTP服務,並能讀寫檔案資料返回到客戶端。

書中很多地方說了REST,原來並不知道這種稱呼,簡單看了一下網上的介紹也不甚了了,基本感覺就是提供了資料訪問的介面的標準定義,都按照這樣的標準請求和返回,方便解析。有點像HTTP的規範定義版本?

3.2. 分散式系統

對於3.1的單機物件儲存,最先要做的就是實現分散式,也就是可擴充套件性。要想做到這一點,一個重要的概念就是請求和資料分離。其實就是有專門的介面服務負責接收請求並轉發(感覺好像代理伺服器,但是不同的是它需要維護資料節點的狀態資訊),有專門的資料服務負責資料的儲存。這樣的話,如果容量不足需要擴容資料儲存,直接拉幾臺機器加入到資料叢集中即可。

CSDN上的PDF版本需要5分,奈何沒那麼多積分,只能手機拍照截圖了。

如上圖所示,為了實現兩個服務的分離,介面儲存和資料儲存需要加入中間層,以隔離兩個服務。 本書使用RabbitMQ(看著像訊息佇列Kafka),負責上下層之間的訊息轉發。當客戶端的寫入操作到達介面層,需要找到相應的資料節點,完成寫入;當客戶端的讀取操作到達介面層,也需要找到儲存的節點,發起讀取;另外介面層需要知道哪些資料節點是alive的,這樣以便於寫入的時候避開掛掉的資料節點。

欲完成以上功能,需要如下:

  • 心跳
    資料節點每間隔一斷時間(5s)發起心跳請求訊息,扔到apiServer exchange中(感覺像kafka的topic)。介面服務作為消費者,消費apiServer中的訊息,收到某個節點心跳就記錄當前節點。注意,這個心跳訊息會廣播到所有的介面服務(說是廣播,其實從本質上是所有的介面服務都獨立訂閱了apiServer,都能收到apiServer中的全部訊息)。這樣每個介面服務都能看到所有的資料節點,介面服務節點之間沒有任何差異,這也就實現了介面服務的平行擴容。
  1. 客戶端發GET到介面服務
  2. 介面服務生產訊息到dataServer
  3. dataServer中的訊息被所有的資料節點消費,只有具有指定物件的data節點才會返回,並返回其監聽地址
  4. 介面服務收到data節點的監聽地址後,向data節點發起資料讀取請求
  5. 介面服務節點將讀取到的資料返回給客戶端

  • 與讀的過程基本一致,不同的是寫入的節點是隨機選取的其中一個,然後是將資料傳遞到資料節點,交給資料節點寫入。

總體來說,讀和寫的過程可以歸結為兩個步驟:

  1. 定位資料節點(對於讀,只有儲存了請求物件的節點返回;對於寫,隨機選擇)
  2. 發起資料讀寫(介面節點和資料節點傳遞物件的內容,或者寫入,或者讀出)

這裡面有一些問題:

  1. 所有的資料讀寫請求,都完整了經過了介面服務和資料服務,似乎有點浪費,是否能夠做到將資料服務的節點返回給客戶端,由客戶端發起讀取操作呢?
  2. Put同一個物件,可能到達不同的資料節點,從而儲存多份資料
  3. 不可容錯,當前的資料節點丟了一臺,資料就丟了
  4. 是否需要一致性Hash?目前的物件的節點定位要求所有資料節點都參與,也就是說介面節點不知道物件的位置,從而不得不向所有的資料節點發起詢問,如果採用hash求模的方案,就可直接根據hash模值定位到資料節點。
  5. 資料版本號。如果能夠使得一份資料有多份版本?

上面的很多問題,需要很多內容才能完善。例如版本號是一種資料一致性的方案(MVCC),那也有很多其他的方案,不過文中並無涉及;關於資料hash,有不少保證一致性hash的方案,也無涉及。

3.3. 元資料服務

說實話,在看到元資料服務的時候,以為要自己實現一套元資料的儲存方案,仔細一讀發現使用的是ES(elasticSearch)。對於ES,同樣不熟悉,等後面看是否專門寫個部落格介紹一下。簡單來說,ES負責儲存物件的元資料資訊,介面服務利用ES返回的元資料從而資料節點中取得物件的Value。

為什麼會有這個ES? 物件要支援多版本訪問(既有最新版本,也有歷史版本),使得物件要有版本號的概念,另外資料本身可能還有長度屬性,類別屬性(是圖片還是文字等)等等的內容,這些內容不可能儲存在介面伺服器中的。因為如果存在介面節點中,介面服務就無法做到資料一致性了,例如通過介面節點A增加了一個新的物件,介面節點B就無法感知到,客戶端向B請求,會報告物件不存在。從本質來說,正是由於介面服務節點的無狀態特性(只負責轉發請求,不過本文中還維護的data節點的alive),才賦予了介面服務節點的平行擴容能力。ES充當了中心化的有狀態服務,給介面節點提供元資料查詢服務,所有的更新操作都會經過ES,從而確保資料的一致性。

那如何設定一個物件的元資料metadata,簡單起見,使用四種屬性:name,version,hash,size
唯一標識:

  • name和version唯一標識一個物件。客戶端可使用name+version向ES發起請求,ES返回其metadata;
  • hash唯一標識一個物件。資料儲存以hash作為唯一標識,請求資料服務時候,需要傳入hash值。

這種上層name+version,下層hash的操作有一定的好處。從上層客戶端使用來說,name才是最容易記憶和關心的,對外公開使用name更易記憶和理解;從底層儲存服務來說,只需要唯一標識?那為何不使用name+version呢?這主要因為hash值提供區分檔案是否unique的屬性。假設檔案A和檔案B的內容完全一樣,如果使用name+version的方式資料需要儲存兩份,而使用hash值,只需要儲存一份(這其實是屬於Dedup研究的範疇Dedup簡介, Dedup一種應用),只要在元資料對映中記錄A -> Hash Identifier, B -> Hash Identifier即可。

以GET操作為例

  1. curl向介面服務發起Get object請求,傳入name;
  2. 介面節點向ES發起name的元資料;
  3. ES返回name的元資料,未指定版本則返回最新版本(name, version, hash, size)的元資料;
  4. 介面節點使用hash值向data節點發起定位請求(資料儲存在哪個data節點上);
  5. 儲存了該object的data節點返回自己監聽的地址;
  6. 介面節點利用返回的data地址,向其發起物件讀取請求;
  7. data節點返回物件的value資訊;
  8. 介面節點將value轉發給curl客戶端。

額外的問題:

  1. 上傳資料的時候需要指定hash值,這個hash值伺服器並無校驗
  2. 資料傳輸通路完整的經過介面服務節點
  3. 元資料需要保證資料一致性,例如多個寫請求同時到達,進行版本增加以及資料儲存要保證原子操作。

3.4. 校驗去重

在分散式系統中,通常有來自不同使用者的相同的資料。儲存多份相同的資料會增加儲存開銷,那麼如何進行資料去重呢?本書採用的是基於物件的hash去重:對整個object計算hash值,如果存在此hash值,那麼可認為已經儲存了此資料。

Hash處理流程

使用者上傳物件的時候提供hash值和資料,這本身需要伺服器進行校驗(比對hash值),只有校驗合格的資料才會被伺服器接收。而另一方面,介面服務必須在收到完整資料的情況下,才能夠進行hash比對。

temp物件的使用

如何確認一個hash值已經存在? 感覺本書的方法還是比較巧妙的。假定使用者上傳的資料X的hash值為hash_ori,上傳後首先將資料寫入到資料節點的temp目錄下,並由介面節點計算其hash值為hash_temp。介面節點隨後比對hash_ori和hash_temp,如果一致,那麼呼叫PUT命令,將temp路徑下的資料轉正,並刪除temp的資料,如果不一致,直接刪除temp資料,並返回客戶端hash不匹配。

操作流程如下:

  1. 客戶端上傳資料X和hash值h
  2. 介面定位此hash值,如果存在,表明檔案已經存在,直接新增元資料到ES中
  3. 如果不存在,那麼向資料服務請求一個uuID,並將資料寫入到temp/uuID中
  4. 介面在傳遞資料的過程中,同時計算hash值,並與客戶端的hash值進行比對
  5. 比對成功之後,將臨時物件轉正(其實就是資料服務移動並重命名檔案)
  6. 刪除臨時物件temp/uuID

3.5. 冗餘恢復

這算是我多年的研究領域了,將一個數據拆分成k個數據塊,然後用這k個數據塊生成m個校驗塊,這k+m個數據塊稱為一個條紋。這個條紋內任意k個數據塊都可以構建出原始資料,因此可以容忍任意m個數據塊的丟失。現在分散式系統常用的是裡德所羅門RS編碼,引數化形式為RS(k, m)。

在實現RS編碼的時候,有兩種資料佈局方式,一種是stripping方式,一種continuous。
Stripping:讀取檔案可以併發讀取,同時建立4個數據流,每個流中讀取一個小的block,一輪讀取完畢後,再下一輪;
Continuous:讀取檔案只能按順序讀取,每次讀取一個完整的block,這個block讀取完畢後,開始讀取下一個block。

假定每個資料塊2MB,包含兩個小塊。對於一個8MB的檔案,使用RS(4, 2)編碼
對於Stripping方式(每列代表一個數據塊),則

1 2 3 4 P P
5 6 7 8 P P 

對於Continuous方式,則

1 3 5 7 P P
2 4 6 8 P P

本書使用的是Stripping方式,介面節點首先與資料節點建立6個數據流通道,對應的資料分片檔名命名為X.0, X.1… X.5(其中X為物件的hash值),然後將客戶端PUT上來的資料流進行切片,每個小片8KB,因此每讀取32KB進行RS(4, 2)編碼,生成6個小片,並將這6個數據片傳輸到資料節點上,直到傳輸完成。

讀取可操作的餘地就非常大了,對於使用者讀取X,一種是進行online recovery,同時返回6個分片的地址,然後全部都讀取,可能出現有些分片丟失,只要有4個分片OK就可以直接重構出來,並寫入;另外一個角度是應對慢節點:只要有4個分片返回,就中斷其他的傳輸,然後重構原始資料返回給客戶端,慢節點的問題能解決的很好。

3.6. 斷點續傳

這個沒啥特別的,最重要是個offset和length的概念。

下載:給出offset,開始下載
上傳:先請求上次寫入的offset,從offset開始上傳資料。

3.7. 資料壓縮

資料壓縮應該最好在客戶端進行,減少網路傳輸。使用現有的壓縮演算法(很多開源的庫)即可。
常用的壓縮演算法:zlib, defalt, gzip, bzip2

3.8. 資料維護

系統維護至關重要

  1. 刪除過期資料(例如很老的版本的資料清除)
    一個物件儲存完整的歷史版本並不實用,這樣儲存開銷會很大。因此,需要有相應的機制來清除歷史版本,釋放儲存開銷,例如只儲存最近的幾個版本即可。
  2. 資料節點的定期清理無用資料
    有些資料可能沒有元資料引用,例如刪除物件,元資料已經刪除,但是資料本身可能由於網路等問題,並沒有立刻被清除,對於這些無用資料也要清理。
  3. 資料節點的檢查和修復(類似於HDFS的block scanner)
    根據檔名hash值與檔案內容計算的hash值進行比對,如果不一致,表明此資料損壞,需要啟動資料恢復。

競爭條件

刪除資料的關鍵點是不能刪除被使用的資料。 例如有一種情況,

  1. 後臺清理髮現有一個無用資料D,
  2. 使用者上傳了一個檔案,它的hash值和這個無用資料D相同,
  3. 後臺維護程式清理了無用資料D
  4. ES添加了一條指向資料D的引用
    也就是說,資料準備刪除過程中,卻被外界引用。本書針對這種情況,先移動此無用資料到回收站,過一個月再刪除,但是這樣一個月後還是會出現這個問題啊,引用的資料還是被刪除了。根本原因在於,不應該引用一個已經被刪除(無元資料)的資料。使用者上傳檔案,判斷hash值是否存在只能基於ES中的內容判斷,而不能基於資料節點上的資料判斷,資料節點上的資料是不可靠的,隨時可能會發生變動。

4. 書籍的額外建議

  1. 每一章前面一個作者的ppt的照片,感覺不是很好,技術書籍很少見,給人以湊頁之嫌。
  2. 書中的很多知識點都是feature之類的,並沒有學到比較深刻的分散式系統知識 。

參考資料: