BlueStore 架构及原理分析

2023-11-12

BlueStore 架构及原理分析

Ceph 底层存储引擎经过了数次变迁,目前最常用的是 BlueStore,在 Jewel 版本中引入,用来取代 FileStore。与 FileStore 相比,Bluesore 越过本地文件系统,直接操控裸盘设备,使得 I/O 路径大大缩短,提高了数据读写效率。并且,BlueStore 在设计之初就是针对固态存储,对目前主力的 SATA SSD 有着更好的支持(相比 FileStore),同时也支持 Nvme SSD 超高速固态。在数据的处理上,BlueStore 选择把元数据和对象数据分开存储,使用高速设备来保存元数据,能够起到性能优化作用。

1. BlueStore 架构总览

在这里插入图片描述

图1.1 ceph_bluestore 架构图

  • BlockDevice:物理块设备,使用 Libaio 操作裸设备,AsyncIO。
  • RocksDB:存储 WAL 、对象元数据、对象扩展属性 Omap、磁盘分配器元数据。
  • BlueRocksEnv:抛弃了传统文件系统,封装 RocksDB 文件操作的接口。
  • BlueFS:小型的 Append 文件系统,实现了 RocksDB::Env 接口,给 RocksDB 用。
  • Allocator:磁盘分配器,负责高效的分配磁盘空间。

根据上图,可以很直观的了解到:BlueStore 把数据分成两条路径。一条是 data 直接通过 Allocator(磁盘空间分配器)分配磁盘空间,然后写入 BlockDevice。另一条是 metadata 先写入 RocksDB(内存数据库),通过 BlueFs(BlueStore 专用文件系统)来管理 RocksDB 数据,经过 Allocator 分配磁盘空间后落入 BlockDevice。

先树立第一层的概念:BlueStore 把元数据和对象数据分开写,对象数据直接写入硬盘,而元数据则先写入超级高速的内存数据库,后续再写入稳定的硬盘设备,这个写入过程由 BlueFS 来控制。

为什么这样设计呢?背后的原因令人暖心。

这里直接引入《Ceph 设计原理与实现》一文中给出的观点(concept)。

在所有的存储系统中,读操作一般都是同步的(前端发出读指令,后端必须返回待读取的数据后,才算读成功)。写操作则可以是异步的(前端发出写指令,后端先接收数据后,直接返回成功写,后续再慢慢把数据写到磁盘中),一般为了性能考虑,所有写操作都会先写内存缓存Page-Cache便返回客户端成功,然后由文件系统批量刷新。但是内存是易失性存储介质,掉电后数据便会丢失,所以为了数据可靠性,我们不能这么做。

通常,将数据先写入性能更好的非易失性存储介质(SSD、NVME等)充当的中间设备,然后再将数据写入内存缓存,便可以直接返回客户端成功,等到数据写入到普通磁盘的时候再释放中间设备对应的空间。

传统文件系统中将数据先写入高速盘,再同步到低速盘的过程叫做“双写”,高速盘被称为日志盘,低速盘被成为数据盘。每个写操作实际都要写两遍,这大大影响了效率,同时浪费了存储空间。

BlueStore 针对上述这一问题重新设计了自身的日志系统,尽可能避免双写,但同时也要保证数据的一致性。其使用增量日志,即针对大范围的覆盖写,只在前后非磁盘块大小对齐的部分使用日志,其他部分不需要改写的(RMW),则使用重定向写(不写入日志)。

书中介绍了块、COW、RWM 等内容,这里也直接拿来主义。

BlockSize:磁盘IO操作的最小单元(原子操作)。HDD为512B,SSD为4K。即读写的数据就算少于 BlockSize,磁盘I/O的大小也是 BlockSize,是原子操作,要么写入成功,要么写入失败,即使掉电不会存在部分写入的情况。

RWM(Read-Modify-Write):指当覆盖写发生时,如果本次改写的内容不足一个BlockSize,那么需要先将对应的块读上来,然后再内存中将原内容和待修改内容合并Merge,最后将新的块写到原来的位置。但是RMW也带来了两个问题:一是需要额外的读开销;二是RMW不是原子操作,如果磁盘中途掉电,会有数据损坏的风险。为此我们需要引入Journal,先将待更新数据写入Journal,然后再更新数据,最后再删除Journal对应的空间。

COW(Copy-On-Write):指当覆盖写发生时,不是更新磁盘对应位置已有的内容,而是新分配一块空间,写入本次更新的内容,然后更新对应的地址指针,最后释放原有数据对应的磁盘空间。理论上COW可以解决RMW的两个问题,但是也带来了其他的问题:一是COW机制破坏了数据在磁盘分布的物理连续性。经过多次COW后,读数据的顺序读将会便会随机读。二是针对小于块大小的覆盖写采用COW会得不偿失。是因为:一是将新的内容写入新的块后,原有的块仍然保留部分有效内容,不能释放无效空间,而且再次读的时候需要将两个块读出来Merge操作,才能返回最终需要的数据,将大大影响读性能。二是存储系统一般元数据越多,功能越丰富,元数据越少,功能越简单。而且任何操作必然涉及元数据,所以元数据是系统中的热点数据。COW涉及空间重分配和地址重定向,将会引入更多的元数据,进而导致系统元数据无法全部缓存在内存里面,性能会大打折扣。

在这里插入图片描述
图1.2 覆盖写示意图

基于以上设计,BlueStore 的写策略综合运用直接写、COW 和 RMW 策略。

注意:

  1. 非覆盖写直接分配空间写入即可;

  2. 块大小对齐的覆盖写采用 COW 策略;小于块大小的覆盖写采用 RMW 策略。

再结合图1.1 ceph_bluestore 架构图,我们需要引入第二层观念:

  1. 非覆盖写直接通过 Allocater 分配空间后写入硬盘设备。

  2. 覆盖写分为两种:一种是可以 COW,也是直接通过 Allocater 分配空间后写入硬盘设备。另一种是需要 RMW,先把数据写入 Journal,在 BlueStore 中就是 RocksDB,再后续通过 BlueFS 控制,刷新写入硬盘设备。
    在这里插入图片描述
    在 ceph 配置中,有 bluestore_prefer_deferred_size 可以设定:当对象大小小于该值时,该对象总是使用延迟写(即先写入 Rocks DB,再落入 BlockDevice)。

2. BlockDevice

Ceph 较新版本中,把 设备 模块单独放到 blk 文件夹中。

 [root@localhost ceph]# cd src/blk
 [root@localhost blk]# tree
 .
 ├── aio
 │  ├── aio.cc          # 封装 aio 操作
 │  └── aio.h
 ├── BlockDevice.cc        # 块设备基类
 ├── BlockDevice.h
 ├── CMakeLists.txt
 ├── kernel            # kernel 设备,目前常用的 HDD、SATA 都是此类设备
 │  ├── io_uring.cc        # bluestore 或者 存在 libaio 的情况下,不适用 libio_uring
 │  ├── io_uring.h
 │  ├── KernelDevice.cc
 │  └── KernelDevice.h
 ... 
 # 后续还有 pmem、spdk、zoned 设备,这里不再列出

2.1 aio

aio 封装了 libaio 相关操作,具体有三个结构:

  • aio_t:一次 io 操作,pwrite,pread,不写到磁盘
  • io_queue_t:io 提交队列,基本不使用,只用作基类
  • aio_queue_t:继承自 io_queue_t,提交 io 操作,真正在此写入磁盘

简单介绍 ceph 中 libaio 使用流程:

  1. init():io_setup(int maxevents, io_context_t *ctxp);
  2. 构造 aio_t
  3. 调用 aiot_t->pwritev 或者 aio_t->preadv
  4. 把 aio_t 加入队列:std::list<aio_t>
  5. int submit_batch(aio_iter begin, aio_iter end, uint16_t aios_size, void *priv, int *retries)
  6. shutdown: io_destroy(io_context_t ctx)

相关方法有:

init():初始化

shutdown():关闭

pwritev():预备写,后续通过 submit_batch() 完成实际写入磁盘操作

preadv():预备读,后续通过 submit_batch() 完成实际写入磁盘操作

submit_batch():按批次提交 io 任务

get_next_completed():获取已经完成 IO 的 aio_t,用于 aio_thread() 回调函数

2.2 从 BlockDevice 到 KernelDevice

BlockDevice 是所有块设备的基类,定义了块设备必备的一些接口,比如 read、write等。在 src/blk/BlockDevice.h 中还定义了 IOContext,用作封装传递 io 具体操作,其中保存着每次 aio 操作的list:pending_aios 和 running_aios,前者表示等待队列,后者表示正在 io 操作的队列,同时还创建两个原子数分别用于表示这两个队列中 aio_t 的数量。

BlockDevice提供创建块设备实例的接口,通过识别 device_type 来创建不同的块设备,如 kernel device。

BlockDevice *BlockDevice::create(
  CephContext* cct, const string& path, aio_callback_t cb,
  void *cbpriv, aio_callback_t d_cb, void *d_cbpriv);

->
create_with_type(device_type, cct, path, cb, cbpriv, d_cb, d_cbpriv);

-> 根据类型创建 device
new KernelDevice(cct, cb, cbpriv, d_cb, d_cbpriv)

-> 创建以下线程
  aio_thread:
discard_thread
  • aio_thread:作用1. 感知已经完成的 io。2. 唤醒等待的 io 或者调用回调函数。具体根据IOContext创建时是否提供了 priv 参数(BlueStore 指针)来决定使用哪种操作,如果提供了,则调用回调函数BlueStore::TransContext::aio_finish()或者BlueStore::DeferredBatch::aio_finish()(根据AioContext 类型)。

  • discard_thread:这是针对 SSD discard 操作的线程,可以通过 cat /sys/block/sda/queue/discard_granularity 命令查看设备是否支持 discard 操作。discard 在 bluestore 中 分为两步:第一步:BlkDev{fd_directs[WRITE_LIFE_NOT_SET]}.discard((int64_t) offset, (int64_t) len); 调用系统函数对指定位置做 discard。第二步:discard_callback() 回调 shared_alloc.a->release(),告诉 Allocater 需要回收的部分(标记为 free)。

在这里插入图片描述
KernelDevice 支持同步、异步读写操作,具体实现方式:在 open操作时,对同一个设备首先创建两种文件描述符fd_direct、fd_buffered(通过O_DIRECT,O_DIRECT不同打开权限来实现),同时告知内核fd_buffered的数据以随机形式访问。后续顺序读写操作统一用 fd_direct,随机读写使用 fd_buffered;同步读写操作调用系统函数pwrite()和pread(),异步读写操作使用aio_t::preadv()和pwritev()。open 块设备的同时也会开启 aio_thread 和 discard_thread。

以下简单介绍KernelDevice部分关键操作:

同步读写:

  1. read:同步读要求块对齐,调用系统函数 pread() 实现,使用fd_directed。
  2. read_random:分为对齐和不对齐两种情况。对齐使用fd_buffered(open()时告知内核,数据以随机形式访问)。不对齐则强制转成对齐操作,再使用同步读的方式,使用fd_directed+pread(),后续返回时再裁剪为原本不对齐的bl大小。
  3. write:同步写要求块对齐。同时支持使用fd_direct或者fd_buffered,每次写完后,立刻调用系统函数sync_file_range()(仅刷新offset~length的数据,不更新元数据,提高性能)。

同步写需要刷新脏页和元数据。

  1. flush():调用系统函数fdatasync()。

异步读写:

异步读写时一般只用 fd_direct,因为libaio要求打开方式为O_DIRECT。

aio_read:要求使用!buffered方式(使用fd_direct),否则转为同步读。调用aio_t->preadv()函数。

aio_write:要求块对齐。要求使用!buffered方式(使用fd_direct),否则转为同步写。调用aio_t->pwritev()函数。

异步读写需要通过submit()把io操作提交到磁盘。

aio_submit():调用io_queue_t::submit_batch()。

3. BlueStore

3.1 mkfs

mkfs 作用是在磁盘第一次使用 bluestore 时,写入一些用户指定的配置到磁盘第一个块——超级块(大小可配置,一般为: BDEV_LABEL_BLOCK_SIZE 4096),这样后续使用该磁盘时,可以直接读取配置项。 之所以需要固化这些配置项,是因为 bluestore 使用不同的配置项对于磁盘数据的组织形式不同,如果前后两次上电使用不同的配置项访问磁盘数据有可能导致数据发生永久损坏。对已经 mkfs 过的磁盘再次使用该函数,则会对磁盘做一次 meta 数据检查。

以下为 LABEL_BLOCK 块中存储的数据,主要有 osd id、设备大小、生日时间、设备描述以及一组元数据 map。

// label for block device

struct bluestore_bdev_label_t {
    
uuid_d osd_uuid;   ///< osd uuid

uint64_t size;    ///< device size

utime_t btime;    ///< birth time

string description; ///< device description

 
map<string,string> meta; ///< {read,write}_meta() content from ObjectStore

 ...

};

mkfs() 主要是初始化 bluestore_bdev_label_t,并写入到磁盘的第一个4K块中,同时建立块设备链并预分配指定大小的空间。同时BlueStore层次的mkfs()里面调用了Bluefs的Bluefs::mkfs(),并且也固化了bluefs的信息位于磁盘的第二个4K块。

以下给出mkfs详细步骤。

  1. read_meta() 读取 “mkfs_done” value,验证是否曾经完成过mkfs,如果之前做过 mkfs 则做一次 fsck 然后 return。
  2. 验证 type 是否是 bluestore,如果 meta 中没有 tpye,说明是第一次 mkfs,需要写入 {type,bluestore}。
  3. 指定了 freelist_type。
  4. 获取路径的 fd:path_fd = TEMP_FAILURE_RETRY(::open(path.c_str(), O_DIRECTORY|O_CLOEXEC));
  5. 创建 fsid 文件 /var/lib/ceph/osd/ceph-0/fsid
  6. 文件锁 fsid: int r = ::fcntl(fsid_fd, F_SETLK, &l);
  7. 读取fsid文件中的 fsid 号,如果等于0,则生成 fsid
  8. 建立 block 和实际块设备的软连接,并截取 block_size 大小,这里调用系统函数::symlinkat()和::ftruncate()。
  9. 根据配置,创建 wal 和 db 的软链接,这个可以没有
  10. 创建设备实例,详见 3.1.1 BlueStore::_open_bdev
  11. 设置 min_alloc_size,默认 4K
  12. 验证 block_size > round_up_to(max(SUPER_RESERVED, min_alloc_size),min_alloc_size)
  13. 保证 min_alloc_size 是2的倍数
  14. 创建 Allocator,详见 Allocator::create
  15. 在 Allocator 中指定 预留区域 为 free 未使用,一般就是 SUPER_RESERVED 大小,默认 8K
  16. 打开 kvdb,_open_db()
  17. 向 kvdb 写入配置数据:{S, nid_max, 0},{S, blobid max, 0},{S, min_alloc_size, ${min_alloc_size}},{S, per_pool_omap, 2},{S, ondisk_format, ${ lastest_ondisk_format = 4}},{S, min_compat_ondisk_format, 3}
  18. 向 path/block superblock 区域写入 meta 数据:{kv_backend, bluestore_kvbackend(默认 rocksdb)}
  19. meta 写入 {bluefs,1|0},是否启用 bluefs,1为启用,0为不启用。
  20. 更新fsid
  21. 关闭上文打开的所有文件或设备

在这里插入图片描述

3.1.1 _open_bdev

开启块设备。此函数用于创建block设备实例(slow设备),并初始化某些配置。WAL和DB设备由BlueFS自行管理,不在此处开启。

BlockDevice:create()函数在2.2节中详细介绍过了,主要作用是打开块设备,获取句柄,用于后续的数据读写操作。

discard用于SSD设备的空间回收,其作用在2.2节中介绍过。

_set_max_defer_interval()设置了max_defer_interval参数,此参数的作用是在MempoolThread线程中定时提交延迟写(try->deferred_try_submit()),默认为3。

set_cache_size()从配置中获取所有cache相关的参数,包括cache总空间,onode内存使用比率、kvdb内存使用比率、hdd、ssd内存使用比率等。

在这里插入图片描述

3.1.2 fsck

在mkfs中,可以看到fsck操作,通常在bluestore格式化或者挂载时使用,可以通过bluestore_fsck_on_umount、bluestore_fsck_on_mkfs配置来开启或关闭。

  1. _fsck()函数中首先开启了相关kvdb、block、Allocator、FreelistManager等相关设备,通过_open_db_and_around()。

  2. 在_upgrade_super()中,根据ondisk_format与atest_ondisk_format比较,判断当前设备的bluestore格式是否最新,若不是最新版本,则向其kvdb中添加 super 信息(super为rocksdb前缀),具体有:ondisk_format、min_alloc_size、min_min_alloc_size、min_readable_ondisk_format、min_compat_ondisk_format。向block超级块添加freelistmanager信息:bfm_blocks、bfm_size、bfm_bytes_per_block、bfm_blocks_per_key。_

  3. open_collection()中从kvdb还原coll_map,collection在kvdb中使用PREFIX_COLL前缀。

  4. deferred_replay()把PREFIX_DEFERRED前缀的所有deferred_transaction延迟事务重放一遍,保证所有数据真正写入block设备。

  5. _fsck_on_open()是真正执行fsck操作的函数。根据注释,有以下操作:

    检测共享blob是否有不可解码的keys或者records,如果有则删除该blob

    修正错误引用的pextent

    尝试创建丢失的共享blob

    删除不可解码的 deferred_transaction

    修正 freelistmanager错误标记(free or used)

    更新 statfs,bluefs文件系统信息

3.2 queue_transaction

Transaction 是bluestore 中用于保证pg上的事务,是一批有顺序的操作的集合。以下简单介绍Transaction中变量的意义及用途:

Op:代表一次操作(增删改),至少有三个参数:ghobject、coll_t、op(此次操作含义,如OP_CREATE)。在调用 touch、write等函数时写入参数。

data:TransactionData,保存transaction的一些关键参数,如ops(操作数量)largest_data_len(最大数据长度)等。注:fadvise_flags作用?

coll_index:coll_t索引。这是每个transaction单独保存的一份用过或者即将使用的collection集合。通过bluestore中coll_map<coll_t, collection>可以查到对应collection。Op.oid即是此索引中的value,这样可以通过两次查询,找到oid对应的collection。

object_index:ghobject索引。Op.oid即此索引value。

coll_id和object_id:分别是两个对应索引中键值对数量。

data_bl:写入的数据。注:如何确定某个op的data在data_bl中的位置?

op_bl:Op集合。

on_applied:事务提交完成之后调用的回调函数。

on_applied_sync:同步,事务应用完成之后调用的回调函数,由bluestore线程调用。

on_applied:异步,事务完成之后调用的回调函数,在Finisher线程中调用。

在这里插入图片描述

这里给出osd、collection、transaction等的关系:

osd作为rados存储层面的基石,实际上可以由三种块设备组合而成(data、wal、slow)。pg则是pool用来划分对象到不同osd上的工具,一个osd上有多个pg,这些pg可以来自不同的或相同的pool。在bluestore 中,用collection来表示pg,每次在向osd做io操作时,需要提供对象的归属pg(CollectionHandle)以及至少一个Transaction,而每一个Transaction中可以有多个io操作(op_bl)。

小结:

1个osd——多个pg

1个pg——多个Transactions,1个Transaction——多个pg(每个op不同pg)

1个Transaction——多个op

1个TransContext——同一批次Transactions(后续会添加到对应collection的OpSequencer)

queue_transaction则把一组Transactions提交到bluestore的操作。

queue_transaction(CollectionHandle &ch,

vector<Transaction> &tls,

TraskedOpRef op,

ThreadPool::TPHandle *handle)

1)收集Transaction中的三种回调函数:on_applied,on_commit,on_applied_sync。

2)根据ch参数获取Collection,ch为Collection的侵入式指针。

3)获取Collection对应的OpSequencer。

4)创建TransContext,从kvdb获取KeyValueDB::Transaction,把TransContext加入到OpSequencer. q_list_t。

5)解析transaction操作,根据op类型,调用对应的函数。如OP_WRITE->_write()。这里给出查询onode的顺序:onode_map -> kvdb -> new Onode()。

6)计算每条txc(TransContext)大致io消耗,便于后续限流操作。

7)更新onode

8)对于延迟写,提交事务到kvdb中。

9)更新空间分配(freelistmanager),删除失效kv数据。

10)throttle限流

11)进入_txc_state_proc状态机。

在这里插入图片描述

上图流程中_txc_add_transaction()完成了transaction到txc的转化,根据提交时设定的transaction的操作,进一步调用不同的处理函数。主要分为对coll的操作和对obj的操作这两类,如:touch、remove、delete、read、write等。

以下为_txc_add_transaction中支持的操作

op操作类型(coll) 用途
OP_RMCOLL 删除指定collection,必须保证collection为空(没有对象)。
OP_MKCOLL 创建一个collection。
OP_SPLIT_COLLECTION2 分裂集合,用于增加pg数量。pg总数必须为2的倍数,用于保证分裂操作能够作用到每个pg上。
OP_MERGE_COLLECTION 合并集合,用于减少pg数量。
OP_COLL_HINT 给予collection一个hint。
op操作类型(obj) 用途
OP_CREATE 创建一个新obj(ghobj,对象)。
OP_TOUCH 优先从kvdb中查询该obj,若失败则创建一个新obj。
OP_WRITE 向一个obj写入数据。
OP_ZERO 清空一个obj。
OP_TRIMCACHE 截取obj的指定长度,删除超出长度的所有内容。
OP_REMOVE 删除指定obj。
OP_SETATTRS 设置attr属性,支持同时添加多条attr。
OP_RMATTRS 清空指定obj所有attr属性。
OP_CLONE 克隆obj。
OP_CLONERAN 克隆一个obj的指定范围内容到另一个obj中。
OP_COLL_MOVE_RENAME 把一个对象转移到新集合中。
OP_TRY_RENAME 把一个对象转移到同一个集合的不同对象中。
OP_OMAP_CLEAR 清空指定obj所有map属性。
OP_OMAP_SETKEYS 设置指定obj的omap key
OP_OMAP_RMKEYS 删除omap 指定key属性
OMAP_RMKEY_RANGE 批量删除omap
OMAP_SETHEADER 设置header omap 的属性
SET_ALLOC_HINT Set allocation hint for an object. make 0 values(expected_object_size, expected_write_size) noops for all implementations

3.3.1 write

write操作涉及bluestore中经典的大小写问题,这里主要介绍_write()。源码中可以看出_write()进一步调用了_do_write() ,并在此完成对象写入操作。

_do_write(TransContext *txc, CollectionRef &c, OnodeRef o, uint64_t offset, uint64_t length, bufferlist &bl, uint32_t fadvise_flags)
  1. 根据配置设置写操作参数:csum_order(校验的单位块大小 = 1 << csum_order)、target_blob_size、compress、buffered。这里涉及到的pool参数,是在PG::init_collection_pool_opts()函数中完成初始化的。
  2. 根据obj的大小、偏移量对obj进行切割,随后对min_alloc_size倍数大小的对象块使用_do_write_big()方式写,小于min_alloc_size大小的对象块调用_do_write_small()方式写。网上关于大小写策略分析很多,这里不再展开,需要注意的是实际落盘的动作并不在此发生,而是在后续的状态机中。
  3. 对于新blob写,需要分配磁盘空间,此步骤在_do_alloc_write中进行,并且进行了压缩、计算校验和、更新extent_map等额外操作。bluestore配置文件中定义了prefer_deferred_write参数,凡是obj.size小于此参数的,统一使用deferred_op(延迟写),目的是小对象写操作不需要等到落盘后才返回成功,这样能够快速响应前端。
  4. 更新对象onode信息,包括blob、shared_blob、extent_map等。
  5. 压缩的blob需要进行gc评估,以此来合并blob或重新分配blob空间。

3.3.2 状态机

为了保证思考的连续性,这一小节介绍的是状态机。状态机中涉及到io保序、延迟写和简单写的提交、kvdb数据的更新等操作,是控制bluestore中数据落盘的幕后黑手。上文介绍的write()函数实际不过是真正写操作的前置工作,write仅仅表示其操作含义是写或改写一个对象。后文介绍的所有对象、pg操作的真正落盘环节都是在此函数中。状态机也是一次queue_transaction的最后一步,当程序跳出状态机也就意味着这次io写操作完成。

需要注意是,状态机中用到大量异步线程,因此前端在queue_transaction时,无需等到整个状态机到达完成状态。一般直接写在STATE_PREPARE中调用bdev->aio_submit()后就返回前端,延迟写在STATE_AIO_WAIT中调用_txc_finish_io()进行保序处理(根据osr顺序依次进入状态机处理)后直接返回前端。

具体过程不一一叙述,网上参考资料较多,或者直接看下述流程图。

在这里插入图片描述

这里给出每个状态迁移到下一状态的延时参数:

状态 参数
STATE_PREPARE l_bluestore_state_prepare_lat
STATE_AIO_WAIT l_bluestore_state_aio_wait_lat
STATE_IO_DONE l_bluestore_state_io_done_lat
STATE_KV_QUEUED l_bluestore_state_kv_queued_lat
STATE_KV_SUBMITTED l_bluestore_state_kv_committing_lat
STATE_KV_DONE l_bluestore_state_kv_done_lat
STATE_DEFERRED_QUEUED l_bluestore_state_deferred_queued_lat
STATE_DEFERRED_CLEANUP l_bluestore_state_deferred_cleanup_lat
STATE_FINISHING l_bluestore_state_finishing_lat
STATE_DONE l_bluestore_state_done_lat

3.3 BlueFS

BlueStore把元数据统统用RocksDB来保存,包括Collection、Onode、omap等,可以通过查看kv store prefixes了解数据类型。RocksDB不支持裸设备,需要给它提供文件系统接口,其本身提供RocksEnv来支持跨平台操作,BlueStore在BlueRocksEnv.cc中实现了RocksDB所要求的全部接口,调用BlueFS api实现功能。

BlueFS是一个简易文件系统,同时管理三种存储空间:Slow、DB、WAL,它们的含义这里不再赘述,需要注意的是:如果三者各自分配一块磁盘,WAL满了会往DB写,DB满了会往Slow写;又或者,WAL、DB可以不使用单独设备而是统一在Slow设备上,此时由BlueStore动态管理,其空间大小根据使用情况动态调整。

除了在BlusStore::mkfs时,bluefs在实际使用中通常是在bluestore挂载的时候打开的,调用流程:_mount() -> _open_db_and_around() -> _prepare_db_environment() -> _open_bluefs()。

在这里插入图片描述

BlueFS的mkfs()在第一次使用bluefs时执行,其中最为重要的是写入了super信息(因为其他操作都会在mount()中重做一遍,比如init_alloc等,这些操作的目的都是为了构建一个简单的环境,便于写如super信息)。

bluefs_super_t中保存了osd.fsid、bluefs.uuid、log_fnode等。其中,log_fnode是bluefs的索引日志,记录了bluefs文件系统的所有操作,通过它可以还原出bluefs的元数据:file_map和dir_map。

在这里插入图片描述

bluefs的文件组织形式比较简单:一级目录+二级文件的形式。其定位某个具体文件只需要通过两次查找,第一次根据dir_map找到对应目录,第二次通过该目录的file_map找到对应文件。

在这里插入图片描述

由于bluefs中存储的文件数量不多(主要是用于rocksdb的存储,一般也就rocksdb的日志文件,sst文件等),所以bluefs的元数据不另外存储,而是通过append日志形式记录操作,每次mount时,回放所有的事务,并根据事务内容,还原出整个bluefs元数据。同样的,其已用空间的freelist也是如此还原。

通过bluefs,我们可以看到文件系统的本质:磁盘空间分配管理+调用磁盘读写操作+所有文件在磁盘的位置。

3.4 cache

BlueStore中cache主要存放两种数据:Onode和Obj_data(用户数据),分别用两种分片类型来管理:OnodeCacheShard和BufferCacheShard。每个bluestore(objectstore)实例中包含多个Cache实例,为了使得不同PG之间的客户端请求可以并发的执行,每个OSD会设置多个PG工作队列(op_sharededwq),cache实例个数与之对应。

collection中的onode_map和cache中分别保存了两类cache实例指针。通过osd_num_cache_shards配置修改cache总数。这就意味着一个cache可能被多个pg享有,为了便于快速判断在cache中是否有某个pg的缓存数据,就需要在pg层引入新的对象,用于保存pg的cache实例以及该cache中保存的数据对象id,即Onodespace和BufferSpace。

对于Onode来说,使用LRU算法维护数据列表,cache实例为LruOnodeCacheShard,指针保存在OnodeSpace中,而OnodeSpace指针保存在Collection中的onode_map变量里。OnodeSpace中保存了指定cache中的onode_map,通过查询该map可以快速知道cache中是否有某个Onode缓存。

data用户数据的缓存有两种实现:LruBufferCacheShard和TwoBufferCacheShard,用户数据使用Buffer封装,并保存在list链表中。BufferSpace用于管理Buffer,其中保存了Buffer表,便于快速查明内存中的buffer,通过Blob->shared_blob->bc调用。

在这里插入图片描述

set_cache_shards()在BlueStore构造函数中调用。OSD在init的时候也会创建多个cache。osd_num_cache_shards配置参数可以设置OSD初始化时创建的cache数量,默认32个。bluestore_cache_type配置参数可以设置Buffer_Cache_Shards类型,默认2q。

set_cache_shards()中循环创建多个cache,并在构造或打开collection时被其获取cache实例,从cache_shards数组中。

在这里插入图片描述

具体功能代码解析,可以参考以下网址:https://blog.csdn.net/lzw06061139/article/details/105656938/

4. BlueStore中的几个重要线程

BlueStore构造函数中初始化了多个线程,如下所示。本节内容主要探究这几个线程的作用。

以下提及的所有线程都在在bluestore mount时或者fsck时开启:_kv_start()(开启finisher、kv_sync_thread、kv_finalize_thread)、mempool_thread.init()(开启mempool_thread)。

BlueStore::BlueStore(CephContext *cct,
                     const string &path,
                     uint64_t _min_alloc_size)
    : ObjectStore(cct, path),
      throttle(cct),
      finisher(cct, "commit_finisher", "cfin"),
      kv_sync_thread(this),
      kv_finalize_thread(this),
      zoned_cleaner_thread(this),

      min_alloc_size(_min_alloc_size),
      min_alloc_size_order(ctz(_min_alloc_size)),
      mempool_thread(this) {
  _init_logger();
  cct->_conf.add_observer(this);
  set_cache_shards(1);
}

这里给出一张bluestore运行时线程图。

在这里插入图片描述

4.1 finisher

finisher线程是用来处理异步回调函数的线程。我们先从线程的entry开始,研究线程执行过程。

在这里插入图片描述

由上图可知,finisher线程作用就是不断的从finisher_queue中获取要执行的context,每当向finisher_queue插入对象时,就会唤醒该线程。

通过分析bluestore.cc代码,发现finisher.queue()函数在三处被调用。第一处在queue_transactions()中,用于处理on_applied回调在一批事务的所有写操作完成后。第二处在_txc_committed_kv()(状态机中调用)中,用于处于on_commit回调。第三处在_deferred_aio_finish()(状态机中调用)中,用于处理未完成的延迟写操作(C_DeferredTrySubmit())。

4.2 kv_sync_thread

kv_sync_thread做四件事:

  1. 处理kv_committing(kv_queue),把它的kvdb事务提交了。kvdb层面。
  2. 处理kv_submitting(kv_queue_unsubmitted)。kv_queue_unsubmitted是状态机中STATE_IO_DONE状态中,发现last_nid>=nid_max或者last_blobid>blob_max的异常,需要更新这两项配置的,因此该次提交不成功,所有事务移交至kv_queue_unsubmitted队列。这里就是更新配置。kvdb层面。
  3. 处理deferred_done(deferred_done_queue),把它的io给落盘了。bdev层面。
  4. 处理deferred_stable(deferred_stable_queue),把它对应kvdb中的wal写给删除了,清洗日志数据。kvdb层面。

具体流程见下图。

image-20211020145917227在这里插入图片描述

4.3 kv_finalize_thread

kv_finalize_thread主要是处理kv_sync线程中的队列,返回状态机。

具体流程看下图。

在这里插入图片描述

4.4 mempool_thread

在这里插入图片描述

5. ceph-objectstore-tool 功能解析

ceph-objectstore-tool是专门用来控制对象和pg的工具,接下来将以这个工具的功能为切入点深入bluestore引擎。

5.1list

ceph-objectstore-tool --data-path $PATH_TO_OSD [–pgid P G I D ] [ PG_ID] [ PGID][OBJECT_ID] --op list

此命令中包含3条子命令,分别是:列出osd所有对象、列出pg所有对象、列出指定对象的xattr信息。因为这三条命令在逻辑上是递进关系,代码上也是互相关联因此放到一起讨论。

在这里插入图片描述

上图中用到了blustore的三个方法:list_collections()(获取ObjectStore所有collection)、collection_list()(获取collection的指定范围[start, end)的所有ghobj)、getattr()(获取对象的xattr属性)。

list_collection()实现很简单,把coll_map中所有key值返回即可。coll_map保存了该OSD的所有{coll_t,CollectionRef}映射,在挂载mkfs存储引擎的时候从kvdb中获取,获取方式为db->get_iterator(PREFIX_COLL)->value()。

collection_list()同样通过kvdb查询db->get_iterator(PREFIX_OBJ),获取osd所有对象,再使用get_coll_range()方法计算出指定pg和范围对应的首尾ghobj对象,通过kv迭代器获取首尾ghobj对象范围的所有ghobj并加入vector。

getattr()获取的是保存在onode->onode.attrs集合。第二个onode是bluestore_onode_t,保存在kvdb中对象的元数据,在get_onode()方法中,从kvdb加载到内存中store->db->get(PREFIX_OBJ, key.c_str(), key.size(), &v)。

注意:以PREFIX_OBJ为前缀的数据中,除了onode外还有extent_shard(extent_map分片),它们通过不同的key值结尾来区分,onode的key末尾最后一个是“o”,extent_shard末尾是“x”。在BlueSotre.cc源码注释中有介绍

/*
 \* object name key structure
 *
 \* encoded u8: shard + 2^7 (so that it sorts properly)
 \* encoded u64: poolid + 2^63 (so that it sorts properly)
 \* encoded u32: hash (bit reversed)
 *
 \* escaped string: namespace
 *
 \* escaped string: key or object name
 \* 1 char: '<', '=', or '>'. if =, then object key == object name, and
 \*     we are done. otherwise, we are followed by the object name.
 \* escaped string: object name (unless '=' above)
 *
 \* encoded u64: snap
 \* encoded u64: generation
 \* 'o'
 */
 \#define ONODE_KEY_SUFFIX 'o'

 /*
 \* extent shard key
 *
 \* object prefix key
 \* u32
 \* 'x'
 */
 \#define EXTENT_SHARD_KEY_SUFFIX 'x'

也可以使用kvstore工具查看,以下分别是RocksDB中onode和extent_shard的key值。

[root@localhost bin]# ./ceph-kvstore-tool bluestore-kv /var/lib/ceph/osd/ceph-admin/ dump O | awk '{print $2}'
%7f%7f%ff%ff%ff%ff%ff%ff%ff%99%1f%bdw%21osbench%21%3d%ff%ff%ff%ff%ff%ff%ff%fe%ff%ff%ff%ff%ff%ff%ff%ffo
%7f%7f%ff%ff%ff%ff%ff%ff%ff%99%1f%bdw%21osbench%21%3d%ff%ff%ff%ff%ff%ff%ff%fe%ff%ff%ff%ff%ff%ff%ff%ffo%00%00%00%00x
…

5.2 get-bytes、set-bytes

在这里插入图片描述

bluestore中read()函数主要分为两步,第一步_read_cache(),尝试在cache中读取obj数据;失败则需要去dev读取,调用_prepare_read_ioc() -> bdev->aio_read()方法读取数据,并且需要调用bdev->aio_submit()函数真正把请求提交到块设备。此步骤中,还会对数据进行解压、校验等操作。

在这里插入图片描述

write()函数封装了op的一系列参数,包括op类型、oid、偏移量、长度等,最重要的obj数据则保存在data_bl中。

set_bytes涉及到改写数据,所有非幂等的操作,都需要通过事务来完成。本文第三节中详细介绍了queue_transaction(),这里不再赘述。

5.3 list-omap、get-omap、set-omap

这些都是关于对象omap的操作。omap是对象的大属性,用来弥补xattr容量太小的缺陷,omap属性以对象的形式保存在kvdb中,可以通过。注意着三条命令都需要指明pg与oid。

list-omap与set-omap都是通过kvdb查询对应的key值,ghobj的key值通过十六进制编码的pool+oid+“.”+key(可没有)拼接而成。在kvdb中omap的前缀为O,key名称中的“.”的位置如果显示为“~”,表示该key的边界。通过这些信息可以通过db->get_iterator()函数找到指定对象的所有kv键值对。

set-omap则是构造一个op操作,通过queue_transaction()来提交到bluestore内部。

5.4 list-attrs、get-attr、set-attr

ceph中使用encode和decode来序列化和反序列化。

encode:把一个对象转成“长度+对象”的16进制字符并append到data_bl中。注意这个“长度“的字节长度是相对固定的,根据不同的对象类型,有8位、16位、32位和64位。也就是说目前最大支持的对象长度是(264 - 1)个字节,转换后(264 - 1)* 8 bit ≈ 247GB ,一个超大的数字。

decode:从data_bl中还原出一个对象,需要指定对象的原本类型,并根据data_bl中记录的数据长度来截取字符流。

在这里插入图片描述

6. ceph-bluestore-tool 功能解析

6.1 show-label、set-label-key、rm-label-key

label是bluestore中的block(SLOW)设备的超级块信息,可以通过show-label命令查看信息,或者通过set-label-key、rm-label-key增改信息。block的超级块信息储存在设备的第一个4kb的块中,可以通过系统读写函数来操控它。

在这里插入图片描述

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

BlueStore 架构及原理分析 的相关文章

  • Ceph — 使用cephadm搭建Ceph集群

    文章目录 准备安装 cephadm部署集群 本文将通过cephadm工具来学习如何简单地搭建一个octopus版集群 准备 服务器 主机名iposcpu 内存数据盘mgr 01192 168 2 15Centos7 72C4G无node 0
  • 4. 在 Proxmox VE 安装Ceph

    4 在 Proxmox VE 安装 Ceph 1 安装 按图操作即可 2 参考 1 https blog csdn net ggeol article details 109112815
  • 5. 在 Proxmox VE 配置Ceph

    Pool 用于存储虚拟机的img xff0c 如果需要实现虚拟机的HA xff0c 那么虚拟机必须创建在Ceph上 xff0c 通过Ceph的多副本来实现故障恢复 CephFS 在PVE中主要用于共享文件 xff0c 如iso文件等 创建O
  • Ceph集群更改IP地址

    由于一些原因 xff0c 有时需要将Ceph集群的IP地址进行更改 xff0c 以下为更改Ceph集群IP地址的步骤 xff1a 1 更改各机器IP地址 2 更改每台机器 etc hosts文件中的ip地址 3 更改ceph conf文件中
  • ceph (cephadm)集群部署

    ceph 集群部署 cephadm 方法1 xff1a ansible ceph ansible使用Ansible部署和管理Ceph集群 xff08 1 xff09 ceph ansible被广泛部署 xff08 2 xff09 ceph
  • Ceph OSD扩容与缩容

    在企业生产环境中 xff0c 随着时间的迁移数据会存在磁盘空间不足 xff0c 或者机器节点故障等情况 OSD又是实际存储数据 xff0c 所以扩容和缩容OSD就很有必要性 随着我们数据量的增长 xff0c 后期可能我们需要对osd进行扩容
  • CentOS 7部署 Ceph分布式存储架构

    一 概述 随着OpenStack日渐成为开源云计算的标准软件栈 Ceph也已经成为OpenStack的首选后端存储 Ceph是一种为优秀的性能 可靠性和可扩展性而设计的统一的 分布式文件系统 ceph官方文档 http docs ceph
  • ceph学习(故障恢复)——mon全部故障,从osd中恢复集群

    在生产环境中 ceph集群要求最少配置3个MON 一般情况下很少出现3个MON同时挂掉的情况 但是也不排除出现这种情况的可能 如果集群中的所有MON都损坏了 是不是集群数据就丢失了呢 能不能恢复集群 当然是可以的 ceph中国的一位开发者写
  • Ceph论文译文--Ceph:一个可扩展,高性能分布式文件系统

    译者注 本文是出于作者对于ceph的兴趣 在开源中国上关注ceph翻译 没有看到ceph论文的相关翻译 索性在阅读过程中把它翻译了出来 花费了几个周末时间 翻译过程中收获颇多 现把译文分享出来 如对您有益则倍感荣幸 肯定有很多不足之处 如有
  • Linux centos 卸载 ceph

    在CentOS上卸载Ceph的操作步骤 1 停止Ceph集群 首先 你需要停止Ceph集群中的所有服务 在每个节点上运行以下命令来停止所有服务 systemctl stop ceph target 2 卸载Ceph软件包 在每个节点上 使用
  • ceph-cursh规则实战及PGS unknown 问题处理

    问题描述 root ceph mon01 ceph s cluster id 92d4f66b 94a6 4c40 8941 734f3c44eb4f health HEALTH ERR 1 filesystem is offline 1
  • 基于dmclock分布式限流策略

    结合Cepb自身的特点 较为合理的做法是将QoS机制直接嵌入每个OSD中来实现 dmclock基本原理 mclock算法 mClock基本原理主要包含以下两个方面 1 为客户端设置一套QoS模板 并在每个1 0请求中携带该QoS模板 2 服
  • Loongnix单机部署Ceph(LoongArch架构、Ceph N版、手动部署MON、OSD、MGR、Dashboard服务)

    基础环境信息 CPU 龙芯3C5000L 2 内存 128G 硬盘 系统盘 一块512G的NVME的SSD 数据盘 三块16T的HDD 操作系统版本 Loongnix 8 4 Ceph版本 Ceph 14 2 21 Nautilus Cep
  • 删除 Ceph 集群里的某个节点的全部OSD (2 of 3)

    前言 如果你的ceph集群的数据只存在在该节点的所有OSD上 删除该节点的OSD会导致数据丢失 如果集群配置了冗余replication或者EC 需要做pg 修复 出于数据安全考虑 请一定 一定 一定 备份好你要删除的OSD上的数据 这里一
  • Ceph 集群在线迁移方案

    一 环境准备 1 1 场景介绍 最近收到一个需求 客户希望将运行了多年的ceph集群服务器全部更换掉 因为这些老服务器性能和容量都已经无法满足当前业务的需求 并希望在迁移到新服务器的过程中 业务不中断 在参考一些网上的方案后 选择了一个方案
  • s3cmd put 时提示 ERROR: S3 error: 403 (QuotaExceeded)

    配置里的rgw配额是10000000写满 s3cmd put 时提示 ERROR S3 error 403 QuotaExceeded rgw bucket default quota max objects 值为 1 查看配额信息 rad
  • Ceph入门到精通-Macvlan网络模式

    Docker中的Macvlan网络模式提供了一种将容器直接连接到宿主机网络的方式 使得容器可以拥有自己的MAC地址和与宿主机网络的直接连接 以下是使用Macvlan网络模式的一般步骤 创建Macvlan网络 docker network c
  • 单节点集群(minikube)上的 rook ceph 中的 1 pg 规模过小运行状况警告

    我正在将 rook ceph 部署到 minikube 集群中 一切似乎都正常 我向虚拟机添加了 3 个未格式化的磁盘并已连接 我遇到的问题是 当我运行 ceph status 时 我收到一条健康温暖消息 告诉我 1 pg 尺寸不足 我到底
  • Ceph:每个 OSD PG 太多

    我使用推荐值配置了 Ceph 使用文档中的公式 我有 3 个 OSD 我的配置 我已将其放在监视器节点和所有 3 个 OSD 上 包括以下内容 osd pool default size 2 osd pool default min siz
  • Flink 使用 Ceph 作为持久存储

    Flink 文档建议 Ceph 可以用作状态的持久存储 https ci apache org projects flink flink docs release 1 3 dev stream checkpointing html http

