注意

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

池迁移设计

背景

池迁移的目标是能够在同一集群内不中断地将所有 RADOS 对象从一个池迁移到另一个池。此功能计划在 Umbrella 版本中实现。

池迁移用例

  • 这提供了不中断地更改纠删码配置文件(特别是 K 和 M 的选择)的能力。将其实现为池之间的不中断迁移比尝试进行原地转换更简单,并且效率不低。

  • 在副本池和纠删码池之间转换。正在进行的向 EC 池添加 OMAP 和类支持的工作将消除在使用 RBD、CephFS 或 RGW 时为元数据设置单独副本池的需要,这应该使这些迁移与该工作相结合变得可行。

  • 希望在两个池之间迁移数据的通用用例。

我们所说的不中断意味着 I/O 或应用程序不需要从使用旧池切换到新池,甚至在迁移开始或结束时也没有很短的中断(例如 RBD 实时迁移所需的中断)。但是,迁移需要读取和写入池中的每个对象,因此在迁移过程中会对性能产生影响——类似于 PG 回填时的影响。池迁移将使用与拆分/合并 PG 和回填单个 PG 时使用的相同技术和控制来管理此影响。

对于第一个版本,我们将要求目标池在开始时是空的(这意味着我们不需要担心具有相同名称的对象)。有关避免名称冲突的详细信息,请参阅相关部分。支持合并池(要么约束条件防止对象名称冲突,要么在迁移过程中自动解决这些冲突)是未来可能的增强功能,但尚不清楚这解决了哪些用例。

在池迁移期间,对源池施加限制,不允许修改 PG 数量(以引起拆分或合并)。有关在迁移期间阻止更改 PG 数量的详细信息,请参阅相关部分以及 CLI 和 UI 部分。在迁移期间不允许删除源池。允许其他操作,例如重新平衡,但也许应该不鼓励这样做,因为数据无论如何都在移动。

在池迁移期间,对目标池施加限制,不允许将此池迁移到另一个池(即不允许菊花链式或循环迁移)。允许拆分、合并和重新平衡。在迁移期间不允许删除目标池。一旦池完成迁移,允许开始对先前迁移的目标池进行新的迁移。

对于第一个版本,一旦池迁移开始,就没有取消、暂停或反转的选项。

对于第一个版本,没有计划在迁移完成后让客户端更新对池的引用,它们将继续引用旧池,并且 objector (librados) 会将请求重新路由到新池。OSDMap 将保留旧池的存根信息,重定向到新池。

该功能需要对客户端代码进行更改;在允许池迁移之前,所有客户端和守护程序都需要升级。两个主要客户端,objector(在 librados 中)和内核客户端将得到更新。内核客户端的更新可能会滞后于 Umbrella 版本。如果客户端集成到其他产品中(例如 ODF),则这些产品需要整合新的客户端才能使用该功能。

对于第一个版本,没有计划支持 Ceph 集群之间的池迁移。理论上,这可以在第一个版本代码的基础上稍后添加,但这需要大量的额外工作。它将要求客户端能够在迁移完成后更新对集群和池的引用,并且要求客户端能够将 I/O 重定向到不同的集群。由于源集群中的所有 OSD 和客户端都需要能够向目标集群提交请求,因此会存在额外的身份验证挑战。

设计

重用现有设计

让我们首先看看我们可以复制/重用/重构/从中获取灵感的现有代码或功能,我们不想重新发明轮子或重复过去的错误。

回填

回填是 PG 运行的一个过程,用于恢复 OSD 上的对象,该 OSD 要么刚刚添加到 PG(从没有对象开始),要么已经离开 PG 一段时间(有一些最新的对象,一些过时的对象,可能缺少新对象,并且可能拥有在它离开期间被删除而不再需要的对象)。

