2021-17: go-storage 的幂等删除
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.
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 idDeleteObject
操作会生成一个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
)
所以我们拆分出 Appender
,Pager
,Multiparter
这样不同形式的 Object 写入接口,但是他们都共享同样的 Stat
/ Read
/ Delete
实现。也就是说,根据 Service 自身支持的情况不同,他们的 Delete 实现也有很大的差异,我们以 qingstor
,fs
和 dropbox
来举例说明。
Delete in go-service-qingstor
qingstor 服务支持 Multiparter
和 Appender
。
其中 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 来沟通~
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK