ceph bluestore中的磁盘空间管理

ceph bluestore摒弃了传统的本地文件系统,而直接使用裸磁盘作为OSD的存储介质,因而需要自行管理磁盘空间的分配与回收

概述

一个设计良好的磁盘空间管理器,需要兼顾空间和时间效率;bluestore中提供了空间管理器FreelistManager来支持空间管理,当前提供了一种基于位图的实现,包含:位图持久化以及内存分配器Allocator两部分。其中,位图的持久化是指将空间分配(置1)和回收(置0)的位图状态持久化到磁盘中,基于rocksdb实现;内存分配器是磁盘位图的一致性视图,用来加速空间分配的速度,基于不同的内存组织形式,包括:stupidbitmap, avlhybrid四种。

位图持久化

bluestore中,当前基于bitmap来实现磁盘空间的分配管理,并将位图持久化到rocksdb中,如下图:
逻辑视图
从下往上看:下层是磁盘,在逻辑上划分为多个固定大小的block(参数为:bdev_block_size, 默认为4KB);上面是空间管理器,最小分配单位为alloc-block(参数为:bluestore_min_alloc_size),需要为block的整数倍,在SSD磁盘上,默认是4KB,在HDD磁盘上,默认是64KB;一个key管理若干个alloc-block,其value记录这些alloc-block的位图状态(参数为:bluestore_freelist_blocks_per_key,默认为128) 。

举个例子,默认情况下:1TB的SSD磁盘, 包含的block数为:1TB/4KB = 256MB个,包含的alloc-block数为:1TB/4KB = 256MB个,包含的rocksdb的key数为:256MB/128 = 2MB个。

至于SSD和HDD采用不同的分配单位,与他们的物理特性有关:HDD由于自身的机械特性原因,在随机IO中性能比较差,所以bluestore会较大的分配块,然后会尝试将多个IO写到相同的块中,以减少随机io,提升性能;而SSD由于特殊的内部结构,随机性能不受类似HDD的机械特性影响,相反为减少写放大,带来空间浪费,应采用较小的块。

bluestore根据请求的<offset, len>以及bluestore_freelist_blocks_per_key,计算需要置位哪些key上的哪些bit,然后通过事务持久化到rocksdb中。

分配器

bluestore实现了4中分配器,他们在性能,空间占用以及性能稳定性上都有所不同,最开始默认使用stupid,然后默认bitmap,最新的版本默认采用hybrid

stupid分配器

这是最早采用的分配器,其原理是:维持最多10棵二叉树(基于google的C++ btree_map实现,参考:https://code.google.com/archive/p/cpp-btree/),每个节点记录<offset,length>的空闲空间,

  1. 构建树时根据请求的length计算相应的空闲区间应该添加到哪棵树【每棵树所在的位置称为bin,计算方式为:bin = MIN(64 - length所在block的二进制表示的前置0的个数, 9),length所在的block = length/bdev_block_size, 9是最大的bin号,因为一共10棵树】,保证将大块的连续空间放在前面的树中,相近大小的块放在相同或者临近的树上,并对连续的块进行合并。
  2. 分配空间时根据请求的length选择一棵树,首先从这棵树开始往后循环的查找,直到找到合适的区间, 或者没有空间则从这棵树后面的树继续循环的查找,直到找到合适的空间或者没有空间报错,找到空间后将该区间从树中删除,如果存在部分分配还需要将剩余的空闲空间重新加入到合适的树中(查找优化:搜索每棵树时,下界为:第一个节点,上界:根据前一次查找的offset+length值来确定查找区间的下界,减小查找区间的范围)。

从上面的分析,我们可以知道:对于新的OSD,连续空闲空间都较大,空闲块都记录在前面的树中;而随着空间的不断分配使用,一块大的空闲空间被切割为更多小的,不连续的空闲空间,空闲块将打散记录到不同的树中,甚至可能集中记录到末尾的几棵树中;所以,整个森林的内存空间占用不稳定,查找速度不稳定,碎片较严重。

bitmap分配器

这是Mimic版本中的默认分配器,其原理是:通过三层位图来管理整个空闲空间,并充分利用x86架构中的缓存行特性,

  1. 初始化的时候根据磁盘大小,最小分配大小以及内建的常量值,生成一个3层的位图空间,全部置位为0,如下:
    分层位图
    从下往上看:最底层的磁盘,逻辑上划分为若干块,默认块大小为4KB;往上一层是L0,根据最小分配单元将磁盘划分为多个逻辑单元,每64个为一组,由一个slot来管理【每个slot管理64个alloc】,全部的slot组成一个数组。再上一层是L1,以“L0中分配单元的512倍”为分配单元将磁盘划分为多个逻辑单元,每32个为一组,由slot来管理【L1中每个slot管理256个L0层的slot,每个l1-alloc包含8个L0层的slot(称为一个slotset),即每个l1-alloc包含512个L0层的alloc】,全部的slot组成一个数组。最上面一层是L2,以“L1中分配单位的256倍”为分配单元将磁盘划分为多个逻辑单元,每64个为一组,由slot来管理【L2中每个slot管理512个L1层的slot,每个l2-alloc包含8个L1层的slot(称为一个slotset),即每个l2-alloc包含256个L1层的l1-alloc】,全部的slot组成一个数组。
    举个例子,假设有个1TB SSD组成的OSD,配置的bluestore_min_alloc_size=4KB,那么,L0中 alloc=4KB,每个slot管理64个alloc,共4KB64 = 256KB,slot总数为:1TB / 256KB = 4MB个;L1中l1-alloc = 4KB512 = 2MB,每个slot管理32个l1-alloc,共 2MB32 = 64MB,slot总数为:1TB / 64MB = 16KB个;L2中l2-alloc = 2MB256 = 512MB,每个slot管理64个l2-alloc,共512MB*64 = 32GB,slot总数为:1TB / 32GB = 32个。
  2. 构建位图的时候,根据<offset, length>,从L0->L2设置各层的位图,L0和L2层的设置相对简单:根据offset, length,以及alloc计算slot以及slot bit,然后置位即可,用1bit表示一个alloc;L1层的设置相对复杂,需要根据其L0层slot置位状态,如:是全设置?是全空闲?是部分设置?来处理slot以及slot bit部分设置的情况,因为L1层每个alloc/slot管理的区间包含L0层多个alloc/slot管理的区间,结合上图说明:看蓝色区域,l1-alloc包含L0层的8个slot,每个slot包含64个alloc,如果所有的8个slot都是空闲状态(每个bit都为1),那么L1层中的空闲块数加1;如果8个slot中有部分空闲有部分被使用(部分bit为1部分bit为0),那么L1层中的“部分块数”加1;然后根据上面的判断设置L1中slot bit的状态【空闲设置为11,部分占用设置为01,全占用为00,L1中使用2个bit来表示每个l1-alloc,所以每个slot占用64bit
  3. 分配空间的时候, 从L2->L0递归查找可用的空间,首先从L2层找到可用的slot和slot bit,接着根据与L1层的映射关系,得到L1层的起始和结束slotset,然后通过判断每个slot的状态来做进一步的处理:1)对于已使用的slot直接跳过,2)对于空闲的slot则根据与L0层的映射关系,得到L0层的起始和结束slotset,3)对于部分空闲的slot,需要先找到第一个空闲slot bit,然后根据偏移以及与L0层的映射关系,得到L0层的起始和结束slotset,在L0层则根据length以及每个slot/slot bit来设置位图,最后回溯设置L1和L2层的位图,并更新可用空间【过程中位图的映射变更过程:L2的slot/slot bit -> L1的slotset / slot -> L0的slotset /slot / alloc】。

从上面的分析,我们可以知道:bitmap空间初始化即预分配,整体上空间占用上比stupid要大,分配空间时从L2层的第一个空闲slot开始查找,因为L2层(往下层逐级减小)每个slot的空间非常的大,在大部分场景下,分配速度会比较稳定,长尾IO问题得到缓解,整体上性能表现要优于stupid方式,这也是后面以bitmap为默认分配器的原因。【在bitmap的分组设置中,利用了CPU Cache Line的特性,可以加速位图操作 - L1和L0层的slotset 都是512bit(64Byte),这是典型的缓存行大小,CPU从主存读取变量数据时,以Cache Line为单位整体读取 - 类似预读,空间局部性原理; 可以通过命令查看系统缓存行:more /sys/devices/system/cpu/cpu1/cache/index0/coherency_line_size】

AVL分配器

AVL树即平衡二叉树,如果是非空树,左右子树的高度差不超过1。在ceph的空间分配器中定义了两颗avl树:1)根据offset排序的avl树,基于boost的avl_set结构实现,后文称为Tree1,2)根据length排序的avl树, 基于boost的avl_multiset结构实现, 后续称为Tree2。分配空间时优先使用第一棵树,

  1. 在初始化树时,根据<offset, length>中的offset在Tree1中查找插入位置(返回插入的后一个迭代器),并将区间插入到两颗树中,当然还需要注意处理与树中前后节点的合并问题:1)不与前后节点合并,直接插入Tree1和Tree2中, 2)与前或后节点连续,对于Tree1之间更新前后节点的end或start,对于Tree2需要删除前或后节点,插入新的节点,3) 与前后节点均连续,对于Tree1是将前节点合并到后节点,然后执行旋转再平衡,删除前节点,对于Tree2需要删除前和后节点,插入新的节点。
  2. 分配空间时,首先根据length以及Tree2中的最后一个节点的大小(最大的连续区间)来决定从哪棵树中开始查找,如果Tree2中的最大连续块比待分配的length小或者剩余空闲空间小于某个比例(默认4%)或者最大连续块小于某个值(默认128KB),那么就从Tree2分配,否则从Tree1分配【我是这么想的:如果Tree2中的最大连续块比较小或者剩余空间比较少,说明当前的空间已经比较离散,碎片比较多,那么从基于按块大小排序的Tree2分配效率会更高】:查找过程比较简单,就是遍历树,具体实现中有个小的优化:设置一个游标数组,每次分配请求后会记录该次请求命中节点的偏移并记录到游标中,再次收到相同大小的分配请求时,会先从游标中记录的偏移往后遍历树,如果没有找到再从头遍历树;空间分配后,需要从Tree1和Tree2中删除已分配的空间,这里需要处理剩余空间的再插入问题:先从Tree2中得到上述以分配节点的迭代器,然后从Tree2中移除节点,1)如果没有剩余空间,Tree1执行旋转平衡并删除节点,2)如果头部或尾部有剩余,更新节点的start和end信息,重新插入Tree2中,3)如果头部和尾部都有剩余,创建新的尾部节点(后半部分),并插入到Tree1中当前节点的后一个节点的前面以及插入到Tree2中,更新当前迭代器的end信息(前半部分),重新插入Tree2中。

从上面的分析,我们可以知道:对于新的OSD,连续空闲空间都较大,所需要的节点(空间)较少;而随着空间的不断分配使用,一块大的空闲空间被切割为更多小的,不连续的空闲空间,需要更多的节点(空间)来记录整个磁盘空间的分配情况,整棵树内存空间占用不稳定,为优化大量碎片以及低容量情况下分配效率,采用两棵树来记录磁盘空间,所需要的内存在量级上是stupid的两倍, avl tree相比btree更稳定,性能表现会更稳定些。

Hybrid分配器

它是bitmap分配器和avl分配器的组合体,基本思路是:限制avl中的节点个数。

  1. 初始化时,根据上一节avl分配器中介绍的方法初始化树,如果节点个数超过限制【我理解:是碎片比较多了】,就启用bitmap分配器,根据上一节bitmap分配器中介绍的方法初始化树,将新的区间插入到bitmap中。
  2. 分配空间时,如果满足下列条件优先从bitmap中分配:1)bitmap已初始化同时有还有足够的空闲空间,2)待分配的空间很小,小于avl中Tree2的最小块, bitmap的分配方式请查阅上文;否则从avl中分配,avl的分配方式请查阅上文。

从上面的分析,我们可以知道:这个分配器是大块的分配用avl,充分发挥avl(小)树的性能优势,尽量避免碎片化;小块分配用bitmap,发挥bitmap在小块分配上的性能优势以及稳定性。

总体上,bitmap应该是最稳定的分配器,而如果对业务的工作负载以及io模型非常清楚,可以考虑用hybrid。

猜你喜欢

转载自blog.csdn.net/lzw06061139/article/details/108220065