注意
本文档适用于 Ceph 的开发版本。
完整的OSD映射版本修剪
对于每个增量osdmap epoch,监视器都会在存储中保留一个完整的osdmap epoch。
虽然这对处理来自客户端的osdmap请求非常有用,允许我们无需从无数增量中重新计算完整的osdmap即可满足他们的请求,但一旦我们开始保留无限数量的osdmap,它也可能成为一种负担。
监视器将尝试在存储中保留有限数量的osdmap。这个数量由 mon_min_osdmap_epochs 定义(且可配置),默认为500个epoch。一般来说,一旦超过此限制,我们将删除较旧的osdmap epoch。
然而,删除osdmap有一些限制。这些限制都在 OSDMonitor::get_trim_to() 中定义。
如果这些条件之一未满足,我们可能会超出 mon_min_osdmap_epochs 定义的范围。如果集群在一段时间内(例如,unclean pgs)未满足修剪标准,监视器可能会开始保留大量osdmap。这可能会对底层键/值存储以及可用磁盘空间造成压力。
缓解此问题的一种方法是停止在磁盘上保留完整的osdmap epoch。我们将不得不按需重建osdmap,或者如果它们最近被服务过,则从缓存中获取它们。我们仍然必须至少保留一个osdmap,并将所有增量应用于存储中保留的最旧的映射epoch或从缓存中获取的更近期的映射之上。虽然这可行,但重建osdmap似乎会消耗大量的CPU(以及潜在的IO)。
此外,这将防止上述问题在未来发生,但对于目前处于不保留osdmap状态的存储来说,这将无济于事。
这就引出了完整的osdmap修剪。
与其不保留完整的osdmap epoch,不如在我们有太多时修剪掉其中一些。
判断我们是否有太多osdmap将由一个可配置选项 mon_osdmap_full_prune_min(默认值:10000)决定。一旦超过此阈值,修剪算法将启动。
我们不会删除所有 mon_osdmap_full_prune_min 个完整的osdmap epoch。相反,我们将在完整的映射序列中打一些洞。默认情况下,自从上次保留的映射以来,每10个映射我们会保留一个完整的osdmap;即,如果我们保留epoch 1,我们也会保留epoch 10,并删除完整的映射epoch 2到9。此间隔的大小可通过 mon_osdmap_full_prune_interval 进行配置。
本质上,我们建议保留大约10%的完整映射,但我们将始终遵守由 mon_min_osdmap_epochs 定义的最小osdmap epoch数量,这些不会用于计算要修剪的最小版本数量。例如,如果我们有磁盘上的版本 [1..50000],我们将允许修剪算法仅对osdmap epoch [1..49500) 进行操作;但是,如果我们有磁盘上的版本 [1..10200],我们将不会修剪,因为算法只会对版本 [1..9700) 进行操作,并且此间隔包含的版本少于 mon_osdmap_full_prune_min 所要求的最小值。
算法
假设我们在存储中有50,000个osdmap epoch,并且我们对所有可配置选项都使用默认值。
-----------------------------------------------------------
|1|2|..|10|11|..|100|..|1000|..|10000|10001|..|49999|50000|
-----------------------------------------------------------
^ first last ^
当满足以下所有约束条件时,我们将进行修剪
版本数量大于
mon_min_osdmap_epochs;first和prune_to之间的版本数量大于(或等于)mon_osdmap_full_prune_min,其中prune_to等于last减去mon_min_osdmap_epochs。
如果这些条件中的任何一个失败,我们将不修剪任何映射。
此外,如果已知我们一直在修剪,但此后我们不再满足上述约束中的至少一个,我们将不会继续修剪。本质上,只有当存储中的epoch数量需要时,我们才会修剪完整的osdmap。
由于修剪会在完整映射序列中创建间隙,我们需要跟踪缺失映射的间隔。我们通过保留一个固定映射的清单来实现这一点——即,一个通过固定而不能被修剪的映射列表。
虽然固定映射不会从存储中移除,但两个连续固定映射之间的映射会被移除;要移除的映射数量将由可配置选项 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_pinned 到 new_pinned 的所有映射,而 new_pinned 不外乎是 last_pinned 加上 mon_osdmap_full_prune_interval。我们知道这两个值 last_pinned 和 new_pinned 之间的所有映射都可以移除,前提是 new_pinned 已经被固定。
一旦两个初始先决条件中的一个未满足,或者我们不满足对算法正确性没有影响的两个附加条件,算法就会停止执行
如果我们无法创建一个与
mon_osdmap_full_prune_interval正确对齐且小于last_pruned的新修剪间隔,我们将停止。除了允许我们保持间隔具有预期的尺寸,并防止最终会发生的小的、不规则的间隔(例如,修剪在几次迭代中继续,每次删除一两个或三个映射)之外,没有特定的技术原因来强制执行此要求。一旦我们知道我们已经修剪了超过一定数量的映射,我们将停止。该值由
mon_osdmap_full_prune_txsize定义,并确保我们不会花费无限数量的周期来修剪映射。我们不会严格遵守此值(删除成本不高),但我们会努力遵守它。
我们可以一次性完成移除,但我们不知道那需要多长时间。因此,我们将执行多次迭代,每次迭代最多移除 mon_osdmap_full_prune_txsize 个osdmap。
最后,我们的磁盘映射序列将类似于
------------------------------------------
|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清单:一旦监视器修剪osdmap并且存储中最早的可用epoch大于我们上次修剪的映射,osdmap清单将不再需要。
与 OSDMonitor::get_trim_to() 相同的条件,强制监视器保留大量osdmap,从而要求我们进行修剪,最终可能会改变并允许监视器移除一些最旧的映射。
映射修剪
如果监视器修剪映射,我们必须调整osdmap清单以反映我们的修剪状态,或者如果保留它不再有意义,则完全删除清单。例如,采用前面的映射序列,但假设我们没有完成修剪所有映射。
-------------------------------------------------------------
|1|10|20|30|..|490|500|501|502|..|49500|49501|..|49999|50000|
-------------------------------------------------------------
^ first ^ pinned.last() last ^
pinned = {1, 10, 20, ..., 490, 500}
现在假设监视器将修剪到epoch 501。这意味着删除早于epoch 501的所有映射,并将 first_committed 指针更新为 501。鉴于删除所有这些映射会使我们现有的修剪工作无效,我们可以认为修剪已完成,并丢弃osdmap清单。这样做也简化了新的修剪开始,如果我们在从存储刷新状态后满足所有起始条件。
然后我们将拥有以下映射序列
---------------------------------------
|501|502|..|49500|49501|..|49999|50000|
---------------------------------------
^ first last ^
然而,想象一个稍微复杂一点的场景:监视器将修剪到epoch 491。在这种情况下,epoch 491先前已从存储中修剪。
鉴于我们始终需要存储中最旧的已知映射,在我们修剪之前,我们将不得不检查该映射是否在修剪间隔中(即,如果该映射epoch属于 [ pinned.first()..pinned.last() ))。如果是这样,我们需要检查这是否是一个固定映射,在这种情况下,我们除了从清单的固定列表中删除较低的epoch之外,不需要担心太多。另一方面,如果修剪到的映射不是固定映射,我们需要重建该映射并将其固定,然后才能删除早于该映射epoch的固定映射。
在这种情况下,我们将得到以下序列:
-----------------------------------------------
|491|500|501|502|..|49500|49501|..|49999|50000|
-----------------------------------------------
^ ^- pinned.last() last ^
`- first
还有一个我们应该提到的边缘情况。考虑我们要修剪到epoch 499,这是最后一个修剪的epoch。
就像上面的场景一样,我们将把osdmap epoch 499写入存储;但我们应该如何处理固定映射和修剪呢?
最简单的解决方案是丢弃osdmap清单。毕竟,鉴于我们修剪到了最后一个修剪的映射,并且我们正在重建此映射,我们可以保证所有大于e 499的映射都是连续的(因为我们没有修剪任何它们)。本质上,在这种情况下丢弃osdmap清单与我们修剪超过最后一个修剪的epoch本质上相同:如果满足所需条件,我们可以稍后再次修剪。
至此,我们已经全面探讨了完整的osdmap修剪。本文档后面可以找到有关整个算法(从修剪到修剪)的详细 REQUIREMENTS, CONDITIONS & INVARIANTS。此外,下一节将详细介绍几个额外的检查,以保证我们的配置选项的合理性。请享用。
配置选项合理性检查
我们在修剪之前执行额外的检查,以确保所有涉及的配置选项都是合理的
如果
mon_osdmap_full_prune_interval为零,我们将不修剪;我们需要一个大于一的正数才能修剪映射。如果间隔为一,我们实际上不会修剪任何映射,因为固定映射之间的间隔本质上是一个epoch。这意味着固定映射之间将有零个映射,因此永远不会修剪任何映射。如果
mon_osdmap_full_prune_min为零,我们将不修剪;我们需要一个大于零的正值,以便我们知道应该修剪的阈值。我们不想猜测。如果
mon_osdmap_full_prune_interval大于mon_osdmap_full_prune_min,我们将不修剪,因为无法确定合适的修剪间隔。如果
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) ) )