注意

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

完整的OSDMAP版本修剪

对于每个增量osdmap周期,监控器将在存储中保留一个完整的osdmap周期。

当从客户端服务osdmap请求时,这很棒,允许我们无需从众多增量中重新计算完整osdmap来满足他们的请求,但一旦我们开始保留无限数量的osdmaps,它也可能成为一个负担。

监控器将尝试在存储中保留有限数量的osdmaps。这个数字是通过mon_min_osdmap_epochs定义(并且可配置)的,默认为500个周期。一般来说,一旦超过这个限制,我们将删除较旧的osdmap周期。

然而,删除osdmaps有一些约束。这些都在OSDMonitor::get_trim_to().

中定义。如果这些条件之一不满足,我们可能会超过mon_min_osdmap_epochs所定义的界限。如果集群一段时间内(例如,不干净的pgs)不满足修剪标准,监控器可能会开始保留大量的osdmaps。这可能会给底层键值存储以及可用磁盘空间带来压力。

解决这个问题的方法之一是停止在磁盘上保留完整的osdmap周期。我们将不得不按需重建osdmaps,或者如果它们最近已被服务,则从缓存中获取它们。我们仍然必须保留至少一个osdmap,并将所有增量应用于存储中保留的最老地图周期或从缓存中获取的更近的地图周期。虽然这是可行的,但似乎会花费大量的cpu(以及潜在的IO)来重建osdmaps。

此外,这会阻止上述问题在未来发生,但不会对当前处于真正受益于不保留osdmaps状态的存储做任何事。

这就引出了全量osdmap修剪。

与不保留全量osdmap周期相反,当我们有太多时,我们将修剪其中的一些。

决定我们是否有太多将取决于可配置选项mon_osdmap_full_prune_min(默认:10000)。一旦超过这个阈值,修剪算法将被启动。

然而,我们不会删除所有mon_osdmap_full_prune_min全量osdmap周期。相反,我们将对全量地图序列的一些孔洞。默认情况下,我们将保留上次保留的每个10个地图中的一个全量osdmap;也就是说,如果我们保留周期1,我们还将保留周期10,并删除全量地图周期2到9。这个间隔的大小可以通过mon_osdmap_full_prune_interval.

配置。本质上,我们提议保留约10%的全量地图,但我们始终会尊重由mon_min_osdmap_epochs定义的最小osdmap周期数,这些不会用于修剪的最小版本计数。例如,如果我们有磁盘版本[1..50000],我们将允许修剪算法仅对osdmap周期[1..49500)操作;但是,如果我们有磁盘版本[1..10200],我们将不会修剪,因为算法将仅对版本[1..9700)操作,这个间隔包含的版本少于mon_osdmap_full_prune_min.

算法

假设我们在存储中有50,000个osdmap周期,并且我们使用所有可配置选项的默认值。

-----------------------------------------------------------
|1|2|..|10|11|..|100|..|1000|..|10000|10001|..|49999|50000|
-----------------------------------------------------------
 ^ first                                            last ^

当所有以下约束都满足时,我们将修剪:

  1. 版本数量大于mon_min_osdmap_epochs;

  2. 版本数量在firstprune_to之间大于(或mon_osdmap_full_prune_min,其中包含prune_to等于last减去mon_min_osdmap_epochs.

如果这些条件中的任何一个不满足,我们将firefly 发布。Firefly 将延迟至少另一个冲刺,以便我们可以对新代码进行一些操作经验,并进行一些额外的测试,然后再承诺长期支持。修剪任何地图。

此外,如果已知我们已经修剪,但自那时起我们不再满足至少一个上述约束,我们将不会继续修剪。本质上,我们仅在存储中的周期数值得时才修剪全量osdmaps。

由于修剪将在全量地图序列中创建间隙,我们需要跟踪缺失地图的间隔。我们通过保留一个固定地图清单来实现这一点——即,一个通过固定而不会被修剪的地图列表。

虽然固定地图不会从存储中删除,但两个连续固定地图之间的地图将被删除;要删除的地图数量由可配置选项mon_osdmap_full_prune_interval决定。该算法努力通过此选项定义的尽可能多的地图来保持固定地图彼此分开,但在角落情况下,它可能允许较小的间隔。此外,由于这是一个在修剪迭代时读取的可配置选项,因此如果用户更改此配置选项,此间隔可能会改变。

固定地图是惰性执行的:当我们删除地图时,我们将固定地图。这使我们能够在修剪过程中更改修剪间隔,但也大大简化了算法以及我们在清单中需要保留的信息。下面我们展示了算法的简化版本::

manifest.pin(first)
last_to_prune = last - mon_min_osdmap_epochs

while manifest.get_last_pinned() + prune_interval < last_to_prune AND
      last_to_prune - first > mon_min_osdmap_epochs AND
      last_to_prune - first > mon_osdmap_full_prune_min AND
      num_pruned < mon_osdmap_full_prune_txsize:

  last_pinned = manifest.get_last_pinned()
  new_pinned = last_pinned + prune_interval
  manifest.pin(new_pinned)
  for e in (last_pinned .. new_pinned):
    store.erase(e)
    ++num_pruned

本质上,该算法确保存储中的第一个版本是始终固定的。毕竟,在重建地图时,我们需要一个起点,而我们不能简单地删除我们拥有的最早地图;否则我们将无法重建第一个被修剪间隔的地图。

一旦我们至少有一个固定地图,算法的每次迭代都可以简单地基于清单的最后一个固定地图(我们可以通过读取清单的固定地图列表的尾部元素来获得)。

接下来,我们需要确定要删除的地图间隔:从last_pinnednew_pinned的所有地图,这不过是last_pinned加上mon_osdmap_full_prune_interval。我们知道在这两个值之间的所有地图last_pinnednew_pinned可以被删除,考虑到new_pinned已被固定。

一旦两个初始先决条件中的任何一个不满足,或者如果我们不满足两个额外的条件,这些条件对算法的正确性没有影响,算法将停止执行:

  1. 如果我们无法创建一个与mon_osdmap_full_prune_interval对齐且低于last_pruned的新修剪间隔,我们将停止。除了允许我们保持具有预期大小的间隔,并防止最终必然会发生的小型不规则间隔(例如,在几次迭代过程中继续修剪,每次删除一个或两个或三个地图),我们没有特别的技术原因来执行此要求。

  2. 一旦我们知道我们已经修剪了超过一定数量的地图,我们将停止。此值由mon_osdmap_full_prune_txsize定义,并确保我们不会花费无限数量的周期来修剪地图。我们没有严格地执行此值(删除成本不高),但我们努力尊重它。

我们可以一次性完成删除,但我们不知道这将需要多长时间。因此,我们将执行几次迭代,每次最多删除mon_osdmap_full_prune_txsizeosdmaps。

最后,我们磁盘上的地图序列将类似于:

------------------------------------------
|1|10|20|30|..|49500|49501|..|49999|50000|
------------------------------------------
 ^ first                           last ^

由于我们不会一次性修剪所有版本,我们需要保留关于我们的修剪进度的状态。考虑到这一点,我们创建了一个数据结构,osdmap_manifest_t,它保存了固定地图的集合::

struct osdmap_manifest_t:
    set<version_t> pinned;

由于我们只是在修剪时固定地图,我们不需要跟踪有关最后一个修剪版本的附加状态。事实上,我们知道在两个连续固定地图之间,我们已经修剪了所有中间地图。

然而,人们可能会问,如果我们,例如,监控器崩溃,我们如何才能确保我们修剪了所有中间地图。为了确保我们受到此类事件的保护,我们总是在删除地图的同一事务中将osdmap清单写入磁盘。这样,如果监控器崩溃,我们有保证将读取最新版本的清单:要么包含新固定的地图,这意味着我们也修剪了中间地图;要么我们将找到osdmap清单的先前版本,它将不包含我们在崩溃时正在固定的地图,因为我们在写入更新osdmap清单的事务中不会应用(以及删除地图)。

osdmap清单将在每次我们修剪时写入存储,并更新固定地图列表。它是在实际修剪地图的事务中写入的,因此我们保证清单始终是最新的。作为此标准的后果,我们第一次写入osdmap清单是我们第一次修剪。如果不存在osdmap清单,我们可以确定我们没有保留修剪地图间隔。

我们将依赖清单来确定我们是否修剪了地图间隔。理论上,这始终将是磁盘上的osdmap清单,但我们确保每次我们从paxos更新时都读取磁盘上的osdmap清单;这样我们始终确保拥有最新的内存osdmap清单。

修剪地图完成后,我们将清单保留在存储中,以便我们能够轻松找到哪些地图已被固定(而不是检查存储直到找到地图)。这还有一个好处,那就是允许我们快速确定下一个我们需要修剪的间隔(即,最后一个固定的加上修剪间隔)。但这并不意味着我们将永远保留osdmap清单:一旦监控器修剪osdmaps并且存储中最早的可用周期大于我们修剪的最后一张地图,osdmap清单将不再需要。

导致监控器保留大量osdmaps并需要我们修剪的相同条件最终可能会改变,并允许监控器删除其中一些最老的地图。OSDMonitor::get_trim_to() that force the monitor to keep a lot of osdmaps, thus requiring us to prune, may eventually change and allow the monitor to remove some of its oldest maps.

地图修剪

如果监控器修剪地图,我们必须调整osdmap清单以反映我们的修剪状态,或者如果不再有保留它的意义,则完全删除清单。例如,取之前的地图序列,但让我们假设我们没有完成修剪所有地图:

-------------------------------------------------------------
|1|10|20|30|..|490|500|501|502|..|49500|49501|..|49999|50000|
-------------------------------------------------------------
 ^ first            ^ pinned.last()                   last ^

pinned = {1, 10, 20, ..., 490, 500}

现在让我们假设监控器将修剪到周期501。这意味着删除周期501之前的所有地图,并更新first_committed指针到501。考虑到删除所有这些地图将使我们现有的修剪工作无效,我们可以认为我们的修剪已经完成并放弃我们的osdmap清单。这样做也简化了开始新的修剪,如果我们刷新状态后从存储中满足所有起始条件。

我们将拥有以下地图序列:

---------------------------------------
|501|502|..|49500|49501|..|49999|50000|
---------------------------------------
 ^ first                        last ^

然而,想象一个稍微复杂的情况:监控器将修剪到周期491。在这种情况下,周期491之前已被从存储中修剪。

考虑到我们始终需要存储中最老的已知地图,在我们修剪之前,我们将必须检查该地图是否在修剪间隔中(即,如果所述地图周期属于[ pinned.first()..pinned.last() ))。如果是,我们需要检查这是否是一个固定地图,如果是,除了从清单的固定列表中删除较低周期之外,我们不太担心。另一方面,如果被修剪的地图不是固定地图,我们需要重建该地图并将其固定,然后我们才会删除该地图之前的固定地图。

在这种情况下,我们会得到以下序列::

-----------------------------------------------
|491|500|501|502|..|49500|49501|..|49999|50000|
-----------------------------------------------
 ^   ^- pinned.last()                   last ^
 `- first

还有另一个边缘情况我们应该提到。考虑我们将修剪到周期499,这是最后一个被修剪的周期。

类似于上面的场景,我们会将osdmap周期499写入存储;但我们应该如何处理固定地图和修剪?

最简单的解决方案是丢弃osdmap清单。毕竟,考虑到我们修剪到最后一个被修剪的地图,并且我们正在重建这张地图,我们可以保证所有大于e 499的地图都是连续的(因为我们没有修剪任何地图)。本质上,在这种情况下丢弃osdmap清单与如果我们修剪到最后一个被修剪的周期相同:如果我们满足所需条件,我们可以稍后修剪。

而且,至此,我们已经完全深入了全量osdmap修剪。本文档的后面可以找到有关整个算法从修剪到修剪的详细要求、条件和不变量。此外,下一节详细介绍了几个额外的检查,以确保我们的配置选项的合理性。享受。

配置选项合理性检查

在修剪之前,我们执行额外的检查,以确保所有涉及的配置选项都是合理的:

  1. 如果mon_osdmap_full_prune_interval如果是零,我们将不修剪;我们需要一个实际的正数,大于一,才能修剪地图。如果间隔是一,我们实际上不会修剪任何地图,因为固定地图之间的间隔本质上是一个周期。这意味着我们在固定地图之间会有零张地图,因此永远不会修剪地图。

  2. 如果mon_osdmap_full_prune_min如果是零,我们将不修剪;我们需要一个正数,大于零,以便我们知道应该修剪的阈值。我们不想猜测。

  3. 如果mon_osdmap_full_prune_interval is greater than mon_osdmap_full_prune_min如果是零,我们将不修剪,因为它不可能确定一个合适的修剪间隔。

  4. 如果mon_osdmap_full_prune_txsize如果低于mon_osdmap_full_prune_interval,我们将不修剪;我们需要一个txsize值至少等于interval,并且(取决于后者的值)理想情况下更高。

要求、条件和不变量

要求

  • 共识中的所有监控器都需要支持修剪。

  • 一旦启用修剪,不支持修剪的监控器将不允许加入共识,也不允许同步。

  • 删除osdmap清单会导致禁用修剪功能共识要求。这意味着不支持修剪的监控器将被允许同步并加入共识,前提是他们支持任何其他所需的功能。

条件和不变量

  • 修剪从未发生过,或者我们已经修剪到其先前间隔:

    invariant: first_committed > 1
    
    condition: pinned.empty() AND !store.exists(manifest)
    
  • 修剪至少发生一次:

    invariant: first_committed > 0
    invariant: !pinned.empty())
    invariant: pinned.first() == first_committed
    invariant: pinned.last() < last_committed
    
      precond: pinned.last() < prune_to AND
               pinned.last() + prune_interval < prune_to
    
     postcond: pinned.size() > old_pinned.size() AND
               (for each v in [pinned.first()..pinned.last()]:
                 if pinned.count(v) > 0: store.exists_full(v)
                 else: !store.exists_full(v)
               )
    
  • 修剪完成:

    invariant: first_committed > 0
    invariant: !pinned.empty()
    invariant: pinned.first() == first_committed
    invariant: pinned.last() < last_committed
    
    condition: pinned.last() == prune_to OR
               pinned.last() + prune_interval < prune_to
    
  • 修剪间隔可以被修剪:

    precond:   OSDMonitor::get_trim_to() > 0
    
    condition: !pinned.empty()
    
    invariant: pinned.first() == first_committed
    invariant: pinned.last() < last_committed
    invariant: pinned.first() <= OSDMonitor::get_trim_to()
    invariant: pinned.last() >= OSDMonitor::get_trim_to()
    
  • 修剪修剪间隔:

    invariant: !pinned.empty()
    invariant: pinned.first() == first_committed
    invariant: pinned.last() < last_committed
    invariant: pinned.first() <= OSDMonitor::get_trim_to()
    invariant: pinned.last() >= OSDMonitor::get_trim_to()
    
    postcond:  pinned.empty() OR
               (pinned.first() == OSDMonitor::get_trim_to() AND
                pinned.last() > pinned.first() AND
                (for each v in [0..pinned.first()]:
                  !store.exists(v) AND
                  !store.exists_full(v)
                ) AND
                (for each m in [pinned.first()..pinned.last()]:
                  if pinned.count(m) > 0: store.exists_full(m)
                  else: !store.exists_full(m) AND store.exists(m)
                )
               )
    postcond:  !pinned.empty() OR
               (!store.exists(manifest) AND
                (for each v in [pinned.first()..pinned.last()]:
                  !store.exists(v) AND
                  !store.exists_full(v)
                )
               )
    

由 Ceph 基金会带给您

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