Ceph剖析:數據分布之CRUSH算法與一致性Hash


作者:吳香偉 發表於 2014/09/05
版權聲明:可以任意轉載,轉載時務必以超鏈接形式標明文章原始出處和作者信息以及版權聲明

數據分布是分布式存儲系統的一個重要部分,數據分布算法至少要考慮以下三個因素:

  1. 故障域隔離。同份數據的不同副本分布在不同的故障域,降低數據損壞的風險;
  2. 負載均衡。數據能夠均勻地分布在磁盤容量不等的存儲節點,避免部分節點空閑部分節點超載,從而影響系統性能;
  3. 控制節點加入離開時引起的數據遷移量。當節點離開時,最優的數據遷移是只有離線節點上的數據被遷移到其它節點,而正常工作的節點的數據不會發生遷移。

對象存儲中一致性Hash和Ceph的CRUSH算法是使用地比較多的數據分布算法。在Aamzon的Dyanmo鍵值存儲系統中采用一致性Hash算法,並且對它做了很多優化。OpenStack的Swift對象存儲系統也使用了一致性Hash算法。

一致性Hash算法

假設數據為x,存儲節點數目為N。將數據分布到存儲節點的最直接做法是,計算數據x的Hash值,並將結果同節點數目N取余數,余數就是數據x的目的存儲節點。即目的存儲節點為 Hash(x) % N。對數據計算Hash值的目的為了可以讓數據均勻分布在N個節點中。這種做法的一個嚴重問題是,當加入新節點或則節點離開時,幾乎所有數據都會受到影響,需要重新分布。因此,數據遷移量非常大。

一致性Hash算法將數據和存儲節點映射到同個Hash空間,如上圖所示。Hash環中的3存儲節點把Hash空間划分成3個分區,每個存儲節點負責一個分區上的數據。例如,落在分區[N2,N0]上的數據存儲在節點N0。

一致性Hash算法能夠很好地控制節點加入離開導致的遷移數據的數量。如圖(b)所示,當節點N0離開時,原來由它負責的[N2, N0]分區將同[N0, N1]分區合並成[N2, N1]分區,並且都由節點N1負責。也就是說,本來存儲在節點N0上的數據都遷移到節點N1,而原來存儲在N1和N2節點的數據不受影響。圖(c)給出了當節點N3加入時,原來[N2, N0]分區分裂成[N3, N0]和[N2, N3]兩個分區,其中[N3, N0]分區上是數據遷移到新加入的N3節點。

虛擬節點

一致性Hash的一個問題是,存儲節點不能將Hash空間划分地足夠均勻。如上圖(a)所示,分區[N2, N0]的大小幾乎是其它兩個分區大小之和。這容易讓負責該分區的節點N0負載過重。假設3個節點的磁盤容量相等,那么當節點N0的磁盤已經寫滿數據時其它兩個節點上的磁盤還有很大的空閑空間,但此時系統已經無法繼續向分區[N2, N0]寫入數據,從而造成資源浪費。

虛擬節點是相對於物理存儲節點而言的,虛擬節點負責的分區上的數據最終存儲到其對應的物理節點。在一致性Hash中引入虛擬節點可以把Hash空間划分成更多的分區,從而讓數據在存儲節點上的分布更加均勻。如上圖(b)所示,黃顏色的節點代表虛擬節點,Ni_0代表該虛擬節點對應於物理節點i的第0個虛擬節點。增加虛擬節點后,物理節點N0負責[N1_0, N0]和[N0, N0_0]兩個分區,物理節點N1負責[N0_0, N1]和[N2_0, N1_0]兩個分區,物理節點N2負責[N2, N1]和[N2_0, N2]兩個分區,三個物理節點負責的總的數據量趨於平衡。

實際應用中,可以根據物理節點的磁盤容量的大小來確定其對應的虛擬節點數目。虛擬節點數目越多,節點負責的數據區間也越大。

分區與分區位置

前文提到,當節點加入或者離開時,分區會相應地進行分裂或合並。這不對新寫入的數據構成影響,但對已經寫入到磁盤的數據需要重新計算Hash值以確定它是否需要遷移到其它節點。因為需要遍歷磁盤中的所有數據,這個計算過程非常耗時。如下圖(a)所示,分區是由落在Hash環上的虛擬節點Ti來划分的,並且分區位置(存儲分區數據的節點)也同虛擬節點相關,即存儲到其順時針方向的第1個虛擬節點。

在Dynamo的論文中提出了分離分區和分區位置的方法來解決這個問題。該方法將Hash空間划分成固定的若干個分區,虛擬節點不再用於划分分區而用來確定分區的存儲位置。如上圖(b)所示,將Hash空間划分成[A,B],[B,C], [C,D]和[D,A]四個固定的分區。虛擬節點用於確定分區位置,例如T1負責分區[B,C],T2負責分區[C,D],T0負責[D,A]和[A,B]兩個分區。由於分區固定,因此遷移數據時可以很容易知道哪些數據需要遷移哪些數據不需要遷移。

上圖(b)中虛擬節點T0負責了[D,A]和[A,B]兩個分區的數據,這是由分區數目和虛擬節點數目不相同導致的。為讓分區分布地更加均勻,Dyanmo提出了維持分區數目和虛擬節點數目相等的方法。這樣每個虛擬節點負責一個分區,在物理節點的磁盤容量都相同並且虛擬節點數目都相同的情況下,每個物理節點負責的分區大小是完全相同的,從而可以達到最佳的數據分布。

CRUSH算法

