大文件传到 48 GiB 就断:S3 分片上传的参数怎么算,以及那些没人清的残片

一次数据库全量备份直接管道进对象存储,命令大概长这样:

bash 复制代码
pg_dump -Fc mydb | zstd -T0 | rclone rcat remote:backup/full-20260804.dump.zst

跑了两个多小时,传到 48 GiB 出头的位置直接退出,报的是分片编号超出允许范围。网络没断,磁盘没满,桶也没配额限制。重跑一次,还是同一个位置。

问题不在链路上,在一道乘法上:分片上传最多 10,000 片,rclone 默认分片 5 MiB,5 MiB × 10000 ≈ 48.8 GiB。流式上传事先不知道总大小,客户端没法替你把分片调大,于是天花板就卡死在这儿。

一、先把这道乘法算清楚

S3 的分片上传(Multipart Upload)有三条硬约束,绝大多数 S3 兼容实现都照着做:

  • 单个分片 5 MiB 到 5 GiB,最后一片允许小于 5 MiB;
  • 一次上传最多 10,000 片
  • 单对象上限 5 TiB,不走分片的单次 PUT 上限 5 GiB。

推论只有一句话:能传多大的对象 = 分片大小 × 10000

这张表最该记的是第三列。同样传一个 200 GiB 的归档,5 MiB / 8 MiB / 16 MiB 三档全部会撞上 10,000 片的墙,只有把分片提到 32 MiB 以上才有富余。而 8 MiB 恰好是 AWS CLI 和 boto3 的出厂默认值。

二、知道文件大小时,SDK 会偷偷替你改参数

这里有个容易被误会的点:用 aws s3 cp 传一个 200 GiB 的本地文件,并不会报错。因为 boto3 底层的 s3transfer 里有一个分片调整器,发现按当前分片会超过 10,000 片,就自己往上调,只在 debug 日志里留一行提示。

python 复制代码
from s3transfer.utils import ChunksizeAdjuster

adj = ChunksizeAdjuster()
size = 200 * 1024**3          # 200 GiB
print(adj.adjust_chunksize(8 * 1024**2, size))   # 8 MiB -> 自动放大

rclone 也有类似行为:本地文件已知大小时会自动抬高分片并打一行 warning。真正会失败的是"大小未知"的那一类上传 ------ rclone rcat、管道流、Transfer-Encoding: chunked 的上传、以及自己手写 CreateMultipartUpload + UploadPart 循环的代码。这些路径上没人替你算乘法。

已知大小的场景,把参数写进配置一劳永逸:

bash 复制代码
aws configure set default.s3.multipart_threshold 64MB
aws configure set default.s3.multipart_chunksize 64MB
aws configure set default.s3.max_concurrent_requests 20

未知大小的流式场景,只能自己声明分片:

bash 复制代码
# 流式上传时显式抬高分片,把上限从 48.8 GiB 提到 625 GiB
pg_dump -Fc mydb | zstd -T0 | \
  rclone rcat --s3-chunk-size 64M remote:backup/full-20260804.dump.zst

或者干脆先落盘再传。多占一份临时磁盘,换回可断点续传、可校验、可重试单片的能力,多数备份场景里这笔账是划算的。

三、并发不是越大越好,先算内存

分片调大之后,第二个坑马上跟上来:内存。分片上传的每一片在发出去之前通常要在内存里攒齐,所以峰值内存大致是

复制代码
峰值内存 ≈ 分片大小 × 单文件并发数 × 同时传输的文件数

按 rclone 的默认值 --transfers 4--s3-upload-concurrency 4,如果把分片提到 64 MiB,就是 64 MiB × 4 × 4 ≈ 1 GiB。在一台 2 GiB 内存的备份机上跑,OOM 只是时间问题。

boto3 这边同样要显式约束:

python 复制代码
import boto3
from boto3.s3.transfer import TransferConfig

cfg = TransferConfig(
    multipart_threshold=64 * 1024**2,
    multipart_chunksize=64 * 1024**2,
    max_concurrency=8,            # 并发线程数
    use_threads=True,
)
boto3.client("s3").upload_file("big.tar.zst", "backup", "big.tar.zst", Config=cfg)

调参的取舍很直白:

  • 分片调大:请求数少、元数据开销小、更容易跑满带宽;代价是单片失败重传的成本变高,内存占用线性上涨。
  • 并发调高:能压满网卡;代价是内存翻倍,且容易把同一前缀打到限流阈值上(AWS S3 的公开指标是每前缀约 3,500 次写、5,500 次读每秒,自建集群的阈值取决于磁盘和 CPU)。
  • 经验起点:千兆网、内存充裕,分片 32--64 MiB、并发 8--16;内存紧张就先砍并发,别砍分片。

四、传失败之后:那些还在计费的残片

