注意
本文档适用于 Ceph 开发版本。
序列化(编码/解码)
当结构通过网络发送或写入磁盘时,它被编码成字节串。通常(但并非总是——Ceph 中存在多种序列化设施)可序列化的结构有encode
和decode
方法,用于从bufferlist
表示字节串的对象中读写。
术语
最好不从守护进程和客户端的领域来思考,而是从编码器和解码器的领域来思考。编码器将结构序列化到缓冲列表中,而解码器则执行相反的操作。
编码器和解码器可以统称为 dencoders。
Dencoders(包括编码器和解码器)存在于守护进程和客户端中。例如,当 RBD 客户端发出 IO 操作时,它会准备一个MOSDOp
结构的实例,并将其编码成一个放在线上的缓冲列表。OSD 读取这些字节并将它们解码回一个MOSDOp
实例。这里客户端使用了编码器,而 OSD 使用了解码器。然而,这些角色可以互换——想象一下响应的处理:OSD 编码MOSDOpReply
,而 RBD 客户端解码。
编码器和解码器根据程序员通过实现encode
和decode
methods.
格式变更的原则
序列化的格式通常会发生变更。这个过程在开发和审查期间都需要仔细关注。
一般规则是,解码器必须理解编码器编码了什么。大多数困难发生在确保旧解码器与新编码器兼容性的连续性,以及确保新解码器与旧解码器兼容性的连续性。应该假设——如果未特别说明——集群中任何新旧组合都是可能的。主要关注点有两个:
升级。虽然有关实体类型顺序(mons/OSDs/客户端)的建议存在,但这并非强制性的,不应做出任何假设。
客户端版本的巨大差异。情况总是如此,
librbd
带来了可变性——现在用户空间库存在于容器本身中:
有几条规则限制了 dencoders 之间互操作性的程度:
n-2
在守护进程之间进行 dencoding,n-3
客户端场景的硬性要求,n-3..
客户端场景的软性要求。理想情况下,每个客户端都应该能够与任何版本的守护进程通信。
由于底层原因相同,dencoders 遵循的规则几乎与我们的功能位弃用的规则相同。参见Notes on deprecation
in src/include/ceph_features.h
.
框架
目前,多种类型的 dencoding 辅助工具共存。
encoding.h(最普遍的一种),
denc.h(性能优化,主要出现在
BlueStore
),参数消息层次结构中。
虽然细节有所不同,但互操作性规则保持不变。
向结构中添加字段
你可以在 Ceph 代码的各个地方看到这个例子,但这里有一个例子:
class AcmeClass
{
int member1;
std::string member2;
void encode(bufferlist &bl)
{
ENCODE_START(1, 1, bl);
::encode(member1, bl);
::encode(member2, bl);
ENCODE_FINISH(bl);
}
void decode(bufferlist::iterator &bl)
{
DECODE_START(1, bl);
::decode(member1, bl);
::decode(member2, bl);
DECODE_FINISH(bl);
}
};
The ENCODE_START
宏写入一个指定了version和compat_version(两者初始值均为 1)。消息版本在编码发生变更时增加。compat_version 仅在变更会破坏现有解码器时增加——解码器对尾部字节具有容忍性,因此向结构末尾添加字段的变化不需要增加 compat_version。
The DECODE_START
宏接受一个参数,指定代码可以处理的最新消息版本。这被与消息中编码的 compat_version 进行比较,如果消息过于新,则会抛出异常。由于 compat_version 的变更很少,因此添加字段时通常不必担心这一点。
实际上,编码的变更通常涉及在encode
和decode
函数的末尾简单地添加所需的字段,并在ENCODE_START
和DECODE_START
中增加版本。例如,以下是向AcmeClass
:
class AcmeClass
{
int member1;
std::string member2;
std::vector<std::string> member3;
void encode(bufferlist &bl)
{
ENCODE_START(2, 1, bl);
::encode(member1, bl);
::encode(member2, bl);
::encode(member3, bl);
ENCODE_FINISH(bl);
}
void decode(bufferlist::iterator &bl)
{
DECODE_START(2, bl);
::decode(member1, bl);
::decode(member2, bl);
if (struct_v >= 2) {
::decode(member3, bl);
}
DECODE_FINISH(bl);
}
};
添加第三个字段的方法。注意,compat_version 没有变化,因为编码后的消息仍然可以被仅理解版本 1 的代码解码——它们将忽略我们编码的尾部字节member3
.
在decode
函数,解码新字段是条件性的:这是因为我们可能仍然会接收到不包含该字段的老版本消息。变量struct_v
是由DECODE_START
宏本地设置的。
# 进入迷宫
我们 dencoders 的追加扩展性是ENCODE_START
和DECODE_FINISH
宏带来的向前兼容性的结果。
它们正在实现扩展性设施。编码器在填充缓冲列表时,会预加三个字段:当前格式的版本、与之兼容的解码器的最小版本以及所有编码字段的总大小。
/**
* start encoding block
*
* @param v current (code) version of the encoding
* @param compat oldest code version that can decode it
* @param bl bufferlist to encode to
*
*/
#define ENCODE_START(v, compat, bl) \
__u8 struct_v = v; \
__u8 struct_compat = compat; \
ceph_le32 struct_len; \
auto filler = (bl).append_hole(sizeof(struct_v) + \
sizeof(struct_compat) + sizeof(struct_len)); \
const auto starting_bl_len = (bl).length(); \
using ::ceph::encode; \
do {
The struct_len
字段允许解码器消耗用户提供的decode
实现中所有未解码的字节。类似地,解码器跟踪在用户提供的decode
methods.
#define DECODE_START(bl) \
unsigned struct_end = 0; \
__u32 struct_len; \
decode(struct_len, bl); \
... \
struct_end = bl.get_off() + struct_len; \
} \
do {
中已解码的输入量。解码器使用这些信息来丢弃它不理解的多余字节。推进缓冲列表至关重要,因为 dencoders 倾向于嵌套;仅仅保留它只适用于嵌套结构中的最后一个deocde
调用。
#define DECODE_FINISH(bl) \
} while (false); \
if (struct_end) { \
... \
if (bl.get_off() < struct_end) \
bl += struct_end - bl.get_off(); \
}
整个协作机制允许编码器(及其后续修订版)生成更多的字节流(例如,在末尾添加新字段),而不用担心残留部分会崩溃旧版解码器修订版。
由 Ceph 基金会带给您
Ceph 文档是一个社区资源,由非盈利的 Ceph 基金会资助和托管Ceph Foundation. 如果您想支持这一点和我们的其他工作,请考虑加入现在加入.