注意

本文档适用于 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

父/子关系

子镜像在其头部存储对父镜像的引用,作为(池ID、镜像ID、快照ID)的元组。这足以打开父镜像并从中读取。

除了知道给定镜像的父镜像外,我们还希望能够判断受保护的快照是否仍然有子镜像。这是通过一个新的每个池对象rbd_children实现的,它将

保护

内部,protection_state是头部对象中的一个字段,可以处于三种状态。“受保护”、“不受保护”和“正在取消保护”。前两种状态是“rbd protect/unprotect”的结果。“正在取消保护”状态是在“rbd unprotect”命令检查任何子镜像时设置的。只有处于“受保护”状态的快照才能被克隆,因此“不受保护”状态可以防止类似以下竞争:

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

  2. B:创建克隆

  3. A:取消保护父镜像

  4. A:rbd snap rm池/父镜像@snap

调整大小

调整rbd镜像的大小类似于截断稀疏文件。新空间被视为零,并且缩小rbd镜像会删除超出旧边界的内容。这意味着如果您有一个10G的数据镜像,并且将其缩小到5G然后再扩大到10G,最后5G被视为零(并且当镜像缩小时,存储该数据的任何对象都会被删除)。

层级使这一点复杂化,因为对象的缺失不再意味着它应该被视为零——如果对象是克隆的一部分,它可能意味着需要从父镜像读取一些数据。

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

超出重叠量的对象被视为零。在该点之前的对象会回退到从父镜像读取。

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

重命名

目前,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(镜像可以引用多少父镜像)

这些存储在“父”键中,该键仅在镜像具有父镜像时存在。

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需要针对层级镜像进行更改,以便它仅截断对象,而不删除它们。如果我们删除对象,我们将无法确定是否需要从父镜像读取。

将添加一个新的克隆方法,它接受与create相同的参数,除了大小(父镜像的大小用于大小)。

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

由 Ceph 基金会带给您

Ceph 文档是一个社区资源,由非盈利的 Ceph 基金会资助和托管Ceph Foundation. 如果您想支持这一点和我们的其他工作,请考虑加入现在加入.