RESTful radosgw 操作的原子性

yehudasa

一段时间前,我们对 radosgw 进行了原子读写操作的处理。

第一个问题是确保两个或多个并发写入者写入同一个对象时不会导致对象不一致。这就是“原子 PUT”问题。

我们还希望确保当一个客户端通过 radosgw 读取一个对象时,另一个客户端正在写入同一个对象,结果是一致的。也就是说,读取一个对象时,客户端应该获得该对象的旧版本或新版本,而绝不会获得两者的混合版本。这就是“原子 GET”问题。

Radosgw 直接构建在 RADOS 之上,是 librados 用户的典型示例。基本问题是 radosgw 通过一系列相对较小的读或写操作将对象从 RADOS 对象流出或流向 RADOS 对象。对于原子 PUT 和原子 GET,我们不想引入锁定。锁定可以解决这个问题,但在 RADOS 之上实现它并非易事,并且会影响可扩展性和网关的相对简单性。Ceph 分布式文件系统在元数据服务器中实现锁定(作为其 POSIX 文件锁定支持的一部分),在网关中引入它需要在每个对象上保存状态并在不同的网关实例之间同步它。我们不想重新实现 MDS。

原子 PUT

当 radosgw 读取或写入对象时,它可以向 RADOS 后端发出多个 librados 请求。RADOS 的一个特性是每个单个操作都是原子的。问题是对于足够大的对象(当然,这些对象不会太大),我们会发出多个写操作,并最终导致对象交错。

原子 PUT 的解决方案是将对象写入临时对象。一旦临时对象完全写入,我们就发出单个 librados clone-range 操作,该操作原子地将整个临时对象克隆到目标对象。一旦数据到位,我们就删除临时对象。这相当于写入临时文件并在完成后将其重命名为目标文件。

由于 RADOS 后端是分布式的,我们需要确保临时对象和目标对象位于同一个放置组(以及同一个 OSD)中。通常,对象位置由对象名称确定,但为了这个目的,我们使用了“对象定位器”功能,该功能允许我们提供提供给哈希函数的替代字符串。在这种情况下,我们使用目标对象名称作为临时对象的对象定位器,确保两个对象都位于同一个放置组的同一个节点上,以便克隆操作可以工作。

原子 GET

有了原子 PUT,我们知道对象是一致的。但是,这并不能帮助客户端在对象正在被写入时读取对象。由于单个 GET 可能涉及多个 librados 读操作,其中一些读操作可能发生在对象被替换之前,而另一些读操作可能发生在之后,从而导致不一致的“撕裂”结果。

除了原子操作之外,RADOS 还有一个很好的特性,称为复合操作,它允许你发送一些捆绑在一起并原子应用的操作。如果其中一个操作失败,则不应用任何操作。我们使用它来进行原子 PUT,以便在一个原子操作中设置目标对象上的数据和元数据。

对于原子 GET,我们引入一个对象“标签”,这是一个我们在每个 PUT 时生成并存储为对象属性(xattr)的随机值。当 radosgw 写入对象时,它首先检查现有对象并获取其标签(它可以原子地完成)。如果对象存在,它将其克隆到一个新对象,标签作为后缀(采取必要的步骤以避免名称冲突),原始对象名称作为定位器。复合克隆操作如下所示

  1. 检查对象是否存在标签属性是
  2. 克隆到_

第一个操作是一个保护,以确保在第一次读取对象后对象没有被重写。(如果已被重写,我们需要重新启动整个操作并重新读取标签。)我们在写入新的对象实例时放置相同的保护,以确保没有竞态操作。

读取对象的客户端也首先读取标签,并在每个后续读取操作之前放置相同的保护。如果保护失败,客户端知道对象已被重写。但是,它也知道由于已被重写,现在可以找到它开始读取的对象_因此,读取名为 foo 的对象如下所示

  • 读取对象 foo 标签 -> 123
  • 验证对象 foo 标签是“123”;读取对象 foo(偏移量 = 0,大小 = 512K) -> 好的,读取 512K
  • 检查对象 foo 标签是“123”;读取对象 foo(偏移量 = 512K,大小 = 512K) -> 不好,对象已被替换
  • 读取对象 foo_123(偏移量 = 512K,大小 = 512K) -> 好的,读取 512K

最终组件是一个意图日志。由于我们最终在不同的名称下创建同一对象的多个实例,我们需要确保在合理的时间后清理这些对象。我们添加了一个日志对象,我们记录每个需要删除的对象。在足够长的时间后(无论多长时间我们期望非常慢的 GET 仍然成功),一个进程遍历日志并删除旧对象。