標籤:
[email protected]:~# ceph osd tree
# id weight type name up/down reweight
-1 0.05997 root default
-2 0.02998 host osd0
1 0.009995 osd.1 up 1
2 0.009995 osd.2 up 1
3 0.009995 osd.3 up 1
-3 0.02998 host osd1
5 0.009995 osd.5 up 1
6 0.009995 osd.6 up 1
7 0.009995 osd.7 up 1
儲存節點
在進一步之前,先考慮這點:Ceph是分布式儲存系統,不管其“分布式邏輯”的細節,資料最終是要儲存到裝置上。
此時有兩種選擇:直接操作裝置,或者由本地檔案系統代理。前者表示直接面對硬碟,怎麼在硬碟上組織資料都是自己做。後者表示不直接面對硬碟,而是使用已有的檔案系統。Ceph使用第二種方式,可選的本地檔案系統有ext4、btrfs和xfs等,我們使用ext4.
crush演算法
設想你是使用者,你有一部動作片要存放到Ceph叢集中。你拿著你的筆記本來到機房,你所看到的Ceph叢集是數個機架的伺服器。你在想,我的動作片最終是儲存在哪裡?
你考慮的實際是資料定位的問題。
常見有兩種資料定位方法:
- 記錄。將“資料A:位置location(A)”這樣的資訊記錄下來,訪問資料時查詢記錄,擷取位置,再進行讀取。
- 計算。存放和資料A時,其儲存位置location(A)是即時計算得到的。能感覺到這種方式更為便利。
常見的計算方式是一致性雜湊(consistent hashing),GlusterFS使用的是這種方式,基本思路是,面對資料A,以資料A的檔案名稱等類似資訊為key,通過一致性雜湊計算consist_hash(keyA) = location(A)得到儲存位置。
ceph使用的是crush演算法:Controlled, Scalable, Decentralized Placement of Replicated Data。crush是ceph核心之一,本文也將重點描述。
簡單來說crush也利用雜湊來計算位置,只不過它更多地利用了叢集的結構資訊。下面通過執行個體來嘗試理解。
前文提到rbd、cephfs和rgw這三種使用情境,它們都基於RADOS層。RADOS層對外提供有librados介面,據此可以實現自己的工具。ceph預設提供一個程式rados,通過rados可以直接上傳一個object到ceph叢集。
mon0# rados put bigfile bigfile.data -p rbd // 將資料bigfile.data上傳為object bigfile
mon0# rados ls -p rbd
bigfile
bigfile的儲存位置是通過CRUSH計算得來,ceph提供命令可查詢一個object的位置。
mon0# ceph osd map rbd bigfile
osdmap e67 pool ‘rbd‘ (1) object ‘bigfile‘ -> pg 1.a342bdeb (1.6b) -> up ([6,3], p6) acting ([6,3], p6)
[6,3]表示bigfile這個object儲存在osd.6和osd.3上(ceph叢集部署參考前文),並且放置在pg 1.6b下,也即放在目錄1.6b下。
驗證這一點:
osd0# ls /var/lib/ceph/osd/ceph-6/current/1.6b_head/ -lh
total 61M
-rw-r--r-- 1 root root 61M 10月 29 08:12 bigfile__head_A342BDEB__1 // 感受下object的命名方式
pg (placement group)
上例中提到pg,這是ceph crush資料對應的一個中介層。
討論pg需要提及pool,pool是ceph的一個邏輯概念,使用者可在ceph叢集上建立數個pool,設定不同的屬性,然後根據需求,將不同的資料放在不同的pool中。比如我有兩種資料,一種只需要儲存2個備份,另一種更重要,我需要儲存3個備份,從而可以建立兩個pool,設定size=2和size=3。
我的實驗環境中只有一個pool,命名為rbd(這個命名是隨意的,不要和rbd塊裝置使用情境混淆),其備份數目是2份:
mon0# ceph osd dump | grep pool
pool 1 ‘rbd‘ replicated size 2 min_size 2 crush_ruleset 0 object_hash rjenkins pg_num 128 pgp_num 128 last_change 48 flags hashpspool stripe_width 0
ceph在pool的概念下提供placement group的概念,並通過參數pg_num指定placement group的數目。從上面的輸出可知我的rbd pool下有128個pg。
pg實際對應目錄,rbd pool下有128個pg,意即rbd pool設定為有128個目錄來存放資料。之所以增加pg層來管理資料,是為了資料管理的便利,也減少了中繼資料資訊量。
rbd pool128個pg,每個pg放置在哪些osd上,這也是計算確定的,計算過程是crush演算法的一部分。128個pg整體分散儲存在所有osd上,因此在某個osd上,我們不會看到所有128個目錄。
1.11_head 1.1f_head 1.27_head 1.33_head 1.39_head 1.42_head 1.4c_head 1.54_head 1.60_head 1.66_head 1.74_head 1.79_head 1.7e_head 1.8_head 1.b_head 1.f_head nosnap
1.15_head 1.21_head 1.2e_head 1.36_head 1.3_head 1.44_head 1.4f_head 1.56_head 1.61_head 1.6a_head 1.75_head 1.7b_head 1.7f_head 1.9_head 1.d_head commit_op_seq omap
1.1b_head 1.26_head 1.2f_head 1.37_head 1.41_head 1.45_head 1.50_head 1.5a_head 1.63_head 1.73_head 1.78_head 1.7c_head 1.7_head 1.a_head 1.e_head meta
說明:pg的命名方式是${pool_id}.${pg.id}_${snap},pgid是16進位值,如1.a_head表示128個pg中的第10個pg。
因此crush資料定位演算法的大致邏輯是:
- step 1: 輸入object id,計算得到它應該被放置在哪個pg下,得到pgid。
- step 2: 輸入pgid,計算pg位於哪些osd上。
- step 3: 訪問object.
crush計算樣本
我們想在這個理解基礎上,來瞭解更多代碼細節,考慮到ceph貌似沒有提供訪問crush演算法的介面,我們在ceph0.86的代碼上做些修改,讓librados提供ceph叢集的layout資訊,同時將crush代碼拎出來做成libcrush,然後利用修改過的librados和libcrush寫程式驗證crush演算法的步驟。
- libcrush代碼以及對ceph0.86的改動見:https://github.com/xanpeng/libcrush
- crush演算法步驟驗證代碼見crush-tester.cc:https://gist.github.com/xanpeng/a41a25b5810cb2c8852c#file-crush-tester-cc
crush-tester.cc main()展示了資料定位的步驟,其邏輯等同於"ceph osd map"的邏輯。需注意:代碼是強依賴於我們前文部署的ceph環境的。
int main(int argc, char **argv) { assert(argc == 2); string objname = argv[argc-1]; // 假設objname=bigfile,這一步是通過objname計算得到一個數值,和pg其實沒有關係,但官方代碼就是這麼命名的,我們這裡也做類似命名 // bigfile->some_value,這裡利用的演算法是ceph_str_hash_rjenkins,演算法這裡不細究,其實也無必要細究。 // 其作用是將不同的objname映射層不同的數值,相信其特點是得到的數值衝突率很低。 pg_t pg = object_to_pg(object_t(objname), object_locator_t(g_pool_id)); printf("objectr_to_pg: %s -> %x\n", objname.c_str(), pg.seed); // bigfile得到的值是a342bdeb,作為這一步的輸入。 // 這一步的作用是將這個數值,也即將object bigfile映射到具體的pg。 // 用到的方法其實是模數,根據pg_num做模數計算,對應函數是crush_stable_mod(a342bdeb, 128, 127)=6b。 pg_t mpg = raw_pg_to_pg(pg); printf("raw_pg_to_pg: %x\n", mpg.seed); // 這一步是crush演算法的核心步驟, // 輸入是pgid=6b,以及ceph叢集的layout,也就是crushmap, // 輸出是6b的儲存位置是哪兩個osd(因為備份數設定size=2), // crush演算法的過程在libcrush中,注意這樣連結本程式"-lcrush -lrados",從而使用libcrush而不是librados中的crush代碼,這樣我們通過介入libcrush去理解crush演算法。 // crush演算法計算6b的位置的過程見下文。 printf("pg_to_osds:\n"); vector<int> up; pg_to_up_acting_osds(mpg, &up); }
# g++ crush_tester.cc -o test_crush -lcrush -lrados --std=c++11 -g -O0
# ./test_crush bigfile
objectr_to_pg: bigfile -> a342bdeb
raw_pg_to_pg: 6b
pg_to_osds:
osd_weight: 0,65536,65536,65536,0,65536,65536,65536, // 這一輸出我覺得奇怪,感覺是錯誤的,但偏偏最後crush映射的結果是正確的
ruleno: 0
placement_ps: 1739805228
// 這是crush演算法計算6b位置的輸出,注意計算依據之一是我們實驗環境的crushmap,
// 有數種方法可以從一個live ceph cluster中得到當前的crushmap,我們環境的crushmap見:https://gist.github.com/xanpeng/a41a25b5810cb2c8852c#file-ceph-env-txt
// 通過crushmap可以知道,從crush演算法的視角,我們的環境是這樣分層的:
// 第一層:root
// id=-1,alg=straw,hash=rjenkins1,包含兩個item [osd0, osd1]
// 第二層:host
// host osd0: id=-2, alg=straw, hash=rjenkins1,包含三個item [osd.1, osd.2, osd.3]
// host osd1: id=-3, alg=straw, hash=rjenkins1,包含三個item [osd.5, osd.6, osd.7],之所以不是4,5,6,表示我操作過程中出過點點紕漏,但無礙。
// 第三層:osd
// 共6個osd,id是[1,2,3,5,6,7],每個osd有個權重,通過設定osd的權重,影響資料是否存放在當前osd的偏好。
---start crush_do_rule---
CHOOSE_LEAF bucket -1 x 1739805228 outpos 0 numrep 2 tries 51 recurse_tries 1 local_retries 0 local_fallback_retries 0 parent_r 0
// 演算法第一步,從第一層開始,擷取第一個資料備份的存放位置,判斷我該進入第二層的哪個host。
// 由於第一層設定的alg=straw,表示用straw演算法去選取第二層的host,straw演算法又利用到rjenkins1雜湊演算法
// 得到第一個備份存放位置是item -3上,即osd1上
crush_bucket_choose -1 x=1739805228 r=0
item -3 type 1
// 演算法第二步,在osd1下的第三層中選一個osd,
// 使用的演算法是osd1這個bucket設定的straw+rjenkins1,
// 這是crush演算法過程中的第二次雜湊。我們的實驗環境簡單,layout只有三層,在層次更多的環境中,crush需要做更多次數的雜湊。
// 得到第一個備份存放位置是osd1上的osd.6
CHOOSE bucket -3 x 1739805228 outpos 0 numrep 1 tries 1 recurse_tries 0 local_retries 0 local_fallback_retries 0 parent_r 0
crush_bucket_choose -3 x=1739805228 r=0
item 6 type 0
CHOOSE got 6
CHOOSE returns 1
CHOOSE got -3
// 演算法第三步,計算第二個資料備份的位置,求得位於osd0
crush_bucket_choose -1 x=1739805228 r=1
item -2 type 1
// 演算法第四步,同理進而求得第二個備份的位置:osd0上的osd.3
CHOOSE bucket -2 x 1739805228 outpos 1 numrep 2 tries 1 recurse_tries 0 local_retries 0 local_fallback_retries 0 parent_r 0
crush_bucket_choose -2 x=1739805228 r=1
item 3 type 0
CHOOSE got 3
CHOOSE returns 2
CHOOSE got -2
CHOOSE returns 2
---finish crush_do_rule---
numrep: 2, raw_osds: [6,3,]
對比執行多次crush-tester和"ceph osd map",證實上述理解應為正確
ceph之crush演算法樣本