您的位置:首页 > 其它

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

2016-04-20 19:35 856 查看
该文首次发表于’盛大游戏G云’微信公众号上,现贴到本人博客,方便大家交流学习

带掉电保护的Rbd Cache方案

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

由于以内存作为缓存,缓存空间不能太大

还是因为以内存做缓存,所以存在掉电数据丢失的风险

为了克服原生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
中的
<offset, len>
创建数据缓存结构
BufferHead
或者合并/拆分原有
BufferHead
得到一个满足当前i/o需求的
BufferHead
并将i/o数据缓存到该结构中,
BufferHead
添加到
bh_lru_dirty
队列。

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

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



上图中磁盘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
移到高速非易失性存储介质(如:
SSD
SAS
),每个虚拟磁盘的
Rbd Cache
是存储介质上的一个文件。多个虚拟磁盘的
Rbd Cache
可以存储在同一个存储介质上。通过使用更大的磁盘缓存空间,空间换时间思想,实现i/o的高速读写及避免掉电引起的数据丢失。

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

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

如何设计缓存文件的格式?

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

由上文的分析我们知道,librbd内部与i/o紧密相关的结构主要有
Object
BufferHead
以及
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
会加载到内存,同时在原有的
Object
BufferHead
结构中增加了
offset
字段,分别用来加快
ObjectMeta
BufferMeta
的查找更新,如下:

class Object:public LRUObject {

...

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

...
}

class BufferHead : public LRUObject {

...

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

...

}


工作原理

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

librbd收到i/o请求后,根据
<offset, len>
将文件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
的改进介绍就结束了,如有不正确的地方欢迎指正,有更好的想法也欢迎留言。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息