跳转到主要内容

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 上是同一条代码路径。

  1. 提交之前,Complete 在每块盘上删除每个已完成分片的 part.N.meta(cmd/erasure-multipart.go:1663-1678、:1725)。这时还不知道提交能否达到 quorum。
  2. 提交调用纠删层的 renameData,把上传目录 rename 成对象(:1734)。在每块盘上,xlStorage.RenameData:
    • 用合并后的对象 xl.meta 覆盖上传的 xl.meta(cmd/xl-storage.go:2900);
    • 把上传的数据目录(包含所有分片)移进对象(:2926);
    • 把 xl.meta 也移过去(:2965);
    • 删除上传目录(:2989-2996)。
  3. 没达到写 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 轮:否决进入本版,理由见起草的修复,以及为什么暂缓。评审还纠正了本记录的几处说法:起草的修复并不充分,遗留竞争被低估了,“已提交的对象不会丢失”也说得太宽,因为撤销步骤里还有那个更早的缺口。

参考