CRUSH演算法介紹
CRUSH資料分佈演算法的全稱是:Controlled, Scalable, Decentralized Placement of Replicated Data.
開源的分散式儲存Ceph採用CRUSH資料分佈演算法以達到以下幾個要求:
1. 資料分佈均衡
2. 負載均衡
3. 靈活應對叢集擴容和縮容:無論是新增或刪除裝置,都能最小化資料遷移
4. 支援大規模叢集,消除因幾種儲存元資料而可能的單點失敗
CRUSH演算法除了要達到以上幾個要求,它的主要目的是為了定位所儲存資料的位置。
在Ceph儲存中,資料都是以object為基本單位進行儲存的,每個object預設為4MB大小;若干個object屬於一個PG(Placement Group,歸置組);而若干個PG又屬於一個OSD;一般來說,一個OSD對應於一塊磁碟。Ceph採用層級化的叢集結構(Hierarchical Cluster Map),並且使用者可自定義該叢集結構,而OSD正是這個層級化叢集結構的葉子節點。使用者可定義整個叢集分為3層,也可定義為5層,具體還可以定義每層的名字。舉個栗子,一個Ceph叢集,最頂層叫做root,root下面分為2個data center,每個data center有3個room,每個room有4個機架,每個機架上有5臺機器,每臺機器有6塊硬碟(即6個OSD)。這些都是可自定義的,並且還可線上修改(
那麼,CRUSH演算法如何定位儲存資料的位置,並且滿足以上四大要求呢?
Sage在2006年關於CRUSH的原論文寫得非常言簡意賅,並且主要是介紹抽象的CRUSH演算法,而沒有結合Ceph來談,比如就完全沒有提及PG和OSD的概念。因此,估計是要看看Sage的其他幾篇論文和Ceph的原始碼才能清楚一點。現根據前人的幾篇文獻總結如下。
CRUSH演算法主要用於在Ceph中定位儲存資料的位置,分為2個步驟:
1. 根據資料物件(object)的物件名,計算得到PG_ID:
PG_ID = Pool_ID + Hash(<object name>) % PG_NUM
2. 根據PG_ID,計算得到一組OSD
首先,為何是一組OSD?
因為在Ceph中,Pool分為2種。第一種是Replicated Pool,比如每個object都有N個副本,就是屬於這種;第二種是EC Pool,運用的是糾刪碼技術,每個object只有一個副本,但有一些用於校驗和還原的資料塊。因此無論對於哪種Pool,一個object都需要多個OSD來儲存其副本或者是儲存其糾刪碼,因此輸出是一組OSD.
那麼,如何根據PG_ID,計算得到一組OSD呢?
首先,使用者在建立Pool的時候,要設定儲存規則(Placement Rule)。當然,如果不設定,那麼就採用預設的規則。規則看起來是這樣的:
Action Resulting
take(root) root
select(1, row) row-2
select(3, cabinet) cab-21, cab-23, cab-24
select(1, disk) disk-210, disk-233, disk-245
emit
效果如下圖所示:
這三步(take、select(n,t), emit),是真正的原CRUSH論文中CRUSH演算法的架構內容。而在涉及到select的時候,也需要介紹下是如何select的 -- 即bucket選擇演算法。詳情如下。
第一步take(root), 代表的是選中了這個root.
注意,在一個Ceph叢集中,是有可能有多個root的,因為層次結構是使用者可自己設定的。比如,使用者把所有SSD的裝置設定成屬於一個root,而把所有HDD的裝置設定成屬於另一個root.
第二步,select(n, t). 這一步是關鍵,意思是,選擇n個型別為t的bucket.
那麼,是如何選擇的呢?
首先,select有2種操作方式:
- choose firstn: 深度優先選擇出n個型別為t的子bucket,只到bucket為止
- chooseleaf: 先選擇出n個型別為t的bucket,再在每個bucket下選擇一個OSD裝置
其次,就是如何去選擇bucket了,即bucket隨機選擇演算法。
一共有4種類型的bucket,對於每種型別的bucket,如何“選出”的方法是不一樣的。而在設定Placement Rule時,可以指定採用哪種bucket隨機選擇演算法,即屬於哪種bucket.
這裡首先介紹一個重要的Hash函式,因為該函式在多個bucket選擇演算法中都用到了,但作用各不相同:
hash(PG_ID, r, bucket_id)
這個Hash函式的輸入有3個:PG_ID,r為 [1-n] 中的一個值(n為副本數),bucket_id
這個Hash函式的輸出是一個 [0-1] 之間的數值
1. Uniform Bucket
每個bucket的權重都相同(這裡應該指的是同層級的節點的weight都相同),並且基本沒有硬體裝置新增和刪除的情況
這種Bucket因為所有item權重都相同,因此選擇速度很快,為O(1),但是一旦需要新增或刪除裝置,就退化成普通Hash,即幾乎所有資料都需要遷移。在實際中,一般不會使用這種bucket.
2. List Bucket
其子item在記憶體中使用資料結構中的連結串列來儲存,其所包含的item可以具有任意權重。(注:此處item和bucket是一個意思) 具體查詢bucket的方法是:
1> 從表頭item開始查詢,先得到表頭item的權重Wh,剩餘連結串列中所有item權重之和為Ws.
2> 根據 hash(PG_ID, r, bucket_id) 計算得到一個 [0-1] 之間的值v,若v在 [0-Wh/Ws) 的範圍,則選擇表頭item,並返回表頭item的id;
3> 否則,繼續遍歷剩餘的連結串列,繼續上述的遞迴查詢。
由以上分析可知,List Bucket的查詢複雜度為 O(n)
這種Bucket在實際中一般也不太會用,因為其刪除硬體時,效率也較差。
3. Tree Bucket
此種類型的Bucket的子item組織成樹的結構。每個OSD是葉子節點;根節點和中間節點是虛擬節點,其權重等於左右子樹的權重之和。具體查詢bucket的方法如下:
1> 從根節點開始遍歷
2> 設左子樹權重為W1,而當前節點權重為Wn,然後根據 hash(PG_ID, r, bucket_id) 計算得到一個 [0-1] 之間的值v;
a> 若v在 [0-W1/Wn) 之間,那麼在左子樹中繼續選擇item;
b> 否則在右子樹中選擇item
c> 一直遍歷,直至葉子節點
由以上分析可知,Tree Bucket的查詢複雜度為 O(log n)
這種Bucket在實際中可以考慮使用。其選擇速度是很快的,而在新增和刪除硬體裝置時,速度也還可以。
4. Straw Bucket
此種類型的bucket選擇演算法是Ceph的預設選擇演算法。具體如下:
1> 函式 f(Wi) 是和item的權重Wi相關的函式,決定了每個item被選中的概率;權重(weight)越高的item則被選中的概率越大。
注意,這裡的weight並不是相當於磁碟可用空間,而是相當於總空間,因此是固定不變的。
2> 給每個item計算一個長度
length = f(Wi) * hash(PG_ID, r, bucket_id)
然後選擇length最大的item.
因為straw在增刪裝置時的表現最佳,而在選擇速度上也還可以,因此,它被選作預設的bucket選擇演算法。
以上各個bucket選擇演算法的對比情況見下表:
Bucket選擇演算法 | 選擇的速度 | item新增的容易程度 | item刪除的容易程度 |
uniform | O(1) | poor | poor |
list | O(n) | optimal | poor |
tree | O(log n) | good | good |
straw | O(n) | better | better |
straw2 | O(n) | optimal | optimal |
當選出一個OSD後,可能出現衝突(重複選擇)、失效(磁碟損壞)、過載的情況,那麼此時就重新選擇一次。對於Replicated Pool和EC Pool,重新選擇的演算法是略有不同的。Replicated Pool是直接再選一個即可(r' = r + f,f為總失敗的次數),而EC Pool則需令r值增加n的倍數後(r' = fr*n, fr為在這個副本上失敗的次數),再運用hash(PG_ID, r', bucket_id)來進行重新選擇。
下圖顯示了兩種Pool在失敗的情況下是如何決定r'的值:
最後一步,第三步,emit,即輸出結果。所謂結果,即一組OSD.
參考文獻:
1. 《Ceph原始碼分析》第四章 CRUSH資料分佈演算法
2. CRUSH: Controlled, Scalable, Decentralized Placement of Replicated Data