Ceph 性能优化 之 带掉电保护的Rbd Cache

该文首次发表于’盛大游戏G云’微信公众号上,现贴到本人博客,方便大家交流学习

带掉电保护的Rbd Cache方案

Ceph是一款开源的统一存储,在单一的系统上提供块、对象及文件存储接口。近年随着公有云/私有云的快速普及,凭借其自身良好的稳定性、扩展性及与Openstack的深度整合,Ceph Rbd块存储被大量的使用,作为VM的数据存储。有Ceph Rbd部署实践经验的IT工程师们对Rbd Cache一定不会陌生,它是Ceph Rbd客户端缓存,开启后能显著提高快设备i/o性能,但是它存在两个问题:

  1. 由于以内存作为缓存,缓存空间不能太大
  2. 还是因为以内存做缓存,所以存在掉电数据丢失的风险

为了克服原生Rbd Cache存在的上述不足,盛大游戏G云对Rbd Cache进行了改良,我们的方案是:用高速非易失存储介质(如:SSD、SAS)替换内存作为Rbd Cache,通过用空间换时间的方式,保证i/o性能并规避上述的缺陷。下文将对原生Rbd Cache及改良后的方案分别加以说明。

下文称未经修改的Ceph Rbd Cache为原生Rbd Cache,改良后的方案称为G云版Rbd Cache,引用的对象术语及代码片段来自Hammer Ceph-0.94.1

原生Rbd Cache

根据上述逻辑图,Rbd Cache是Ceph块存储客户端库librbd内实现的一个缓存层,主要提供读缓存和写合并功能,用来提高读写性能,默认情况下Rbd Cache处于开启状态。需要注意的是:Ceph既支持以内核模块方式动态地为Linux主机提供块设备,也支持以Qemu Block Driver的形式为VM提供虚拟块设备,本文描述的是第二种形式。下面我们来看看Rbd Cache的内部实现

上述逻辑图并未完整画出i/o流经的所有组件,还请读者注意。

Rbd Cache的实现机制

librbd模块内默认以4MB为单位对虚拟磁盘进行切分,每个4MB的chunk称为一个Object,以ObjectExtent为单位进行数据缓存;应用i/o通常会有不同的大小,每个i/o请求的数据以Object为单位缓存到一个或多个ObjectExtent中。Rbd Cache常用配置参数如下:

  • rbd cache size : librbd能使用的最大缓存大小
  • rbd cache max dirty : 缓存中允许的脏数据最大值,用来控制回写大小,不能超过rbd cache size
  • rbd cache target dirty :开始执行回写的脏数据阀值,不能超过rbd cache max dirty
  • rbd cache max dirty max :缓存中脏数据的最大缓存时间,用来避免因脏数据未达到回写要求而长时间存在

在i/o处理过程中,根据ObjectExtent中的创建数据缓存结构BufferHead或者合并/拆分原有BufferHead得到一个满足当前i/o需求的BufferHead并将i/o数据缓存到该结构中,BufferHead添加到bh_lru_dirty队列。

i/o数据缓存后librbd会立即尝试合并相邻的i/o请求,以提高数据写入性能。并基于配置参数通知flush_thread线程向后端Ceph集群发送i/o数据,i/o请求完成BufferHeadbh_lru_dirty队列移除并添加到bh_lru_rest队列。当缓存数据达到缓存大小限制后,bh_lru_rest队列中的数据会被删除。

下图展示了一个虚拟磁盘Image、应用i/o映射、ObjectBufferHead的关系:

上图中磁盘Image逻辑切割为多个Object,每个Object可能包含0到多个BufferHead,每个BufferHead包含一个应用数据片段,如果是脏数据会被加入到bh_lru_dirty队列,如果数据已经下刷到Ceph集群就会加入到bh_lru_rest队列;

