天天看點

Ceph BlueStore BlueFSIntroductionData StructureBlueFS InitConfigMetricSummary

Author:Eric

Source:http://blog.wjin.org/posts/ceph-bluestore-bluefs.html

Declaration: this work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License. 

Ceph BlueStore BlueFSIntroductionData StructureBlueFS InitConfigMetricSummary

Introduction

BlueStore存儲引擎實作中,需要存儲資料和中繼資料。由于kv存儲系統自身的高效性以及對事務的支援,是以選擇kv存儲中繼資料是理所當然的(對象的omap屬性算作資料,也是存放在kv中的)。Luminous目前預設采用RocksDB來存儲中繼資料(RocksDB本身存在寫放大以及compaction的問題,後續可能會針對Ceph的場景量身定制kv),但是BlueStore采用裸裝置,RocksDB不支援raw disk,幸運的是,RocksDB提供RocksEnv運作時環境來支援跨平台操作,那麼能夠想到的方案就是Ceph自己實作一個簡單的檔案系統,這個檔案系統隻提供RocksEnv需要的操作接口,這樣就可以支援RocksDB的運作,而這個檔案系統就是BlueFS。

作為檔案系統本身,需要存放日志,保護檔案系統資料的一緻性。對于RocksDB,也可以對.log檔案單獨配置性能更好的磁盤。是以在BlueFS内部實作的時候,支援多種不同類型的裝置(wal/db/slow),實作非常靈活,大緻原則是RocksDB的.log檔案和BlueFS自身的日志檔案優先使用wal,BlueFS中的普通檔案(RocksDB的.sst檔案)優先使用db,當目前裝置空間不足的時候,自動降級到下一級的裝置。

檔案系統本身需要使用磁盤空間存放資料,但是BlueFS并不需要管理磁盤空閑空間,它将檔案配置設定和釋放空間的操作記錄在日志檔案中。每次重新加載的時候,掃描檔案系統的日志,在記憶體中還原整個檔案系統的中繼資料資訊。運作過程中,磁盤空間使用情況大緻如下(借用Ceph作者Sage的圖):

Ceph BlueStore BlueFSIntroductionData StructureBlueFS InitConfigMetricSummary

Data Structure

先看看在BlueFS中辨別一個檔案的inode:

// 實體磁盤的位移和長度,代表塊裝置的一個存儲區域
class AllocExtent {
	public:
		uint64_t offset; // BlockDevice的實體位址
		uint32_t length; // 長度
};

class bluefs_extent_t : public AllocExtent{
	public:
		uint8_t bdev; // 屬于哪個block device
};

// 檔案的inode
truct bluefs_fnode_t {
	uint64_t ino; // inode編号
	uint64_t size; // 檔案大小
	utime_t mtime; // 修改時間
	uint8_t prefer_bdev; // 優先使用哪個block device
	mempool::bluefs::vector<bluefs_extent_t> extents; // 檔案對應的磁盤空間
	uint64_t allocated; // 檔案實際占用的空間大小,extents的length之和。應該是小于等于size
};
           

和一般檔案系統類似,需要一個檔案系統超級塊,在mount檔案系統的時候,需要讀取超級塊裡的資料,才能識别檔案系統:

struct bluefs_super_t {
  uuid_d uuid; // 唯一的uuid
  uuid_d osd_uuid; // 對應的osd的uuid
  uint64_t version; // 版本
  uint32_t block_size; // 塊大小

  bluefs_fnode_t log_fnode; // 記錄檔案系統日志的檔案
};
           

接下來就是檔案系統的操作,這些操作對檔案系統進行修改,需要封裝成事務并記錄在日志中:

struct bluefs_transaction_t {
	typedef enum {
		OP_NONE = 0,
		OP_INIT,        ///< initial (empty) file system marker

		// 給檔案配置設定和釋放空間
		OP_ALLOC_ADD,   ///< add extent to available block storage (extent)
		OP_ALLOC_RM,    ///< remove extent from availabe block storage (extent)

		// 建立和删除目錄項
		OP_DIR_LINK,    ///< (re)set a dir entry (dirname, filename, ino)
		OP_DIR_UNLINK,  ///< remove a dir entry (dirname, filename)

		// 建立和删除目錄
		OP_DIR_CREATE,  ///< create a dir (dirname)
		OP_DIR_REMOVE,  ///< remove a dir (dirname)

		// 檔案更新
		OP_FILE_UPDATE, ///< set/update file metadata (file)
		OP_FILE_REMOVE, ///< remove file (ino)

		// bluefs日志檔案的compaction操作
		OP_JUMP,        ///< jump the seq # and offset
		OP_JUMP_SEQ,    ///< jump the seq #
	} op_t;

