条件 DELETE:一个条件对应一个逻辑对象
单对象条件删除(DeleteObject 上的 If-Match)由 PR #145 引入,随后 PR #178 修复了多池串行化与跨池清理。这两项改动均已随 Server 20260916 发布。
八月围绕 PR #12 的早期方案曾设想更宽的范围——包括批量 DeleteObjects 拒绝保护、额外的 s3:GetObject 读取授权检查,以及“仅比较当前版本”规则——但实际合入的实现并未采纳这些扩展。本文记录 f99ed829b 中已交付的契约,并阐述为什么前置条件判断必须位于单池存储层之上。
已实现的契约
| 请求 | 已交付行为 |
|---|---|
单个 DeleteObject,非空 If-Match: <ETag> |
在外层删除锁内比较客户端可见 ETag;不匹配时先返回 412 PreconditionFailed,不执行任何删除 |
If-Match: * |
要求选中的对象表示存在且不是删除标记(Delete Marker) |
显式 versionId |
比较所指定的版本,而非无关的当前版本 |
未携带 If-Match 或请求头值为空 |
不安装条件回调,按普通删除处理 |
批量 DeleteObjects 的逐项 <ETag> |
ObjectToDelete 结构体无 ETag 字段,该 XML 元素不提供删除保护 |
内部递归 x-minio-force-delete |
前缀删除先于单对象条件路径返回,不能用作条件删除 |
条件判断不会额外增加 s3:GetObject 授权检查。普通删除授权仍然生效,包括显式版本对应的 s3:DeleteObjectVersion、Object Lock 保护检查,以及独立的可信复制路径。
为什么条件判断属于跨池协调层
在多存储池部署中,不同存储池可能保留同一对象在不同时期的副本。请求前置条件针对的是本次操作选中的单一逻辑对象。如果让每个存储池独立判断条件并直接修改磁盘状态,就可能先在一个池中删掉副本,随后在另一个池中返回 412 PreconditionFailed;或者删掉最新副本后,反而让旧池中的陈旧副本重新暴露为当前版本。
因此,多池协调层(erasureServerPools.DeleteObject)会先获取共享命名空间写锁,收集各池状态,对选中的逻辑表示只执行一次条件判断,随后清空向下传递的回调函数,并跨池协调删除清理。并发写入与元数据更新共用同一把外层命名空间锁。
早期设计中的反例
假设存储池 1 保留旧副本(ETag 为 A),存储池 2 保留当前最新副本(ETag 为 B):
If-Match: A(返回412却发生部分删除): 若逐池独立判断,存储池 1 会先匹配并删除副本A,随后在存储池 2 因与B不匹配而返回412 PreconditionFailed,导致被拒绝的请求依然修改了底层存储。If-Match: B(成功删除后旧副本复活): 若逐池独立判断,存储池 2 删除了最新副本B,而存储池 1 因不匹配或跳过而保留了旧副本A,导致陈旧数据重新成为客户端可见的当前对象。
此外,在逐池并发工作协程中直接调用 HTTP 条件回调,还可能引发对同一个 http.ResponseWriter 的并发写入竞争。在跨池协调层集中消费一次请求条件,能够同时消除这两类隐患。
失败边界
跨池协调路径在判断条件前会先读取各池状态,并向上报告读仲裁失败,而不会把不可达的存储池误判为空池。不过,物理清理失败仍可能发生在部分底层修改之后:单次请求失败并不等价于分布式多池回滚事务。存储层报错后,调用方仍需通过重试与状态核对来确认最终结果;详见多池对象一致性。
选择与判断
只判断一次
erasureServerPools.DeleteObject 持有外层命名空间锁。多池部署调用 deleteObjectReconciled;单池部署先读取选中表示,并在删除标记快捷返回分支之前调用 CheckPrecondFn。下层存储引擎不再针对每份物理副本重复解释条件。
通配符与删除标记
DELETE 专用前置条件辅助函数将 If-Match: * 解释为“目标必须存在有效的非删除标记对象表示”;删除标记不能满足 *。缺失对象键或缺失指定版本会进入常规的未找到路径,不会被伪造成空 ETag 匹配。
授权
处理器依据实际生效的目标版本检查删除权限(s3:DeleteObject 或 s3:DeleteObjectVersion),不会在 If-Match 携带具体 ETag 时额外要求 s3:GetObject 权限。
SSE-C ETag
条件删除直接比较元数据中已保存的客户端可见 ETag 投影,不会读取或解密对象数据载荷。SSE-C 读取密钥认证与对象删除属于相互独立的契约。
显式版本
当请求提供显式 versionId 时,系统会选中该特定版本执行 ETag 比对与删除。因此,即使当前最新版本拥有不同的 ETag,只要所指定的历史版本 ETag 匹配,该历史版本仍可被正常删除。这与早期方案中“只允许比较当前版本”的设想不同。
尚未支持的边界
空 If-Match 请求头不会安装条件。内部递归前缀删除扩展(x-minio-force-delete)会绕过单对象条件路径。ObjectToDelete 不会解析批量 DeleteObjects XML 中的 <ETag> 元素,也没有整请求级别的 NotImplemented 拒绝保护。需要“比较后删除”语义的调用方必须使用带非空 If-Match 请求头的单对象 DeleteObject 接口。
被拒绝的替代方案与范围边界
- 仅修改底层共享 ETag 比较函数: 改动字符串比较辅助函数无法解决多池胜出版本选择,也无法阻止在条件检查前先修改其他池的数据。
- 逐池执行回调后再汇总错误: 当某个池已经物理删除了副本后,即使外层汇总返回
412也无法撤销已发生的删除。 - 引入新的分布式事务框架: 在既有外层命名空间锁内消费一次条件回调已足以满足单对象
DeleteObject;批量条件删除与 IAM 策略键跨越了其他接口边界,应作为独立工作推进。
源码证据
核心实现位于 cmd/object-handlers.go、cmd/erasure-server-pool.go 与 cmd/api-datatypes.go,回归测试覆盖了 ETag 不匹配、通配符与删除标记、显式 versionId、读仲裁失败以及多池状态协调。
后续工作
批量条件删除
在 DeleteObjects 中支持逐项 <ETag> 条件需要解析该 XML 字段、在对应外层锁内逐一判断每个逻辑对象、兼容 Quiet 模式,并在逐项响应结果中报告条件失败。在此之前,批量请求中的 <ETag> 会被忽略,切勿将其用作并发控制手段。
策略强制键
IAM 策略包目前尚未定义 s3:if-match 条件键。支持通过策略强制要求客户端提供 If-Match 需要先在 silo-pkg 中新增该条件键、在服务端处理器中填充请求条件上下文,并补充策略评估测试。
设计权衡
单对象条件 DELETE 直接复用了既有的多池对象选择与外层命名空间锁边界,无需引入新的事务层。完整的批量条件删除与 IAM 策略条件键涉及更多 API 与外部包边界,仍作为独立特性管理。核心不变量非常明确:当受支持的单对象 If-Match 条件不成立时,必须在任何存储池删除数据之前,针对选中的逻辑表示一次性完成判定。