注意

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

RBD 分层

RBD 分层是指创建块设备的写时复制克隆。这允许快速创建映像,例如将虚拟机的“黄金主映像”克隆到新实例中。为了简化语义,您只能创建快照的克隆 - 快照始终是只读的,因此映像的其余部分不受影响,并且不可能意外写入它们。

从用户的角度来看,克隆就像任何其他 rbd 映像一样。您可以对它们进行快照、读/写它们、调整它们的大小等等。从用户的角度来看,克隆没有任何限制。

注意:下面的术语 子映像父映像 分别指的是通过克隆创建的 rbd 映像,以及从中克隆子映像的 rbd 映像快照。

命令行界面

在克隆快照之前,您必须将其标记为受保护,以防止在子映像引用它时将其删除

$ rbd snap protect pool/image@snap

然后您可以执行克隆操作

$ rbd clone [--parent] pool/parent@snap [--image] pool2/child1

您可以创建与父映像具有不同对象大小的克隆

$ rbd clone --order 25 pool/parent@snap pool2/child2

要删除父映像,您必须首先将其标记为不受保护,这将检查是否没有剩余的子映像

$ rbd snap unprotect pool/image@snap
Cannot unprotect: Still in use by pool2/image2
$ rbd children pool/image@snap
pool2/child1
pool2/child2
$ rbd flatten pool2/child1
$ rbd rm pool2/child2
$ rbd snap rm pool/image@snap
Cannot remove a protected snapshot: pool/image@snap
$ rbd snap unprotect pool/image@snap

然后可以像往常一样删除快照

$ rbd snap rm pool/image@snap

实施

数据流

在初始实现中,称为“简单分层”,将不会跟踪克隆中存在哪些对象。命中不存在对象的读取将尝试从父快照读取,这将递归地继续,直到找到对象或找到没有父映像的映像。这是通过从父映像进行正常读取路径完成的,因此父映像和子映像之间的不同对象大小并不重要。

在对对象执行写入之前,会检查对象是否存在。如果它不存在,则执行复制操作,这意味着从父快照读取相关数据范围并将其(加上原始写入)写入子映像。为了防止多个写入尝试复制同一对象的竞争条件,此复制操作将包括原子创建。如果原子创建失败,则执行原始写入。此复制操作实现为类方法,以便将来可以存储额外的元数据。在简单分层中,复制操作将所需的全范围复制到子对象(即子对象的完整大小)。未来的优化可以使这种复制更加精细。

另一个未来的优化可能是存储一个位图,其中包含子映像中实际存在哪些对象。这将避免在每次写入之前检查是否存在,并允许在需要时直接读取父映像。

这些优化在以下位置进行了讨论

http://marc.info/?l=ceph-devel&m=129867273303846

父/子关系

子映像在其头部中存储对其父映像的引用,作为 (pool id, image id, snapshot id) 的元组。这些信息足以打开父映像并从中读取。

除了知道给定映像拥有哪个父映像之外,我们还希望能够判断受保护的快照是否仍有子映像。这是通过一个新的按池对象 rbd_children 实现的,它将(父池 id,父映像 id,父快照 id)映射到子映像 id 列表。这存储在与子映像相同的池中,因为创建克隆的客户端已经对该池中的所有内容具有读/写访问权限,但可能对父映像的池没有写入访问权限。这允许对一个池具有只读访问权限的客户端将该池中的快照克隆到他们具有完全访问权限的池中。它增加了取消保护映像的成本,因为这需要检查每个池中的子映像,但这是一个不常见的操作。它可能只在删除旧映像之前完成,而这已经昂贵得多,因为它涉及删除映像中的每个数据对象。

保护

在内部,protection_state 是头部对象中的一个字段,可以处于三种状态。“protected”(受保护)、“unprotected”(未受保护)和“unprotecting”(正在取消保护)。前两个是“rbd protect/unprotect”的结果设置的。“unprotecting”状态是在“rbd unprotect”命令检查是否有任何子映像时设置的。只有处于“protected”状态的快照才能被克隆,因此“unprotected”状态可以防止像下面这样的竞争条件

  1. A:遍历所有池,查找克隆,未找到

  2. B:创建克隆

  3. A:取消保护父映像

  4. A:rbd snap rm pool/parent@snap

调整大小

调整 rbd 映像的大小就像截断稀疏文件一样。新空间被视为零,缩小 rbd 映像会删除超出旧边界的内容。这意味着如果您有一个充满数据的 10G 映像,然后将其缩小到 5G,然后再放大到 10G,则最后 5G 将被视为零(并且在映像缩小时,容纳该数据的任何对象都已被删除)。