Ceph分布數據的過程:首先計算數據x的Hash值並將結果和PG數目取余,以得到數據x對應的PG編號。然后,通過CRUSH算法將PG映射到一組OSD中。最后把數據x存放到PG對應的OSD中。這個過程中包含了兩次映射,第一次是數據x到PG的映射。如果把PG當作存儲節點,那么這和文章開頭提到的普通Hash算法一樣。不同的是,PG是抽象的存儲節點,它不會隨着物理節點的加入或則離開而增加或減少,因此數據到PG的映射是穩定的。

在這個過程中,PG起到了兩個作用:第一個作用是划分數據分區。每個PG管理的數據區間相同,因而數據能夠均勻地分布到PG上;第二個作用是充當Dyanmo中Token的角色,即決定分區位置。實際上,這和Dynamo中固定分區數目,以及維持分區數目和虛擬節點數目相等的原則是同一回事。

在沒有多副本的情況下,Dynamo中分區的數據直接存儲到Token,而每個Token對應唯一的一個物理存儲節點。在多副本(假設副本數目為N)的情況下,分區的數據會存儲到連續的N個Token中。但這會引入一個新問題:因為副本必須保持在不同的物理節點,但是如果這組Token中存在兩個或多個Token對應到同個物理存儲節點,那么就必須要跳過這樣的節點。Dynamo采用Preference列表來記錄每個分區對應的物理節點。然而,Dynmao論文中沒有詳述分區的Preference列表如何選取物理節點,以及選取物理節點時該如何隔離故障域等問題。

(osd0, osd1, osd2 … osdn) = CRUSH(x)

Ceph的PG擔當起Dynamo中Token、固定分區以及Preference列表的角色,解決的是同樣的問題。PG的Acting集合對應於Dynamo的Preference列表。CRUSH算法解決了Dynamo論文中未提及的問題。

OSD層級結構和權重大小

CRUSH算法的目的是,為給定的PG(即分區)分配一組存儲數據的OSD節點。選擇OSD節點的過程,要考慮以下幾個因素:

  1. PG在OSD間均勻分布。假設每個OSD的磁盤容量都相同,那么我們希望PG在每個OSD節點上是均勻分布的,也就是說每個OSD節點包含相同數目的PG。假如節點的磁盤容量不等,那么容量大的磁盤的節點能夠處理更多數量的PG。
  2. PG的OSD分布在不同的故障域。因為PG的OSD列表用於保存數據的不同副本,副本分布在不同的OSD中可以降低數據損壞的風險。

Ceph使用樹型層級結構描述OSD的空間位置以及權重(同磁盤容量相關)大小。如上圖所示,層級結構描述了OSD所在主機、主機所在機架以及機架所在機房等空間位置。這些空間位置隱含了故障區域,例如使用不同電源的不同的機架屬於不同的故障域。CRUSH能夠依據一定的規則將副本放置在不同的故障域。

OSD節點在層級結構中也被稱為Device,它位於層級結構的葉子節點,所有非葉子節點稱為Bucket。Bucket擁有不同的類型,如上圖所示,所有機架的類型為Rack,所有主機的類型為Host。使用者還可以自己定義Bucket的類型。Device節點的權重代表存儲節點的性能,磁盤容量是影響權重大小的重要參數。Bucket節點的權重是其子節點的權重之和。

CRUSH通過重復執行Take(bucketID)和Select(n, bucketType)兩個操作選取副本位置。Take(bucketID)指定從給定的bucketID中選取副本位置,例如可以指定從某台機架上選取副本位置,以實現將不同的副本隔離在不同的故障域; Select(n, bucketType)則在給定的Bucket下選取n個類型為bucketType的Bucket,它選取Bucket主要考慮層級結構中節點的容量,以及當節點離線或者加入時的數據遷移量。

算法流程

上圖給出了CRUSH選取副本的流程圖。

bucket: Take操作指定的bucket;
type: Select操作指定的Bucket的類型;
repnum: Select操作指定的副本數目;

rep:當前選擇的副本編號;
x: 當前選擇的PG編號;
item: 代表當前被選中的Bucket;
c(r, x, in): 代表從Bucket in中為PG x選取第r個副本;
collide: 代表當前選中的副本位置item已經被選中,即出現了沖突;
reject: 代表當前選中的副本位置item被拒絕,例如,在item已經處於out狀態的情況下;

ftotal: 在Descent域中選擇的失敗次數,即選擇一個副本位置的總共的失敗次數;
flocal: 在Local域中選擇的失敗次數;
local_retries: 在Local域選擇沖突時的嘗試次數;
local_fallback_retries: 允許在Local域的總共嘗試次數為bucket.size + local_fallback_retires次,以保證遍歷完Buckt的所有子節點;
tries: 在Descent的最大嘗試次數,超過這個次數則放棄這個副本。

當Take操作指定的Bucket和Select操作指定的Bucket類型之間隔着幾層Bucket時,算法直接深度優先地進入到目的Bucket的直接父母節點。例如,從根節點開始選擇N個Host時,它會深度優先地查找到Rack類型的節點,並在這個節點下選取Host節點。為了方便表述,將Rack的所有子節點標記為Local域,將Take指定的Bucket的子節點標記為Descent域,如上圖所示。

選取過程中出現沖突、過載或者故障時,算法先在Local域內重新選擇,嘗試有限次數后,如果仍然找不到滿足條件的Bucket,那就回到Descent域重新選擇。每次重新選擇時,修改副本數目為r += ftotal。因此每次選擇失敗都會遞增ftotal,所以可以盡量避免選擇時再次選到沖突的節點。

Bucket選取Item算法

流程圖中的item=c(r,x,in)從給定的Bucket in中選取一個子節點。

CRUSH rule和POOL的關系

(待續)

參考資料

1、Ceph的CRUSH數據分布算法介紹
2、CRUSH:Controlled,Scalable,Decentralized Placement of Replicated Data
3、Dynamo:Amazon Highly Available Key-value Store


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM