1. 程式人生 > >CRUSH演算法介紹

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)。這些都是可自定義的,並且還可線上修改(

見此篇)。整個層次結構中,除了葉子節點OSD之外,其他的都稱之為bucket. 而上面提到的PG,不屬於該層級結構。因為層級結構反映的是物理情況,相當於設定故障域,方便定位問題,而PG則是一個邏輯概念(在實際中,PG相當於磁碟上的一個目錄,而屬於這個PG的諸多object則是這個目錄下的檔案)。在一個Ceph叢集上,可建立若干個Pool,每個Pool在建立的時候,就需要指明該Pool的PG數目。因此,Pool也是一個邏輯的概念。而PG,其實還相當於一致性Hash中的虛擬節點 -- PG的數目不會改變就如同虛擬節點的數目不會改變。而OSD相當於是一致性Hash中的物理節點。一旦一塊磁碟損壞,其對應的OSD上的資料會遷移到其他的OSD上,這就相當於一致性Hash中的物理節點損壞後,其所管轄的虛擬節點被劃分給了其他的物理節點。

那麼,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種操作方式:

  1. choose firstn: 深度優先選擇出n個型別為t的子bucket,只到bucket為止
  2. 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

3. 大話Ceph - CRUSH那點事兒

4.  Ceph剖析:資料分佈之CRUSH演算法與一致性Hash