Multipart 提交失败可能让上传卡死
本记录说明在下一版本地验收中发现的一个缺陷。它在 69fcf50cd 上能复现,继承自上游 MinIO。目前还没有对应的 GitHub issue。
**状态:**已分析并复现。起草过一版修复,但对抗评审认为它对本版风险太大,予以否决。修复推迟到下一版,与存储操作的完成确认(fencing)一起做。下一版把它列为已知问题。
简短回答
CompleteMultipartUpload 没有达到写 quorum 时,这个上传可能变得无法使用,即使之后所有盘都恢复正常:
- 再次 Complete 返回 503;
- 用同一个 upload ID 重新上传分片返回 503;
- ListParts 列不出任何分片。
上传会一直保持这个状态,直到过期清理把它删掉,客户端只能新建上传、重新发送所有分片。如果 rename 成功的盘数超过校验盘数,已上传的数据甚至无法重建。还有一种变体:某块盘执行了 rename,但应答丢失,重试会返回 404 NoSuchUpload,客户端可能把它理解成“之前的 Complete 已经成功”。
这个缺陷不影响已提交的对象。受损的是进行中的上传,而且只在提交时恰好遇到盘故障时发生。
把分片移回、恢复上传元数据的小修复能让复现测试通过,但它会和迟到的存储操作、以及过期上传清理发生竞争。在这些竞争下,它可能发布一个没有数据的对象,或者把一块没有分片的盘算作写入成功。那会把一个可用性缺陷变成数据缺陷,所以不进本版。
**临时办法:**如果 Complete 返回 503,并且在集群恢复正常后仍然失败,就中止这个上传,重新上传对象。
定性
| 方面 | 评估 |
|---|---|
| 性质 | multipart 提交的失败路径丢弃了上传。继承自上游 MinIO,从 2020 年起就存在。 |
| 影响 | 提交时遇到盘故障的上传必须从头重传;在应答丢失的变体中,还可能看起来已完成,实际没有。 |
| 数据安全 | 这个缺陷不删除已提交的对象。rename 成功的那些盘上,进行中的上传数据会丢失。同一撤销步骤里还有一个更早就存在的缺口(T4),可能丢失与旧版本并存、被覆盖的 null 版本。 |
| 严重度 | S2:可用性和重试缺陷,需要 Complete 期间出现盘故障。 |
| 工作量 | 安全的修复属于中等:它依赖存储操作的 fencing。 |
何时发生
Complete 原地把上传 rename 成对象。以 4 块盘、EC 2+2(写 quorum 3)为例,下列情况会触发:
- rename 在 2 块盘上失败,在另外 2 块上成功(权限错误、盘满、盘故障);
- 一块盘离线,另一块盘上 rename 失败;
- 一块盘执行了 rename 但应答丢失,另一块盘上 rename 失败。
如果只有一块盘的 rename 失败,quorum 仍然达到,提交成功。如果失败的盘不止一块,但上传仍保留在足够多的盘上,那么第一次重试会返回 400 InvalidPart,只有重新上传分片才行。
根因
行号对应下一版的本地集成;69fcf50cd 上是同一条代码路径。
- 提交之前,Complete 在每块盘上删除每个已完成分片的
part.N.meta(cmd/erasure-multipart.go:1663-1678、:1725)。这时还不知道提交能否达到 quorum。 - 提交调用纠删层的
renameData,把上传目录 rename 成对象(:1734)。在每块盘上,xlStorage.RenameData:- 用合并后的对象
xl.meta覆盖上传的xl.meta(cmd/xl-storage.go:2900); - 把上传的数据目录(包含所有分片)移进对象(
:2926); - 把
xl.meta也移过去(:2965); - 删除上传目录(
:2989-2996)。
- 用合并后的对象
- 没达到写 quorum 时,撤销步骤在 rename 成功的盘上调用
DeleteVersion(UndoWrite)(cmd/erasure-object.go:1140-1167),把这个版本的数据目录,也就是上传的分片,移进回收站(cmd/xl-storage.go:1387-1412)。没有任何东西被移回.minio.sys/multipart。
结果是,上传只留在 rename 失败的那些盘上,而且那里保存的是合并后的对象元数据,不是上传自己的元数据。这些盘不够写 quorum,所以 PutObjectPart 和 Complete 返回 503;所有盘上都没有 part.N.meta,所以 ListParts 什么也列不出。
来源
- 上游 #9377(
4915433bd,2020 年)把原来“把上传 rename 回去”的撤销,换成了丢弃数据的版本删除。在此之前,失败的 Complete 可以重试。 - 上游 #15346(
f939d1c18,2022 年)引入了part.N.meta,以及在 rename 之前删除它。 - 把合并后的元数据写回源目录,来自
2d58a8d86(2020 年)。
方案
方案 A:撤销步骤把分片移回
只对 multipart 来源:在原有的 DeleteVersion(UndoWrite) 之前,先把数据目录 rename 回上传目录。这是必要的,但单靠它不够:分片元数据已经在所有盘上被删掉,上传元数据也已被移走或覆盖。
方案 B:等调用方决定之后再删上传目录
某块盘上 rename 成功时,上传目录已经空了,推迟删除省不下任何东西。而且这需要改存储 RPC,所有节点都要升级。不采用。
方案 C:用仍保留上传的盘重建上传
数据可能已经没了,分片 ETag 也丢了;而且少数盘上的残留,无法和一次已经成功的 Complete 或 Abort 留下的残余区分开,可能导致同一个上传被提交两次。不采用。
方案 D:把 part.N.meta 保留到提交之后
这会改变成功路径,崩溃后还会在对象目录里留下分片元数据;而且仍然需要方案 A 和元数据恢复。不采用。
方案 E:返回 NoSuchUpload,或者直接中止上传
这不是修复。S3 客户端重试 Complete 时收到 NoSuchUpload,会理解为“可能已经成功”。
起草的修复,以及为什么暂缓
起草的修复是方案 A 加上 Complete 中的恢复步骤:保存每块盘上上传的元数据;提交失败时,在每块盘上先用 CheckParts 检查分片是否齐全,再重写每个 part.N.meta,最后重写上传的 xl.meta。代码约 45 行,只改失败路径,不改存储 RPC 和格式,复现测试的所有用例都能通过。
对抗评审否决了它进入本版:
- **迟到的 rename 可能发布一个空对象。**远端 rename 可能先暂存对象元数据,在移动数据之前停住,而调用方此时已经看到了错误。恢复步骤把上传元数据写回去之后,迟到的 rename 继续执行,就会把它当作对象发布:大小为零,没有分片。迟到的撤销同样可能删掉一次成功的重试,因为它只按复用的版本 ID 匹配。
- **过期上传清理不取锁。**它可能在
CheckParts通过之后删掉一个已过期的上传。恢复步骤的写入随后会重建一个只有分片元数据的数据目录,重试时可能把这块没有分片的盘算作写入成功。 - 空上传会丢失零长度的分片文件,恢复步骤没有重建它们。
- **恢复步骤不理会锁丢失,还可能在持锁期间运行很久:**分布式锁丢失后它仍继续写;分片成千上万的上传意味着每块盘成千上万次持久化写入。
- **数据移走之后的明确失败,仍会让上传卡住:**一块盘如果在移动分片之后、提交元数据之前失败,它既不会被撤销,也不会被恢复。
前两条会把可用性缺陷变成数据缺陷。它们都来自同一个缺失的保证:调用方无法判断一个返回错误、或者已被放弃的存储操作到底有没有执行完。这个缺口已经针对所有写路径单独跟进。
完整的修复需要什么
- 在复用上传路径之前,先能对每块盘上的
RenameData和撤销操作做 fencing,或确认其结果; - 过期上传清理与恢复步骤互斥,并在写完分片元数据之后重新校验;数据目录在最后一次校验之后绝不能被重建;
- 空上传的零长度分片文件;
- 锁丢失时取消、总时间预算,以及有上限的并行写入;
- 恢复已明确失败、但数据已被移走的那些盘,或者明确收窄修复范围。
后续修复的代价与兼容性
起草的做法不改存储 RPC 和盘上格式,只有协调节点运行新逻辑。存储操作的 fencing 可能改变这一点,因为它需要盘按代际报告或拒绝操作。相关设计归入存储操作完成确认的单独跟进。
验证
一个确定性的单元测试在 4 块盘、EC 2+2 的纠删集上,上传 5 MiB 和 1 MiB 两个分片,制造盘故障后执行 Complete,再恢复这些盘,然后要求:ListParts 列出两个分片;直接重试和重新上传后的 Complete 都成功;内容一致;没有哪块盘持有对象元数据却缺少分片。在 69fcf50cd 和本地集成上,所有用例都失败:
| 场景 | 当前结果 |
|---|---|
| 2 块盘上的桶目录只读 | 卡住,503 |
| 2 块盘拒绝 rename | 卡住,503 |
| 一块盘离线,另一块拒绝 rename | 卡住,503 |
| 一块盘执行了 rename 但应答丢失,另一块拒绝 | 404 NoSuchUpload |
| 覆盖未开版本控制的对象,2 块盘拒绝 rename | 卡住,旧对象保留 |
| 空对象 | 卡住 |
进程级验收在 69fcf50cd 和下一版上,以同样的结果复现了桶目录只读和盘离线两种情况。
顺带发现的相关缺陷
- 已上传、但没有列入 Complete 清单的分片,会被移进对象的数据目录,成为孤立文件。上游从 2022 年起就存在。
- 当足够多的盘因为权限错误导致提交失败时,Complete 返回 403 而不是 503,客户端可能不会重试。
两者都列入下一版跟进。
评审记录
- 对抗评审第 1 轮:否决进入本版,理由见起草的修复,以及为什么暂缓。评审还纠正了本记录的几处说法:起草的修复并不充分,遗留竞争被低估了,“已提交的对象不会丢失”也说得太宽,因为撤销步骤里还有那个更早的缺口。