This work is licensed under a Creative Commons Attribution 3.0
Unported License.
http://creativecommons.org/licenses/by/3.0/legalcode

自动化分层支持

1. 问题描述

存储在长期存储系统上的数据在其信息生命周期中会经历访问模式的逐渐变化。例如,Facebook 等公司的经验研究表明,随着图像数据超过其创建时间,用户访问它们的可能性越来越小,访问速率有时呈指数级下降 [1]。 长期保留期限,就像存储在 Swift 等冷存储系统上的数据一样,会增加这种变化的可能性。

分层是许多传统文件和块存储系统提供的重要功能,用于处理数据“温度”的变化。它能够无缝地将不活跃的数据从高性能存储介质移动到低成本、大容量存储介质,以满足客户的 TCO(总拥有成本)要求。 随着 Swift 等可扩展对象存储系统开始原生支持多种介质类型,如 SSD、HDD、磁带以及不同的存储策略,例如复制和纠删编码,用自动数据分层来补充广泛的可用存储层级(虚拟和物理层级)变得至关重要。

2. Swift 中的分层用例

Swift 用户和操作员可以通过透明地转换其存储策略来适应对象访问特征的变化,从而满足整体业务需求($/GB、性能、可用性)以及对象存储的位置和方式。

以下是一些对象如何随着老化在不同存储策略的 Swift 容器之间移动的示例。

[基于 SSD 的容器] –> [基于 HDD 的容器]

[基于 HDD 的容器] –> [基于磁带的容器]

[复制策略容器] –> [纠删编码策略容器]

在某些客户环境中,Swift 容器可能不是最后的存储层级。低于 Swift 成本的归档级存储示例包括专门的磁带系统 [2]、Amazon Glacier 和 Google Nearline Storage 等公共云归档解决方案。 类似于 Swift 中提出的此分层功能,Amazon S3 已经内置了根据用户定义的规则在 S3 和 Glacier 之间移动对象的支持。 Redhat Ceph 最近也添加了分层功能。

3. 目标

本文档的主要目标是在 Swift 中提出一项分层功能,该功能能够无缝地在属于不同存储策略的容器之间移动对象。它是“无缝的”,因为用户不会遇到命名空间、访问 API 或对象可用性的任何中断。

通过新的 Swift API 增强功能,Swift 用户和操作员将能够指定两个容器之间的分层关系以及相关的数据移动规则。

本提案的重点是识别、创建和整合在 Swift 中实现基本分层所需的构建块。 虽然这种狭窄的范围是故意的,但期望是基本分层实现将奠定基础,并且不会排除未来更高级的分层功能。

4. 功能依赖项

以下正在进行中的 Swift 功能(又称规范)已被确定为本分层提案的核心依赖项。

  1. Swift 符号链接 [3]
  2. 更改存储策略 [4]

还有一些规范被归类为“锦上添花”的依赖项,这意味着如果它们演变为完整的实现,我们将能够使用高级用例和功能来演示分层功能。 但是,它们不被认为是分层第一版本的强制要求。

  1. 元数据存储/搜索 [5]
  2. Swift 中的磁带支持 [6]

5. 实现

所提出的分层实现依赖于几个构建块,其中一些是分层独有的,例如必要的 API 更改。 将对其进行全面描述。 其他构建块,例如符号链接,是独立的功能,并且用途超出了分层范围。 与其重新发明轮子,分层实现旨在利用这些正在进行的功能中提供的特定结构。

5.1 概述

有关分层实现的快速概述,请参阅图 (images/tiering_overview.png)。 它突出显示了所提出的分层引擎中发生的动作流程。

1. Swift 客户端通过使用适当的元数据标记源容器,在两个 Swift 容器之间创建分层关系。 2. 一个名为 tiering-coordinator 的后台进程检查源容器并迭代其对象。 3. tiering-coordinator 识别用于移动的候选对象,并通过向对象服务器发出复制请求将每个对象去阶段到目标容器。 4. 在复制对象后,tiering-coordinator 通过指向目标容器中相应对象的符号链接替换它。

5.2 API 更改

Swift 客户端可以通过将以下元数据添加到源容器来创建两个容器(即源容器和目标容器)之间的分层关系。

X-Container-Tiering-Target: <target_container_name> X-Container-Tiering-Age: <threshold_object_age >

可以在源容器的创建(PUT)操作期间设置元数据值,也可以作为容器元数据更新(POST)操作的一部分稍后设置。 对象年龄是指自对象创建时间起经过的时间(创建时间与对象一起存储在 ‘X-Timestamp’ 标头中)。

设置上述容器元数据的用户语义如下。 当源容器中的对象变得比指定的阈值时间更老时,它们就成为候选对象,可以去阶段到目标容器。 不保证它们确切地何时移动或在任何给定时间对象的确切位置。 Swift 将异步地对它们进行操作,并根据用户指定的分层规则重新定位对象。 一旦在源容器上设置了分层元数据,用户就可以期望其对象的性能、可靠性等水平与源容器或目标容器的存储策略相称。

可以通过设置以下每个对象的元数据来覆盖源容器中单个对象的分层元数据,

X-Object-Tiering-Target: <target_container_name> X-Object-Tiering-Age: <object_age_in_minutes>

对象上的分层元数据的存在意味着它将优先于设置在托管容器上的分层元数据。 但是,如果容器未标记任何分层元数据,则无论其是否标记任何分层相关元数据,其内部的对象都不会被视为分层。 此外,如果对象元数据上的分层年龄阈值低于容器上设置的值,则在满足容器年龄标准之前它将不会生效。

分层功能保留的一个重要不变性是对象的命名空间。 如后续章节所述,对象移动后,它们将立即被符号链接替换,允许用户继续对对象执行前台操作,就好像没有发生迁移一样。 请参阅第 7 节的开放问题,了解有关 API 主题的更多评论。

总而言之,Swift 用户必须执行以下步骤才能随着时间的推移启动从源容器 (S) 到目标容器 (T) 的对象分层。

1. 使用所需的存储策略(例如复制和纠删编码)创建容器 S 和 T。 2. 如本节中所述,在容器 S 上设置分层相关元数据 (X-Container-Tiering-*)。 3. 将对象存入容器 S。 4. 如果需要,通过设置对象元数据 (X-Object-Tiering-*) 覆盖容器 S 内部的默认容器设置。

也可以通过设置适当的分层元数据在两个以上的容器之间建立级联分层关系。 例如,可以在容器 C1 -> C2 -> C3 之间建立分层关系。 当 C1 中的对象足够旧以移动到 C2 时,它将被存入 C2。 然后,计时器将在 C2 中移动的对象上启动,并且根据 C2 上的年龄设置,该对象最终将被迁移到 C3。

5.3 分层协调器进程

tiering-coordinator 是一个后台进程,类似于 container-sync、container-reconciler 和在每个容器服务器上运行的其他 container-* 进程。 我们有可能重用现有的容器进程,特别是 container-sync 或 container-reconciler 来执行 tiering-coordinator 的工作,但出于讨论的目的,假设它是一个单独的进程。

tiering-coordinator 执行的关键操作是

  1. 遍历标记有分层元数据的容器
  2. 识别这些容器中的分层候选对象
  3. 启动候选对象的复制请求
  4. 用相应的符号链接替换源对象

我们将在本节中讨论 (a) 和 (b),并在后续章节中介绍 (c) 和 (d)。 请注意,在分层的第一版本中,只有一种指标 <对象年龄> 将用于确定对象是否有资格进行迁移。

tiering-coordinator 以一系列轮次执行其操作。 在每个轮次中,它迭代它在容器服务器上具有直接访问权限的 SQLite DB 的容器。 它检查容器是否具有正确的 X-Container-Tiering-* 元数据。 如果存在,它将启动扫描过程以识别候选对象。 扫描过程利用容器 DB 的一个方便(但不是必需的)属性,即对象按其创建时间的先后顺序列出。 也就是说,容器 DB 中的第一个索引指向创建时间最旧的对象,然后是下一个较新的对象,依此类推。 因此,下面描述的扫描过程针对分层 v1 实现中选择的对象年龄标准进行了优化。 对于扩展到其他分层指标,请参阅第 6.1 节的讨论。

每个容器 DB 将具有两个持久标记来跟踪分层的进度 – tiering_sync_start 和 tiering_sync_end。 标记 tiering_sync_start 指向容器 DB 中已经处理的对象的起始索引。 标记 tiering_sync_end 指向尚未考虑进行分层的对象的索引。 所有位于两个标记之间的对象都是当前正在进行分层的对象。 请注意,容器 DB 中存在的持久标记有助于在容器服务器崩溃/重新启动时快速从以前的工作恢复。

