参考:Ceph源码分析 常涛
1. EC的基本原理
云存储领域比较流行的数据冗余存储方法,原理和传统的RAID类似,但是比RAID灵活。
它将写入的数据分成N份原始数据,通过这N份原始数据计算出M份校验数据。把N+M份数据分别保存在不同的设备或者节点中,并通过N+M份中的任意N份数据块还原出所有的数据块。
EC包含了编码和解码的两个过程:
编码:将原始的N份数据计算出M校验数据。
解码:通过这N+M份数据中的任意N份数据来还原出原始数据的过程称为解码。
EC可以容忍M份数据失效,任意小于等于M份的数据失效能通过剩下的数据还原出原始数据。
目前,一些主流的云存储商都采用EC编码方式:
Google GFS II---RS(6,3)编码
Facebook HDFS RAID---RS(10,4)编码
Mircosoft Azure---LRC(12,2,2)编码
2. EC的不同插件
ceph支持以插件形式来指定不同的EC编码方式。各种编码的不通电,实质上就是在ErasureCode的三个指标之间折中的结果,这三个指标是:空间利用率、数据可靠性和恢复效率。
2.1 RS编码
目前最广泛的纠删码是ReedSolomon编码,简称RS码。
RS编码实现之一:Jerasure,是一个ErasureCode开源实现库,实现了EC的RS编码,目前ceph中默认的就是Jerasure方式。
RS编码实现之二:ISA,是Intel提供的一个EC库,只能运行在Intel CPU上,它利用了Intel处理器本地指令来加速EC的计算。
RS编码不足之处:在N+K个数据块中有任意一块数据失效,都需要读取N块数据来恢复丢失的数据。在数据恢复的过程中引起的网络开销比较大。因此,LRC编码和SHEC编码分别从不同角度做了相关优化。
2.2 LRC
LRC编码的核心思想为:将校验块(parity block)分为全局校验块(global parity)和局部校验块(local reconstruction parity),从而减少恢复数据的网络开销。其目标在于解决当单个磁盘失效后恢复过程的网络开销。
LRC(M,G,L)的三个参数分别为:
M:原始数据块的数量
G:全局校验块的数量
L:为局部校验块的数量
编码过程为:把数据分为M个同等大小的数据块,通过该M个数据块计算出G份全局校验数据块。然后把M个数据块平均分为L组,每组计算出一个本地数据校验块,这样共有L个局部数据校验块。
下面以Azure的LRC(12,2,2)和Facebook的HDFS RAID的早起编码方式RS(10,4)为例来比较LRC和RC编码在恢复过程的开销:
LRC(12,2,2) | RS(12+4) |
D1 D2 D3 D4 D5 D6 D7 D8 D9 D10 D11 D12 L1 L2 G1 G2 |
D1~D12 P1~P4 |
表中LRC编码:总共有12 个数据块,分别为D1~D12.有两个本地数据校验块L1和L2,L1为通过第一组数据块D1~D6计算而得到的本地校验数据块;L2为第二组数据块D7~D12计算而得到的本地校验数据块。有两个全局数据校验块G1和G2,它是通过所有数据块D1~D12计算而来。对应RS编码,数据块D1~D12,计算出的校验块为P1~P4.
不同情况下的数据恢复开销:
***如果数据块D1~D12只有一个数据块损坏,LRC只需要读取6个额外的数据块来恢复。而RS需要读取12个其他的数据块来修复。
***如果L1或者L2其中一个数据块损坏,LRC需要读取6个数据块。如果G1,G2其中一个数据损坏,LRC扔需要读取12个数据块来修复。
最大允许失效的数据块:
***RS允许数据块和校验块中任意的小于等于4个数据的失效。
***LRC:
数据块中,只允许任意的小于等于2个数据块失效。
允许所有的校验块(G1,G2,L1,L2)同时失效。
允许至多两个数据块和两个本地校验块同时失效。
综上分析:对于只有一个数据块失效,或者一个本地数据校验块失效的情况下,再恢复该数据块时,LRC比RS可以减少一般的磁盘IO和网络带宽。所以LRC重点在单个磁盘失效后恢复的优化。但是对于数据可靠性来说,通过最大允许失效的数据块个数的讨论可知,LRC会有一定的损失。
2.3 SHEC编码
SHEC的编码方式为SHEC(K,M,L),其中K代表data chunk的数量,M代表parity chunk的数量,L代表计算parity chunk时所需要的data chunk数量。其最大允许失败的数据块为ML/K. 这样恢复失效的单个数据块只需要额外读取L个数据块。
以SHEC(10,6,5)为例,其最大允许失效的数据块为:
M(6) * L(5) / K(10) = 3
D1~D10为数据块
P1:D1~D5计算出的校验块
P2:D3~D7计算出的校验块
P3:D5~D9
P4:D6~D10
P5:D1~D2 D8~D10
P6:D1~D4 D10
2.4 EC和副本的比较
三副本 | RS(10,4) | LRC(10,6,5) | SHEC(10,6,5) | |
数据容量开销 | 3X | 1.4X | 1.8X | 1.6X |
数据恢复开销 (单个数据块失效) |
1X | 10X | 5X | 5X |
可靠性 | 高 | 中 | 中 | 中下 |
说明:
***在三副本的情况下,恢复效率和可靠性都比较高,缺点就是数据容量开销比较大
***对于EC的RS编码,和三副本比较,数据开销显著降低,以恢复效率和可靠性为代价。
***LRC编码以数据容量开销率高的代价,换取了数据恢复开销的显著降低。
***SHEC编码用可靠性换代价,在LRC的基础上进一步降低了容量开销。
3. Ceph中EC的实现方式
3.1 基本概念
首先介绍一些EC的基本概念。注意,这里提到的stripe是RADOS系统定义的,可能与其他系统的定义不同。
***chunk:一个数据块就叫data chunk,简称chunk,其大小为chunk_size设置的字节数。
***stripe:用来计算同一个校验块的一组数据块,称为data_stripe,简称stripe,其大小为stripe_width,参与的数据块的数目为stripe_size.
stripe_width = chunk_size * stripe_size
如图:一个EC(4+2):stripe_size = 4, chunk_size = 1K, 那么stripe_width = 4K.
在ceph中,默认stripe_width就是4K.
3.2 EC支持的写操作
当前ceph的EC写入还有一定的限制,目前支持的操作如下:
***create object:创建对象
***remove object:删除对象
***write full:写整个对象
***append write(stripe width aligned):追加写入(限定追加操作的起始偏移以stripe_width对齐)
目前ceph只支持上述操作,而不支持overwrite操作,其主要有如下两个条件的限制:
***由于编码和解码的过程都以stripe width整块数据计算
***EC在特殊场景需要回滚场景
所以,目前EC只支持append写操作中,写操作的起始偏移offset以stripe_width对齐的情况,如果ennd不是以stripe_width对齐,就补0对齐即可。
目前不支持以下情况:
***append写操作,写操作的起始偏移offset没有以stripe_width对齐。
***overwrite写操作,offset和end都不以stripe_width对齐。
(由于计算数据校验块需要读取整个stripe的数据块,所以前两种情况都需要读取该stripe确实的数据块,来计算校验块,由于性能原因,目前不支持)
***overwrite写操作,写操作的起始偏移offset和结束为止end都以stripe_width对齐。
(overwrite写操作不支持是由EC的回滚机制导致的)
3.3 EC的回滚机制
依据EC原理可知,EC(N+M)的写操作如果小于等于M个OSD失效,不会导致数据丢失没数据可恢复。EC在理论上就最多只能容忍M个OSD失效。如果OSD失效的数量大于M,这种情况就超出了理论设计的范畴,系统无法处理这种情况。可以说这是合理的。
但是对于所有的存储系统,必须应对一种特殊情况:整个机房或者整个数据中心全部断电,系统重启后可以恢复,并且数据不丢失。
当存储烯烃全局断电时,其数据的写入状态就有可能出现:小于N个磁盘的数据成功写入,而其他磁盘没有写成功的情况。
以之前EC(4+2)为例,假设写操作只有三个OSD写成功了,其他的3个OSD没有来得及把数据写入磁盘。这种情况下,不但导致新数据写入失败,而且导致旧数据也无法读取成功。这就需要EC的回滚机制,回滚到最后一次成功写入的旧数据版本。
Ceph目前支持的EC操作都是回滚比较容易实现的,实现机制如下:
***create object操作的回滚实现比较简单,删除该对象即可
***对于remove object操作,在执行时并不删除该对象,而是暂时保留该对象;如果需要回滚,就可以直接恢复。
***writeFull操作,暂时保留旧的对象,创建一个新的对象完成写操作。当需要回滚时,恢复旧的数据对象。
***append操作,记录append时的size到PG日志中,当需要回滚时,对该对象做truncate操作即可。
4. EC源码分析
对应EC的上述三种变更操作,其本地回滚的信息都记录在对应的PG日志记录的mod_desc中:
struct pg_log_entry_t{
.......
objectModDesc mod_desc;
......
};
在函数ReplicatedPG::do_osd_ops中实现操作的事务封装,下面重点分析EC的写操作和write_full操作的实现。
4.1 EC的写操作
首先验证如果是EC类型,写操作的offset必须以stripe_width对齐,否则不支持。
case CEPH_OSD_OP_WRITE:
if(pool.info.requires_aligned_append() &&
(op.extent.offset % pool.info.required_alignment() != 0)) {
result = -EOPNOTSUPP;
break;
}
如果对象不存在,就在mod_desc中添加创建的信息,否则在mod_desc中添加old size的信息:
ctx->mod_desc.create();
否则就追加写:
ctx->mod_desc.append(oi.size);
最后把写操作添加到事务中:
if(pool.info.require_rollback())
t->append(soid, op.extent.offset, op.extent.length, osd_op.indata, op_flags);
4.2 EC的write_full
如果对象已经存在,调用函数ctx->mod_desc.rmobject,如果返回false,说明已经记录了信息,直接删除,如果返回true,就调用stash保存旧的对象数据,用来恢复:
case CEPH_OSD_OP_WRITEFULL:
......
if(obs.exists){
if(ctx->mod.desc.rmobject(ctx->at_version.version)) {
t-stash(soid, ctx->at_version.version);
}else{
t->remove(soid);
}
}
在事务中写入数据:
t->append(soid, 0, op.entent.length, osd_op.indata, op.flags);
4.3 ECBackend
类ECBackend实现了EC的读写操作。ECUtil里定义了编码和解码的函数实现。ECTransaction定义了EC的事务。
目前纠删码的研究是一个热点,它可以极大的提供存储利用率,降低存储成本。目前研究都在着力研究纠删码如何直接支持块存储,也就是随机overwrite操作的能力。