Ceph源碼解析:CRUSH算法

一、簡介

     隨着大規模分佈式存儲系統(PB級的數據和成百上千臺存儲設備)的出現。這些系統必須平衡的分佈數據和負載(提升資源利用率),最大化系統的性能,並要處理系統的擴展和硬件失效。ceph設計了CRUSH(一個可擴展的僞隨機數據分佈算法),用在分佈式對象存儲系統上,能夠有效映射數據對象到存儲設備上(不須要中心設備)。由於大型系統的結構式動態變化的,CRUSH可以處理存儲設備的添加和移除,並最小化因爲存儲設備的的添加和移動而致使的數據遷移。html

     爲了保證負載均衡,保證新舊數據混合在一塊兒。可是簡單HASH分佈不能有效處理設備數量的變化,致使大量數據遷移。ceph開發了CRUSH(Controoled Replication Under Scalable Hashing),一種僞隨機數據分佈算法,它可以在層級結構的存儲集羣中有效的分佈對象的副本。CRUSH實現了一種僞隨機(肯定性)的函數,它的參數是object id或object group id,並返回一組存儲設備(用於保存object副本OSD)。CRUSH須要cluster map(描述存儲集羣的層級結構)、和副本分佈策略(rule)。node

     CRUSH有兩個關鍵優勢:算法

    • 任何組件均可以獨立計算出每一個object所在的位置(去中心化)。
    • 只須要不多的元數據(cluster map),只要當刪除添加設備時,這些元數據才須要改變。

     CRUSH的目的是利用可用資源優化分配數據,當存儲設備添加或刪除時高效地重組數據,以及靈活地約束對象副本放置,當數據同步或者相關硬件故障的時候最大化保證數據安全。支持各類各樣的數據安全機制,包括多方複製(鏡像),RAID奇偶校驗方案或者其餘形式的校驗碼,以及混合方法(好比RAID-10)。這些特性使得CRUSH適合管理對象分佈很是大的(PB級別)、要求可伸縮性,性能和可靠性很是高的存儲系統。簡而言之就是PG到OSD的映射過程。數組

2.映射過程安全

2.1 概念服務器

    ceph中Pool的屬性有:1.object的副本數   2.Placement Groups的數量    3.所使用的CRUSH Ruleset網絡

    數據映射(Data Placement)的方式決定了存儲系統的性能和擴展性。(Pool,PG)→ OSD set的映射由四個因素決定:數據結構

   (1)CRUSH算法架構

   (2)OSD MAP:包含當前全部pool的狀態和OSD的狀態。OSDMap管理當前ceph中全部的OSD,OSDMap規定了crush算法的一個範圍,在這個範圍中選擇OSD結合。OSDMap其實就是一個樹形的結構,葉子節點是device(也就是osd),其餘的節點稱爲bucket節點,這些bucket都是虛構的節點,能夠根據物理結構進行抽象,固然樹形結構只有一個最終的根節點稱之爲root節點,中間虛擬的bucket節點能夠是數據中心抽象、機房抽象、機架抽象、主機抽象等以下圖。併發

                                      191305_azzU_2460844

                                                                       osd組成的邏輯樹形結構

struct crush_bucket
{
    __s32 id;        /* this'll be negative */
    __u16 type;      /* non-zero; type=0 is reserved for devices */
    __u8 alg;        /* one of CRUSH_BUCKET_* */
    __u8 hash;       /* which hash function to use, CRUSH_HASH_* */
    __u32 weight;    /* 16-bit fixed point *///權重通常有兩種設法。一種按容量,通常是1T爲1,500G就是0.5。另一種按性能。具體按實際設置。
    __u32 size;      /* num items */
    __s32 *items;

    /*
     * cached random permutation: used for uniform bucket and for
     * the linear search fallback for the other bucket types.
     */
    __u32 perm_x;  /* @x for which *perm is defined */
    __u32 perm_n;  /* num elements of *perm that are permuted/defined */
    __u32 *perm;
};

   (3)CRUSH MAP:包含當前磁盤、服務器、機架的層級結構。

   (4)CRUSH Rules:數據映射的策略。這些策略能夠靈活的設置object存放的區域。好比能夠指定 pool1中全部objects放置在機架1上,全部objects的第1個副本放置在機架1上的服務器A上,第2個副本分佈在機架1上的服務器B上。 pool2中全部的object分佈在機架二、三、4上,全部Object的第1個副本分佈在機架2的服務器上,第2個副本分佈在機架3的服器上,第3個副本分佈在機架4的服務器上。

2.2 流程

     Ceph 架構中,Ceph 客戶端是直接讀或者寫存放在 OSD上的 RADOS 對象存儲中的對象(data object)的,所以,Ceph 須要走完 (Pool, Object) → (Pool, PG) → OSD set → OSD/Disk 完整的鏈路,才能讓 ceph client 知道目標數據 object的具體位置在哪裏。

     數據寫入時,文件被切分紅object,object先映射到PG,再由PG映射到OSD set。每一個pool有多個PG,每一個object經過計算hash值並取模獲得它所對應的PG。PG再映射到一組OSD(OSD個數由pool的副本數決定),第一個OSD是Primary,剩下的都是Replicas。

     Ceph分佈數據的過程:首先計算數據x的Hash值並將結果和PG數目取餘,以獲得數據x對應的PG編號。而後,經過CRUSH算法將PG映射到一組OSD中。最後把數據x存放到PG對應的OSD中。這個過程當中包含了兩次映射,第一次是數據x到PG的映射。PG是抽象的存儲節點,它不會隨着物理節點的加入或則離開而增長或減小,所以數據到PG的映射是穩定的。

