注意

本文档适用于 Ceph 开发版本。

序列化(编码/解码)

当结构通过网络发送或写入磁盘时,它被编码成字节串。通常(但并非总是——Ceph 中存在多种序列化设施)可序列化的结构有encodedecode方法,用于从bufferlist表示字节串的对象中读写。

术语

最好不从守护进程和客户端的领域来思考,而是从编码器和解码器的领域来思考。编码器将结构序列化到缓冲列表中,而解码器则执行相反的操作。

编码器和解码器可以统称为 dencoders。

Dencoders(包括编码器和解码器)存在于守护进程和客户端中。例如,当 RBD 客户端发出 IO 操作时,它会准备一个MOSDOp结构的实例,并将其编码成一个放在线上的缓冲列表。OSD 读取这些字节并将它们解码回一个MOSDOp实例。这里客户端使用了编码器,而 OSD 使用了解码器。然而,这些角色可以互换——想象一下响应的处理:OSD 编码MOSDOpReply,而 RBD 客户端解码。

编码器和解码器根据程序员通过实现encodedecode methods.

格式变更的原则

序列化的格式通常会发生变更。这个过程在开发和审查期间都需要仔细关注。

一般规则是,解码器必须理解编码器编码了什么。大多数困难发生在确保旧解码器与新编码器兼容性的连续性,以及确保新解码器与旧解码器兼容性的连续性。应该假设——如果未特别说明——集群中任何新旧组合都是可能的。主要关注点有两个:

  1. 升级。虽然有关实体类型顺序(mons/OSDs/客户端)的建议存在,但这并非强制性的,不应做出任何假设。

  2. 客户端版本的巨大差异。情况总是如此,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宏写入一个指定了versioncompat_version(两者初始值均为 1)。消息版本在编码发生变更时增加。compat_version 仅在变更会破坏现有解码器时增加——解码器对尾部字节具有容忍性,因此向结构末尾添加字段的变化不需要增加 compat_version。

The DECODE_START宏接受一个参数,指定代码可以处理的最新消息版本。这被与消息中编码的 compat_version 进行比较,如果消息过于新,则会抛出异常。由于 compat_version 的变更很少,因此添加字段时通常不必担心这一点。

实际上,编码的变更通常涉及在encodedecode函数的末尾简单地添加所需的字段,并在ENCODE_STARTDECODE_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_STARTDECODE_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. 如果您想支持这一点和我们的其他工作,请考虑加入现在加入.