当首次选择容器进行分层时,两个标记都初始化为 -1。 如果第一个对象足够旧以满足 X-Container-Tiering-Age 标准,则将 tiering_sync_start 设置为 0。 然后,第二个标记 tiering_sync_end 将推进到小于以下两个值的索引 - (i) tiering_sync_start + tier_max_objects_per_round(后者将是 /etc/swift/container.conf 中的可配置值)或 (ii) 容器 DB 中相应对象满足分层年龄标准的最大的索引。

上述标记设置将确保两个不变性。 首先,tiering_sync_start 和 tiering_sync_end 之间的所有对象都是候选对象,可以移动到目标容器。 其次,它将保证在单个轮次中容器上处理的对象数量受配置参数 (tier_max_objects_per_round,例如 = 200) 约束。 这将确保协调器进程将有效地在服务器上的所有容器之间轮询,而不会将过多的时间花在少数容器上。

修复标记后,tiering-coordinator 将为范围内的每个对象发出复制请求。 当复制请求完成时,它更新 tiering_sync_start = tiering_sync_end 并转到下一个容器。 当 tiering-coordinator 在完成当前轮次后重新访问相同的容器时,它将从 tiering_sync_start = tiering_sync_end 重新启动扫描例程(除了它们这次都不是 -1)。

在典型的 Swift 集群中,每个容器 DB 会被复制三次,并驻留在多个容器服务器上。 因此,如果没有适当的同步,tiering-coordinator 进程可能会因处理相同的容器和相同的对象而发生冲突。 这可能会导致非确定性行为的竞争条件。 我们可以通过采用 container-sync 进程采用的“分而治之”方法来克服这个问题。 范围内的对象索引 (tiering_sync_start, tiering_sync_end) 最初可以分成与在同一容器上运行的 tiering-coordinator 进程数量一样多的不相交区域。 随着它们处理对象索引,每个进程可能会根据集体进度完成其他进程的部分。 有关 container-sync 进程如何隐式通信和取得群体进展的详细描述,请参阅 [7]。

5.4 对象复制机制

对于 tiering-coordinator 认为有资格移动到目标容器的每个候选对象,它将使用对象服务器支持的 API 调用发出“对象复制”请求。 API 调用将映射到对象传输器守护程序在对象服务器上使用的方法。 tiering-coordinator 可以选择任何对象服务器(通过查找与源容器策略中的对象对应的环数据结构)作为请求的目的地。

对象转移守护进程应该针对将对象从一种存储策略转换为另一种存储策略进行优化。根据“更改策略”规范,对象转移守护进程将配备正确的技术,以便在复制 -> EC、EC -> EC 等之间移动对象。或者,如果没有对象转移守护进程,分层协调器可以简单地利用 Swift 公开给普通客户端的服务器端“COPY”API。它可以将 COPY 请求发送到 Swift 代理服务器,以克隆源对象到目标容器。代理服务器将首先读取(GET 请求)源对象服务器中的对象,然后创建目标对象服务器中的对象副本(PUT 请求),从而执行复制。虽然这对于分层协调器来说可以正常工作,但使用对象转移接口可能是一个更好的选择。通过一个定义良好的接口利用对象转移中的专用代码来复制两个不同存储策略容器之间的对象,将使整体分层过程高效。

以下是在对象转移代码中用函数调用表示的一个示例接口

def copy_object(source_obj_path, target_obj_path)

上述方法可以作为对象转移守护进程使用的类似功能的包装器。分层协调器将使用此接口通过 HTTP 调用来调用该函数。

copy_object(/A/S/O, /A/T/O)

其中 S 是源容器,T 是目标容器。请注意,目标容器中的对象名称将与源容器中的对象名称相同。

收到复制请求后,对象服务器将首先检查源路径是否为符号链接对象。如果是符号链接,它将向分层协调器响应一个错误,以指示已存在一个符号链接。这种行为将确保幂等性,并防止分层协调器崩溃并重试先前完成的对象复制请求的情况。此外,它避免了对用户创建的稀疏对象(如符号链接)进行分层。其次,对象服务器将检查源对象是否具有以 X-Object-Tiering-* 形式存在的分层元数据,该元数据会覆盖源容器上的默认分层设置。它可能会或可能不会执行对象复制,具体取决于结果。

6. 未来工作

6.1 其他分层标准

分层实现的第一个版本将高度定制(特别是分层协调器的扫描机制)为对象年龄标准。容器 DB 具有将对象按创建/覆盖顺序存储的便捷属性,这有利于对候选对象进行非常高效的线性扫描。

在未来,我们应该能够支持高级标准,例如读取频率计数、对象大小、基于元数据的选择等。例如,考虑以下假设标准