分层使这变得复杂,因为缺少对象不再意味着它应该被视为零 - 如果对象是克隆的一部分,则可能意味着需要从父映像读取一些数据。

为了保留克隆的调整大小行为,我们需要跟踪哪些对象可以存储在父映像中。我们可以将此跟踪为子映像与父映像的重叠量,因为调整大小只会更改映像的末尾。创建子映像时,其重叠量是父快照的大小。在随后的每次调整大小中,重叠量为 min(overlap, new_size)。也就是说,缩小映像可能会缩小重叠量,但增加映像大小不会改变重叠量。

超出重叠范围的对象被视为零。在此之前不存在的对象会回退到从父映像读取。

由于此重叠会随着时间而变化,因此我们将其作为快照元数据的一部分进行存储。

重命名

目前,rbd 头部对象(存储有关映像的所有元数据)以映像的名称命名。这使得重命名会干扰打开映像的客户端(例如从父映像读取的子映像)。为了避免这种情况,我们可以根据映像的 id 来命名头部对象,该 id 不会改变。也就是说,头部对象的名称可以是 rbd_header.$id,其中 $id 是映像在池中的唯一 id。

当客户端打开映像时,它只知道名称。已经有一个按池的 rbd_directory 对象将映像名称映射到 id,但如果我们依赖它来获取 id,那么如果单个对象不可用,我们将无法打开该池中的任何映像。为了避免这种依赖关系,我们可以将映像的 id 存储在名为 rbd_id.$image_name 的对象中,其中 $image_name 是映像的名称。然而,按池的 rbd_directory 对象对于列出池中的所有映像仍然有用。

头部更改

头部需要一些新字段

  • int64_t parent_pool_id

  • string parent_image_id

  • uint64_t parent_snap_id

  • uint64_t overlap (映像可能引用父映像的程度)

这些存储在“parent”键中,只有当映像有父映像时才存在。

cls_rbd

需要一些新方法

/***************** methods on the rbd header *********************/
/**
 * Sets the parent and overlap keys.
 * Fails if any of these keys exist, since the image already
 * had a parent.
 */
set_parent(uint64_t pool_id, string image_id, uint64_t snap_id)

/**
 * returns the parent pool id, image id, snap id, and overlap, or -ENOENT
 * if parent_pool_id does not exist or is -1
 */
get_parent(uint64_t snapid)

/**
 * Removes the parent key
 */
remove_parent() // after all parent data is copied to the child

/*************** methods on the rbd_children object *****************/

add_child(uint64_t parent_pool_id, string parent_image_id,
          uint64_t parent_snap_id, string image_id);
remove_child(uint64_t parent_pool_id, string parent_image_id,
             uint64_t parent_snap_id, string image_id);
/**
 * List ids of a given parent
 */
get_children(uint64_t parent_pool_id, string parent_image_id,
             uint64_t parent_snap_id, uint64_t max_return,
             string start);
/**
 * list parent
 */
get_parents(uint64_t max_return, uint64_t start_pool_id,
            string start_image_id, string start_snap_id);


/************ methods on the rbd_id.$image_name object **************/

set_id(string id)
get_id()

/************** methods on the rbd_directory object *****************/

dir_get_id(string name);
dir_get_name(string id);
dir_list(string start_after, uint64_t max_return);
dir_add_image(string name, string id);
dir_remove_image(string name, string id);
dir_rename_image(string src, string dest, string id);

如果映像支持分层,两个现有方法将发生变化

snapshot_add - stores current overlap and has_parent with
               other snapshot metadata (images that don't have
               layering enabled aren't affected)

set_size     - will adjust the parent overlap down as needed.

librbd

打开子映像会打开其父映像(这将根据需要递归地继续)。这意味着 ImageCtx 将包含指向父映像上下文的指针。不同的对象大小并不重要,因为从父映像读取将通过父映像上下文进行。

对于分层映像,Discard 需要更改,使其仅截断对象,而不是删除它们。如果我们删除了对象,我们将无法判断是否需要从父映像读取它们。

将添加一个新的 clone 方法,它接受与 create 相同的参数,但没有 size(使用父映像的大小)。

我们将把元数据检索分解为几个 API 调用,而不是扩展 rbd_info struct。目前,除了“rbd info”之外,rbd_stat() 的唯一用户只使用它来检索映像大小。

由 Ceph 基金会为您呈现

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