注意

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

序列化(编码/解码)

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

术语

最好不要以守护程序和客户端的视角来思考,而应以编码器和解码器的视角。编码器将结构体序列化为 bufferlist,而解码器则执行相反的操作。

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

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

编码器和解码器根据程序员通过实现 encodedecode 方法所定义的格式进行操作。

格式变更原则

序列化格式的更改并不少见。这个过程在开发和评审过程中都需要仔细关注。

一般规则是,解码器必须理解编码器编码的内容。大多数困难出现在确保旧解码器与新编码器兼容性的连续性,以及确保新解码器与旧解码器兼容性的连续性。人们应该假设——除非另有说明——集群中任何旧版本和新版本的混合都是可能的。有两个主要担忧:

  1. 升级。 尽管有关于实体类型(mons/OSDs/clients)顺序的建议,但这不是强制性的,不应做出任何假设。

  2. 客户端版本的巨大可变性。 内核升级(以及随之而来的内核客户端)与 Ceph 升级脱钩一直如此。容器化甚至给 librbd 带来了可变性——现在用户空间库存在于容器本身中。

有一些规则限制了编解码器之间的互操作程度:

  • 守护程序之间的编解码为 n-2

  • 客户端场景的硬性要求为 n-3

  • 客户端场景的软性要求为 n-3..。理想情况下,每个客户端都应该能够与任何版本的守护程序通信。

由于根本原因相同,编解码器遵循的规则与我们的功能位弃用规则几乎相同。请参阅 src/include/ceph_features.h 中的 Notes on deprecation

框架

目前存在多种编解码辅助工具:

  • encoding.h(最流行的),

  • denc.h(性能优化,主要出现在 BlueStore 中),

  • “Message” 层次结构。

尽管细节有所不同,但互操作性规则保持不变。

向结构体添加字段

你可以在 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);
    }
};

ENCODE_START 宏写入一个标头,指定一个 *version* 和一个 *compat_version*(两者初始值均为 1)。每当对编码进行更改时,消息版本都会递增。只有当更改会破坏现有解码器时,compat_version 才会递增——解码器对尾随字节具有容忍性,因此在结构体末尾添加字段的更改不需要递增 compat_version。

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 宏设置的本地变量。

# 深入细节

我们的编解码器的追加可扩展性是 ENCODE_STARTDECODE_FINISH 宏带来的向前兼容性的结果。

它们正在实现可扩展性设施。当编码器填充 bufferlist 时,它会在前面添加三个字段:当前格式的版本、与其兼容的最小解码器版本以及所有编码字段的总大小。

/**
 * 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 {

struct_len 字段允许解码器吃掉用户提供的 decode 实现中未解码的所有字节。类似地,解码器会跟踪用户提供的 decode 方法中已解码的输入量。

#define DECODE_START(bl)                                        \
  unsigned struct_end = 0;                                      \
  __u32 struct_len;                                             \
  decode(struct_len, bl);                                       \
  ...                                                           \
  struct_end = bl.get_off() + struct_len;                       \
  }                                                             \
  do {

解码器使用此信息来丢弃它不理解的额外字节。推进 bufferlist 至关重要,因为编解码器往往是嵌套的;如果保持不变,它只适用于嵌套结构中最后一次 decode 调用。

#define DECODE_FINISH(bl)                                       \
  } while (false);                                              \
  if (struct_end) {                                             \
    ...                                                         \
    if (bl.get_off() < struct_end)                              \
      bl += struct_end - bl.get_off();                          \
  }

这个完整的、协作的机制允许编码器(其后续修订版)生成更多的字节流(例如,在末尾添加一个新字段),并且不必担心残留部分会使旧的解码器修订版崩溃。

由 Ceph 基金会为您呈现

Ceph 文档是由非营利性 Ceph 基金会 资助和托管的社区资源。如果您希望支持这项工作和我们的其他努力,请考虑 立即加入