随机推荐

  • Linux开机&关机

    走进Linux系统 开机登录 开机会启动许多程序 它们在Windows叫做 服务 service 在Linux就叫做 守护进程 daemon 开机成功后 它会显示一个文本登录界面 这个界面就是我们经常看到的登录界面 在这个登录界面会提示用户
  • 关于String内存分配的深入探讨

    2019独角兽企业重金招聘Python工程师标准 gt gt gt public class Test public static final String MESSAGE taobao public static void main St
  • Sqli-labs之Less-34

    Less 34 基于错误 POST 单引号 字符型 addslashes 宽字节注入 这一关是POST型的注入 同样的将post传递过来的内容进行了转义处理 过滤了单引号 反斜杠 有之前的例子我们可以看到 df可以将转义的反斜杠给吃掉 而G
  • 数据结构 十进制和十六进制进制间的相互转换

    一 十进制转十六进制 例题 输入十进制数 654321 输出十六进制数 9FBF1 解题步骤 十进制数对16取余 因为最终结果是从下往上依次书写 说以我们可以利用栈的特性 先进 的后出 将余数存入栈中依次弹出 再将弹出的数进行拼接输出即可
  • C++迭代器-------array的基本用法总结

    基本用法中主要总结有 遍历和比较大小 注意 加上头文件 include
  • 利用python来制作动态二维码

    前言 为什么要学习python 是因为不仅很多工作需要用到python 同时我们可以利用python做很多好玩儿的事儿 今天就来教大家如何利用python制作动态二维码 代码说明 我们以小猪佩奇gif图片为例 如果我们利用的背景图是gif动
  • 工厂实施MES系统可以带来哪些效益?

    众多工厂生产现状 1 设施设备先进 但是管理方式落后 手工管理模式的存在 造成数据的不准确和不完全 没有完全实现信息化管理 2 生产计划协调性差 作业调度困难 生产作业计划主要依据调度员的经验制定 设备利用率低 任务进度监控难 紧急插单普遍
  • C++虚基类

    问题引出 问题 A中数据 在D中保存了两份 虚继承 虚继承的目的是让某个类做出声明 承诺愿意共享它的基类 其中 这个被共享的基类就称为虚基类 Virtual Base Class 虚派生只影响从指定了虚基类的派生类中进一步派生出来的类 它不
  • 扎心!为何HR看了你的简历却不通知面试?

    还只是老老实实地写简历 投简历 默默地等待面试通知 那只有两种可能 你太天真 或者是 你真的很久没有 求职 了 如果你细心观察会发现 当你完成一份简历之后 它的状态也会有变化 然而 却有很多求职者并没有搞清楚这些 状态 到底代表着什么 但小
  • idea 配置 JavaWeb 项目的 tomcat

    目录 第一步 单击 idea 靠右上部位的 添加配置 Add Config Run Config 第二步 点击 添加新 或者图中箭头指向的任意一个地方 第三步 选择 Tomcat 服务器 本地 不是 TomEE 第四步 若以前从未配置 To
  • 使用SARIMA做季节时间序列预测全流程(附MATLAB代码)

    在之前的专栏中我们用ARIMA的方法做了时间序列的趋势性预测 不过我们经常还会遇到一种情况 即某些时间序列中存在明显的周期性变化 这种周期是由于季节性变化 季度 月度等 引起的 如下图所示 为1949年到1960年每月国际航空公司的乘客人数
  • C# pdf文件加数字证书,防篡改

    C 指定文件夹内新创建pdf文件加签数字证书 代码实现 引用 Spire Pdf string files Directory GetFiles Config pdfPath 需加数字证书pdf存放文件夹地址 string filesLis
  • 进程信号生命周期详解

    信号和信号量半毛钱关系都没有 每个信号都有一个编号和一个宏定义名称 这些宏定义可以在signal h中找到 例如其中有定 义 define SIGINT 2 查看信号的机制 如默认处理动作man 7 signal SIGINT的默认处理动作
  • c++自定义类型和预处理

    struct 内部初始化变量时 不能用 struct data int x 12 float f 122 5f int pi x data da cout lt lt da f lt lt endl data dap new data 使用
  • 配置 nginx 遇到错误排查(初级)

    系统版本 ubuntu 14 04 nginx 版本 nginx 1 4 6 Ubuntu 本文不是一步步搭建 nginx 的过程 而是我在使用 nginx 的过程中 整理自己遇到的的一些问题 适用于 nginx 遇到问题 排查问题的 ch
  • Neo4J 初次启动与密码

    初次安装成功Neo4J在安装的文件中会有一个bin文件夹 powershell进入bin文件夹执行 neo4j sonsole会有以下结果 D neo4j bin gt neo4j console 2020 09 04 00 57 31 0
  • Python基于PyTorch实现卷积神经网络分类模型(CNN分类算法)项目实战

    说明 这是一个机器学习实战项目 附带数据 代码 文档 视频讲解 如需数据 代码 文档 视频讲解可以直接到文章最后获取 1 项目背景 卷积神经网络 简称为卷积网络 与普通神经网络的区别是它的卷积层内的神经元只覆盖输入特征局部范围的单元 具有稀
  • oracle 9i在线重定义,在oracle 9i下在线重定义表

    9i提供了联机重定义表的方法 可以让你在基本不影响原表的DML情况下修改表结构 实际上 联机重定义表并不是完全的联机重定义 在最后交换表名的时候会短暂地锁定原表和中间表 但这个过程很短暂 相对于传统方法来说 这是一个进步 9i提供了联机重定
  • CNN、BiGRU、BiLSTM代码

    toxicCommentsclassification BiLSTMAttentionNetwork py at master AmritSatpathy toxicCommentsclassification GitHubtoxic co
  • BlueStore 架构及原理分析

    BlueStore 架构及原理分析 Ceph 底层存储引擎经过了数次变迁 目前最常用的是 BlueStore 在 Jewel 版本中引入 用来取代 FileStore 与 FileStore 相比 Bluesore 越过本地文件系统 直接操