注意

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

清单

简介

控制。如../deduplication.rst,向 RADOS 添加透明重定向机制将使分层解决方案比 RADOS 目前的“缓存/分层”更强大。

请参阅../deduplication.rst

从高层次来看,每个对象都嵌入了一部分元数据,可以映射对象数据有效负载的子集到(引用计数的)其他池中的对象。object_info_t本文档旨在详细说明:

This document exists to detail:

  1. 文件清单数据结构

  2. 用于操作文件清单的 RADOS 操作。

  3. 状态和计划

预期使用模型

RBD

对于 RBD,主要目标是让 OSD 内部代理或集群外部代理能够透明地在构成 4MB 的 extent 之间移动部分内容,这些 extent 之间在去重池和热基础池之间。

因此,RBD 操作(包括类操作和快照)必须无论对象的当前状态如何,都具有相同可观察的结果。

此外,分层/去重操作必须与 RBD 操作交错进行,而不会改变结果。

因此,这是我预期的分层代理执行基本操作的草图:

  • 将冷 RBD 数据块降级到慢速池:

    1. 读取对象,注意当前 user_version。

    2. 在内存中运行 CDC 实现来为对象生成指纹。

    3. 使用 CAS 类将每个结果 extent 写入冷池中的对象。

    4. 提交操作到基础池:

      • ASSERT_VER使用从读取中获取的用户版本,如果对象自读取以来已发生变异,则失败。

      • SET_CHUNK为每个 extent 提交操作到基础池中的相应对象。

      • EVICT_CHUNK为每个 extent 释放基础池中的空间。MISSING.

    RBD 用户应该看到降级之前的或降级之后的状态。

    注意,在 3 和 4 之间,我们可能会泄漏引用,因此需要定期清理以验证引用计数。

  • 将冷 RBD 数据块提升到快速池。

    1. 提交TIER_PROMOTE

对于克隆,所有上述内容都相同,只是初始读取需要一个LIST_SNAPS来确定哪些克隆存在,并且PROMOTESET_CHUNK/EVICT操作需要在操作中包括cloneid.

RadosGW

对于读取,RADOS 网关 (RGW) 可以像 RBD 一样操作,依赖于 OSD 中的文件清单机制来隐藏正在去重的对象或存在于基础池中的对象的区别

对于写入,RGW 可以像 RBD 一样操作,但可以选择在写入之前进行指纹识别。在这种情况下,它可以立即将目标对象写入 CAS 池,然后原子性地写入具有相应 chunks 的对象。

状态和未来工作

目前,文件清单数据结构的初始版本以及 IO 路径支持和 rados 控制操作都存在。本节旨在概述下一步工作。

从高层次来看,我们的未来工作计划是:

  • 清理:解决下一节中概述的立即不一致和不足之处。

  • 测试:Rados 严重依赖于 teuthology 失败测试来验证缓存/分层等功能。我们需要相应的测试来支持文件清单操作。

  • 快照:我们希望在 rados 快照系统的级别以下去重克隆的部分。因此,以下 rados 操作需要扩展以正确地在克隆上工作(例如:我们应该能够在克隆上调用SET_CHUNK清除基础池中的相应 extent,并正确维护 OSD 元数据)。

  • 缓存/分层:最终,我们希望能够弃用现有的缓存/分层实现,但要做到这一点,我们需要确保能够解决相同的用例。

清理

