原文鏈接:http://www.sysnote.org/2016/08/19/ceph-bluestore/
ceph后端支持多種存儲引擎,以插件式的方式來進行管理使用,目前支持filestore,kvstore,memstore以及最新的bluestore,目前默認使用的filestore,但是因為filestore在寫數據前需要先寫journal,會有一倍的寫放大,並且filestore一開始只是對於機械盤進行設計的,沒有專門針對ssd做優化考慮,因此誕生的bluestore初衷就是為了減少寫放大,並針對ssd做優化,而且直接管理裸盤,從理論上進一步減少文件系統如ext4/xfs等部分的開銷,目前bluestore還處於開發優化階段,在jewel版本還是試用版本,並且最新的master相比jewel已經做了大的重構,預期會在后續的大版本中穩定下來成為默認的存儲引擎。本文基於master分支對bluestore存儲引擎進行分析。
bluestore整體架構
bluestore直接管理裸設備,拋棄了ext4/xfs等本地文件系統,BlockDevice實現在用戶態下使用linux aio直接對裸設備進行I/O操作。既然是直接管理裸設備就必然需要進行裸設備的空間管理,對應的就是Allocator,目前支持StupidAllocator和BitmapAllocator兩種分配器。相關的元數據以kv的形式保存到kv數據庫里,默認使用的是rocksdb,由於rocksdb本身是基於文件系統的,不是直接操作裸設備,但是rocksdb也比較靈活,將系統相關的處理抽象成Env,用戶可用實現相應的接口,rocksdb默認的Env是PosixEnv,直接對接本地文件系統,為此,bluestore實現了一個BlueRocksEnv,繼承自EnvWrapper,來為rocksdb提供底層系統的封裝,為了對接BlueRocksEnv,實現了一個小的文件系統BlueFS,只實現rocksdb Env需要的接口,在系統啟動mount這個文件系統的時候將所有的元數據都加載到內存中,BluesFS的數據和日志文件都通過BlockDevice保存到裸設備上,BlueFS和BlueStore可以共享裸設備,也可以分別指定不同的設備。
bluestore元數據
在之前的存儲引擎filestore里,對象的表現形式是對應到文件系統里的文件,默認4MB大小的文件,但是在bluestore里,已經沒有傳統的文件系統,而是自己管理裸盤,因此需要有元數據來管理對象,對應的就是Onode,Onode是常駐內存的數據結構,持久化的時候會以kv的形式存到rocksdb里。
在onode里又分為lextent,表示邏輯的數據塊,用一個map來記錄,一個onode里會存在多個lextent,lextent通過blob的id對應到blob(bluestore_blob_t ),blob里通過pextent對應到實際物理盤上的區域(pextent里就是offset和length來定位物理盤的位置區域)。一個onode里的多個lextent可能在同一個blob里,而一個blob也可能對應到多個pextent。
另外還有Bnode這個元數據,它是用來表示多個object可能共享extent,目前在做了快照后寫I/O觸發的cow進行clone的時候會用到。
I/O讀寫映射邏輯
寫I/O處理
到達bluestore的I/O的offset和length都是對象內(onode)的,offset是相對於這個對象起始位置的偏移,在_do_write里首先就會根據最小分配單位min_alloc_size進行判斷,從而將I/O分為對齊和非對齊的。如下圖所示:
當一個寫請求按照min_alloc_size進行拆分后,就會分為對齊寫,對應到do_write_big,非對齊寫(即落到某一個min_alloc_size區間的寫I/O(對應到do_write_small)。
do_write_big
對齊到min_alloc_size的寫請求處理起來比較簡單,有可能是多個min_alloc_size的大小,在處理時會根據實際大小新生成lextent和blob,這個lextent跨越的區域是min_alloc_size的整數倍,如果這段區間是之前寫過的,會將之前的lextent記錄下來便於后續的空間回收。
do_write_small
在處理落到某個min_alloc_size區間的寫請求時,會首先根據offset去查找有沒有可以復用的blob,因為最小分配單元是min_alloc_size,默認64KB,如果一個4KB的寫I/O就只會用到blob的一部分,blob里剩余的還能放其他的。
1)沒有找到可以復用的blob,新生成blob
在處理還還需要根據offset和len是否對齊到block_size(默認是4KB)進行補零對齊的操作,之所以需要補齊是與后續的寫盤操作有關,真正寫盤時有兩種方式,一種是Direct I/O的方式,這種要求偏移和緩沖區都對齊的,另外一種非Direct I/O,即Buffered I/O,這種可以不對齊,但是是寫到cache里,然后再sync刷到磁盤上,比如只寫了100字節,在內核里是需要先從設備上讀出來補齊成一個完整的扇區,然后再刷的,這樣反而降低了效率。因此在bluestore里直接處理好對齊,對於后面的寫盤來說比較有利,這里對齊到block_size,是個可配置的參數。
進行對齊補零時就是按照如上圖那樣把前后對齊到block_size,然后再把對齊后的offset和len作為lextent,進而放到blob里。
2)找到可以復用的blob
對於可以復用的blob,也是先按照block_size進行對齊補零的動作,然后再判斷是否可以直接使用blob里空閑的空間進行區分做不同的處理。
a)直接寫在blob未使用的空間上
這種情況下直接新生成lextent放到blob里。
b)覆蓋寫的情況
比如下面的這種情況,寫I/O會覆蓋部分已經寫過的數據。
對於這種情況的處理如下圖:也是需要先處理對齊補零的情況,如果覆蓋的區域剛好是已經對齊到block_size,那么就不需要從磁盤讀數據,但是如果覆蓋的區域沒有對齊到block_size,那么就需要把不對齊的那部分讀出來,拼成一個對齊的buffer,然后新生成lextent,並且會對原來那個lextent進行調整,會記錄需要回收的那部分區域。對於覆蓋寫的情況,都不是直接寫盤,而是通過wal寫到rocksdb。
整體寫I/O的邏輯
之前組內同事畫過一個流程圖,這里借用一下算是一個簡單的總結。
讀I/O的處理
讀I/O請求的處理時也是通過尋找相關聯的lextent,可能會存在空洞的情況,即讀到未寫過的數據,這部分就直接補零。
clone及extent共享
前面說到Bnode就是用來記錄共享的lextent,目前是在做完快照后對原卷進行寫I/O會觸發cow,從而產生clone操作。clone時就是將原對象的blob從onode->blob_map移到onode->bnode->blob_map,並且將blob id置為負的,並設置共享標記,然后將新的快照對象的onode->bnode指向原對象的onode->bnode,並且用原onode里的lextents里的值賦給新的onode的lextents,從而達到共享extent的目的,圖示僅供參考。
在clone完之后,繼續對原對象進行寫I/O操作時,當碰到共享的blob時就需要跳過,新生成blob,並且取消對原來那部分lextent的引用,在后續的空間釋放時的判斷依據就是否還有引用。
小結
本文總體上介紹了bluestore的架構、相關元數據及內部I/O映射的邏輯,這還只是bluestore的冰山一角,后續會陸續對bluestore的處理流程、空間分配器、緩存管理、壓縮等實現進行分析。