上传中断最贵的后果不是重传,是残片 。已经上传成功的分片会一直躺在服务端,直到你显式 Complete 或者 Abort。在没 Complete 之前它们不出现在 ListObjects 里,s3 ls 看不见,但空间照占、账单照算。

先查有多少:

bash 复制代码
# 列出桶里所有未完成的分片上传
aws s3api list-multipart-uploads --bucket backup \
  --query 'Uploads[].{Key:Key,Id:UploadId,Init:Initiated}' --output table

# 手动中止某一次
aws s3api abort-multipart-upload --bucket backup \
  --key full-20260804.dump.zst --upload-id "<UploadId>"

用 mc 的话是 mc ls --incomplete myminio/backupmc rm --incomplete --recursive

手动清一次治标,规则挂上去才治本。给桶加一条生命周期规则,让服务端自动回收超过 7 天的残片:

json 复制代码
{
  "Rules": [
    {
      "ID": "abort-stale-multipart",
      "Status": "Enabled",
      "Filter": { "Prefix": "" },
      "AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 7 }
    }
  ]
}
bash 复制代码
aws s3api put-bucket-lifecycle-configuration \
  --bucket backup --lifecycle-configuration file://abort-mpu.json

天数别设太小。7 天是个安全值:既不会让残片过夜太久,也不会误杀一个正在慢慢传的超大对象。如果确实有跨天的上传任务,把规则改成按前缀生效,别一刀切到全桶。

五、校验:分片对象的 ETag 不是 MD5

传完之后想比对完整性,很多脚本习惯拿 ETag 去和本地 md5sum 对:

bash 复制代码
aws s3api head-object --bucket backup --key full.dump.zst --query ETag
# "9c7a3f...e12-3200"

后面那个 -3200 就是提示:这是分片对象,ETag 是"每片 MD5 拼起来再算一次 MD5",还带上分片数量后缀。它跟整个文件的 MD5 天然对不上,而且分片大小一变,ETag 就变,所以它也不能当作跨集群比对的依据。

要可靠校验,改用显式 checksum:

bash 复制代码
aws s3api put-object --bucket backup --key small.bin \
  --body small.bin --checksum-algorithm CRC32C

分片上传时给每一片带上 checksum,Complete 时服务端会做整体校验。这条路的代价是客户端要多算一遍哈希,CPU 占用会上去一点;换来的是"传完就知道对不对",不用事后再下载一遍比对。

六、下次遇到大文件上传失败,按这个顺序查

  1. 先看报错里有没有 part number / part size 字样。有的话直接算乘法:分片大小 × 10000 是不是够。
  2. 确认这是不是流式上传 。管道、rcat、chunked 编码,客户端不会替你调分片,必须手动指定。
  3. 算峰值内存:分片 × 单文件并发 × 文件并发。跟机器可用内存对一下,OOM 和"网络抖动"长得很像。
  4. 查残片list-multipart-uploads 跑一遍,看看历史上失败的上传攒了多少空间。
  5. 挂生命周期规则AbortIncompleteMultipartUpload 设 7 天,这条规则应该是新桶的默认动作之一。
  6. 别用 ETag 做完整性校验,改 checksum。

这些约束是 S3 协议层面的,换成哪家实现都得遵守。自建这一侧的差别主要在两处:分片的落盘与合并路径快不快,以及有没有把残片回收做进生命周期管理里。RustFS 这类 S3 兼容实现走的是同一套 API 语义,上面这些命令可以原样跑,不用改客户端代码 ------ 迁移时真正要重测的,反而是分片大小和并发这组参数在新集群上的最优值,因为它跟磁盘数量、纠删码配置直接相关。

先跑第 4 步。大概率你会在某个桶里翻出几十 GiB 谁也不记得的残片。

相关推荐
foggyprojects1 小时前
客户说“销售额”,系统怎样落到订单含税金额或开票含税销售额?
后端
十六年开源服务商1 小时前
导航栏设计优化:WordPress网站转化率提升实战指南
开源
ZJU_统一阿萨姆1 小时前
【Git】Github 开源许可证详解
git·开源·github
念何架构之路1 小时前
路由注册:RouterGroup(routergroup.go)
开发语言·后端·golang
小当家.1051 小时前
LLM服务缓存与连接池管理:三层缓存架构与ChatClient池化
java·后端·spring·缓存·llm·agent
人间凡尔赛2 小时前
当 AI Agent 攻陷网关:Istio agentgateway 与 Gateway API Inference Extension 实战解析
后端·云原生·架构
小小龙学IT2 小时前
gRPC 开源高性能 RPC 框架深度解析:从 HTTP/2 到跨语言微服务实战
http·rpc·开源
wwwzhouzy2 小时前
SpringBoot 响应式编程
java·spring boot·后端·响应式编程
Python私教2 小时前
请求超时不等于失败:把写操作恢复设计成事实回读
后端·python