1、簡介
隨着大規模分布式存儲系統(PB級的數據和成百上千台存儲設備)的出現。這些系統必須平衡的分布數據和負載(提高資源利用率),最大化系統的性能,並要處理系統的擴展和硬件失效。ceph設計了CRUSH(一個可擴展的偽隨機數據分布算法),用在分布式對象存儲系統上,可以有效映射數據對象到存儲設備上(不需要中心設備)。因為大型系統的結構式動態變化的,CRUSH能夠處理存儲設備的添加和移除,並最小化由於存儲設備的的添加和移動而導致的數據遷移。
為了保證負載均衡,保證新舊數據混合在一起。但是簡單HASH分布不能有效處理設備數量的變化,導致大量數據遷移。ceph開發了CRUSH(Controoled Replication Under Scalable Hashing),一種偽隨機數據分布算法,它能夠在層級結構的存儲集群中有效的分布對象的副本。CRUSH實現了一種偽隨機(確定性)的函數,它的參數是object id或object group id,並返回一組存儲設備(用於保存object副本OSD)。CRUSH需要cluster map(描述存儲集群的層級結構)、和副本分布策略(rule)。
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節點可以是數據中心抽象、機房抽象、機架抽象、主機抽象等如下圖。
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分布在機架2、3、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:
- 客戶端輸入 pool ID 和 object ID (比如 pool = “liverpool” and object-id = “john”)
- ceph 對 object ID 做哈希
- ceph 對該 hash 值取 PG 總數的模,得到 PG 編號 (比如 58)(第2和第3步基本保證了一個 pool 的所有 PG 將會被均勻地使用)
- ceph 對 pool ID 取 hash (比如 “liverpool” = 4)
- 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,然后直接與它通信。
- Ceph client 從 MON 獲取最新的 cluster map。
- Ceph client 根據上面的第(2)步計算出該 object 將要在的 PG 的 ID。
- 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硬盤設備來存儲等等。
函數入口:
/**
* 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。
如表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個選項時已經跳過了故障設備,這使得它對於奇偶檢驗方案很難處理。
3.3 Map的變化和數據移動
在大型文件系統中一個比較典型的部分就是數據在存儲資源中的增加和移動。為了避免非對稱造成的系統壓力和資源的不充分利用,CRUSH主張均衡的數據分布和系統負載。當存儲系統中個別設備宕機后,CRUSH會對這些宕機設備做相應標記,並且會將其從存儲架構中移除,這樣這些設備就不會參與后面的存儲,同時也會將其上面的數據復制一份到其它機器進程存儲。
當集群架構發生變化后情況就比較復雜了,例如在集群中添加節點或者刪除節點。在添加的數據進行移動時,CRUSH的mapping過程所使用的按決策樹中層次權重算法比理論上的優化算法∆w /w更有效。在每個層次中,當一個香港子樹的權重改變分布后,一些數據對象也必須跟着從下降的權重移動到上升的權重。由於集群架構中每個節點上偽隨機位置決策是相互獨立的,所以數據會統一重新分布在該點下面,並且無須獲取重新map后的葉子節點在權重上的改變。僅僅更高層次的位置發送變化時,相關數據才會重新分布。這樣的影響在圖3的二進制層次結構中展示了出來。
架構中數據移動的總量有一個最低限度∆w/w,這部分數據將會根據∆w權重重新分布在新的存儲節點上。移動數據的增量會根據權重h以及平滑上升的界限h ∆w決定。當∆w非常小以至於幾乎接近W時移動數據的總量會通過這個上升界限進行變化,因為在每個遞歸過程中數據對象移動到一個子樹上會有一個最低值和最小相關權重。
代碼流程圖:
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,所以可以盡量避免選擇時再次選到沖突的節點。
3.4 Bucket類型
一般而言,CRUSH的開發是為了協調兩個計算目標:map計算的高效性和可伸縮性,以及當添加或者移除存儲設備后的數據均衡。在最后,CRUSH定義了4種類型的buckets來代表集群架構中的葉子節點:一般的buckets、列表式buckets、樹結構buckets以及稻草類型buckets。對於在數據副本存儲的進程中的偽隨機選擇嵌套項,每個類型的bucket都是建立在不同的內部數據結構和充分利用不同c(r,x)函數的基礎上,這些buckets在計算和重構效率上發揮着不同的權衡性。一般的bucket會被所以具有相同權重的項限制,然而其它類型的bucket可以在任何組合權重中包含混合項。這些bucket的差異總結如下表所示:
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];
}
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的權重可以不相同,也不需要按順序排列。
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];
}
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。
鏈表的查找復雜度是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都作為葉子節點。父節點的重量等於左右子節點的重量之和,遞歸到根節點如下圖。
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];
}
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類型。
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://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
感謝以上作者的無私分享。