回填需要很长时间,因此在回填发生时必须允许 I/O 继续。它利用了所有对象都有哈希值并且可以按哈希顺序列出对象的事实。这意味着回填可以按哈希顺序恢复对象,并且可以简单地保留一个水印哈希值来跟踪已取得的进度。哈希值低于水印的对象 I/O 是针对已恢复的对象,需要更新所有 OSD,包括回填 OSD。哈希值高于水印的对象 I/O 可以忽略回填 OSD,因为回填过程稍后会恢复此对象。目前正在被回填过程恢复的对象被锁定,以防止 I/O 在回填对象所需的短时间内发生。

回填的另一个特性是该过程是幂等的,虽然保留水印有性能优势,但如果水印重置为开始并且回填过程再次开始,则没有正确性问题,因为重复该过程将确定对象已经恢复。这简化了设计,因为水印不必严格复制和检查点,尽管对于回填而言,它是 pg_info_t 的一部分,因此进度会非常频繁地检查点。

与池迁移的相关性

  • 池迁移可以按哈希顺序列出对象并将它们迁移到新池。

  • 水印可用于跟踪已迁移的对象。水印不需要持久化。

  • 客户端可以缓存水印副本以帮助将 I/O 指向正确的池和 PG。

  • 客户端的缓存副本可能会过时,如果 I/O 被错误指向,它们将失败并提供最新的水印,以便 I/O 可以重试。

  • 回填会恢复 RADOS 对象的所有部分 - 属性、数据和 OMAP。大对象分阶段恢复(例如一次 2MB),并利用一个临时对象,该对象在恢复结束时重命名以实现原子性。如果对等循环中断了该过程,则临时对象将被丢弃。如果池迁移使用此技术,则需要注意对等循环可能会中断目标池但不会中断源池,因此如果目标丢弃了该对象,则可能需要重新启动对象的迁移。

  • 回填会同时恢复 RADOS head 对象及其关联的快照,并使用锁定(例如 PrimaryLogPG::is_degraded_or_backfilling_objectPrimaryLogPG::is_unreadable_object)来确保在恢复过程中不能访问这些对象,因为它们之间存在依赖关系。池迁移需要同时迁移 head 对象和快照,并且需要确保我们不会在此过程进行到一半时处理对对象的 I/O。

  • 回填旨在通过使用 snapset 属性中的信息来确定快照的哪些区域是克隆来恢复快照时保留快照的空间效率——参见 calc_clone_subsets / calc_head_subsets。这尚未针对 EC 池实现,并且 tracker 72753 显示它目前也不适用于副本池。我们将希望使用它(以及 PushOp 重新建立克隆区域的方式)进行迁移。

与回填不同,对于迁移,我们希望客户端知道水印,以便它们可以将 I/O 路由到旧池/新池。我们不关心客户端是否有过时的水印——这只会导致一些 I/O 被错误地路由到旧池,旧池会使它们失败回客户端并传达一个新的水印,以便 I/O 可以重新提交给新池。

我们故意使更新客户端的水印副本变得懒惰——可能存在数百或数千个客户端,因此一直更新它们会很昂贵。将水印放入 OSDMap 并发布新纪元以将其分发给所有客户端会更昂贵。相比之下,我们正在考虑记录哪些 PG 正在迁移/已完成迁移在 OSDMap 中——经验法则是尝试在迁移期间每秒只更新一次 OSDMap

为了使迁移能够支持直接读取,我们确实需要 PG 中的所有 OSD 知道水印的位置,并且随着每个对象被迁移而更新此位置。迁移对象涉及从源池读取它,将其写入目标池,然后将其从源池中删除。其他 OSD 可以在处理删除请求时更新迁移进度。关于直接读取和迁移对象 + 其快照将存在一些复杂性。已经有一些代码在对象 + 其快照尚未全部恢复时,会使直接读取失败并带有 EAGAIN(以将这些重定向到主 OSD),我们可能需要在迁移对象 + 快照进行到一半时使用此方法,然后让主 OSD 暂停 I/O,直到对象 + 快照全部迁移完毕,然后再使 I/O 再次失败以重定向到新池。

水印不一定需要检查点到磁盘,在 PG 中查找哈希值最低的对象很便宜,所以我们可以在对等开始迁移时重新计算水印。