现有实现有一些需要清理的地方:

  • SET_REDIRECT: 如果对象不存在,则应创建该对象,否则无法作为重定向原子地创建对象。

  • SET_CHUNK:

    • 看起来会触发一个新的克隆,因为 user_modify 在do_osd_ops中被设置。这可能不是理想的,请参阅下文的快照部分,了解如何通常将这些操作与快照混合。至少,SET_CHUNK可能不应该设置 user_modify。

    • 看起来假设对象的相应部分不存在(设置FLAG_MISSING但没有检查对象中是否已存在相应的 extent。应始终保持 extent 清洁。

    • 看起来如果未分块,则无条件清除文件清单,这可能是不正确的。如果它是REDIRECT

      case CEPH_OSD_OP_SET_CHUNK:
        if (oi.manifest.is_redirect()) {
          result = -EINVAL;
          goto fail;
        }
      
  • TIER_PROMOTE:

    • SET_REDIRECT清除对象的内容。0af3d1: 看起来会将它们复制回去,但不会取消重定向或清除引用。这违反了重定向对象在基础池中应为空的约束。特别是,只要重定向被设置,看起来所有操作都将被代理,即使是在提升之后,这会破坏目的。我们确实想要PROMOTE appears to copy them back in, but does not unset the redirect or clear the reference. This violates the invariant that a redirect object should be empty in the base pool. In particular, as long as the redirect is set, it appears that all operations will be proxied even after the promote defeating the purpose. We do want PROMOTE能够原子地用实际对象替换重定向,所以解决方案是在提升结束时清除重定向。

    • 对于分块文件清单,看起来在提升之前会刷新。提升通常用于准备对象以进行低延迟读取和写入,因此,唯一的效果应该是将任何MISSINGextent 读取到基础池中。不应执行刷新。

  • 高层次:

    • 看起来FLAG_DIRTY不应用于指向去重 extent 的 extent。将变异的 extent 写回去重池需要写入一个新对象,因为之前的对象不能被修改,就像它还没有被去重一样。因此,我们应始终丢弃引用并删除文件清单指针。

    • 目前还没有办法“驱逐”一个对象区域。随着对SET_CHUNK的更改,始终保留现有对象区域,我们需要一个EVICT_CHUNK操作来删除 extent。

测试

我们非常依赖于随机失败测试。因此,我们需要将测试扩展到包括去重/文件清单支持。以下是几个触点:

  • 类似于qa/suites/rados/thrash/workloads/cache-snaps.yaml

    的 Thrasher 测试。当然,该测试测试现有的缓存/分层机制。向该目录添加其他文件,这些文件设置去重池。添加对ceph_test_rados (src/test/osd/TestRados*).

  • RBD 测试的支持。

    添加一个测试,该测试在 RBD 工作负载与盲提升/驱逐操作并发运行时运行。

  • RGW

    添加一个测试,该测试在 rgw 工作负载与盲提升/驱逐操作并发运行时运行。

快照

基本上,我们需要能够操作克隆的文件清单状态,因为我们希望能够动态提升、刷新(如果克隆创建时的状态是脏的)以及从克隆中驱逐 extent。

因此,计划是允许每个克隆的object_manifest_t独立。以下是高层次任务的未完成列表:

  • 修改 op 处理管道以允许SET_CHUNK, EVICT_CHUNK直接在克隆上操作。

  • 确保恢复检查对象_manifest 之前尝试使用 clone_range 中的重叠。ReplicatedBackend::calc_*_subsets是两个可能需要修改的方法。

请参阅snaps.rst关于librados快照系统和 OSD 支持细节的概述。我想指出我们可能想要利用的一个特定数据结构。

dedup-tool 需要更新以使用LIST_SNAPS作为泄漏检测的一部分来发现克隆。

一个重要的问题是,我们如何处理许多克隆经常在相同的偏移量处引用相同的底层 chunks。特别是,make_writeable通常会创建一个共享相同object_manifest_t引用的克隆,除了在该事务中修改的 extent 之外。作为该事务提交的一部分提交的元数据必须映射到相同的引用计数,否则我们必须首先增加底层对象的引用计数(或者冒着引用一个已死亡对象的风险)。因此,我们引入一个简单的约定:连续共享相同偏移量引用的克隆共享相同的引用计数。这意味着,调用make_writeable的写入可能会减少引用计数,但不会增加它们。这会对删除克隆产生一些影响。考虑以下序列

write foo [0, 1024)
flush foo ->
  head: [0, 512) aaa, [512, 1024) bbb
  refcount(aaa)=1, refcount(bbb)=1
snapshot 10
write foo [0, 512) ->
  head:               [512, 1024) bbb
  10  : [0, 512) aaa, [512, 1024) bbb
  refcount(aaa)=1, refcount(bbb)=1
flush foo ->
  head: [0, 512) ccc, [512, 1024) bbb
  10  : [0, 512) aaa, [512, 1024) bbb
  refcount(aaa)=1, refcount(bbb)=1, refcount(ccc)=1
snapshot 20
write foo [0, 512) (same contents as the original write)
  head:               [512, 1024) bbb
  20  : [0, 512) ccc, [512, 1024) bbb
  10  : [0, 512) aaa, [512, 1024) bbb
  refcount(aaa)=?, refcount(bbb)=1
flush foo
  head: [0, 512) aaa, [512, 1024) bbb
  20  : [0, 512) ccc, [512, 1024) bbb
  10  : [0, 512) aaa, [512, 1024) bbb
  refcount(aaa)=?, refcount(bbb)=1, refcount(ccc)=1

e3b18a: 在结束时应该是多少引用计数?根据我们上面的规则,它应该是aaa be at the end? By our above rule, it should be 2,因为这两个`aaa`引用不是连续的。但是,考虑删除克隆20

initial:
  head: [0, 512) aaa, [512, 1024) bbb
  20  : [0, 512) ccc, [512, 1024) bbb
  10  : [0, 512) aaa, [512, 1024) bbb
  refcount(aaa)=2, refcount(bbb)=1, refcount(ccc)=1
trim 20
  head: [0, 512) aaa, [512, 1024) bbb
  10  : [0, 512) aaa, [512, 1024) bbb
  refcount(aaa)=?, refcount(bbb)=1, refcount(ccc)=0

在这一点上,我们的规则规定refcount(aaa)1。这意味着删除20需要检查两侧克隆持有的引用,然后匹配。

请参阅osd_types.h:object_manifest_t::calc_refs_to_drop_on_removal用于实现此规则的逻辑。

这看起来很复杂,但它让我们获得了两个宝贵的属性:

  1. make_writeable 引用的引用计数变化不会阻塞增加引用

  2. 我们不需要加载object_manifest_t来确定如何处理删除每个克隆——只需立即之前和之后的克隆。

所有克隆操作在添加或删除引用时都需要考虑相邻chunk_maps

数据结构

每个 RADOS 对象都包含一个object_manifest_t嵌入其中object_info_t的远程文件系统osd_types.h):

struct object_manifest_t {
        enum {
                TYPE_NONE = 0,
                TYPE_REDIRECT = 1,
                TYPE_CHUNKED = 2,
        };
        uint8_t type;  // redirect, chunked, ...
        hobject_t redirect_target;
        std::map<uint64_t, chunk_info_t> chunk_map;
}

The type枚举反映了对象可能处于的三种状态:

  1. TYPE_NONE: 普通的 RADOS 对象

  2. TYPE_REDIRECT: 对象有效负载由redirect_target

  3. TYPE_CHUNKED: object payload is distributed among objects with size and offset specified by the ``chunk_map. chunk_map指定的单个对象支持chunk_info_t如下所示,也指定了length,目标OID, and flags.

struct chunk_info_t {
  typedef enum {
    FLAG_DIRTY = 1,
    FLAG_MISSING = 2,
    FLAG_HAS_REFERENCE = 4,
    FLAG_HAS_FINGERPRINT = 8,
  } cflag_t;
  uint32_t offset;
  uint32_t length;
  hobject_t oid;
  cflag_t flags;   // FLAG_*

FLAG_DIRTY在此时间可以发生,如果写入具有指纹的 extent。这应该更改为丢弃指纹。

请求处理

类似于缓存/分层,初始触点是maybe_handle_manifest_detail.

对于下面列出的文件清单操作,我们返回NOOP并继续在do_osd_ops.

内部进行专用处理。oi.size > 0指示它存在?),我们代理读取和写入。

