这个五一过的比较平淡,在家想了两个 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 来沟通~