“如果对象年龄超过 1 个月 AND 大小 > 1GB AND 标记有元数据‘surveillance-video’,则将容器 S 中的对象分层到容器 T”

当 Swift 中的元数据搜索功能 [5] 可用时,分层协调器应该能够运行查询以快速检索与用户和系统元数据上的临时标准匹配的对象名称集。随着元数据搜索功能的不断发展,我们应该能够利用它来添加自定义元数据,例如读取计数等,以用于我们的目的。

6.2 与外部存储层集成

分层的首次实现仅支持 Swift 容器之间的对象移动。为了在 Swift 容器和外部存储后端之间建立分层关系,必须通过 DiskFile API 或其他集成机制将后端安装在 Swift 中作为本机容器。例如,完全托管在 GlusterFS 或 Seagate Kinetic 驱动器上的目标容器可以通过 Swift-on-file 或 Kinetic DiskFile 实现分别创建。

Swift 社区认为,类似的集成方法对于支持外部存储系统作为分层目标是必要的。目前正在进行集成基于磁带的系统的工作。同样,需要集成外部系统,如 Amazon Glacier 或供应商存档产品,通过 DiskFile 驱动程序或其他方式。

7. 开放问题

本节结构化为一系列问题和可能的答案。通过来自 Swift 社区的更多反馈,开放问题将被解决并合并到主文档中。

Q1:目标容器是否可以存在于与源容器不同的帐户中?

Ans:建议的 API 假定目标容器始终位于与源容器相同的帐户中。如果取消此限制,则需要相应地修改建议的 API。

Q2:当客户端在源容器上设置分层元数据时,目标容器是否存在?如果用户对目标容器没有权限怎么办?所有错误检查何时完成?

Ans:错误检查可以推迟到分层协调器进程。后台进程在检测到目标容器不可用时,可以跳过对源容器执行任何分层活动,并继续处理下一个容器。但是,最好在客户端路径中检测错误并尽早报告。如果选择后者方法,则需要中间件功能来检查容器上设置的分层元数据。

Q3:目标容器如何呈现给客户端?它是否就像具有读写权限的任何其他容器一样?

Ans:目标容器就像任何其他容器一样。客户端负责正确操作目标容器中的内容。特别是,它应该意识到源容器中可能存在指向目标对象的符号链接。直接使用目标容器命名空间删除或覆盖对象可能会使某些符号链接变得无用或过时。

Q4:当在一段时间内设置冲突的分层元数据时会发生什么情况。例如,如果分层年龄阈值通过 POST 元数据操作在容器上增加,之前取消分层的对象是否会被带回到源容器以匹配新的分层规则?

Ans:可能不会。新的分层元数据可能仅应用于尚未由分层协调器处理的对象。基于旧元数据执行的先前操作不需要反转。

Q5:当用户对先前取消分层到目标容器的对象发出 PUT 操作时会发生什么情况?

Ans:默认符号链接行为应该适用,但尚不清楚它会是什么。覆盖 PUT 是否会导致符号链接中间件删除符号链接和指向的对象?

Q6:当用户对先前取消分层到目标容器的对象发出 GET 操作时,它是否会被提升回源容器?

Ans:建议的实现不会无缝地将对象提升回上层容器。如果需要,可以借助代理服务器中的分层中间件轻松添加此行为。

Q7:提到能够设置多个容器之间的级联分层关系,C1 -> C2 -> C3。如果在此关系图中存在循环怎么办?

Ans:应该防止循环,否则我们至少会遇到一种复杂的情况,即符号链接可能指向具有相同名称的同一容器上的对象,从而覆盖符号链接!可以在客户端路径中使用分层特定中间件在创建分层元数据时通过迭代现有的分层关系来检测循环。

Q8:分层与现有或新功能(如 SLO/DLO、加密、容器分片等)之间是否存在意外的交互?

Ans:SLO 和 DLO 段应该继续按预期工作。如果对象服务器收到来自分层协调器的 SLO 清单对象的对象复制请求,它将迭代地为每个组成对象执行复制。每个组成对象将被替换为符号链接。加密也应该可以正常工作,因为它几乎完全将对象视为不透明的字节集,并且不关心对象是密码文本还是不是。处理容器分片可能很棘手。分层协调器期望线性地遍历容器 DB 的索引。如果容器 DB 被碎片化并存储在许多不同的容器服务器中,则扫描过程可能会变得复杂。有什么想法吗?