对于TYPE_CHUNKED,如果can_proxy_chunked_read上的读取(基本上,所有 ops 都是读取object_manifest_t chunk_map中的 extent),我们将请求代理到那些对象。

RADOS接口

要设置去重,必须配置两个池。一个将作为基础池,另一个将作为块池。基础池需要使用以下选项进行配置。fingerprint_algorithm option as follows.

ceph osd pool set $BASE_POOL fingerprint_algorithm sha1|sha256|sha512
--yes-i-really-mean-it

创建对象

rados -p base_pool put foo ./foo
rados -p chunk_pool put foo-chunk ./foo-chunk

创建一个文件清单对象

rados -p base_pool set-chunk foo $START_OFFSET $END_OFFSET --target-pool chunk_pool foo-chunk $START_OFFSET --with-reference

操作:

  • set-redirect

    base_object。Cephadm 还支持使用base_pooltarget_object。Cephadm 还支持使用target_pool之间设置重定向。target_object.

    void set_redirect(const std::string& tgt_obj, const IoCtx& tgt_ioctx,
                  uint64_t tgt_version, int flag = 0);
    
    rados -p base_pool set-redirect <base_object> --target-pool <target_pool>
     <target_object>
    

    返回ENOENT链接起来如果对象不存在(TODO:为什么?)EINVAL如果对象已经是重定向。

    作为操作的一部分,获取目标引用,如果行为集重置并且客户端在获取引用和记录重定向之间死亡,可能会泄漏引用。

    截断对象,清除 omap,并清除 xattrs 作为副作用。

    do_osd_ops的顶部,不设置 user_modify。

    此操作不是用户变异,并且不会触发创建克隆。

    有两个set_redirect:

    1. 将所有操作重定向到目标对象(类似于代理)

    2. tier_promote被调用时缓存(此时重定向将被清除)。

  • set-chunk

    Set the chunk-offsetsource_object中创建一个链接,将其与target_object.

    void set_chunk(uint64_t src_offset, uint64_t src_length, const IoCtx& tgt_ioctx,
               std::string tgt_oid, uint64_t tgt_offset, int flag = 0);
    
    rados -p base_pool set-chunk <source_object> <offset> <length> --target-pool
     <caspool> <target_object> <target-offset>
    

    返回ENOENT链接起来如果对象不存在(TODO:为什么?)EINVAL如果对象已经是重定向。EINVAL如果参数缓冲区格式不正确。ENOTSUPP如果现有映射的 chunks 与新的 chunk 映射重叠。

    作为操作的一部分,获取目标的引用,如果行为集重置并且客户端在获取引用和记录重定向之间死亡,可能会泄漏引用。

    截断对象,清除 omap,并清除 xattrs 作为副作用。

    此操作不是用户变异,并且不会触发创建克隆。

    TODO:SET_CHUNK看起来如果未分块,则无条件清除文件清单。

    if (!oi.manifest.is_chunked()) {
      oi.manifest.clear();
    }
    
  • evict-chunk

    从对象中清除 extent,只留下它和target_object.

    void evict_chunk(
      uint64_t offset, uint64_t length, int flag = 0);
    
    rados -p base_pool evict-chunk <offset> <length> <object>
    

    返回EINVAL之间的文件清单链接如果 extent 不存在于文件清单中。

    注意:这还不存在。

  • tier-promote

    提升对象,确保后续读取和写入将是本地

    void tier_promote();
    
    rados -p base_pool tier-promote <obj-name>
    

    返回ENOENT如果对象不存在

    对于重定向文件清单,将数据复制到头部。

    TODO: 在重定向对象上提升需要清除重定向。

    对于分块文件清单,将所有缺失的 extent 读取到基础池中,后续的读取和写入将从基础池提供服务。

    实现说明:对于分块文件清单,调用start_copy在自身上。结果copy_get操作将发出读取,然后由正常的文件清单读取机制重定向。

    不设置user_modify标志指示 cephadm 移除主机以及 CRUSH 桶。

    未来工作将涉及添加支持以指定一个clone_id.

  • unset-manifest

    在具有文件清单的对象中清除文件清单信息。

    void unset_manifest();
    
    rados -p base_pool unset-manifest <obj-name>
    

    清除文件清单 chunks 或重定向。懒惰地释放引用,可能会泄漏。

    do_osd_ops看起来不包括它user_modify=false ignorelist,因此将触发快照。注意,即使对于重定向也是如此SET_REDIRECT也不翻转user_modify。这应该被修复——unset-manifest不应该是一个user_modify.

  • tier-flush

    将具有 chunks 的对象刷新到块池。

    void tier_flush();
    
    rados -p base_pool tier-flush <obj-name>
    

    包括在user_modify=false ignorelist中,不会触发克隆。

    不会驱逐 extent。

ceph-dedup-tool

ceph-dedup-tool有两个功能:找到去重分块的最佳 chunk 偏移量和修复引用计数(见./refcount.rst).

  • 找到最佳 chunk 偏移量

    1. 固定 chunk

    要找到固定 chunk 长度,您需要多次运行以下命令,同时更改chunk_size.

    ceph-dedup-tool --op estimate --pool $POOL --chunk-size chunk_size
      --chunk-algorithm fixed --fingerprint-algorithm sha1|sha256|sha512
    
    1. Rabin chunk(Rabin-Karp 算法)

    Rabin-Karp 是一种基于滚动哈希的字符串搜索算法。但是,滚动哈希不足以进行去重,因为我们不知道 chunk 边界。因此,我们需要使用滚动哈希进行基于内容的切片,以进行内容定义的 chunking。

    希望使用去重的用户需要找到理想的 chunk 偏移量。要找到理想的 chunk 偏移量,用户应该通过ceph-dedup-tool发现其数据工作负载的最佳配置。这些信息将用于通过set-chunkAPI 为集群设置默认值。

    ceph-dedup-tool --op estimate --pool $POOL --min-chunk min_size
      --chunk-algorithm rabin --fingerprint-algorithm rabin
    

    ceph-dedup-tool对象分块。rabin chunk。这些是rabin chunk.

               --mod-prime <uint64_t>
               --rabin-prime <uint64_t>
               --pow <uint64_t>
               --chunk-mask-bit <uint32_t>
               --window-size <uint32_t>
               --min-chunk <uint32_t>
               --max-chunk <uint64_t>
    
    Users need to refer following equation to use above options for ``rabin chunk``. ::
    
               rabin_hash =
                 (rabin_hash * rabin_prime + new_byte - old_byte * pow) % (mod_prime)
    
    1. 固定 chunk 与内容定义 chunk 的选项

    内容定义 chunking 可能是或可能不是最佳解决方案。例如,

    数据 chunkA : abcdefgabcdefgabcdefg

    让我们考虑 Data chunkA的去重。理想的 chunk 偏移量是从1to7 (abcdefg)。所以,如果我们使用固定 chunk,7是最佳 chunk 长度。但是,在内容切片的情况下,最佳 chunk 长度可能无法找到(去重率不会是 100%)。

    数据 chunkB : abcdefgabcdefgabcdefg

    数据 chunkC : Tabcdefgabcdefgabcdefg

  • 修复引用计数

    引用计数去重的关键思想是误报,这意味着(manifest object (no ref),, chunk object(has ref))发生而不是(manifest object (has ref), chunk 1(no ref))。为了修复这种不一致,ceph-dedup-tool支持chunk_scrub.

    ceph-dedup-tool --op chunk_scrub --chunk_pool $CHUNK_POOL
    

由 Ceph 基金会带给您

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