	uuid_d uuid;          ///< fs uuid
	uint64_t seq;         ///< sequence number
	bufferlist op_bl;     ///< encoded transaction ops
};
           

最後看看檔案系統本身的結構:

class BlueFS {
	public:
		// 檔案系統支援不同種類的塊裝置
		static constexpr unsigned MAX_BDEV = 3;
		static constexpr unsigned BDEV_WAL = 0;
		static constexpr unsigned BDEV_DB = 1;
		static constexpr unsigned BDEV_SLOW = 2;

		enum {
			WRITER_UNKNOWN,
			WRITER_WAL, // RocksDB的log檔案
			WRITER_SST, // RocksDB的sst檔案
		};

		// 檔案
		struct File : public RefCountedObject {
			bluefs_fnode_t fnode; // 檔案inode
			int refs; // 引用計數
			uint64_t dirty_seq; // dirty序列号
			bool locked;
			bool deleted;
			boost::intrusive::list_member_hook<> dirty_item;

			// 讀寫計數
			std::atomic_int num_readers, num_writers;
			std::atomic_int num_reading;
		};

		// 目錄
		struct Dir : public RefCountedObject {
			mempool::bluefs::map<string,FileRef> file_map; // 目錄包含的檔案
		};

		// 檔案系統的記憶體映像
		mempool::bluefs::map<string, DirRef> dir_map; // 所有的目錄
		mempool::bluefs::unordered_map<uint64_t,FileRef> file_map; // 所有的檔案

		map<uint64_t, dirty_file_list_t> dirty_files; // 髒檔案,根據序列号排列

		// 檔案系統超級塊和日志
		......

		// 結構體FileWriter/FileReader/FileLock,用來對一個檔案進行讀寫和加鎖
		......

		vector<BlockDevice*> bdev; // BlueFS能夠使用的所有BlockDevice,包括wal/db/slow
		vector<IOContext*> ioc; // bdev對應的IOContext
		vector<interval_set<uint64_t> > block_all;  // bdev對應的磁盤空間
		vector<Allocator*> alloc; // bdev對應的allocator
		......
};
           

BlueFS Init

BlueFS的使用者(RocksDB/RocksEnv)隻會對檔案系統進行正常的操作,比如建立/删除檔案,打開檔案進行讀寫等操作。但是在使用檔案系統之前,檔案系統必須格式化,這個是由BlueStore存儲引擎統一管理的,部署osd的時候會完成BlueFS的初始化,流程如下:

int OSD::mkfs(CephContext *cct, ObjectStore *store, const string &dev,
		uuid_d fsid, int whoami)
{
	......
	ret = store->mkfs();
	......
}

int BlueStore::mkfs()
{
	......
	r = _open_db(true);
	......
}

int BlueStore::_open_db(bool create)
{
	......
	bluefs = new BlueFS(cct);
	......

	// 依次添加 slow/db/wal等裝置
	bluefs->add_block_device(...) // 添加裝置,會建立一個BlockDevice及其對應的IOContext
	bluefs->add_block_extent(...) // 添加裝置的存儲空間,一般為SUPER_RESERVED到磁盤空間的上限,SUPER_RESERVED為8192,即從第三個4k開始
	......

	bluefs->mkfs(fsid); // 格式化檔案系統,主要工作包括生成檔案系統的超級塊/log檔案等
	bluefs->mount(); // mount檔案系統
	......
}
           

整個流程比較簡單,這裡隻需要明白,BlueFS是一個記憶體檔案系統,mount的時候,通過掃碼日志,在記憶體中還原出整個檔案系統的狀況,包括dir_map和file_map等。之是以這樣做,是因為BlueFS僅僅為RocksDB服務,檔案系統本身隻包含少量的檔案,記憶體空間和磁盤日志空間占用均不大。

