注意
本文档适用于 Ceph 的开发版本。
纠删码开发人员注释
简介
本文档的每一章都解释了 Ceph 中纠删码实现的一个方面。它主要基于示例进行解释,以演示事物的工作原理。
从 OSD 读取和写入编码块
纠删码存储池将每个对象存储为 K+M 个块。它被分为 K 个数据块和 M 个编码块。存储池配置为具有 K+M 个大小,以便每个块存储在活动集中的一个 OSD 中。块的排名作为对象的属性存储。
假设创建了一个纠删码存储池以使用五个 OSD ( K+M = 5 ) 并承受其中两个 OSD 的丢失 ( M = 2 )。
当包含 ABCDEFGHI 的对象 NYAN 被写入其中时,纠删码编码功能将内容分成三个数据块,简单地将内容分为三部分:第一部分包含 ABC,第二部分包含 DEF,最后一部分包含 GHI。如果内容长度不是 K 的倍数,则内容将进行填充。该功能还会创建两个编码块:第四个包含 YXY,第五个包含 GQC。每个块存储在活动集中的一个 OSD 中。这些块存储在具有相同名称 ( NYAN ) 但位于不同 OSD 上的对象中。创建块的顺序必须保留,并作为对象的属性 ( shard_t ) 存储,除了其名称之外。块 1 包含 ABC 并存储在 OSD5 上,而块 4 包含 YXY 并存储在 OSD3 上。
+-------------------+
name | NYAN |
+-------------------+
content | ABCDEFGHI |
+--------+----------+
|
|
v
+------+------+
+---------------+ encode(3,2) +-----------+
| +--+--+---+---+ |
| | | | |
| +-------+ | +-----+ |
| | | | |
+--v---+ +--v---+ +--v---+ +--v---+ +--v---+
name | NYAN | | NYAN | | NYAN | | NYAN | | NYAN |
+------+ +------+ +------+ +------+ +------+
shard | 1 | | 2 | | 3 | | 4 | | 5 |
+------+ +------+ +------+ +------+ +------+
content | ABC | | DEF | | GHI | | YXY | | QGC |
+--+---+ +--+---+ +--+---+ +--+---+ +--+---+
| | | | |
| | | | |
| | +--+---+ | |
| | | OSD1 | | |
| | +------+ | |
| | +------+ | |
| +------>| OSD2 | | |
| +------+ | |
| +------+ | |
| | OSD3 |<----+ |
| +------+ |
| +------+ |
| | OSD4 |<--------------+
| +------+
| +------+
+----------------->| OSD5 |
+------+
当从纠删码存储池读取对象 NYAN 时,解码功能读取三个块:包含 ABC 的块 1、包含 GHI 的块 3 和包含 YXY 的块 4,并重建对象的原始内容 ABCDEFGHI。解码功能被告知块 2 和 5 丢失了(它们被称为 erasures)。块 5 无法读取,因为 OSD4 处于 out 状态。
一旦读取了三个块,就可以调用解码功能: OSD2 是最慢的,它的块不需要考虑在内。此优化未在 Firefly 中实现。
+-------------------+
name | NYAN |
+-------------------+
content | ABCDEFGHI |
+--------+----------+
^
|
|
+------+------+
| decode(3,2) |
| erasures 2,5|
+-------------->| |
| +-------------+
| ^ ^
| | +-----+
| | |
+--+---+ +------+ +--+---+ +--+---+
name | NYAN | | NYAN | | NYAN | | NYAN |
+------+ +------+ +------+ +------+
shard | 1 | | 2 | | 3 | | 4 |
+------+ +------+ +------+ +------+
content | ABC | | DEF | | GHI | | YXY |
+--+---+ +--+---+ +--+---+ +--+---+
^ . ^ ^
| TOO . | |
| SLOW . +--+---+ |
| ^ | OSD1 | |
| | +------+ |
| | +------+ |
| +-------| OSD2 | |
| +------+ |
| +------+ |
| | OSD3 |-----+
| +------+
| +------+
| | OSD4 | OUT
| +------+
| +------+
+------------------| OSD5 |
+------+
纠删码库
使用参数 K+M 的 Reed-Solomon,通过将对象 O 分成块 O1, O2, … OM 并计算编码块 P1, P2, … PK 来对对象 O 进行编码。可以使用可用的 K+M 块中的任意 K 个块来获取原始对象。如果数据块 O2 或编码块 P2 丢失,可以使用 K+M 块中的任意 K 个块进行修复。如果丢失的块多于 M 个,则无法恢复对象。
读取对象 O 的原始内容可以是 O1, O2, … OM 的简单拼接,因为插件使用的是 系统码。否则,必须将块提供给纠删码库的 decode 方法以检索对象的内容。
性能取决于编码函数的参数,也受调用编码函数时使用的包大小(例如 Cauchy 或 Liberation)的影响:较小的包意味着更多的调用和更多的开销。
尽管提供了 Reed-Solomon 作为默认设置,但 Ceph 通过 抽象 API 使用它,该 API 旨在允许每个存储池选择使用存储在 纠删码配置文件 中的 key=value 对来实现它的插件。
$ ceph osd erasure-code-profile set myprofile \
crush-failure-domain=osd
$ ceph osd erasure-code-profile get myprofile
directory=/usr/lib/ceph/erasure-code
k=2
m=1
plugin=isa
technique=reed_sol_van
crush-failure-domain=osd
$ ceph osd pool create ecpool erasure myprofile
插件 从 目录 动态加载,并期望实现 int __erasure_code_init(char *plugin_name, char *directory) 函数,该函数负责在注册表中注册派生自 ErasureCodePlugin 的对象。 ErasureCodePluginExample 插件读取
ErasureCodePluginRegistry &instance =
ErasureCodePluginRegistry::instance();
instance.add(plugin_name, new ErasureCodePluginExample());
派生自 ErasureCodePlugin 的对象必须提供一个工厂方法,从中可以生成 ErasureCodeInterface 对象的具体实现。 ErasureCodePluginExample 插件 读取
virtual int factory(const map<std::string,std::string> ¶meters,
ErasureCodeInterfaceRef *erasure_code) {
*erasure_code = ErasureCodeInterfaceRef(new ErasureCodeExample(parameters));
return 0;
}
parameters 参数是在创建存储池之前在纠删码配置文件中设置的 key=value 对列表。
ceph osd erasure-code-profile set myprofile \
directory=<dir> \ # mandatory
plugin=isa \ # mandatory
m=10 \ # optional and plugin dependent
k=3 \ # optional and plugin dependent
technique=reed_sol_van \ # optional and plugin dependent
注意事项
如果对象很大,在内存中对其进行编码和解码可能不切实际。然而,当使用 RBD 时,一个 1TB 的设备被分成许多单独的 4MB 对象,并且 RGW 也是如此。
编码和解码是在 OSD 中实现的。尽管它可以为读写在客户端实现,但 OSD 在 scrubbing 时必须能够自行编码和解码。