从GitHub上Clone Ceph项目,我是基于(ceph version 12.2.11 luminous 版本)的代码来分析的
一、EC(Erasure Code)是什么?
Ceph的纠删码特性EC:将写入的数据分成N份原始数据,通过这N份原始数据计算出M份效验数据。把N+M份数据分别保存在不同的设备或者节点中,并通过N+M份中的任意N份数据块还原出所有数据块。EC包含了编码和解码两个过程:将原始的N份数据计算出M份效验数据称为编码过程;通过这N+M份数据中的任意N份数据来还原出原始数据的过程称为解码过程。EC可以容忍M份数据失效,任意小于等于M份的数据失效能通过剩下的数据还原出原始数据。Ceph支持以插件的形式来指定不同的EC编码方式。不同的EC编码方式是三个指标间的折中结果,这个三指标就是是:空间利用率、数据可靠性和恢复效率。
二、不同的 EC 编码方式
1. RS类型编码, 目前应用最广泛的纠删码是ReedSolomon编码,简称RS码。下面是RS编码的两个实现(ISA + Jerasure):
1). .ISA:ISA是Intel提供的一个EC库,只能运行在Intel CPU上,它利用了Intel处理器本地指令来加速EC的计算。
2). Jerasure是一个ErausreCode开源实现库,它实现了EC的RS编码。目前Ceph中默认的编码就是Jerasure方式
RS编码的不足之处在于:在N+K个数据块中有任意一块数据失效,都需要读取N块数据来恢复丢失数据。在数据恢复的过程中引起的网络开销比较大。因此,LRC编码和SHEC编码分别从不同的角度做了相关优化。
2. LRC类型编码(特点:恢复数据块时,减少了读取网络数据块的数量)
LRC编码的核心思想为:将校验块(parity block)分为全局校验块(global parity)和局部校验块(local reconstruction parity),从而减少恢复数据的网络开销。
LRC(M,G,L)的三个参数分别为:
·M是原始数据块的数量。
·G为全局校验块的数量。
·L为局部校验块的数量。
编码过程为:把数据分成M个同等大小的数据块,通过该M个数据块计算出G份全局效验数据块。然后把M个数据块平均分成L组,每组计算出一个本地数据效验块,这样共有L个局部数据校验块。
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,且当一个数据块失效时,只读取5个数据块就可以恢复。
三、纠删码 EC 和 副本 Replicated 的比较
众所周知在创建Ceph的pool时,可以设置pool的冗余恢复方式,EC类型或者Replicated副本类型。指定EC类型时,可以设置N和M的参数。各种纠删码(EC的) 和 副本(Replicated)的比较如下表所示:
说明如下:
·在副本类型(三副本)的情况下,恢复效率和可靠性都比较高,缺点就是数据容量开销比较大。
·EC的RS编码,和三副本比较,数据开销显著降低,以恢复效率和可靠性为代价。
·EC的LRC编码以数据容量开销略高的代价,换取了数据恢复开销的显著降低。
·EC的SHEC编码用可靠性换代价,在LRC的基础上进一步降低了容量开销。
四、先来过一下OSD 处理写操作的序列图,后面分析的EC写流程都是走这个框架
根据上面的OSD序列图来分析一下execute_ctx里发生了什么。execute_ctx的函数调用关系为:
PrimaryLogPG::execute_ctx(OpContext *ctx)
=> PrimaryLogPG::prepare_transaction(OpContext *ctx) //准备transaction
=> PrimaryLogPG::do_osd_ops(OpContext *ctx, vector<OSDOp>& ops) //填充ctx变量的相关成员
=> PrimaryLogPG::issue_repop(RepGather *repop, OpContext *ctx)
=> PGBackend::submit_transaction(...) //提交transaction给PGBackend,见上图
五、EC 写操作源代码的分析
举例EC写操作的代码流程分析,来看相关的函数和数据结构
1. 下面分析EC的写操作时,函数PrimaryLogPG::do_osd_ops中实现操作的事务封装
//源代码文件 src/osd/PrimaryLogPG.cc
int PrimaryLogPG::do_osd_ops(OpContext *ctx, vector<OSDOp>& ops)
{
...
PGTransaction* t = ctx->op_t.get();
...
1)多处代码都验证如果是EC类型,写操作的offset必须以stripe_width对齐,否则不支持。
源代码文件 osd_types.h里定义的requires_aligned_append() 函数判断POOL是否是EC类型
/*
* pg_pool
*/
struct pg_pool_t {
...
bool requires_aligned_append() const {
return is_erasure() && !has_flag(FLAG_EC_OVERWRITES);
}
...
}
源代码文件 src/osd/PrimaryLogPG.cc里 do_osd_ops() 函数的 CEPH_OSD_OP_WRITE 写操作:
// --- WRITES ---
// -- object data --
case CEPH_OSD_OP_WRITE:
++ctx->num_write;
...
if (pool.info.requires_aligned_append() &&
(op.extent.offset % pool.info.required_alignment() != 0)) {
result = -EOPNOTSUPP;
break;
}
2)如果对象不存在,do_osd_ops() 函数里调用PrimaryLogPG::maybe_create_new_object来创建
maybe_create_new_object(ctx);
来看看PrimaryLogPG::maybe_create_new_objec()函数的定义
void PrimaryLogPG::maybe_create_new_object(
OpContext *ctx,
bool ignore_transaction)
{
ObjectState& obs = ctx->new_obs;
if (!obs.exists) {
ctx->delta_stats.num_objects++;
obs.exists = true;
assert(!obs.oi.is_whiteout());
obs.oi.new_object();
if (!ignore_transaction)
ctx->op_t->create(obs.oi.soid);
} else if (obs.oi.is_whiteout()) {
dout(10) << __func__ << " clearing whiteout on " << obs.oi.soid << dendl;
ctx->new_obs.oi.clear_flag(object_info_t::FLAG_WHITEOUT);
--ctx->delta_stats.num_whiteouts;
}
}
3)最后把写操作添加到事务中(t是一个PGTransaction类型的变量,通过ctx->op_t.get()):
if (op.extent.length == 0) {
...
} else {
t->write(
soid, op.extent.offset, op.extent.length, osd_op.indata, op.flags);
}
2. 在函数PrimaryLogPG::do_osd_ops实现事务封装后,由PGBackend提交整个操作上下文信息OpContext ctx给FileStore/BlueStore
PrimaryLogPG::execute_ctx(OpContext *ctx) => PrimaryLogPG::issue_repop(RepGather *repop, OpContext *ctx) => PGBackend::submit_transaction(...),其中PGBackend::submit_transaction为虚函数,具体函数由子类ReplicatedPGBackend/ECPGBackend实现,该函数submit_transaction的参数如下:
PGBackend::submit_transaction(
const hobject_t &hoid,
const object_stat_sum_t &delta_stats,
const eversion_t &at_version,
PGTransactionUPtr &&t,
const eversion_t &trim_to,
const eversion_t &roll_forward_to,
const vector<pg_log_entry_t> &log_entries,
boost::optional<pg_hit_set_history_t> &hset_history,
Context *on_all_commit,
ceph_tid_t tid,
osd_reqid_t reqid,
OpRequestRef client_op
)
3. EC*类介绍
类ECBackend实现了EC的读写操作。ECUtil里定义了编码和解码的函数实现。ECTransaction定了EC的事务。
参考:《Ceph 源代码分析》