2

2021-17: go-storage 的幂等删除

 3 years ago
source link: https://xuanwo.io/reports/2021-17/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

2021-17: go-storage 的幂等删除

这个五一过的比较平淡,在家想了两个 go-storage 的新 RFC,然后看了看 IPFS 的白皮书,顺便开了一些脑洞。这一期的周报我们聊聊 go-storage 新通过的 Proposal: 幂等删除 AOS-46: Idempotent Storager Delete Operation

首先搞清楚什么叫做幂等,让我们来看一下 MDN

一个HTTP方法是幂等的,指的是同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。换句话说就是,幂等方法不应该具有副作用(统计用途除外)。在正确实现的条件下, GET,HEAD,PUT 和 DELETE 等方法都是幂等的,而 POST 方法不是。所有的 safe 方法也都是幂等的。

MDN 关于幂等的介绍中还有这样一条:

幂等性只与后端服务器的实际状态有关,而每一次请求接收到的状态码不一定相同。例如,第一次调用 DELETE 方法有可能返回 200,但是后续的请求可能会返回 404。

而本文中所讲的幂等要更严格一些:在没有外部干涉的前提下,每一次成功的操作返回的状态都是一致的。

对于 Delete 操作而言,不同的服务有不同的处理方式,我们这里列举三种服务的 Delete 实现作为参考。

S3 DeleteObject

S3 现在已经完整的实现了 Versioned Object,它的大体逻辑是这样的:

  • PutObject 会生成一个全新的 version id
  • DeleteObject 操作会生成一个 delete marker,并作为最新的 version 插入
  • GetObject 总是会 Get 最新的 version,如果这个 Object 不存在或者是一个 delete marker,则会返回 404 Not Found

这里需要特别解释一下 DeleteObject 的行为:

  • 出于兼容性考虑,在开启版本管理功能之前的 Object 都会作为一个特殊的 null version 存在,此时 DeleteObject 会删除这个 null version
  • 但是不管这个 null version 是否存在,DeleteObject 都会返回删除成功的状态码,即: 204 No Content

由于 go-storage 目前还没有提供 version 支持的计划,所以 DeleteObject with versionId 的行为暂时不做探讨

总结一下就是,S3 的 DeleteObject 是幂等的,重复删除同一个 Path 总是会得到一致的状态。

Azblob DeleteBlob

Azblob 除了实现了 Versioned Object 之外,还实现了软删除(Soft Delete):

  • azblob 的 Delete 都是标记删除,索引中不存在,数据在 GC 过程中才会彻底删除
  • 用户在启用软删除之后,blob 并不会彻底删除
  • 用户通过配置 DeleteRetentionPolicy 来决定被软删除的 blob 保留多久
  • blob 允许通过 UndeleteBlob 接口来恢复

此外,Azblob 还有彻底删除功能:

  • 在启用彻底删除之后,文件允许通过指定 deletetype=permanent 来彻底一个被 (soft) delete 的 snapshot 或者 version
  • 注意,这个功能与软删除是正交的

但无论用户是否启用了软删除 / 彻底删除,Azblob 的 DeleteBlob 返回的状态是一致的:

  • 如果 blob 之前存在,则返回 202 Accepted,表示这个删除请求已经接收到了
  • 如果 blob 不存在,则返回 404 Not Found,表示这个资源不存在

也就是说,Azblob 的 DeleteObject 实现不是幂等的。

File System

本地文件系统的 Delete 操作也不是幂等的。

以 Linux 平台为例,用于删除文件的 syscall unlink 会在文件不存在时,会返回错误 ENOENT 表示文件不存在:

> strace unlink x
execve("/usr/bin/unlink", ["unlink", "x"], 0x7ffed2e39128 /* 51 vars */) = 0
...
unlink("x")                             = -1 ENOENT (No such file or directory)
...
write(2, "unlink: ", 8unlink: )                 = 8
write(2, "cannot unlink 'x'", 17cannot unlink 'x')       = 17
...
write(2, ": No such file or directory", 27: No such file or directory) = 27
...
+++ exited with 1 +++

我们常用的 rm 命令通常会在调用 unlink 之前,调用 stat 来检查文件的状态,如果文件不存在则会结束删除流程:

> rm x
rm: cannot remove 'x': No such file or directory

通过 strace 我们能看到 rm 首先调用了一次 newfstatat,在它返回错误后就直接走错误处理流程了:

> strace rm x
execve("/usr/bin/rm", ["rm", "x"], 0x7ffca67862f8 /* 51 vars */) = 0
...
newfstatat(AT_FDCWD, "x", 0x5632bebcf778, AT_SYMLINK_NOFOLLOW) = -1 ENOENT (No such file or directory)
...
write(2, "rm: ", 4rm: )                     = 4
write(2, "cannot remove 'x'", 17cannot remove 'x')       = 17
...
write(2, ": No such file or directory", 27: No such file or directory) = 27
...
+++ exited with 1 +++

Delete in go-storage

好,我们前面已经看了三种服务的实现,接下来我们看看 go-storage 中的 Delete 操作。

AOS-25: Object Mode 中,我们引入了 Object Mode 的概念,将用户能对 Object 执行的操作进行了正交分解:

const (
	// ModeDir means this Object represents a dir which can be used to list with dir mode.
	ModeDir ObjectMode = 1 << iota
	// ModeRead means this Object can be used to read content.
	ModeRead
	// ModeLink means this Object is a link which targets to another Object.
	ModeLink
	// ModePart means this Object is a Multipart Object which can be used for multipart operations.
	ModePart
	// ModeBlock means this Object is a Block Object which can be used for block operations.
	ModeBlock
	// ModePage means this Object is a Page Object which can be used for random write with offset.
	ModePage
	// ModeAppend means this Object is a Append Object which can be used for append.
	ModeAppend
)

所以我们拆分出 AppenderPagerMultiparter 这样不同形式的 Object 写入接口,但是他们都共享同样的 Stat / Read / Delete 实现。也就是说,根据 Service 自身支持的情况不同,他们的 Delete 实现也有很大的差异,我们以 qingstorfsdropbox 来举例说明。

Delete in go-service-qingstor

qingstor 服务支持 MultiparterAppender

其中 Multiparter 对应 qingstor 的分段上传接口,用户需要调用 AbortMultipartUpload(path, uploadId) 来删除一个分段上传,在分段上传完成前,这个 Object 是不可读的。所以 Delete PartObject 需要传入 multipart_id

Appender 对应 qingstor 的 AppendObject 接口,一旦 Append 成功,这个 Object 就是可读的。所以 Delete AppendObject 不需要额外的参数。

Delete in go-service-fs

我们知道文件系统本身就支持追加写和随机写操作,所以 fs 底层并没有做额外的处理,无论删除什么 Mode 的 Object 都是调用 os.Remove

Delete in go-service-dropbox

dropbox 支持 Appender 的方式是返回一个临时的 upload-session-id,只有持有这个 id 才能上传,在显式的调用 close 之后才可读。此外没有任何方式能再次获取到这个 id,也无法取消这次上传(48 小时后会自动过期,相关的数据也会被清除)。所以对 Dropbox 来说,它的 Delete 操作只能删除一个常规的文件,无法删除一个 AppendObject。

前面我们花了一些时间探讨了 AOS-46 的背景,接下来可以进入正题了。既然 go-storage 立志要做一个存储抽象层,那就需要屏蔽掉底层实现的细节,我们需要明确的告诉调用者在使用 Delete 的时候会发生什么以及不会发生什么。

摆在我们面前的有两种选择:

  • 将删除操作定义为 幂等操作
  • 将删除操作定义为 删除已存在的对象

幂等操作 意味着只要不出现其他报错,无论这次 Delete 操作是否真的删除了数据,无论被 Delete 的对象是否存在,go-storage 总是返回成功。也就是说,go-storage 会忽略所有在 Delete 期间出现的形如 ObjectNotExist 的报错。

而定义为 删除已存在的对象 意味着所有服务都必须确保被删除的对象真的存在。

  • 对删除是幂等的 s3 服务来说,它需要先 HeadObject 来确认对象存在
  • 对不是幂等的其他服务来说,它可以直接返回来自底层的错误

在我看来 删除已存在的对象 这样的定义有这些的问题:

  • 不公平:正如 AOS-7 中提到的那样,采用 删除已存在的对象 的定义使得不在乎文件是否真的存在的用户也需要承担额外的 Stat 开销
  • 不完备:Stat & Delete 并不是一个原子操作,作为一个服务的外部调用者,我们实现不了这样的定义

而采纳 幂等操作 的定义则不存在这样的问题:

  • 关心文件是否存在的用户可以自行调用 Stat 来检查
  • go-storage 也能封装形如 CheckedDelete(path string) (deleted bool, err error) 这样的操作来满足用户的需求

所以我们最终采用了 幂等删除 的方案。

相关的实现已经在 Issue Implement AOS-46: Idempotent Storager Delete Operation 中展开,感兴趣的同学欢迎加入 #go-storage:aos.dev 来沟通~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK