前言
在實現容器化的初期,計划使用 Ceph 作為容器的存儲。都說存儲是虛擬化之母,相對容器來說,存儲也起到了至關重要的作用。
選用 Ceph 作為容器化存儲理由如下:
- 方便后期橫向擴展;
- Ceph能夠同時支持快存儲、對象存儲、文件存儲,容器使用塊存儲,后期也會用到對象存儲來取代 OSS 服務。
基於以上理由,采用 Ceph 分布式存儲應該是個不錯的選擇。容器化第一步從 存儲 開始。
Ceph 官方文檔(英文):https://docs.ceph.com/docs/master/start/intro/
Ceph 官方文檔(中文):http://docs.ceph.org.cn/start/intro/
Ceph 簡介
不管你是想為 雲平台 提供 Ceph 對象存儲或塊設備,還是想部署一個 Ceph 文件系統 ,所有 Ceph 存儲集群的部署都始於一個個 Ceph節點、網絡和 Ceph存儲集群。
創建Ceph 存儲,至少需要以下服務:
- 一個Ceph Monitor
- 兩個OSD守護進程
而運行 Ceph 文件系統客戶端時,則必須要有元數據服務器(Metadata Server) ,使用 Ceph 作為文件系統這種需求場景應該不會很多,而本次的容器化項目也不會涉及到 文件系統存儲,因此不深究。
Ceph OSDs:Ceph OSD 守護進程(Ceph OSD)的功能是存儲數據,處理數據的復制、恢復、回填、再均衡,並通過檢查其他OSD 守護進程的心跳來向 Ceph Monitors 提供一些監控信息。當 Ceph 集群設定有2個副本時,至少需要2個OSD守護進程,集群才能達到 active+clean 狀態( Ceph 默認有3個副本,但你可以調整副本數)。
可以這樣理解:Ceph 是通過一個個 OSD 來存儲數據的,Ceph 默認的OSD 是3個,但是可以手動設置為2個,最少2個才能使集群達到健康可用的狀態,並且使用 OSD 可以實現數據的高可用。
Monitors:Ceph Monitor維護着展示集群狀態的各種圖表,包括監視器圖、 OSD 圖、歸置組( PG )圖、和 CRUSH 圖。 Ceph 保存着發生在Monitors 、 OSD 和 PG上的每一次狀態變更的歷史信息(稱為 epoch )。
可以這樣理解:Monitor 就是Ceph 的一個監視器,Ceph 在存儲數據的時候,各項數據指標都會記錄顯示出來。至於什么 PG / CURSH 后面會討論到。
Ceph 把客戶端數據保存為存儲池內的對象。通過使用 CRUSH 算法, Ceph 可以計算出哪個歸置組(PG)應該持有指定的對象(Object),然后進一步計算出哪個 OSD 守護進程持有該歸置組。 CRUSH 算法使得 Ceph 存儲集群能夠動態地伸縮、再均衡和修復。
官方文檔一句話,將Ceph的原理總結出來,但是對於初次接觸Ceph的來說,簡直生澀難懂。總結幾個關鍵字:對象、CRUSH 算法、歸置組(PG)、動態地伸縮
到此,就需要明白 Ceph 整體的工作原理是怎樣的?不然這個疑問會一直圍繞着我們。
Ceph 存儲原理介紹
通過查詢資料,發現胖哥這篇文章寫的非常不錯,值得反復理解:大話Ceph - CRUSH 那點事兒
【以下理論知識來源胖哥文章及自己的一些理解】
首先拋出一個問題:將一份數據存儲到 Ceph 集群中,一共需要幾步走?
Ceph 的答案是兩步:
- 計算PG,也就是官方文檔中的歸置組
- 計算OSD
既然提到了計算,那肯定就會有算法,那算法是不是所謂的 CRUSH 呢?
計算PG
首先,要明確Ceph的一個規定:在Ceph 中,一切皆對象。(這里提到了對象,和官方文檔吻合,一切皆對象不難理解,同 python一樣,一切皆對象)
一下舉例來說明了,一切皆對象,無論是視頻、照片、文字還是其他格式文件:
不論是視頻,文本,照片等一切格式的數據,Ceph統一將其看作是對象,因為追其根源,所有的數據都是二進制數據保存於磁盤上,所以每一份二進制數據都看成一個對象,不以它們的格式來區分他們。
既然是對象,那對象就應該有對象名。而區分2個對象的就是通過 對象名 來區分的。那如果兩個對象的文件名一樣呢?
現在一開始的問題就變成:把一個對象存儲到Ceph 集群中分幾步走?
已知:Ceph 集群是有若干服務器,確切的說就是一堆磁盤組成,而在Ceph中 每塊磁盤就看作是一個 OSD。
文件又簡化為:把一個對象存儲到 OSD 中分幾步走?
Ceph中的邏輯層
Ceph為了保存一個對象,對上構建了一個邏輯層,也就是池(Pool),這個不難理解,就像虛擬化中的存儲池化,用來保存對象,如果把Pool比喻為一個中國象棋棋盤,那么保存一個對象的過程類似於把一粒芝麻放置到棋盤中。
簡單如圖:
Pool 再一次細分,即將一個 Pool 划分為若干的PG(歸置組),這類似於棋盤上的方格,所有方格構成了整個棋盤,也就是說所有的PG構成了一個 Pool。
通過這兩個圖,我們可以總結下:文件是一個個對象,對象則是存儲在每個PG里的,而多個PG 構成了Pool
現在問題又來了,對象怎么知道要保存到哪個PG上呢?假定這里我們的 Pool 名叫 rbd,共有 256 個PG,給每個PG 編號分別叫做 0, 1,2, 3, 4
要解決這個問題,首先看目前有什么?
- 不同的對象名
- 不同的PG編號
這里就引入了Ceph的第一個算法:HASH
對於對象名分別為 bar 和 foo 兩個對象,對他們的對象名進行計算即可:
- HASH(‘bar’) = 0x3E0A4162
- HASH(‘foo’) = 0x7FE391A0
- HASH(‘bar’) = 0x3E0A4162
HASH算法應該是最常用的算法,對對象名進行HASH后,得到一串十六進制的輸出值,也就是說通過HASH我們將一個對象名轉化為一串數字,那么上面的第一行和第三行是一樣的有什么意義?意義就是對於一個同樣的對象名,計算出來的結果永遠都是一樣的,但是HASH算法的確將對象名計算得出了一個隨機數。有了這個隨機數,就對這個隨機數除以PG總數,比如 256 ,得到的余數一定是落在 1-256 之間的,也就是這 256 個PG中的某一個。
公式:HASH('bar') % PG數
- 0x3E0A4162 % 0xFF ===> 0x62
- 0x7FE391A0 % 0xFF ===> 0xA0
通過上面的計算,對象bar 保存到編號為 0x62 的PG中,對象 foo 保存在編號 0xA0 的PG中。對象 bar 永遠都會保存在 PG 0x62中!對象 foo 永遠都會保存到PG 0xA0中!
目前,可以總結一個對象是如何存入PG 中的:
對 對象名進行 HASH 取到一個隨機數,然后在用這個隨機數 對 PG 總數取余,得到的值一定落在 1- PG總數之間,改對象名的數據就會永遠的存儲在這個PG 中。
所以,自對象名確定了, 那么該對象保存數據的PG也就確定了。
由此衍生出來一個問題,對象名確定唯一性,難道就不管對象數據的大小了嗎?
答案是肯定的,也就是Ceph不區分對象的真實大小內容以及任何形式的格式,只認對象名。
這里給出更Ceph一點的說明,實際上在Ceph中,存在着多個pool,每個pool里面存在着若干的PG,如果兩個pool里面的PG編號相同,Ceph怎么區分呢? 於是乎,Ceph對每個pool進行了編號,比如剛剛的rbd池,給予編號0,再建一個pool就給予編號1,那么在Ceph里,PG的實際編號是由pool_id+.+PG_id
組成的,也就是說,剛剛的bar
對象會保存在0.62
這個PG里,foo
這個對象會保存在0.A0
這個PG里。其他池里的PG名稱可能為1.12f, 2.aa1,10.aa1
等。
Ceph 中的物理層
在邏輯層中,已經知道一個文件是如何存儲在Ceph 中PG 里,簡單來說就是通過對對象名進行hash 然后對PG總數取余,得到的余數就是對應的PG,然后將數據存儲在這個PG歸置組里。
接下來看看Ceph里的物理層。若干服務器,服務器上有若干磁盤,通常,Ceph 將一個磁盤看作一個OSD(實際上,OSD是管理磁盤的一個程序),於是物理層由若干OSD組成,我們最終目標是將對象保存到磁盤上,在邏輯層里,對象是保存到PG里的,那么現在的任務就是 打通PG和OSD 之間的隧道。PG相當於一堆余數相同的對象的組合,PG把這一部分對象打了個包,現在我們需要把很多的包平均的安放在各個OSD上,這就是CRUSH 算法所有做的事:CRUSH計算 PG -> OSD的映射關系
此時,加上剛剛邏輯層的對象到PG的算法,可以總結出兩個公式:
- 池ID+HASH('對象名') % PG_num --> PG_ID
- CRUSH(PG_ID) --> OSD
在這里采用兩種算法,HASH 和 CRUSH ,這兩種算法有何差異呢?為什么不能直接用 HASH(PG_ID) 到對應的 OSD 上呢?
CURSH(PG_ID) ==> 改為 HASH(PG_ID) % OSD_num ==> OSD
以下是胖哥的推斷:
- 如果掛掉一個OSD,OSD_num=1,於是所有的 PG % OSD_num 的余數都會變化,也就是說這個PG保存的磁盤發生了變化,這最簡單的解釋就是,這個PG 上的數據要從一個磁盤轉移到另一個磁盤上去,一個優秀的存儲架構應當在磁盤損壞時使得數據遷移量降到最低,CRUSH 可以做到;
- 如果保存多個副本,我們希望得到多個OSD 結果的輸出,HASH只能獲得一個,但是CRUSH 可以獲得任意多個;
- 如果增加OSD 的數量,OSD_num 增大了,同樣導致PG 在OSD之間的胡亂遷移,但是CRUSH 可以保證數據向新增機器均勻的擴散。
總結起來就是 HASH 算法只適合 1 對 1 的映射關系,並且計算的兩個值還不能變,因此這里就不適合 PG -> OSD 的映射計算。因此,這里開始引入 CRUSH 算法。
CRUSH 算法
這里不打算詳細介紹 CRUSH 源碼,只是通過舉例的方式來理解 CRUSH 算法。
首先來看要做什么:
- 把已有的PG_ID 映射到OSD上,有了映射關系就可以把一個PG保存到一塊磁盤上;
- 如果想要保存三個副本,可以把一個PG映射到三個不同的 OSD 上,這三個 OSD 上保存着一模一樣的PG內容。
在來看看有什么:
- 互不相同的PG_ID;
- 如果給OSD也編個號,那么就有了互不相同的 OSD_ID;
- 每個OSD最大的不同的就是它們的容量,即 4T還是800GB的容量,我們將每個OSD的容量又稱為OSD的權重(weight),規定4T權重為4,800G為0.8,也就是以 T 位單位的值。
現在的問題轉化為:如何將 PG_ID 映射到有各自權重的 OSD 上。這里直接使用 CRUSH 算法中的 straw 算法,翻譯過來就是 抽簽,說白了就是挑個最長的簽,這里的簽指的是 OSD 的權重。
那總不能每次都挑選容量最大的OSD吧,這不分分鍾都把數據存滿那個最大的OSD了嗎? 所以在挑之前把這些OSD 搓一搓,這里直接介紹CRUSH 算法:
- CRUSH_HASH( PG_ID, OSD_ID, r ) ===> draw
- ( draw &0xffff ) * osd_weight ===> osd_straw
- pick up high_osd_straw
第一行,我們姑且把r當做一個常數,第一行實際上就做了搓一搓的事情:將PG_ID, OSD_ID和r一起當做CRUSH_HASH的輸入,求出一個十六進制輸出,這和HASH(對象名)完全類似,只是多了兩個輸入。所以需要強調的是,對於相同的三個輸入,計算得出的draw
的值是一定相同的。
這個draw
到底有啥用?其實,CRUSH希望得到一個隨機數,也就是這里的draw
,然后拿這個隨機數去乘以OSD的權重,這樣把隨機數和OSD的權重搓在一起,就得到了每個OSD的實際簽長,而且每個簽都不一樣長(極大概率),就很容易從中挑一個最長的。
說白了,CRUSH希望隨機
挑一個OSD出來,但是還要滿足權重越大的OSD被挑中的概率越大,為了達到隨機的目的,它在挑之前讓每個OSD都拿着自己的權重乘以一個隨機數,再取乘積最大的那個。那么這里我們再定個小目標:挑個一億次!從宏觀來看,同樣是乘以一個隨機數,在樣本容量足夠大之后,這個隨機數對挑中的結果不再有影響,起決定性影響的是OSD的權重,也就是說,OSD的權重越大,宏觀來看被挑中的概率越大。
上面的內容不理解也沒關系,這里在簡單梳理下PG 選擇一個OSD時做的事情:
- 給出一個 PG_ID ,作為 CRUSH_HASH 的輸入;
- CRUSH_HASH(PG_ID, OSD_ID, r) 得出一個隨機數(重點是隨機數,不是HASH);
- 對於所有的OSD用他們的權重乘以每個OSD_ID 對應的隨機數,得到乘積;
- 選出乘積最大的OSD;
- 這個PG就會保存到這個OSD上。
通過上面的說明,已經可以解決一個PG映射到多個OSD的問題了, 而常量r,當 r+1,在求一遍隨機數,再去乘以每個OSD的權重,再去選出乘積最大的OSD,如果和之前的OSD 編號不一樣,那么就選中它,如果和之前的OSD 編號一樣的話,那么再把 r+2,再選一次隨機數,直到選出我們需要的三個不一樣編號的OSD為止!
當然實際選擇過程還要稍微復雜一點,我這里只是用最簡單的方法來解釋CRUSH在選擇OSD的時候所做的事情。
CRUSH 算法的應用
理解了上面CRUSH選擇OSD的過程,我們就很容易進一步將CRUSH算法結合實際結構,這里給出Sage在他的博士論文中畫的一個樹狀結構圖:
最下面的藍色長條可以看成一個個主機,里面的灰色圓柱形可以看成一個個OSD,紫色的cabinet可以也就是一個個機櫃, 綠色的row可以看成一排機櫃,頂端的root是我們的根節點,沒有實際意義,你可以把它看成一個數據中心的意思,也可以看成一個機房的意思,不過只是起到了一個樹狀結構的根節點的作用。
基於這樣的結構選擇OSD,我們提出了新的要求:
- 一共選出三個OSD;
- 這三個OSD 需要都位於一個row下面;
- 每個cabinet內至多有一個OSD。
這樣的要求,如果用上一節的CRUSH 選 OSD 的方法,不能滿足二三兩個要求,因為 OSD 的分布是隨機的。
那么要完成這樣的要求,先看看有什么:
- 每個OSD的weight;
- 每個主機也可以有一個weight,這個weight由主機內的所有 OSD 的weight累加而得;
- 每個cabinet的weight由所有主機的weight累加而得,其實就是這個cabinet下的所有OSD的權重值和;
- 同理推得每個row的weight有cabinet累加而得;
- root的weight其實就是所有的OSD的權重之和。
所以在這棵樹狀結構中,每個節點都有了自己的權重,每個節點的權重由下一層
節點的權重累加而得,因此根節點root的權重就是這個集群所有的OSD的權重之和,那么有了這么多權重之后,我們怎么選出那三個OSD呢?
仿照CRUSH 選 OSD 的方法:
- CRUSH從root下的所有的row中選出一個row;
- 在剛剛的一個row下面的所有cabinet中,CRUSH選出三個cabinet;
- 在剛剛的三個cabinet下面的所有OSD中,CRUSH分別選出一個OSD。
因為每個row都有自己的權重,所以CRUSH選row的方法和選OSD的方法完全一樣,用row的權重乘以一個隨機數,取最大。然后在這個row下面繼續選出三個cabinet,再在每個cabinet下面選出一個OSD。
這樣做的根本意義在於,將數據平均分布在了這個集群里面的所有OSD上,如果兩台機器的權重是16:32,那么這兩台機器上分布的數據量也是1:2。同時,這樣選擇做到了三個OSD分布在三個不同的cabinet上。
那么結合圖例這里給出CRUSH算法的流程:
- take(root) ============> [root]
- choose(1, row) ========> [row2]
- choose(3, cabinet) =====> [cab21, cab23, cab24] 在[row2]下
- choose(1, osd) ========> [osd2107, osd2313, osd2437] 在三個cab下
- emit ================> [osd2107, osd2313, osd2437]
這里給出CRUSH算法的兩個重要概念:
- BUCKET/OSD:OSD和我們的磁盤一一對應,bucket是除了OSD以外的所有非子節點,比如上面的 cabinet,row,root等都是;
- RULE:CRUSH選擇遵循一條條選擇路徑,一個選擇路徑就是一個rule。
RULE 一般分為三步走: take -> choose N -> emit.
Take這一步負責選擇一個根節點,這個根節點不一定是root,也可以是任何一個Bucket。
choose N 做的就是按照每個 Bucket 的weight 以及每個 choose 語句選出符合條件的 Bucket,並且,下一個 choose 的選擇對象為上一步得到的結果。
emit 就是輸出最終結果相當於出棧。
這里再舉個簡單的例子,也就是我們最常見的三個主機每個主機三個OSD的結構:
我們要從三個host下面各選出一個OSD,使得三個副本各落在一個host上,這時候,就能保證掛掉兩個host,還有一個副本在運行了,那么這樣的RULE就形如:
- take(root) ============> [default] 注意是根節點的名字
- choose(3, host) ========> [ceph-1, ceph-2, ceph-3]
- choose(1, osd) ========> [osd.3, osd.1, osd.8]
- emit()
這里在簡單總結下:
我們把一個生產環境的機房畫成一個樹狀結構:
最下面一層為OSD層,每個OSD有自己的權重。
OSD的上面由host/rack/row/room/root等等節點構成,每個節點的權重都是由下層的節點累加而成。
CRUSH選擇每個節點的算法(straw)都是一樣的,用它們的weight乘以一個隨機數取其中最大,只是我們通過choose的語句來判斷選擇的節點類型和個數。
最后不要忘了選出來的結果是PG->OSD的映射,比如:pg 0.a2 ---> [osd.3, osd.1, osd.8]和pg 0.33 ---> [osd.0, osd.5, osd.7], 每個PG都有自己到OSD的映射關系,這個關系用公式總結就是: CRUSH(pg_id) ---> [osd.a, osd.b ...osd.n]。
到目前為止,我們已經完成了一份數據保存到一群Server的第二步,再整體回顧下這個流程:
- 每個文件都有一個唯一的對象名;
- Pool_ID + HASH(對象名) % PG_NUM 得到PG_ID;
- CRUSH(PG_ID) 得到該PG將要保存的OSD組合;
- 這個對象就會保存到位於這些OSD上的PG上(PG就是磁盤上的目錄)
所以,HASH算法負責計算對象名到PG的映射,CRUSH負責計算PG到OSD的映射,暫且記住這一點。
Ceph 架構
通過上面對原理的理解,再看下圖Ceph架構就很容理解了。
從上往下看:
1. Ceph 對外提供了 4 個接口:
- 應用直接訪問RADOS,這個需要通過自行開發調用接口,適合自主開發;
- 對象存儲接口,支持(Amazon)S3 和 Swift 的調用方式,適合存儲對象存儲,如圖片、視頻等;
- 塊存儲接口(RBD),主要對外提供塊存儲,例如提供給虛擬機使用的塊設備存儲;
- 文件存儲(CephFS),類似於NFS 的掛載目錄存儲。
2. 應用直接訪問、對象存儲、塊存儲都是依賴於 LibRados庫文件的
3. Ceph將底層是一個RADOS對象存儲系統,通過RADOS對象存儲系統對外提供服務
4. MDS 元數據節點、MON 管理控制節點、很多的 Pool存儲池
5. 在Pool存儲池中包含了很多PG歸置組,然后通過 CRUSH 算法將PG中的數據存儲到各個 OSD 組中。
在上面的步驟中,我們需要重點關注第 5 點,上文說過,在 Ceph中,一切皆對象。首先通過將對象名 通過 HASH 算法得到一個16進制的數,再對 Pool中的PG總數取余,得到的余數一定落在 PG總數中的一個節點上,Ceph就將這個對象名的數據存儲在這個PG里,然后再通過 CRUSH_HASH(PG_ID, OSD_ID, r) 計算得到一個值,這樣就可以將數據落到OSD上,而在Ceph中,OSD 就是磁盤的代名詞。通過圖示來表示這一過程:
Ceph 特點及核心組件
Ceph 特點:
高性能
- 摒棄了傳統的集中式存儲元數據尋址的方案,采用 CRUSH 算法,數據分布均衡,並行度高;
- 考慮了容災域的隔離,能夠實現各類負載的副本放置規則;
- 能夠支持上千個存儲節點的規模,支持TB到PB級的數據。
高可用
- 副本數可以靈活控制;
- 支持故障域分隔,數據強一致性;
- 多種故障場景自動進行修復自愈;
- 沒有單點故障,自動管理。
高擴展
- 原生分布式去中心化;
- 擴展靈活;
- 隨着節點增加而線性增長。
豐富特性
- 支持三種存儲接口:塊存儲、對象存儲、文件存儲;
- 支持自定義接口,支持多個語言。
核心組件:
Monitor
- 一個Ceph集群需要多個Monitor組成的小集群,用來保存OSD的元數據。
OSD
- OSD 全稱Object Storage Device,也就是負責響應客戶端請求返回具體數據的進程。一個Ceph集群一般都有很多個OSD,Ceph最終數據也是由OSD保存到磁盤上的。
MDS
- MDS全稱Ceph Metadata Server,是CephFS服務依賴的元數據服務。如果不使用文件存儲,則不需要安裝。
Object
- Ceph最底層的存儲單元是Object對象,每個Object包含元數據和原始數據。
PG
- PG全稱Placement Grouops,是一個邏輯的概念,一個PG包含多個OSD。引入PG這一層其實是為了更好的分配數據和定位數據。
RADOS
- RADOS全稱Reliable Autonomic Distributed Object Store,是Ceph集群的精華,用戶實現數據分配、Failover等集群操作。
Libradio
- Librados是Rados提供庫,因為RADOS是協議很難直接訪問,因此上層的RBD、RGW和CephFS都是通過librados訪問的,目前提供PHP、Ruby、Java、Python、C和C++支持。
CRUSH
- CRUSH是Ceph使用的數據分布算法,類似一致性哈希,讓數據分配到預期的地方。
RBD
- RBD全稱RADOS block device,是Ceph對外提供的塊設備服務。
RGW
- RGW全稱RADOS gateway,是Ceph對外提供的對象存儲服務,接口與S3和Swift兼容,如果不使用對象存儲則不需要安裝。
CephFS
- CephFS全稱Ceph File System,是Ceph對外提供的文件系統服務。
到此, Ceph的一些基礎概念、原理及架構的學習完畢,接下來嘗試搭建一個Ceph集群。
參考鏈接:
http://xuxiaopang.com/2016/11/08/easy-ceph-CRUSH/
https://blog.csdn.net/uxiAD7442KMy1X86DtM3/article/details/81059215