int BlueFS::mount()
{
	// 讀取超級塊
	int r = _open_super();
	......

	// 初始化allocator為磁盤所有的空間
	_init_alloc();
	......

	// 回放檔案系統日志,日志項即為上面的事務OP,針對每個事務進行回放,檔案系統的dir_map/file_map就會被更新
	r = _replay(false);

	for (auto& p : file_map) {
		for (auto& q : p.second->fnode.extents) {
			alloc[q.bdev]->init_rm_free(q.offset, q.length); // 将檔案已經占用的内容從allocator中删除
		}
	}
	......
}
           

mount完成後,檔案系統的所有資料,包括檔案和目錄,在記憶體中就初始化完成,後續就可以對檔案系統進行讀寫等操作。打開檔案進行寫的操作是open_for_write,如果是新檔案,更新dir_map,注意打開檔案的時候,檔案的prefer_bdev預設采用的BDEV_DB裝置,然後會根據目錄名稱(slow/wal目錄有特殊字尾)進行适當調整,是以BlueFS的普通檔案,優先使用db裝置,而不會使用空間較少的wal裝置。

檔案系統提供的API比較少,其他實作也比較簡單,同樣是記錄日志和更新記憶體中的dir_map和file_map等。當發生異常或重新開機程序的時候,回放檔案系統的日志,将檔案系統的記憶體狀态還原到之前的狀态。

另外一個值得注意的地方是BlueFS檔案系統日志的compact操作,這個分為sync和async兩種,大緻流程是将檔案系統的記憶體映像(檔案和目錄)重新生成事務,然後寫入新的日志檔案,然後将舊的日志檔案删除,而不會對舊的日志檔案做讀寫。

Config

// 普通檔案
bluefs_alloc_size // 最小配置設定大小,預設為1MB
bluefs_max_prefetch // 預讀時的最大位元組數,預設為1MB,主要用在順序讀場景

// 日志檔案
bluefs_min_log_runway // bluefs日志檔案的可用空間小于此值時,新配置設定空間。預設為1MB
bluefs_max_log_runway // bluefs日志檔案的單次配置設定大小,預設為4MB
bluefs_log_compact_min_ratio // 通過目前日志檔案大小和預估的日志檔案的大小的比率控制compact,預設為5
bluefs_log_compact_min_size // 通過日志檔案大小控制compact,小于此值不做compact。預設為16MB
bluefs_compact_log_sync // 日志檔案compact的方式,有sync和async兩種,預設為false,即采用async方式

bluefs_min_flush_size // 因為寫檔案内容是寫到記憶體中的,當檔案内容超過此值就重新整理到磁盤。預設為512kb

bluefs_buffered_io // bluefs調用BlockDevice的read/write時的參數,預設為false,即采用fd_direct
bluefs_sync_write // 是否采用synchronous寫。預設為false,即采用aio_write。這時候在flush block device的時候,需要等待aio完成。參見函數_flush_bdev_safely

bluefs_allocator // bluefs配置設定磁盤空間的配置設定器,預設為stupid,即基于extent的方式。

bluefs_preextend_wal_files // 是否預先更新rocksdb wal檔案的大小。預設為false

// 另外還有一些參數和BlueStore相關,以bluestore_bluefs_開頭,這些參數主要控制将BlueStore的slow存儲空間配置設定給BlueFS使用
           

Metric

監控名額比較好了解,看看描述就能明白,重點應該關注db/wal的使用情況,因為通常情況下會使用更快的ssd,不要因為BlueFS的空間不夠而使用BlueStore中的slow空間。

"bluefs": {
        "gift_bytes": 0,
        "reclaim_bytes": 0,
        "db_total_bytes": 240043163648,
        "db_used_bytes": 631242752,
        "wal_total_bytes": 0,
        "wal_used_bytes": 0,
        "slow_total_bytes": 0,
        "slow_used_bytes": 0,
        "num_files": 14,
        "log_bytes": 5361664,
        "log_compactions": 0,
        "logged_bytes": 0,
        "files_written_wal": 0,
        "files_written_sst": 0,
        "bytes_written_wal": 0,
        "bytes_written_sst": 0
    },
           

Summary

  • BlueFS同時支援多個裝置(wal/db/slow)
  • BlueFS是個簡單的記憶體檔案系統,隻提供簡單的操作用來支援RocksDB運作
  • BlueFS有自己的日志,用來記錄對檔案系統的修改。異常情況下,回放日志可重建檔案系統的完整的記憶體映像

繼續閱讀