调度回填/恢复

决定如何优先回填/恢复以及运行此过程的速度与处理来自客户端的 I/O 相比是一个复杂的问题。首先,决定哪些 PG 应该回填/恢复,哪些应该等待。这涉及 OSD 之间的消息,并考虑 I/O 是否被阻塞以及 PG 还剩下多少冗余(例如,具有 2 个故障的 replica-3 池优先于具有 1 个故障的 replica-3 池)。其次,一旦选择了 PG 进行回填/恢复,调度程序必须决定执行回填/恢复的频率与处理客户端 I/O 的频率。这发生在主 OSD 中,使用加权成本。

与池迁移的相关性

  • 池迁移不如回填或恢复那么关键。它需要适应相同的过程来确定 PG 何时应该开始迁移。

  • 一旦允许 PG 开始迁移,OSD 调度程序就需要控制工作速度。迁移对象的开销与回填对象的开销相似,因此希望我们只需复制回填调度以进行迁移。

目标是尽可能重用调度程序(例如 mclock),只是告诉它迁移的优先级低于回填或异步恢复,但高于深度 scrub。

Mclock 通过为每个回填/恢复操作和每个客户端 I/O 请求分配权重来工作,它还在启动时对 OSD 进行基准测试以了解 OSD 的最大性能。然后使用此信息来确定何时调度后台工作。相同的概念应该适用于迁移请求。我们需要为迁移工作分配一个权重;这应该与回填的权重相似/相同。

对于支持使用 WeightedPriorityQueue 调度的集群,我们将采用类似的方法。

期望是迁移不需要新的可调设置,现有的回填/恢复可调设置应该足够了,我们不想使这部分 UI 进一步复杂化。

统计数据

我相信收集了一些关于回填/恢复性能的统计数据。我们应该用关于迁移过程的类似统计数据来补充这些。

我们需要考虑由 Prometheus 收集的 OSD 统计数据以及通过 HealthCheck 和/或 UI 呈现的任何进度摘要。

CopyFrom

CopyFrom 是一个 RADOS 操作,可以将对象的内容复制到新对象中。它发送给将存储新对象的 OSD 和 PG。OSD 负责读取源对象,这涉及向另一个 OSD 和 PG 发送消息,然后将读取的数据写入新对象。如果正在复制的对象很大,则复制操作会分解为多个阶段,并且通过使用临时对象存储新数据直到写入最后的数据,然后将临时对象重命名为新对象来实现原子性。

与池迁移的相关性

  • 池迁移需要将对象从旧池复制到新池——这将涉及一个 OSD 和 PG 读取对象,另一个 OSD 和 PG 写入对象。

  • 池迁移将希望从源端驱动复制操作,因此我们可能需要一个 CopyTo 类型的操作。

  • OSD 之间发送消息的方式、大对象复制的分阶段方式以及在分阶段时使用临时对象名称都是可以重用的概念。

或者,池迁移可能希望复制 ECBackend 中的恢复对象实现,该实现用于恢复正在恢复或回填的对象。这也使用临时对象对大对象的恢复进行分阶段,并使用 PushOp 消息将数据发送到正在回填的 OSD。可以使用大部分恢复对象过程而无需更改,只需更改发送给不同 PG 的 PushOp 消息,并发送所有分片的消息,因为整个对象正在迁移。

让我们考虑后端恢复操作和 CopyFrom 之间的区别

  • CopyFrom 是一个在 PrimaryLogPG 中运行的过程,位于副本或 ECBackend 之上,它将对象从一个 PG 的主 OSD 复制到另一个 PG 的主 OSD。在 EC 的情况下,主 OSD 可能需要向其他 OSD 发出 SubOp 命令来读取/写入数据。

  • 由副本和 EC 池实现的 run_recovery_op 在主 OSD 上运行并读取数据(在 EC 的情况下向其他 OSD 发出 SubOp 命令),然后发出 PushOp 命令将恢复的数据写入目标 OSD。

  • PrimaryLogPG 级别工作的 CopyFrom 确保复制的对象包含在 PG 统计数据中并获取其自己的 PG 日志条目,以便更新可以向前/向后滚动,并且可以通过异步恢复进行恢复。

  • run_recovery_opPGBackend 级别实现,并假定 PG 已经有对象的统计数据和 PG 日志条目,它只负责使 PG 中的其他分片保持最新。

  • CopyFrom 最终向 PGBackend 发出读写操作,它不提供复制快照并保留其空间效率的技术。

  • run_recovery_op 旨在保留克隆的空间效率(尚未针对 EC 池实现,副本池有 bug)——PushOp 消息包含一种描述对象哪些部分应该是克隆的方式。

对于池迁移,我们可能需要一个混合实现。我们可能会重用大量的 run_recovery_op 代码来读取我们想要迁移的对象,并且理想情况下处理快照中的空间效率。我们可能不想发出 PushOps,而是想向目标池的主 PG 发出新的 COPY_PUT 类型操作,但传递与 PushOp 相同类型的信息,以便我们可以跟踪需要克隆的内容。然后目标池可以将写入和克隆操作的混合提交给 PGBackend 层来创建对象,以及更新 PG 统计数据和创建 PG 日志条目。

拆分 PG

通常一个池的 PG 数量是 2 的幂。这是因为我们希望每个 PG 包含大致相同数量的对象,并且我们使用对象哈希的最高 N 位来选择使用哪个 PG。然而,当 PG 数量加倍时,这会导致池中大约一半的对象需要移动到新的 PG。我们不希望所有这些迁移都同时发生;我们希望它随着时间的推移而进行,以减少影响。为了解决这个问题,MGR 控制 PG 数量的增加,它有一个池应该拥有的 PG 数量的目标,并缓慢增加 PG 数量,等待 PG 完成恢复后再进行进一步拆分。

当池的 PG 数量不是 2 的幂时,这意味着并非所有 PG 都具有相同的大小。例如,如果有 5 个 PG,则 PG 0 和 4 将是 PG 1 到 3 大小的一半,因为 PG 0 和 4 之间的选择基于对象哈希的一个额外位。虽然这不是理想的长期状态,但在拆分过程中没问题。

与池迁移的相关性

  • 池迁移需要将旧池中所有 PG 中的所有对象迁移到新池。就像拆分一样,我们不希望在执行迁移时使系统过载。

  • 因此,池迁移应该一次迁移一个(或少量)PG。

  • 一个过程需要监控迁移进度,注意 PG 何时完成迁移并启动下一个 PG。这可以在 MON 中实现(在这种情况下,它需要是事件驱动的,OSD 告诉 MON PG 何时完成迁移 - 与 PG 合并的工作方式有些相似),或者可以在 MGR 中实现(在这种情况下,MGR 可以轮询 PG 的状态,然后通过 CLI 命令告诉 MON 开始下一次 PG 迁移)。

直接 I/O / 平衡读取

EC 直接 I/O 功能正在更改客户端以决定将客户端 I/O 请求发送到哪个 OSD,它建立在副本池的平衡读取标志之上,该标志告诉客户端将读取 I/O 平均分配给副本 PG 中的所有 OSD,而不是将它们全部发送给主 OSD。

与池迁移的相关性

  • 它正在更改客户端代码中与我们希望客户端实现池迁移决定将 I/O 发送到哪个池(因此是 PG 和 OSD)类似的位置。

  • OSD 允许直接 I/O / 平衡读取失败并带有 EAGAIN,以处理 OSD 无法处理 I/O 的边缘情况。在这种情况下,客户端重试 I/O,但将其发送到主 OSD。当客户端向错误的池发出 I/O 时,需要类似的重试机制,因为对象最近已迁移。当重试 I/O 时,我们需要担心排序,因为这会产生 I/O 超越或重新排序的机会。请参阅下面的读取/写入排序部分。

  • 直接 I/O 正在向 OSDMap 的一部分 pg_pool_t 结构添加额外信息,该信息由 monitor 发送给每个 Ceph 守护程序和客户端。此额外信息用于确定是否支持直接 I/O 并帮助确定路由 I/O 请求的位置。池迁移同样需要将详细信息添加到 pg_pool_t 结构中,以便客户端知道正在发生迁移。

读取/写入排序

Ceph 有一些相当严格的读取/写入排序规则。一旦写入完成到客户端,任何读取都必须返回新数据。在写入完成之前,读取应返回所有旧数据或所有新数据(不允许混合)。如果写入 A 和 B 同时连续发送到同一个对象,则写入 A 应在写入 B 之前应用——写入的顺序应通过客户端、信使和 OSD 保留。如果写入 A 和读取 B 同时发送,则读取 B 有可能超越写入 A。有一个标志 RWORDERED 可以设置,以防止发生这种超越。

当读取或写入发送到不同对象时,没有排序保证——这些对象几乎肯定存储在不同的 OSD 上,即使它们在同一个 OSD 上,也会由不同的线程使用不同的锁处理,因此很容易重新排序。

似乎没有太多使用 RWORDERED 标志的场景,RBD 和 RGW 不使用该标志,CephFS 在 MDS RecoveryQueue 中使用该标志(调用 filer.probe,该函数在 osdc/Filer.cc 中实现),我认为这只用于某些恢复场景。

这些规则使得在客户端实现水印并使用它来决定将 I/O 请求路由到哪个池变得棘手,除非使用类似于新纪元来推进水印。问题在于,如果在不停止 I/O 的情况下推进水印,则可能会导致请求重新排序。

例如

  • 写入 A 发送到旧池。

  • 写入 B 发送到旧池。

  • 写入 A 失败,带有更新的水印,并重试发送到新池。

  • 读取 B 和 RWORDERING 发送到新池。

  • 写入 B 失败,需要重试发送到新池。

在这个例子中,读取 B 超越了写入 B。

也许更令人担忧的是,如果我们将读取 B 替换为对 B 的另一次写入,规则也会被打破。

防止重新排序违规的最简单方法是,当存在未完成的写入(或设置了 RWORDERING 标志的读取)时,不推进水印。这并不理想,因为它可能导致相当多的 I/O 失败以重试,然后才能更新水印。

更复杂的实现是,当存在发送到旧水印和新水印之间哈希值的对象的其他写入时,暂停向哈希值位于旧水印和新水印之间的对象发出新写入。

其他池迁移问题

我们需要考虑池迁移的其他主题。

避免名称冲突

对于第一个版本,我们将要求目标池在迁移开始时是空的(通过提供一个 UI 界面,该界面只在创建新池时开始迁移)。我们还可以通过添加客户端代码来拒绝向目标池发起请求的尝试(客户端代码本身仍然允许将请求从源池重定向到目标池)来防止在迁移期间将对象写入目标池。因为我们将要求最低客户端版本才能使用池迁移,这将确保所有客户端都包含此额外的策略。OSD 本身无法实现策略,因此无法防止流氓客户端——如果发现名称冲突,我们可能应该让迁移停止而不是崩溃。

在第一个版本之后,如果存在合并池的用例,则理论上可以通过另外使用客户端访问对象的池来使名称唯一来处理名称冲突。这需要在从客户端到 OSD 的请求中包含额外信息。

在迁移期间阻止更改 PG 数量

在迁移期间,我们真的不想更改源池中的 PG 数量。有三个原因:

  1. 我们真的不想在即将迁移对象时在源池中移动对象——我们可能最好继续进行迁移,而不是试图修复源池中的任何不平衡。

  2. 在源池中拆分/合并 PG 会使迁移调度更加困难。调度分两个级别完成——我们说一次有多少源 PG 正在迁移,然后控制源 PG 内的迁移速率。如果拆分/合并源池,这会使选择要迁移的 PG 更加困难。

  3. 如果我们阻止拆分和合并并按相反顺序迁移 PG(从池中编号最高的 PG 开始),那么随着 PG 完成迁移,我们可以减少源池中的 PG 数量。这有助于保持 PG 总数更易于管理。