(1)建立 Pool 和它的 PG。根據上述的計算過程,PG 在 Pool 被建立後就會被 MON 在根據 CRUSH 算法計算出來的 PG 應該所在若干的 OSD 上被建立出來了。也就是說,在客戶端寫入對象的時候,PG 已經被建立好了,PG 和 OSD 的映射關係已是肯定了的。

(2)Ceph 客戶端經過哈希算法計算出存放 object 的 PG 的 ID:

  1. 客戶端輸入 pool ID 和 object ID (好比 pool = 「liverpool」 and object-id = 「john」)
  2. ceph 對 object ID 作哈希
  3. ceph 對該 hash 值取 PG 總數的模,獲得 PG 編號 (好比 58)(第2和第3步基本保證了一個 pool 的全部 PG 將會被均勻地使用)
  4. ceph 對 pool ID 取 hash (好比 「liverpool」 = 4
  5. ceph 將  pool ID 和 PG ID 組合在一塊兒(好比 4.58)獲得 PG 的完整ID。

   也就是:PG-id = hash(pool-id). hash(objet-id) % PG-number

                           

(3)客戶端經過 CRUSH 算法計算出(或者說查找出) object 應該會被保存到 PG 中哪一個 OSD 上。(注意:這裏是說」應該「,而不是」將會「,這是由於 PG 和 OSD 之間的關係是已經肯定了的,那客戶端須要作的就是須要知道它所選中的這個 PG 到底將會在哪些 OSD 上建立對象。)。這步驟也叫作 CRUSH 查找。  

   對 Ceph 客戶端來講,只要它得到了 Cluster map,就可使用 CRUSH 算法計算出某個 object 將要所在的 OSD 的 ID,而後直接與它通訊。

  1. Ceph client 從 MON 獲取最新的 cluster map。
  2. Ceph client 根據上面的第(2)步計算出該 object 將要在的 PG 的 ID。
  3. Ceph client 再根據 CRUSH 算法計算出 PG 中目標主和次 OSD 的 ID。

也就是:OSD-ids = CURSH(PG-id, cluster-map, cursh-rules)。

                           

    具體數據讀寫流程下次整理分析。

3 CRUSH 算法

     CRUSH算法根據種每一個設備的權重儘量機率平均地分配數據。分佈算法是由集羣可用存儲資源以及其邏輯單元的map控制的。這個map的描述相似於一個大型服務器的描述:服務器由一系列的機櫃組成,機櫃裝滿服務器,服務器裝滿磁盤。數據分配的策略是由定位規則來定義的,定位規則指定了集羣中將保存多少個副本,以及數據副本的放置有什麼限制。例如,能夠指定數據有三個副本,這三個副本必須放置在不一樣的機櫃中,使得三個數據副本不公用一個物理電路。

     給定一個輸入x,CRUSH 算法將輸出一個肯定的有序的儲存目標向量 ⃗R 。當輸入x,CRUSH利用強大的多重整數hash函數根據集羣map、定位規則、以及x計算出獨立的徹底肯定可靠的映射關係。CRUSH分配算法是僞隨機算法,而且輸入的內容和輸出的儲存位置之間是沒有顯式相關的。咱們能夠說CRUSH 算法在集羣設備中生成了「僞集羣」的數據副本。集羣的設備對一個數據項目共享數據副本,對其餘數據項目又是獨立的。

     CRUSH算法經過每一個設備的權重來計算數據對象的分佈。對象分佈是由cluster map和data distribution policy決定的。cluster map描述了可用存儲資源和層級結構(好比有多少個機架,每一個機架上有多少個服務器,每一個服務器上有多少個磁盤)。data distribution policy由 placement rules組成。rule決定了每一個數據對象有多少個副本,這些副本存儲的限制條件(好比3個副本放在不一樣的機架中)。

     CRUSH算出x到一組OSD集合(OSD是對象存儲設備):

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

     CRUSH利用多參數HASH函數,HASH函數中的參數包括x,使得從x到OSD集合是肯定性的和獨立的。CRUSH只使用了cluster map、placement rules、x。CRUSH是僞隨機算法,類似輸入的結果之間沒有相關性。

     Cluster map由device和bucket組成,它們都有id和權重值。Bucket能夠包含任意數量item。item能夠都是的devices或者都是buckets。管理員控制存儲設備的權重。權重和存儲設備的容量有關。Bucket的權重被定義爲它所包含全部item的權重之和。CRUSH基於4種不一樣的bucket type,每種有不一樣的選擇算法。

3.1 分層集羣映射(cluster map)

     集羣映射由設備和桶(buckets)組成,設備和桶都有數值的描述和權重值。桶能夠包含任意多的設備或者其餘的桶,使他們造成內部節點的存儲層次結構,設備老是在葉節點。存儲設備的權重由管理員設置以控制相設備負責存儲的相對數據量。儘管大型系統的設備含不一樣的容量大小和性能特色,隨機數據分佈算法能夠根據設備的利用率和負載來分佈數據。

     這樣設備的平均負載與存儲的數據量成正比。這致使一維位置指標、權重、應來源於設備的能力。桶的權重是它所包含的元素的權重的總和。

     桶可由任意可用存儲的層次結構組成。例如,能夠建立這樣一個集羣映射,用名爲「shelf」的桶表明最低層的一個主機來包含主機上的磁盤設備,而後用名爲「cabinet」的桶來包含安裝在同一個機架上的主機。在一個大的系統中,表明機架的「cabinet」桶可能還會包含在「row」桶或者「room」桶裏。數據被經過一個僞隨機類hash函數遞歸地分配到層級分明的桶元素中。傳統的散列分佈技術,一旦存儲目標數量有變,就會致使大量的數據遷移;而CRUSH算法是基於桶四個不一樣的類型,每個都有不一樣的選擇算法,以解決添加或刪除設備形成的數據移動和總體的計算複雜度。

3.2 副本放置(Replica Placement)

      CRUSH 算法的設置目的是使數據可以根據設備的存儲能力和寬帶資源加權平均地分佈,並保持一個相對的機率平衡。副本放置在具備層次結構的存儲設備中,這對數據安全也有重要影響。經過反射系統的物理安裝組織,CRUSH算法能夠將系統模塊化,從而定位潛在的設備故障。這些潛在故障的資源包括物理的,好比共用電源,共用的網絡。經過向集羣映射編碼信息,CRUSH副本放置策略能夠將數據對象獨立在不一樣故障域,同時仍然保持所需的分佈。例如,爲了定位可能存在的併發故障,應該確保設備上的數據副本放置在不一樣的機架、主機、電源、控制器、或其餘的物理位置。

     CRUSH算法爲了適應千篇一概的腳本,像數據複製策略和底層的硬件配置,CRUSH對於每份數據的複製策略或者分佈式策略的部署方式,它容許存儲系統或 者管理員精確地指定對象副本如何放置。例如,有的會選擇兩個鏡像來存儲一對數據對象,有的會選擇3個鏡像來存儲2個不一樣的數據對象,還有的會選擇6個甚至更多的便宜廉價RAID-4硬盤設備來存儲等等。

                                  161157_uiKq_865233

 

函數入口:            

/**
* crush_do_rule - calculate a mapping with the given input and rule
* @map: the crush_map
* @ruleno: the rule id
* @x: hash input
* @result: pointer to result vector
* @result_max: maximum result size
* @weight: weight vector (for map leaves)
* @weight_max: size of weight vector
* @scratch: scratch vector for private use; must be >= 3 * result_max
*/
int crush_do_rule(const struct crush_map *map,
int ruleno, int x, int *result, int result_max,
const __u32 *weight, int weight_max,
int *scratch)                  //對照此函數與算法僞代碼基本能夠看出crush在作什麼事情。部分數值計算我也看不懂爲何他這麼作,水平有限。

 

CRUSH_RULE_TAKE    /* arg1 = value to start with */

CRUSH_RULE_CHOOSE_FIRSTN = 2, /* arg1 = num items to pick */  crush_choose_firstn()
/* arg2 = type */
CRUSH_RULE_CHOOSE_INDEP = 3, /* same */ crush_choose_indep()

CRUSH_RULE_EMIT = 4,          /* no args */   return results

 

      在算法1的僞代碼中,每一個規則都包含了一系列應用在一個簡單運行環境的操做。CRUSH函數的整型輸入參數就是一個典型的對象名或者標示符,這個參數就像一堆能夠被複制在相同機器上的對象複製品。操做take(a)選擇了一個在存儲層次的bucket並把這個bucket分配給向量i,這是爲後面的操做作準備。操做select(n,t)迭代每一個元素i,而且在這個點中的子樹中選擇了n個t類型的項。存儲設備有一個綁定類型,而且每一個bucket在系統中擁有一個用於分辨buckets中classes的類型區域(例如哪些表明rows,哪些表明cabinets等)。對於每一個i,select(n,t)都會從1到n迭代調用,同時經過任何中間buckets降序遞歸,它僞隨機地選擇一個經過函數c(r,x)嵌套的項,直到它找到請求t中的一個項。去重後的結果項n|i|會返回給輸入變量i,同時也會做爲隨後被調用的select(n,t)操做的輸入參數,或者被移動到用於觸發操做的結果向量中。

  • tack(a) :選擇一個item,通常是bucket,並返回bucket所包含的全部item。這些item是後續操做的參數,這些item組成向量i。
  • select(n, t):迭代操做每一個item(向量i中的item),對於每一個item(向量i中的item)向下遍歷(遍歷這個item所包含的item),都返回n個不一樣的item(type爲t的item),並把這些item都放到向量i中。select函數會調用c(r, x)函數,這個函數會在每一個bucket中僞隨機選擇一個item。
  • emit:把向量i放到result中

     存儲設備有一個肯定的類型。每一個bucket都有type屬性值,用於區分不一樣的bucket類型(好比」row」、」rack」、」host」等,type能夠自定義)。rules能夠包含多個take和emit語句塊,這樣就容許從不一樣的存儲池中選擇副本的storage target。

                                                        161333_gcg1_865233

      如表1中示例所示,該法則是從圖1架構中的root節點開始,第一個select(1.row)操做選擇了一個row類型的單例bucket。隨後的select(3,cabinet)操做選擇了3個嵌套在下面row2(cab21, cab23, cab24)行中不重複的值,同時,最後的select(1,disk)操做迭代了輸入向量中的三個buckets,也選擇了嵌套在它們其中的人一個單例磁盤。最後的結果集是三個磁盤空間分配給了三個塊,可是全部的結果集都在同一行中。所以,這種方法容許複製品在容器中被同時分割和合並,這些容器包括rows、cabinets、shelves。這種方法對於可靠性和優異的性能要求是很是有利的。這些法則包含了屢次take和emit模塊,它們容許從不一樣的存儲池中獲取不一樣的存儲對象,正如在遠程複製腳本或者層疊式設備那樣。

3.2.1 衝突,失敗和過載

      select(n,t) 操做可能會在多種層次的存儲體系中查找以定位位於其起始點下的n個不一樣的t類型項,這是一個由選擇的複製數 r =1,..., n部分決定的迭代過程。在此過程當中,CRUSH可能會因爲如下三個不一樣緣由而丟棄(定位)項並使用修改後的輸入參數 r′來從新選擇(定位)項:若是某一項已經位於當前集合中(衝突——select(n,t) 的結果必須互不相同),若是設備出現故障,或者過載。雖然故障或過載設備在集羣map中儘量地被標記出來,但他們仍是被保留在體系中以免沒必要要的數據遷移。CRUSH利用集羣map中的可能性,特別是與過分利用相關的可能性,經過僞隨機拒絕有選擇的轉移過載設備中的一小部分數據。對於故障或過載設備,CRUSH經過在select(n,t) 開始時重啓遞歸來達到項在存儲集羣中的均勻分佈(見算法1第11行)。對於衝突狀況,替代參數r′首先在迭代的內部級別使用以進行本地查找(見算法1的第14行),這樣能夠遠離比較容易出現衝突的子樹以免所有數據的分佈不均(好比桶(數量)比n小的時候)。

  • 衝突:這個item已經在向量i中,已被選擇。
  • 故障:設備發生故障,不能被選擇。
  • 超載:設備使用容量超過警惕線,沒有剩餘空間保存數據對象。

3.2.2 複製排名

     奇偶檢驗和糾刪碼方案相比複製在配置要求上都有些許不一樣。在本來複制方案中,出現故障後,原先副本(已經擁有該數據的副本)成爲新的本來經常是須要的。在這種狀況下,CRUSH可使用r′ = r + f 從新進行選擇並使用前n個合適項,其中 f表示執行當前操做select(n,t)過程當中定位失敗的次數(見算法1第16行)。然而,在奇偶檢驗和糾刪碼方案中,CRUSH輸出中的存儲設備排名或位置是特定的,由於每一個目標保存了數據對象中的不一樣數據。特別是,若是存儲設備出現故障,它應在CRUSH輸出列表⃗R 的特定位置被替換掉,以保證列表中的其餘設備排名保持不變(即查看圖2中 ⃗R的位置)。在這種狀況下,CRUSH使用r′=r+frn進行從新選擇,其中fr是r中的失敗嘗試次數,這樣就能夠爲每個複製排名肯定一系列在統計上與其餘故障獨立的候選項。相反的是,RUSH同其餘存在的哈希分佈函數同樣,對於故障設備沒有特殊的處理機制,它想固然地假設在使用前n個選項時已經跳過了故障設備,這使得它對於奇偶檢驗方案很難處理。

                                                              165430_9P4R_865233

3.3 Map的變化和數據移動

     在大型文件系統中一個比較典型的部分就是數據在存儲資源中的增長和移動。爲了不非對稱形成的系統壓力和資源的不充分利用,CRUSH主張均衡的數據分佈和系統負載。當存儲系統中個別設備宕機後,CRUSH會對這些宕機設備作相應標記,而且會將其從存儲架構中移除,這樣這些設備就不會參與後面的存儲,同時也會將其上面的數據複製一份到其它機器進程存儲。

                                                        image

     當集羣架構發生變化後狀況就比較複雜了,例如在集羣中添加節點或者刪除節點。在添加的數據進行移動時,CRUSH的mapping過程所使用的按決策樹中層次權重算法比理論上的優化算法∆w /w更有效。在每一個層次中,當一個香港子樹的權重改變分佈後,一些數據對象也必須跟着從降低的權重移動到上升的權重。因爲集羣架構中每一個節點上僞隨機位置決策是相互獨立的,因此數據會統一從新分佈在該點下面,而且無須獲取從新map後的葉子節點在權重上的改變。僅僅更高層次的位置發送變化時,相關數據纔會從新分佈。這樣的影響在圖3的二進制層次結構中展現了出來。

             164028_HyuI_865233

架構中數據移動的總量有一個最低限度∆w/w,這部分數據將會根據∆w權重從新分佈在新的存儲節點上。移動數據的增量會根據權重h以及平滑上升的界限h ∆w決定。當∆w很是小以致於幾乎接近W時移動數據的總量會經過這個上升界限進行變化,由於在每一個遞歸過程當中數據對象移動到一個子樹上會有一個最低值和最小相關權重。

                                                 

代碼流程圖:

 

                                                  051618505162658

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的最大嘗試次數,超過這個次數則放棄這個副本。

                                            051619033448705

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

     選取過程當中出現衝突、過載或者故障時,算法先在Local域內從新選擇,嘗試有限次數後,若是仍然找不到知足條件的Bucket,那就回到Descent域從新選擇。每次從新選擇時,修改副本數目爲r += ftotal。所以每次選擇失敗都會遞增ftotal,因此能夠儘可能避免選擇時再次選到衝突的節點。

3.4 Bucket類型

     通常而言,CRUSH的開發是爲了協調兩個計算目標:map計算的高效性和可伸縮性,以及當添加或者移除存儲設備後的數據均衡。在最後,CRUSH定義了4種類型的buckets來表明集羣架構中的葉子節點:通常的buckets、列表式buckets、樹結構buckets以及稻草類型buckets。對於在數據副本存儲的進程中的僞隨機選擇嵌套項,每一個類型的bucket都是創建在不一樣的內部數據結構和充分利用不一樣c(r,x)函數的基礎上,這些buckets在計算和重構效率上發揮着不一樣的權衡性。通常的bucket會被因此具備相同權重的項限制,然而其它類型的bucket能夠在任何組合權重中包含混合項。這些bucket的差別總結以下表所示:

                                                             171452_Vcw3_865233

3.4.1 通常的Bucket

     這些存儲設備純粹按個體添加進一個大型存儲系統。取而代之的是,新型存儲系統上存儲的都是文件塊,就像將機器添加進機架或者整個機櫃同樣。這些設備在退役後會被分拆成各個零件。在這樣的環境下CRUSH中的通常類型Bucket會被當成一個設備集合同樣進行使用,例如多個內存組成的計算集合和多個硬盤組成的存儲集合。這樣作的最大好處在於,CRUSH能夠一直map複製品到通常的Bucket中。在這種狀況下,正常使用的Bucket就能夠和不能正常使用的Bucket直接互不影響。

     當咱們使用c(r,x)=(hash(x)+rp)函數從m大小的Bucket中選擇一個項時,CRUSH會給一個輸入值x和一個複製品r,其中,p是從大於m的素數中隨機產生。當r<=m時,咱們可使用一些簡單的理論數據來選擇一個不重複的項。當r>m時,兩個不一樣的r和一個x會被分解成相同的項。實際上,經過這個存儲算法,這將意味着出現一個非零數衝突和回退的機率很是小。

     若是這個通常類型的Bucket大小發生改變後,數據將會在這些機器上出現徹底重組。

     bucket的全部子節點都保存在item[]數組之中。perm_x是記錄此次隨機排布時x的值,perm[]是在perm_x時候對item隨機排列後的結果。r則是選擇第幾個副本。

定位子節點過程。這時咱們從新來看uniform定位子節點的過程。根據輸入的x值判斷是否爲perm_x,若是不是,則須要從新排列perm[]數組,而且記錄perm_x=x。若是x==perm_x時,這時算R = r%size,算後獲得R,最後返回 perm[R]。

/*
* Choose based on a random permutation of the bucket.
*
* We used to use some prime number arithmetic to do this, but it
* wasn't very random, and had some other bad behaviors.  Instead, we
* calculate an actual random permutation of the bucket members.
* Since this is expensive, we optimize for the r=0 case, which
* captures the vast majority of calls.
*/
static int bucket_perm_choose(struct crush_bucket *bucket,
                              int x, int r)
{
    unsigned int pr = r % bucket->size;
    unsigned int i, s;

    /* start a new permutation if @x has changed */
    if (bucket->perm_x != (__u32)x || bucket->perm_n == 0)
    {
        dprintk("bucket %d new x=%d\n", bucket->id, x);
        bucket->perm_x = x;

        /* optimize common r=0 case */
        if (pr == 0)
        {
            s = crush_hash32_3(bucket->hash, x, bucket->id, 0) %
                bucket->size;
            bucket->perm[0] = s;
            bucket->perm_n = 0xffff;   /* magic value, see below */
            goto out;
        }

        for (i = 0; i < bucket->size; i++)
            bucket->perm[i] = i;
        bucket->perm_n = 0;
    }
    else if (bucket->perm_n == 0xffff)
    {
        /* clean up after the r=0 case above */
        for (i = 1; i < bucket->size; i++)
            bucket->perm[i] = i;
        bucket->perm[bucket->perm[0]] = 0;
        bucket->perm_n = 1;
    }

    /* calculate permutation up to pr */
    for (i = 0; i < bucket->perm_n; i++)
        dprintk(" perm_choose have %d: %d\n", i, bucket->perm[i]);
    while (bucket->perm_n <= pr)
    {
        unsigned int p = bucket->perm_n;
        /* no point in swapping the final entry */
        if (p < bucket->size - 1)
        {
            i = crush_hash32_3(bucket->hash, x, bucket->id, p) %
                (bucket->size - p);
            if (i)
            {
                unsigned int t = bucket->perm[p + i];
                bucket->perm[p + i] = bucket->perm[p];
                bucket->perm[p] = t;
            }
            dprintk(" perm_choose swap %d with %d\n", p, p + i);
        }
        bucket->perm_n++;
    }
    for (i = 0; i < bucket->size; i++)
        dprintk(" perm_choose  %d: %d\n", i, bucket->perm[i]);

    s = bucket->perm[pr];
out:
    dprintk(" perm_choose %d sz=%d x=%d r=%d (%d) s=%d\n", bucket->id,
            bucket->size, x, r, pr, s);
    return bucket->items[s];
}

                                                     191336_tVE3_2460844

uniform bucket 適用的狀況:

a.適用於全部子節點權重相同的狀況,並且bucket不多添加刪除item,這種狀況查找速度應該是最快的。由於uniform的bucket在選擇子節點時是不考慮權重的問題,所有隨機選擇。因此在權重上不會進行特別的照顧,爲了公平起見最好是相同的權重節點。

b.適用於子節點變化機率小的狀況。當子節點的數量進行變化時,size發生改變,在隨機組合perm數組時,即便x相同,則perm數組須要徹底從新排列,也就意味着已經保存在子節點的數據要所有發生重排,形成不少數據的遷移。因此uniform不適合子節點變化的bucket,不然會產生大量已經保存的數據發生移動,全部的item上的數據均可能會發生相互之間的移動。

3.4.2 List類型buckets

     List類型的buckets組織其內部的內容會像list的方式同樣,而且裏面的項都有隨機的權重。爲了放置一個數據副本,CRUSH在list的頭部開始添加項而且和除這些項外其它項的權重進行比較。根據hash(x,r,item)函數的值,每一個當前項會根據適合的機率被選擇,或者出現繼續遞歸查找該list。這種方法重申了數據存儲所存在的問題「是大部分新加項仍是舊項?」這對於一個擴展中的集羣是一個根本且直觀的選擇:一方面每一個數據對象會根性相應的機率從新分配到新的存儲設備上,或者依然像之前同樣被存儲在舊的存儲設備上。這樣當新的項添加進到bucket中時這些項會得到最優的移動方式。當這些項從list的中間或者末尾進行移動時,list類型的bucket將比較適合這種環境。    

     它的結構是鏈表結構,所包含的item能夠具備任意的權重。CRUSH從表頭開始查找副本的位置,它先獲得表頭item的權重Wh、剩餘鏈表中全部item的權重之和Ws,而後根據hash(x, r, i)獲得一個[0~1]的值v,假如這個值v在[0~Wh/Ws)之中,則副本在表頭item中,並返回表頭item的id,i是item的id號。否者繼續遍歷剩餘的鏈表。

     list bucket的造成過程。list  bucket 不是真的將全部的item都穿成一個鏈表,bucket的item仍然保存在item數組之中。這時的list bucket 每一個item 不只要保存的權重(根據容量換算而來)weight,還要記錄前全部節點的重量之和sum_weight如圖,list bucket的每一個item的權重能夠不相同,也不須要按順序排列。

                                                      191403_mUiz_2460844

list bucket定位數據在子節點的方法。從head開始,會逐個的查找子節點是否保存數據。

如何判斷當前子節點是否保存了數據呢?首先取了一個節點以後,根據x,r 和item的id 進行crush_hash獲得一個w值。這個值與sum_weight之積,最後這個w再向右移16位,最後判斷這個值與weight的大小,若是小於weight時,則選擇當前的這個item,不然進行查找下一個item。

static int bucket_list_choose(struct crush_bucket_list *bucket,
                              int x, int r)
{
    int i;

    for (i = bucket->h.size - 1; i >= 0; i--)
    {
        __u64 w = crush_hash32_4(bucket->h.hash, x, bucket->h.items[i],
                                 r, bucket->h.id);
        w &= 0xffff;
        dprintk("list_choose i=%d x=%d r=%d item %d weight %x "
                "sw %x rand %llx",
                i, x, r, bucket->h.items[i], bucket->item_weights[i],
                bucket->sum_weights[i], w);
        w *= bucket->sum_weights[i];
        w = w >> 16;
        /*dprintk(" scaled %llx\n", w);*/
        if (w < bucket->item_weights[i])
            return bucket->h.items[i];
    }

    dprintk("bad list sums for bucket %d\n", bucket->h.id);
    return bucket->h.items[0];
}

                                                              191428_LZC1_2460844

 

list bucket使用的狀況:

a.適用於集羣拓展類型。當增長item時,會產生最優的數據移動。由於在list bucket中增長一個item節點時,都會增長到head部,這時其餘節點的sum_weight都不會發生變化,只須要將old_head 上的sum_weight和weight之和添加到new_head的sum_weight就行了。這樣時其餘item之間不須要進行數據移動,其餘的item上的數據 只須要和 head上比較就好,若是算的w值小於head的weight,則須要移動到head上,不然還保存在原來的item上。這樣就得到了最優最少的數據移動。

b.list bucket存在一個缺點,就是在查找item節點時,只能順序查找 時間複雜度爲O(n)。

3.4.3 樹狀 Buckets

     像任何鏈表結構同樣,列表buckets對於少許的數據項仍是高效的,而遇到大量的數據就不合適了,其時間複雜度就太大了。樹狀buckets由RUSHT發展而來,它經過將這些大量的數據項儲存到一個二叉樹中來解決這個問題(時間複雜度過大)。它將定位的時間複雜度由 O(n)下降到O(logn),這使其適用於管理大得多設備數量或嵌套buckets。 RUSHT i等價於一個由單一樹狀bucket組成的二級CRUSH結構,該樹狀bucket包含了許多通常buckets.

    樹狀buckets是一種加權二叉排序樹,數據項位於樹的葉子節點。每一個遞歸節點有其左子樹和右子樹的總權重,並根據一種固定的算法(下面會講述)進行標記。爲了從bucket中選擇一個數據項,CRUSH由樹的根節點開始(計算),計算輸入主鍵x,副本數量r,bucket標識以及當前節點(初始值是根節點)標誌的哈希值,計算的結果會跟(當前節點)左子樹和右子樹的權重比進行比較,儀肯定下次訪問的節點。重複這一過程直至到達(存儲)相應數據項的葉子節點。定位該數據項最多隻須要進行logn次哈希值計算和比較。

     該buckett二叉樹結點使用一種簡單固定的策略來獲得二進制數進行標記,以免當樹增加或收縮時標記更改。該樹最左側的葉子節點一般標記爲「1」, 每次樹擴展時,原來的根節點成爲新根節點的左子樹,新根節點的標記由原根節點的標記左移一位獲得(好比1變成10,10變成100等)。右子樹的標記在左子樹標記的基礎上增長了「1」,擁有6個葉子節點的標記二叉樹如圖4所示。這一策略保證了當bucket增長(或刪除)新數據項而且樹結構增加(或收縮)時,二叉樹中現有項的路徑經過在根節點處增長(或刪除)額外節點便可實現,決策樹的初始位置隨之發生變化。一旦某個對象放入特定的子樹中,其最終的mapping將僅由該子樹中的權重和節點標記來決定,只要該子樹中的數據項不發生變化mapping就不會發生變化。雖然層次化的決策樹在嵌套數據項項之間會增長額外的數據遷移,可是這一(標記)策略能夠保證移動在可接受範圍內,同時還能爲很是巨大的bucket提供有效的mapping。

                                                       174602_Z6Ub_865233

     鏈表的查找複雜度是O(n),決策樹的查找複雜度是O(log n)。item是決策樹的葉子節點,決策樹中的其餘節點知道它左右子樹的權重,節點的權重等於左右子樹的權重之和。CRUSH從root節點開始查找副本的位置,它先獲得節點的左子樹的權重Wl,獲得節點的權重Wn,而後根據hash(x, r, node_id)獲得一個[0~1]的值v,假如這個值v在[0~Wl/Wn)中,則副本在左子樹中,否者在右子樹中。繼續遍歷節點,直到到達葉子節點。Tree Bucket的關鍵是當添加刪除葉子節點時,決策樹中的其餘節點的node_id不變。決策樹中節點的node_id的標識是根據對二叉樹的中序遍從來決定的(node_id不等於item的id,也不等於節點的權重)

     tree bucket 會藉助一個叫作node_weight[ ]的數組來進行幫助搜索定位item。首先是node_weight[ ]的造成,nodeweight[ ]中不只包含了item,並且增長了不少中間節點,item都做爲葉子節點。父節點的重量等於左右子節點的重量之和,遞歸到根節點以下圖。

                                                          191452_t6Ky_2460844

     tree bucket的搜索過程,經過必定的方法造成node tree。這tree的查找從根節點開始直到找到葉子節點。當前節點的重量weight使用crush_hash(x,r)修正後,與左節點的重量left_weight比較,若是比左節點輕 則繼續遍歷左節點,不然遍歷右節點以下圖。因此該類型的bucket適合於查找的,對於變更的集羣就沒那麼合適了。

static int bucket_tree_choose(struct crush_bucket_tree *bucket,
                              int x, int r)
{
    int n;
    __u32 w;
    __u64 t;

    /* start at root */
    n = bucket->num_nodes >> 1;

    while (!terminal(n))
    {
        int l;
        /* pick point in [0, w) */
        w = bucket->node_weights[n];
        t = (__u64)crush_hash32_4(bucket->h.hash, x, n, r,
                                  bucket->h.id) * (__u64)w;
        t = t >> 32;

        /* descend to the left or right? */
        l = left(n);
        if (t < bucket->node_weights[l])
            n = l;
        else
            n = right(n);
    }

    return bucket->h.items[n >> 1];
}

                                                                     191526_1azY_2460844

3.4.4 Straw類型Buckets

     列表buckets和樹狀buckets的結構決定了只有有限的哈希值須要計算並與權重進行比較以肯定bucket中的項。這樣作的話,他們採用了分而治之的方式,要麼給特定項以優先權(好比那些在列表開頭的項),要麼消除徹底考慮整個子樹的必要。儘管這樣提升了副本定位過程的效率,但當向buckets中增長項、刪除項或從新計算某一項的權重以改變其內容時,其重組的過程是次最優的。

     Straw類型bucket容許全部項經過相似抽籤的方式來與其餘項公平「競爭」。定位副本時,bucket中的每一項都對應一個隨機長度的straw,且擁有最長長度的straw會得到勝利(被選中)。每個straw的長度都是由固定區間內基於CRUSH輸入 x, 副本數目r, 以及bucket項 i.的哈希值計算獲得的一個值。每個straw長度都乘以根據該項權重的立方得到的一個係數 f(wi),這樣擁有最大權重的項更容易被選中。好比c(r,x)=maxi(f(wi)hash(x,r,i)). 儘管straw類型bucket定位過程要比列表bucket(平均)慢一倍,甚至比樹狀bucket都要慢(樹狀bucket的時間複雜度是log(n)),可是straw類型的bucket在修改時最近鄰項之間數據的移動是最優的。

     Bucket類型的選擇是基於預期的集羣增加類型,以權衡映射方法的運算量和數據移動之間的效率,這樣的權衡是很是值得的。當buckets是固定時(好比一個存放徹底相同磁盤的機櫃),通常類型的buckets是最快的。若是一個bucket預計將會不斷增加,則列表類型的buckets在其列表開頭插入新項時將提供最優的數據移動。這容許CRUSH準確恰當地轉移足夠的數據到新添加的設備中,而不影響其餘bucket項。其缺點是映射速度的時間複雜度爲O(n) 且當舊項移除或從新計算權重時會增長額外的數據移動。當刪除和從新計算權重的效率特別重要時(好比存儲結構的根節點附近(項)),straw類型的buckets能夠爲子樹之間的數據移動提供最優的解決方案。樹狀buckets是一種適用於任何狀況的buckets,兼具高性能與出色的重組效率。

     這種類型讓bucket所包含的全部item公平的競爭(不像list和tree同樣須要遍歷)。這種算法就像抽籤同樣,全部的item都有機會被抽中(只有最長的籤才能被抽中)。每一個籤的長度是由length = f(Wi)*hash(x, r, i) 決定的,f(Wi)和item的權重有關,i是item的id號。c(r, x) = MAX(f(Wi) * hash(x, r, i))。

     這種類型是一種抽籤類型的bucket,他選擇子節點是公平的,straw和uniform的區別在於,straw算法考慮了子節點的權重,因此是最公平的bucket類型。

                                                                   191556_YSJl_2460844

     straw bucket首先根據每一個節點的重量生成的straw,最後組成straw[] 數組。在straw定位副本的過程當中,每個定位都須要遍歷全部的item,長度draw = crush(x,r,item_id)*straw[i]。找出那個最長的,最後選擇這個最長,定位到副本。

static int bucket_straw_choose(struct crush_bucket_straw *bucket,
                               int x, int r)
{
    __u32 i;
    int high = 0;
    __u64 high_draw = 0;
    __u64 draw;

    for (i = 0; i < bucket->h.size; i++)
    {
        draw = crush_hash32_3(bucket->h.hash, x, bucket->h.items[i], r);
        draw &= 0xffff;
        draw *= bucket->straws[i];
        if (i == 0 || draw > high_draw)
        {
            high = i;
            high_draw = draw;
        }
    }
    return bucket->h.items[high];
}

 

3.5 CRUSH RULE

crush rule主要有3個重點:a.從OSDMap中的哪一個節點開始查找,b.使用那個節點做爲故障隔離域,c.定位副本的搜索模式(廣度優先 or 深度優先)。

# rules

rule replicated_ruleset                            #規則集的命名,建立pool時能夠指定rule集
{
    ruleset 0                                      #rules集的編號,順序編便可
    type replicated                                #定義pool類型爲replicated(還有esurecode模式)
    min_size 1                                     #pool中最小指定的副本數量不能小1\

    max_size 10                                    #pool中最大指定的副本數量不能大於10   

    step take default                              #定義pg查找副本的入口點

    step chooseleaf  firstn  0  type  host         #選葉子節點、深度優先、隔離host
    step emit        #結束
}

pg 選擇osd的過程,首先要知道在rules中 指明從osdmap中哪一個節點開始查找,入口點默認爲default也就是root節點,而後隔離域爲host節點(也就是同一個host下面不能選擇兩個子節點)。由default到3個host的選擇過程,這裏由default根據節點的bucket類型選擇下一個子節點,由子節點再根據自己的類型繼續選擇,知道選擇到host,而後在host下選擇一個osd。

 

還在學習階段,整理了一些資料,本身也看了看crush的代碼實現,後續有新理解再增長內容,有錯誤懇請指教。

參考資料:

CRUSH: Controlled, Scalable, Decentralized Placement of Replicated Data

http://www.oschina.net/translate/crush-controlled-scalable-decentralized-placement-of-replicated-data?cmp&p=2#

做者:一隻小江 http://my.oschina.net/u/2460844/blog/531722?fromerr=ckWihIrE

做者:劉世民(Sammy Liu)http://www.cnblogs.com/sammyliu/p/4836014.html

做者:吳香偉 http://www.cnblogs.com/shanno/p/3958298.html?utm_source=tuicool&utm_medium=referral

感謝以上做者的無私分享。

相關文章
相關標籤/搜索