正如上一节中所述,librbd将所有的数据都缓存在内存中,如果宿主出现掉电故障,bh_lru_dirty队列中的脏数据将丢失,也就是用户数据丢失。DT时代,数据是企业的核心资产,关键业务数据的丢失会给企业带来不可估量的损坏。为解决该缺陷,盛大游戏G云对原生的Rbd Cache进行了改良。

G云版Rbd Cache

我们的方案是:将Rbd Cache移到高速非易失性存储介质(如:SSDSAS),每个虚拟磁盘的Rbd Cache是存储介质上的一个文件。多个虚拟磁盘的Rbd Cache可以存储在同一个存储介质上。通过使用更大的磁盘缓存空间,空间换时间思想,实现i/o的高速读写及避免掉电引起的数据丢失。

设计该方案时,我们需要考虑的问题有:

  • 如何最大化利用原有的代码框架?
  • 如何设计缓存文件的格式?

如何最大化利用原有的代码框架

由上文的分析我们知道,librbd内部与i/o紧密相关的结构主要有ObjectBufferHead以及bufferlist,Object是虚拟磁盘Image基于固定大小逻辑切分的一个数据块,从0开始编号;BufferHead可以理解为Object内的一个连续数据分片,数据分片大小随机,BufferHead之间的间隔表示空闲空间,由于应用i/o大小不固定,一个BufferHead可能被拆分,相邻的BufferHead也可能被合并;BufferHead中包含的应用数据实际存储在bufferlist标示的内存结构中。三个结构间的关系如下;

有了上述的理论基础,本着最大化利用原有代码的原则,我们设计了如下的磁盘对象与内存对象映射关系:

  • 定义ObjectMeta结构,持久存储Object对象信息,并定义相关的转换接口
  • 定义BufferMeta结构,持久存储BufferHead对象信息,并定义相关的转换接口
  • bufferlist是承载用户数据的内存区,直接用缓存文件存储用户数据

如何设计缓存文件的格式

数据结构映射关系建立起来了,那该按怎样一种方式在缓存文件中组织各种对象,并能实现高效的对象查找、更新呢?由于上层应用i/o的不确定性,缓存层可能就会出现大量的随机操作,缓存层的查找算法效率的高低直接决定了缓存层的性能。从实现的复杂度和效率两个维度考虑,我们最终选择了用bitmap来组织管理各种对象。

再次,从上文的分析我们了解到一个Object对象包含的Bufferhead对象个数是动态变动的,并且每个Bufferhead包含的数据长度也各不一样;换句话说就是,缓存文件中每个ObjectMeta对象包含的BufferMeta对象个数不固定。然而采用bitmap来组织对象,需要预先知道各类对象的个数,这样才能确定bitmap的大小。为了解决这组矛盾,我们引入了一个参数rbd ssd chunk size来表示上层应用i/o的大小。再继续前,让我们先了解下新引入的配置参数:

  • rbd ssd cache : 缓存开关,默认开启
  • rbd ssd chunk size : 预设的i/o数据块大小,最小值为16(2^16 = 64KB)librbd基于此值组织缓存文件,不能超过Object大小
  • rbd ssd cache size : librbd能使用的最大磁盘缓存
  • rbd ssd cache max dirty : 缓存中允许的脏数据最大值,用来控制回写大小,不能超过rbd cache size
  • rbd ssd cache target dirty : 开始执行回写的脏数据阀值,不能超过rbd cache max dirty
  • rbd ssd cache path : 缓存文件路径

回到上面的问题,引入rbd ssd chunk size参数后,我们基于如下的公式来确定各对象的个数:

  • ObjectMeta的个数 = rbd ssd cache size/rbd ssd chunk size
  • BufferMeta的个数 = ObjectMeta的个数 * 4

加上日志文件头部FileHead,最终的缓存文件格式是这样的:

缓存文件中的各部分都按512字节对齐。另外,为了加快查找速度,FileHead及所有的bitmap会加载到内存,同时在原有的ObjectBufferHead结构中增加了offset字段,分别用来加快ObjectMetaBufferMeta的查找更新,如下:

class Object:public LRUObject {

    ...

    loff_t offset;  //对应的ObjectMeta在缓存文件中的位置

    ...
}

class BufferHead : public LRUObject {

    ...

    loff_t offset;  //对应的BufferMeta在缓存文件中的位置

    ...

}

工作原理

盛大游戏G云版的Rbd Cache内部修改就是上面描述的那个样子(其实也不全是哦!亮点在下面),下面让我们一起来看看它是怎么工作的。

  • librbd收到i/o请求后,根据将文件i/o映射为块设备i/o,生成ObjectExtent数组
  • 从内存中的Obj Bitmap, 为ObjectExtent数组中指向的Object获取一个存放位置,创建ObjectMeta对象并赋值,然后存储到缓存文件上述指定的位置处
  • 创建BufferHead或者从缓存中合并/拆分出满足要求的BufferHead,接着创建BufferMeta对象并用前述BufferHead对其赋值,然后从内存中的Buffer Bitmap获取一个空闲位置,将BufferMeta存储到缓存文件中上述指定位置处
  • 从内存中的Chunk Bitmap获取一个空闲位置,将i/o数据存储到缓存文件上述指定位置处

上述4步概要的描述了写i/o在librbd内部的处理逻辑。读i/o也是相似的处理逻辑,不同的是,Ceph集群作为了librbd的数据写入方,写入缓存文件的数据来自Ceph集群。

优化

还记得上面我们是基于预设的rbd ssd chunk size来确定缓存文件bitmap大小的吗。细心的读者,一定注意到了这种假设很可能带来缓存空间的巨大浪费,而且也影响性能,让我们一起来看看到底是怎么回事吧。

假定我们有如下的缓存配置:

  • rbd ssd chunk size = 4M
  • rbd ssd cache size = 10G
  • rbd ssd cache max dirty = 8G
  • rbd ssd cache target dirty = 5G

那么基于上面的计算公式:

  • ObjectMeta个数 = 10G/4M = 2560

也就是最大能缓存2560个对象,实际上写入1280(rbd ssd cache target dirty = 5G)个对象后会写线程就会触发回写,写入2048个对象后清理线程就会触发缓存清理, 由于缓存文件的独占访问,写入性能会极大的下降。如果应用i/o的平均大小是我们设定的4M,那持续写入5G的数据才触发回写,这也是我们期望的;但如果应用i/o的平均大小是64KB呢,写入160M(2560*64KB)就触发了回写。一个是5G,一个是160M,空间浪费有多大不言而喻。用户看到的现象是,10G的缓存,怎么写了320M就满了,性能达不到要求。

为解决上面的问题,我们引入了“存储应用感知”功能,原理就是:librbd周期性的统计应用i/o的平均大小,动态调整rbd ssd chunk size,在业务不繁忙的时候, 重置缓存。

性能对比

说了这么多,来看看实际的测试效果吧,测试参数如下:

  • 测试命令:fio –filename=/vdisk/fio –direct=1 –ioengine=libaio –rw={randwrite/randread/read/write} -numjobs=2 –bs={4k/8k/4m} –size=5G –runtime=300 –iodepth=16 –group_reporting –name=test
  • 缓存磁盘:SAS 10000 RPM, 缓存大小 10G

通过挂载Rbd块设备到KVM虚拟机上,并格式为xfs文件系统进行测试;其中的一组测试结果如下:

表格中SAS Cache表示用SAS作缓存的情况;Mem Cache表示原生Rbd Cache;表格的前半部分,显示的是用单台VM测试情况下的性能对比;后半部分,显示的是用20台VM压测情况下的性能对比;结果表明,性能提升效果还是挺明显的。如果用SSD作为缓存磁盘,相信性能提升会更明显。

至此我们对于Rbd Cache的改进介绍就结束了,如有不正确的地方欢迎指正,有更好的想法也欢迎留言。

你可能感兴趣的:(Ceph)