Ring 文件格式

Ring 是 Swift 中最重要的数据结构。这种数据结构序列化到磁盘的方式多年来发生了变化。

最初,Ring 文件包含三个关键信息

  • part_power 值(通常存储为 part_shift := 32 - part_power

    • 它决定了 Ring 中的分区数量,

  • 设备列表

    • 包括参与 Ring 的所有磁盘,以及

  • 副本到分区到设备的表

    • 它包含所有 replica_count * (2 ** part_power) 分区分配。

但是,扩展序列化格式以在 Ring 序列化格式中添加更多数据结构的能力意味着创建了一个新的 Ring v2 格式。

Ring 文件在序列化时始终是 gzip 压缩的,尽管内部的原始格式多年来发生了演变。

Ring v0

最初,Ring 只是 RingData 对象的 pickle 转储。 在 Swift 1.3.0 中,这改为 pickle 一个纯标准库数据结构,但核心概念相同。

Ring v1

然而,pickle 存在一些问题。虽然 对解 pickle 不受信任的数据存在安全问题,但安全边界通常被划定,以至于 Ring 被假定为受信任的。最终,促使我们采用新格式的是 性能考虑

Swift 1.7.0 开始,Swift 开始使用一种新的格式(同时仍然愿意读取旧格式)。 新格式以一些魔术数字开头,以便我们可以识别它

+---------------+-------+
|'R' '1' 'N' 'G'| <vrs> |
+---------------+-------+

其中 <vrs> 是一个网络字节序的两位版本号(始终为 1)。 之后,JSON 对象被序列化为

+---------------+-------...---+
| <data-length> | <data ... > |
+---------------+-------...---+

其中 <data-length><data> 的网络字节序的四字节长度(以字节为单位),<data> 是 ASCII 编码的 JSON 序列化对象。 该对象至少包含三个键

  • devs 用于设备列表

  • part_shift(即 32 - part_power

  • replica_count 用于读取的副本到设备行数

然后是副本到分区到设备的表

+-------+-------+...+-------+-------+
| <dev> | <dev> |...| <dev> | <dev> |
+-------+-------+...+-------+-------+
| <dev> | <dev> |...| <dev> | <dev> |
+-------+-------+...+-------+-------+
|                ...                |
+-------+-------+...+-------+-------+
| <dev> | <dev> |...|
+-------+-------+...+

每个 <dev> 都是 devs 列表中的主机字节序的两位索引。 除了最后一行,每一行都恰好包含 2 ** part_power 个条目;最后一行可能包含相同数量或更少的条目。

元数据对象已被证明非常通用:已添加新键以提供其他信息,同时保持向后兼容。 按照顺序,添加了以下新字段

  • byteorder 指定副本到分区到设备的表的字节序是“大端”还是“小端”。 在 Swift 2.12.0 中添加,这允许在大小端机器上写入的 Ring 在小端机器上读取,反之亦然。

  • next_part_power 指示分区幂增加是否正在进行中。 在 Swift 2.15.0 中添加,如果存在,这将具有以下两个值之一:Ring 的当前 part_power,指示可能需要清理硬链接,或者 part_power + 1 指示可能需要创建硬链接。 有关更多信息,请参阅 文档

  • version 指定用于写入此 Ring 的 Ring 构建器的版本号。 在 Swift 2.24.0 中添加,这允许比较来自不同机器的 Ring 以确定哪个更新。

Ring v2

v1 Ring 处理分数量副本的方式使得在副本到分区到设备的表之后可靠地序列化其他大型数据结构变得不可能。 v2 格式旨在可扩展。

新格式以类似于 v1 的魔术数字开头

+---------------+-------+
|'R' '1' 'N' 'G'| <vrs> |
+---------------+-------+

其中 <vrs> 再次是网络字节序的两位版本号(现在为 2)。 通过增加版本号,我们确保旧版本的 Swift 不会读取 Ring,而是会错误地解释内容。

之后,序列化一系列 BLOB,每个 BLOB 如下所示

+-------------------------------+-------...---+
| <data-length>                 | <data ... > |
+-------------------------------+-------...---+

其中 <data-length><data> 的网络字节序的八字节长度(以字节为单位)。 每个 BLOB 前面都有一个 Z_FULL_FLUSH,以便在不读取整个文件的情况下对其进行解压缩。

BLOB 的顺序并不重要,尽管它们倾向于以 Swift 在加载时读取的顺序写入。 这减少了加载所需的磁盘寻道次数。

最后的 BLOB 是一个索引:一个将命名部分映射到文件中偏移量数组的 JSON 对象,如下所示

{
    section: [
        compressed start,
        uncompressed start,
        compressed end,
        uncompressed end,
        checksum method,
        checksum value
    ],
    ...
}

节名称可以是任意字符串,但“swift/”前缀是为上游使用保留的。 start/end 值标记节 BLOB 的开始和结束。 请注意,如果写入索引时未知,某些 end 值可能为 null – 特别是,索引本身将为真。 校验和方法应该是 "md5""sha1""sha256""sha512";其他值将被忽略,以预期需要支持进一步的算法。 校验和值将是未压缩部分的字节的十六进制摘要。 与 end 值一样,如果写入索引时未知,校验和数据也可能为 null

最后,写入“尾部”

  • gzip 流使用另一个 Z_FULL_FLUSH 被刷新,

  • 流切换为未压缩,

  • 写入未压缩索引的起始偏移量的八个字节,

  • gzip 流使用另一个 Z_FULL_FLUSH 被刷新,

  • 写入压缩索引的起始偏移量的八个字节,

  • gzip 流使用另一个 Z_FULL_FLUSH 被刷新,并且

  • gzip 流被关闭;这涉及

    • 使用 Z_FINISH 刷新基础的 deflate 流

    • 写入 CRC32(完整未压缩数据)

    • 写入 ISIZE(完整未压缩数据的长度 mod 2 ** 32

通过切换到未压缩,我们可以确切地知道将写入尾部多少字节,以便在读取时我们可以快速寻到并读取索引偏移量,寻到索引开始位置,并读取索引。 从那里我们可以对任何其他部分做类似的事情。

  • 寻到文件末尾

  • 在底层文件中回退 31 个字节;这应该让我们位于包含压缩起始偏移量的 deflate 块的开头

  • 从 deflate 流中解压缩 8 个字节以获取索引 BLOB 的压缩起始位置

  • 寻到该位置

  • 读取/解压缩索引 BLOB 的大小

  • 读取/解压缩 JSON 序列化的索引。

注意

这 31 个字节是包含 8 个字节位置、Z_FULL_FLUSH 块、Z_FINISH 块以及 CRC32ISIZE 的 deflate 块。 有关更多信息,请参阅 RFC 1951(用于 deflate 流)和 RFC 1952(用于 gzip 格式)。

当前定义的上游部分和部分名称如下

  • swift/index - swift 索引

  • swift/ring/metadata - 序列化为 json 的 Ring 元数据

  • swift/ring/devices - 设备 json 序列化数据结构。

    • 这已从 v1 中的 Ring 元数据结构中分离出来,因为它本身会变得很大

  • swift/ring/assignments - Ring replica2part2dev_id 数据结构

注意

第三方可能会发现添加自己的部分很有用;但是,swift/ 前缀是为未来的上游增强保留的。

swift/ring/metadata

此 BLOB 是一个 ASCII 编码的 JSON 对象,其中包含大量元数据,类似于 v1 Ring。 它具有以下必需的键

  • part_shift

  • dev_id_bytes 指定副本到分区到设备的表中每个 <dev> 使用的字节数;将是 2、4 或 8 中的一个

此外,还有几个可选键可能存在

  • next_part_power

  • 版本

请注意,不再存在两个键:replica_count 不再需要,因为副本到分区到设备的表的大小是显式的,并且 byteorder 不再需要,因为 v2 Ring 中的所有数据都应使用网络字节序写入。

swift/ring/devices

此 BLOB 包含 swift 设备字典列表。 并且从元数据 BLOB 中分离出来,因为这本身会成为一个大型结构。

swift/ring/assignments

此 BLOB 是副本到分区到设备的表。 它的长度将是 replicas * (2 ** part_power) * dev_id_bytes,其中 replicas 是 Ring 的确切(可能为分数)副本计数。 与 v1 不同,每个 <dev> 都使用网络字节序写入。

请注意,这就是为什么我们增加了与 v1 格式相比 <data-length> 的大小 – 否则,我们可能无法表示同时具有高 replica_count 和高 part_power 的 Ring。