相比之下,我们不太关心目标池——我们可以在迁移进行时轻松应对拆分/合并。然而,从性能角度来看,我们确实希望避免将对象迁移到目标池,然后发生拆分/合并,从而第二次复制对象。这意味着通常我们希望将目标池 PG 数量设置为与迁移开始时的源池相同。

我们可能还希望在迁移期间默认禁用目标池的自动缩放器,因为我们不希望它看到一个几乎为空且有大量 PG 的目标池,并认为它应该减少 PG 数量。

CLI 和 UI

池迁移将需要一个新的 CLI 来开始迁移,还需要一种方法来监控正在迁移的 PG 和迁移进度。开始迁移的 CLI 需要由 MON 实现(OSDMonitor.cc 已经实现了大部分池 CLI 命令),因为迁移需要更新 OSDMap 中的 pg_pool_t 结构来记录迁移的详细信息。

然后新的 map 将分发给客户端和 OSD,以便它们知道迁移已开始。已安排开始迁移的 PG 需要在对等过程结束时确定它们不需要恢复或回填,并且它们应该尝试安排迁移(将需要新的 PG 状态 MIGRATION_WAITMIGRATING)。

我们需要与仪表板团队合作,将池迁移支持添加到仪表板中,并提供用于开始迁移的 REST API。

我们希望在池迁移进行时阻止一些 CLI:

  • 我们不希望在迁移源池时能够拆分/合并 PG(见上文)。

  • 我们不希望目标池成为另一次迁移的源(没有链式迁移)。

其中一些 CLI 由 MGR 发出,在这种情况下,我们可能需要更改 MGR 代码以应对故障和/或检测池是否正在迁移并避免发出 CLI。我们可能两者都需要,因为尽管在发出 CLI 之前检查池是否正在迁移可能更有效,但它面临一个竞态条件,即迁移可能在检查和发出 CLI 之间开始。

我们需要查看 UI 中如何报告回填和恢复等事物的进度(可能是通过 HealthCheck?),并考虑如何报告池迁移的进度。我们需要考虑报告进度的正确单位是什么(例如,已迁移对象数占总对象数的比例、已迁移 PG 数占总 PG 数的比例,或者只是一个百分比)。

向后兼容性/软件升级

池迁移需要更改 Ceph 守护程序(MON、OSD 和可能的 MGR)以及发出 I/O 的 Ceph 客户端中的代码。我们不能允许池迁移发生,而其中任何一个正在运行旧代码,因为旧代码不会理解正在发生池迁移。旧客户端将无法将 I/O 指向正确的池、PG 和 OSD,并且 OSD 转发所有这些请求到正确的 OSD 会太昂贵。

Ceph 守护程序和客户端有一组功能位,指示它们支持哪些功能,并且有机制可以设置守护程序所需的最低功能位集以及单独设置客户端所需的最低功能位集。一旦设置,这会阻止低级别守护程序和客户端连接到集群。还有一些机制可以确保一旦设置了最低级别,就不能反转。

池迁移将需要定义一个新的功能位,并使用现有机制来设置守护程序和客户端所需的最低级别。新的池迁移 CLI 需要在未设置最低级别时拒绝开始迁移的尝试。

迁移结束

当迁移完成时,我们将所有对象从池 A 移动到池 B,但是客户端(例如 RBD、CephFs、RGW 等)仍将在其自己的数据结构中嵌入池 A。我们不想强迫所有客户端更新其数据结构以指向新池,因此我们将保留有关池 A 的存根信息,说明它已迁移,并且所有 I/O 现在都应提交给池 B。

OSDMap 中保留一个存根 pg_pool_t 结构很便宜——不会有数千个池,并且为池存储的数据不多。我们需要确保旧池没有与之关联的 PG,我们可以通过将其 PG 数量减少到 0 来实现这一点,并让与 PG 合并时运行的相同代码清理并删除旧 PG。

我们需要考虑这对 UI 界面的影响。虽然在代码中我们从池 A 开始并创建对象并将其迁移到池 B,但从 UI 的角度来看,我们可能希望将其显示为对池 A 的转换并对用户隐藏池 B 的存在。

另一种实现方式是只在 UI 中显示池重定向,因此用户会看到 RBD 映像使用池 A,但随后会发现池 A 已迁移到池 B。如果我们计划将来支持合并池(迁移到非空目标池),这种替代实现可能会更好。

池迁移工作原理演练

发起池迁移

  1. 用户创建一个新池,也许他们使用新标志 --migratefrom 来表示他们想要开始池迁移。

  2. 将迁移作为池创建的一部分开始意味着我们知道池最初是空的。

  3. 除非用户指定 PG 数量,否则我们可以确保新创建的池具有与源池相同的 PG 数量。PG 数量没有要求必须相同,它只是避免了执行迁移,然后随着 PG 数量调整以适应池中最终的对象数量,执行第二次数据复制。

  4. CLI 命令在 OSDMap 中设置 pg_pool_t 结构,以指示池迁移正在开始。我们记录池 A 正在迁移到池 B,并记录我们将开始迁移哪个 PG。如果我们要一次迁移多个 PG,我们可能希望指定一组正在迁移的 PG(例如 0,1,2,3)。集合中的任何 PG 都在迁移。集合中未包含但高于集合中最低值的任何 PG 都假定已完成迁移,集合中未包含但低于集合中最低值的任何 PG 都假定尚未开始迁移。

  5. 我们按相反顺序迁移 PG——例如,如果一个池有 PG 0-15,那么我们将从迁移 PG 15 开始。

  6. MON 将新的 OSDMap 作为新纪元发布。

客户端

  1. 客户端使用 OSDMap 中的 pg_pool_t 结构来确定迁移正在进行中。

  2. 从正在迁移的 PG 范围中,他们可以确定哪些 PG 已迁移,哪些尚未开始迁移,哪些正在迁移过程中。

    1. 如果 I/O 提交给已迁移的 PG,则使用对象哈希和新池来确定将 I/O 请求路由到哪个 PG 和 OSD。

    2. 如果 I/O 提交给尚未开始迁移的 PG,则使用对象哈希和旧池来确定将 I/O 请求路由到哪个 PG 和 OSD。

    3. 如果 I/O 提交给标记为正在迁移的 PG,客户端会检查是否为该 PG 缓存了水印。如果有,则使用它来决定是将请求路由到旧池还是新池。如果没有缓存水印,它会猜测并发送 I/O 到旧池。

  3. 如果 I/O 被错误地路由到错误的池,OSD 将使请求失败,并提供水印更新。客户端需要更新其水印缓存副本并重新提交 I/O。

OSD

  1. OSD 使用 OSDMap 中的 pg_pool_t 结构来确定 PG 是否需要迁移。

  2. 在对等结束时,如果 PG 需要迁移且未执行回填或恢复,则它将 PG 状态设置为 MIGRATION_WAIT,并检查其他 OSD 是否有资源和空闲容量来开始迁移。

  3. 如果一切正常,PG 状态更改为 MIGRATING,将水印设置为 0,并指示调度程序开始调度迁移工作。

  4. 迁移从扫描下一个要迁移的对象范围开始,创建对象 OID 列表。

  5. 然后迁移每个对象,并在迁移对象后更新水印。

    1. 主 OSD 读取对象并将其发送给目标 PG 的主 OSD,然后目标 PG 写入对象。

    2. 如果对象很大,则分阶段完成,目标使用临时对象名称,该名称在写入最后数据时重命名。

    3. 一旦对象被迁移,它就会从源池中删除。

  6. 客户端 I/O 将客户端 I/O 的对象哈希与水印进行比较。如果 I/O 低于水印,则它会失败以重试到新池,并提供当前水印供客户端缓存。

  7. 如果 PG 完成迁移,则它会向 MON 发送消息,告知迁移已完成。

MON

  1. 当 MON 从 OSD 收到迁移完成的消息时,它会更新 pg_pool_t 中的集合,记录 PG 已完成迁移,并且下一个 PG 正在开始迁移。新的 OSDMap 作为新纪元发布。

  2. 因为迁移是按相反顺序调度的,并且对象在迁移发生时被删除,这意味着随着 PG 迁移完成,我们应该有空的 PG,可以通过简单地减少源池的 PG 数量来删除这些 PG。PG 迁移可能不会按开始顺序完成,因此我们可能会有几个空的 PG 闲置,直到另一个 PG 迁移完成才能删除。

  3. 在迁移结束时,没有更多的 PG 可以开始迁移,因此迁移中的 PG 集合会减少。当集合变空时,我们应该也将源池的 PG 数量减少到零,此时迁移完成。MON 可以对 pg_pool_t 状态进行最终更新,以指示迁移已完成。pg_pool_t 结构需要保留,以便客户端知道将所有 I/O 请求指向此池到新池。

  4. 池可以迁移多次,这可能导致保留多个存根 pg_pool_t 结构。我们不希望在提交 I/O 时必须递归地遍历这些存根,因此在迁移结束时,MON 应该尝试将这些重定向减少到单个级别。

测试和测试工具

测试池迁移的目标是

  1. 验证源池中的所有对象都迁移到目标池,并且它们的内容(数据、属性和 OMAP)得以保留。

  2. 验证在迁移期间可以读取尚未迁移的对象、已迁移的对象以及正在迁移中的对象(数据、属性、OMAP)。

  3. 验证在迁移期间可以更新尚未迁移的对象、已迁移的对象以及正在迁移中的对象(创建、删除、写入、更新属性、更新 OMAP)。

  4. 验证错误场景下的池迁移,包括重置和使 OSD 失败。

  5. 验证快照、克隆是否迁移,并且可以在池迁移期间使用。

  6. 验证池迁移的 UI,包括迁移期间对 UI 施加的限制。

  7. 验证多个不同池的并行迁移。验证单个池的多次串行迁移。

  8. 验证带有不可读对象(过多的介质错误加上可能导致副本/EC 池冗余失效的其他故障而未使其脱机)的池迁移。

  9. 验证守护程序(OSD、MON、MGR)和客户端的软件升级/兼容性。

  10. 验证迁移期间的性能影响。

池迁移更改了客户端代码,因此所有修改后的客户端都需要测试。

现有工具,例如 ceph_test_rados,非常适合创建和练习一组对象并执行一些对象的一致性检查。

一个简单的脚本可能更适合创建大量对象,然后验证对象的内容。编写脚本可能更适合测试属性和 OMAP。如果脚本有两个阶段(创建对象和验证对象),则这些阶段可以在不同时间运行(池迁移之前、期间、之后)以测试不同的方面。脚本可以使用 rados 等命令行工具来创建和验证对象,使用伪随机数生成数据模式、属性和 OMAP 数据,然后可以进行验证。脚本需要并行运行许多 rados 命令才能生成像样的 I/O 工作负载。可能已经存在可以执行此操作的脚本,可能可以改编 ceph_test_rados 来执行此操作。

诸如 VDBench 之类的工具可以测试块卷的数据完整性,要么创建数据集然后验证它,要么可以不断创建和更新数据并保留日志,以便在任何时候都可以验证。但是块卷工具只能测试对象数据,不能测试属性或 OMAP。

诸如 FIO 之类的工具最适合进行性能测量。

I/O 序列工具 ceph_test_rados_io_sequence 可能对测试池迁移没有用——它专门用于测试非常少量的对象,并专注于对象内的边界条件(例如 EC 块大小、条带大小)和数据完整性。

目标应该是使用 teuthology 执行大部分池迁移测试(至少是上面列表中的 1 到 5)。应该可以将池迁移作为选项添加到 RADOS 套件中的现有测试中,扩展 thrashOSD 类以包括开始迁移的选项。

由 Ceph 基金会为您呈现

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