分片、断点续传与临时文件:失败传输怎样恢复并清理
一个 2 GiB 文件传到 90% 时连接断开,恢复传输需要知道三件事:此前的数据保存在哪里,哪些字节已经确认,剩余字节还能否接到同一个文件上。客户端进度条保存的是发送进度;服务器保存的上传会话,才决定下一次请求能从哪里继续。
分片降低了一次失败需要重传的数据量,也引入了独立存在的临时数据。上传、恢复、合并与清理必须使用同一个会话身份。
断点由什么协议和记录表达
四种容易混淆的“分块”
| 机制 | 被划分的对象 | 恢复时查询什么 | 常见用途 |
|---|---|---|---|
| HTTP multipart/form-data | 一次请求中的表单字段和文件 part | 没有标准化的跨请求上传检查点 | 浏览器表单上传 |
| HTTP 消息分帧、HTTP/1.1 chunked | 一次消息的传输编码或帧 | 连接断开后通常由上层重发请求 | 未知长度传输 |
| S3 Multipart Upload | 对象存储上传会话中的独立 part | uploadId、已上传部件及其 ETag | 大对象并行上传 |
| tus offset 协议 | 上传资源的连续字节区间 | 服务器返回的 Upload-Offset | 浏览器、移动端断点续传 |
下载的 Range 是另一方向:客户端读取服务器已有表示的某一段。它并不定义怎样把新字节写入服务器。普通表单和区间下载的请求结构见上传、下载与流式处理。
tus 的核心流程是先用 HEAD 取得当前偏移,再用 PATCH 携带相同偏移和后续字节;偏移不符时返回冲突,客户端重新查询。并行拼接、校验和、过期及终止属于可协商的扩展,选用实现时要核对它实际支持哪些扩展。tus 协议规范
如果接入的对象存储已经提供 S3 multipart,应用通常只需组织会话和业务授权。自行设计 offset 接口适合需要统一不同存储后端的场合,但还要自行承担并发写入、崩溃恢复及持久化偏移的一致性。
一个 S3 上传会话包含哪些字段
业务上传记录
├─ applicationUploadId 对外标识;每次访问都检查租户和操作权限
├─ sourceIdentity 源文件版本、总长度、校验算法与预期摘要
├─ destination bucket + 独占 object key
├─ providerUploadId CreateMultipartUpload 返回的存储会话
├─ partSize 常规部件大小;末片可以较小
├─ parts
│ ├─ partNumber 合并顺序,通常从 1 连续编号
│ ├─ offset / length 对应源文件的字节范围
│ ├─ checksum 应用采用的内容校验值
│ └─ ETag UploadPart 返回、Complete 需要的服务标记
└─ state / lease / expiry 是否接收新部件、谁在操作、何时可回收applicationUploadId 可以隐藏存储提供方细节。即便浏览器直接拿到 provider uploadId,它也只是定位参数;授权仍由签名、凭证和业务接口分别执行。上传者不能凭一个其他租户的 ID 查询清单、完成文件或中止会话。
S3 允许各部件独立上传,再按 Complete 请求中提交的部件顺序组成对象。相同 uploadId 下再次上传相同 partNumber,会替换该编号原来的部件,包括内容不同的情况。ETag 应原样保存;多部件对象、加密和不同存储实现下,不能把它统一当作整文件 MD5。S3 Multipart Upload 概述
如果产品要求“同一部件的重试只能是相同内容”,需要在应用数据库增加 (uploadId, partNumber) 唯一记录,保存预期大小和摘要,在发放上传权限或接收数据时检查冲突。存储端的覆盖规则不会代替这个检查。浏览器直传时还要把校验值绑定到提供方支持的签名请求参数,并在完成前验证;单靠浏览器口头回报一个 hash,服务端无法信任内容。
分片大小和并发一起决定成本
以 64 MiB 部件上传 2 GiB 文件,共有 32 个部件。失败一个部件时只重传这 64 MiB。若每个请求先把部件读入堆,8 路并发仅载荷就需要约 512 MiB。
换成文件区间流可以降低堆占用。每条上传仍占用连接和读盘带宽,源文件也要保留到可能发生的重试结束。
小部件提高请求次数和清单规模,大部件提高失败重传量。先根据文件规模、可用带宽、客户端内存与重试成本选择部件大小,再给每个用户和整个服务设置并发上限。等待队列也要有限,不能把暂时没有连接的请求无限堆积在内存里。
S3 的常用约束是非末片至少 5 MiB;部件数、最大部件及对象大小应以目标服务当前限制为准。小文件直接 PUT 通常更简单,无法只依据文件“大”或“小”给所有系统设定同一分片阈值。S3 Multipart Upload 限制表
用真实对象存储保存并恢复上传
环境、身份与第一份持久化记录
下载并解压 S3 实验工程,进入 file-s3-lab 目录。操作环境为 Linux、Bash、Docker Engine、Compose v2、openssl 和 unzip;当前用户需要 Docker 权限。构建和应用容器均使用宿主 UID/GID,工作目录与 Maven 缓存由该用户创建。
实验使用 Garage 2.2.0 单节点、AWS SDK for Java 2.54.13、Maven 3.9.12 和 Java 17。服务只绑定 127.0.0.1:18390,没有 TLS,不能作为公网服务。Garage 提供真实 S3 请求处理和本地持久化,但不完整实现 AWS IAM、对象版本化等功能,兼容性范围见 Garage S3 兼容表。
在空的解压目录依次执行:
umask 077
mkdir -p .m2
docker run --rm --user "$(id -u):$(id -g)" --entrypoint mvn \
-e MAVEN_CONFIG=/m2 \
-v "$PWD:/work" -v "$PWD/.m2:/m2" -w /work \
maven:3.9.12-eclipse-temurin-17 \
-B -Dmaven.repo.local=/m2 clean package
bash setup.sh
bash run.sh object work
bash run.sh begin work/upload-asetup.sh 生成随机 RPC 密钥、启动 Compose 服务、建立单节点布局,然后创建 file-lab 桶和仅该桶读写的访问密钥。节点 ID 从运行服务查询,不需要复制别人的机器标识。Garage 2.2 使用这些显式初始化步骤;新版快速入门的自动配置参数不能直接套到旧镜像上。Garage 快速入门
正常情况下,构建以 BUILD SUCCESS 结束,两个应用命令的关键输出为:
object: file upload/download exact; delete HEAD=404
begin: persisted session; confirmedParts=1; remainingBytes=123第一条实际执行文件上传、下载、逐字节比较与删除。第二条创建另一个独占对象名,上传 5 MiB 首片,把会话和 ETag 写入 work/upload-a/upload.properties,然后退出进程。剩余 123 字节尚未上传;此时 HEAD 最终对象不能当作查询上传进度的接口。
work/credentials.env、work/key.txt 和 work/garage.env 含敏感凭证,仅供本机实验使用。不要复制到公开日志、截图或仓库。再次启动已有环境时执行:
docker compose --env-file work/garage.env -p fs21 up -dsetup.sh 会拒绝覆盖已有密钥文件,以免重新生成 RPC secret 后把恢复环境弄坏。若 18390 已占用,先确认占用者;调整 Compose 映射时也要同步 run.sh 的 endpoint。
兼容设置必须有明确用途
AWS SDK 新版本会默认为支持的 S3 操作计算额外校验和。该实验的 Garage 2.2 与默认流式签名载荷组合会返回 400 Invalid payload signature。因此 run.sh 为这个本地兼容组合设置:
AWS_REQUEST_CHECKSUM_CALCULATION=WHEN_REQUIRED
AWS_RESPONSE_CHECKSUM_VALIDATION=WHEN_REQUIRED签名鉴权仍然执行,实验还会比较完整下载字节。这里调整的是可选校验和行为,不是关闭 TLS 校验或忽略服务错误。接入 AWS S3 时应保留其适用的默认完整性保护,再根据业务要求选择校验算法,不要把本地兼容配置当成统一生产配置。AWS SDK S3 数据完整性设置
恢复进程怎样选择下一步
resume 入口重新读取同一 manifest,校验源内容身份,再查询存储侧部件。实验固定使用约 5 MiB 的可重复样本,便于直接观察两片合并;面向真实大文件时应读取同一不可变源文件的区间,不把整个文件装入 byte 数组。
SDK 调用之间的数据关系如下,完整可运行实现位于工程的 S3Lab.java:
String uploadId = s3.createMultipartUpload(b -> b
.bucket(bucket).key(key)).uploadId();
String firstETag = s3.uploadPart(b -> b
.bucket(bucket).key(key).uploadId(uploadId).partNumber(1),
RequestBody.fromBytes(firstPart)).eTag();
CompletedPart first = CompletedPart.builder()
.partNumber(1).eTag(firstETag).build();Create 成功后要尽快持久化 uploadId。如果进程在收到 Create 响应前退出,会留下无法仅靠本地 manifest 找回的存储会话;生产系统还需要按受控前缀列出未完成上传并对账。每次 UploadPart 成功后保存该部件的返回值。若保存前退出,可以重传同一源区间,使该编号恢复为预期字节。
ListParts 用于核对存储现状。生产清单超过一页时必须继续翻页,并把请求限定在当前会话,不能拿第一页的数量判断全部上传完成。Complete 的部件列表来自应用确认的上传结果;不要直接把服务上任意可见的 part 都纳入业务文件。
本地 manifest 先写同目录临时文件,再原子替换旧记录,这样不会用半份文本覆盖检查点。该实现适用于单一上传进程,未提供多进程锁、数据库事务或断电级落盘保证。
多实例应用需要用数据库版本条件更新记录;完成操作还要取得该会话的独占提交权,防止多个进程同时推进合并。
合并结果未知时,先核对已经发生的写入
重传和错误部件是可观察的服务行为
运行恢复入口时,实验先把首片的一个字节改掉并上传到同一个 partNumber,再用原始字节覆盖回来。两次 ETag 不同,最终读回等于原始样本。这验证了 Garage 的同号覆盖行为;实验是单一可信生产者,不包含多租户应用的摘要冲突拒绝接口。
接着程序使用错误 ETag 请求 Complete,并要求服务返回 InvalidPart。只有识别到该错误,才使用保存的正确部件列表完成上传。未知错误会中止,不能把任意异常当作负例验证成功。
Complete 的 HTTP 处理还有一个容易遗漏的细节:AWS S3 可能先返回 200 响应头,再在响应体中返回处理错误。SDK 会解析这种结果;自行发送 HTTP 请求时需要同时理解响应体,不能只拿状态行登记完成。CompleteMultipartUpload API
复现完成后未保存本地结果的窗口
在前面 begin 产生的会话上执行:
if bash run.sh resume-crash work/upload-a > work/interruption.log 2>&1; then
printf '%s\n' '预期的中断没有发生' >&2
exit 1
fi
grep -q 'INJECTED_AFTER_COMPLETE_BEFORE_MANIFEST' work/interruption.log || exit 1
bash run.sh resume work/upload-a
bash run.sh resume work/upload-aresume-crash 在真实 Complete 已成功后、保存本地 COMPLETE 之前抛出指定异常,进程非零退出。它模拟的是两次持久化之间的中断窗口,没有模拟网络丢包。失败判断同时检查退出结果和异常标识,其他连接或权限错误会让这段命令失败。
随后两次恢复的关键输出为:
resume: NoSuchUpload; exact completed object recovered; state=COMPLETE
resume: already complete; exact object verified第一次恢复查询 uploadId 得到 NoSuchUpload:存储侧已经结束上传会话。程序继续对会话独占的 UUID key 执行 HEAD,核对长度和预存 SHA-256 元数据,再完整 GET 并比较实际字节,最后补写本地 COMPLETE。第二次运行读到完成记录,只验证已存在的对象。
这里能够归属本次上传,依赖独占对象名和固定源内容。若所有人都向 reports/latest.xlsx 写入,HEAD 看到一个同样大的文件,也可能是另一个人的更新。生产系统可使用不可变 key 或明确 versionId 固定目标,再将业务文件记录切换到这个已验证版本。
预存 metadata 中的摘要是上传方提供的声明。实验通过实际下载比较验证字节;生产上可以使用服务支持并实际验证的对象校验和,或者异步读取检查。仅比较两处由客户端填入的同一个摘要字符串,不能发现上传内容错误。
NoSuchUpload 也可能来自过期回收或 Abort,因此它本身无法区分完成与取消。目标对象不存在时,保留原会话记录及请求标识,核对清理操作和重试历史,再决定新建会话。未经判断就分配新 uploadId,会同时失去恢复线索并制造更多临时部件。
临时数据怎样回收,失败怎样定位
先停止写入,再结束存储会话
正常取消可以先把应用会话标记为不再接受部件,等待已发放的上传操作退出,然后 Abort。S3 官方提醒:Abort 发出时,已在途的部件上传仍可能成功,因此要等待在途请求结束,必要时再次检查和中止,才能确认没有遗留部件。这个过程应有最大等待时间,超过后交给后台对账。S3 Multipart Upload 概述
实验的 Abort 路径没有并行上传,命令为:
bash run.sh abort work
bash run.sh begin work/upload-b
bash run.sh resume work/upload-b第一条真实创建会话、上传一个部件并中止,再要求 ListParts 返回 NoSuchUpload。后两条使用新的工作目录完成另一份上传,验证中止后仍可正常使用服务。这个实验验证顺序执行的中止,不声称复现了并发 Abort 竞态。
后台回收时,仅按目录修改时间删除是不够的。可以采用如下数据库操作顺序:用过期条件和版本号领取会话,禁止新的上传租约,确认已发出的写入均结束,执行 Abort,再记录终态。会话续租和清理领取要竞争同一版本条件;谁先成功,另一方就重新读取当前状态。对已经终止的会话重复清理应保持可重入。
临时文件也分几类,清理条件各不相同:
| 临时数据 | 使用者 | 可以回收的条件 |
|---|---|---|
| Servlet 或代理上传暂存 | 当前请求、解析器 | 请求结束并释放句柄;崩溃残留由单独扫描器处理 |
| 应用分片文件 | 上传会话、合并进程 | 会话已终止且没有持有者;保留恢复宽限 |
| S3 未完成部件 | 对象存储 multipart 会话 | Complete 或可确认的 Abort,另设未完成上传回收策略 |
| 上传源文件 | 重试和摘要计算 | 所有需要重放的请求已经结束 |
| manifest 和错误记录 | 恢复程序、审计排查 | 对象及业务记录完成对账,满足留存要求 |
finally 能处理正常异常退出,无法在 SIGKILL、主机掉电或磁盘丢失时执行。临时目录应可被新进程枚举,并能从记录判断归属。扫描器要限制每批数量和删除速率,避免回收任务抢占正常读写带宽。
磁盘不只会耗尽字节
大批小部件可能先用完 inode;合并时若同时保留原部件和新文件,会短暂占用接近两份数据空间。df -h 查看可用容量,df -i 查看 inode。挂载盘、容器可写层和代理临时目录分别统计,不能只看应用数据卷。
本地合并应在私有目录写完整新文件,核对长度和摘要后发布。跨文件系统移动可能退化或无法执行原子移动;需要复制时,先保留源文件,等目标核对完成再删除源。Java 文件移动的保证与文件系统有关,相关选项见 JDK Files 文档。
合并失败的日志至少关联 applicationUploadId、存储请求 ID、当前步骤和错误类型。不要记录长期访问密钥、完整预签名 URL 或完整用户路径。对象名含个人信息时也需要脱敏。
从返回结果选择下一步
| 现象 | 首先确认 | 下一步 |
|---|---|---|
| 403、签名错误 | 凭证有效性、桶权限、region、endpoint、时钟、已签名头 | 修正实际请求,不以重传整个文件掩盖授权错误 |
| InvalidPart | ETag 是否来自该 uploadId 的该编号,部件是否被覆盖 | 查询并核对清单,重传确定的源区间,重新保存结果 |
| EntityTooSmall 或尺寸错误 | 非末片大小和最终列表中的末片位置 | 调整分片计划后重建会话,不能只改列表里的长度 |
| NoSuchUpload | 会话是否完成、过期、中止,目标对象是否能归属 | 核对固定对象及清理记录后决定恢复或重建 |
| Complete 超时 | 请求是否已送达、固定对象是否已出现 | 保留未知结果,查询对象,不立即把业务状态写成失败 |
| ENOSPC、inode 耗尽 | 代理、应用、合并区和数据卷的实际使用 | 停止接收新大上传,保留活跃会话,按归属回收已过期数据 |
| 重试后摘要不同 | 源文件是否在上传期间改变、区间是否重叠或遗漏 | 固定源版本,拒绝继续拼接不同内容 |
Complete 超时后,恢复程序依靠原会话和固定对象继续核对;凭证或部件参数被明确拒绝时,先修正请求再提交。直接重传整个文件会丢掉这两类结果之间的重要区别。
实验完成后停止本组服务,保留数据以便继续分析:
docker compose --env-file work/garage.env -p fs21 downstorage 中仍有存储数据,work 中仍有会话记录和凭证。需要继续检查未完成上传时,保留两者;确认这些记录不再用于恢复后,才清理自己创建的实验目录。
权威资料与规范地址
续传协议与分段上传语义
- tus 断点续传协议:offset、校验、过期和终止扩展
- S3 Multipart Upload:会话、部件、覆盖及中止
- S3 Multipart Upload 限制表
- CompleteMultipartUpload API 和错误响应
