boto3.upload_file() 不支持大文件高效上传,因其单线程同步、无断点续传;应使用分片上传(multipart upload)配合多线程调用 upload_part(),并自行管理 partnumber、偏移读取、etag 收集、续传及清理。

为什么不能直接用 boto3.upload_file() 传大文件
因为 boto3.upload_file() 是单线程同步上传,大文件(比如 >100MB)会卡住主线程、无法断点续传、失败就得重头来。S3 原生支持分片上传(Multipart Upload),但必须手动管理每个 part 的上传和最终合并——而 boto3 的 upload_fileobj() 虽然底层用了分片,却默认不并发;想真正并行,得自己调 create_multipart_upload + upload_part + complete_multipart_upload。
怎么用多线程跑 upload_part()
核心是:把文件按固定大小(如 5MB/块)切片,每个 slice 丢给一个线程调 upload_part(),并记录 PartNumber 和返回的 ETag。注意三点:
-
PartNumber必须从 1 开始连续,不能跳号(哪怕某 part 上传失败重试,也复用原编号) - 每个
upload_part()必须传入原始文件句柄的对应偏移 + 长度,不能用seek()后读整个文件——多线程下seek()会互相干扰 - 必须用线程安全的容器收集结果,比如
threading.local()或带锁的dict,避免ETag错位
示例关键片段:
def upload_part(args):
s3_client, bucket, key, upload_id, part_number, file_path, offset, length = args
with open(file_path, 'rb') as f:
f.seek(offset)
data = f.read(length)
resp = s3_client.upload_part(
Bucket=bucket,
Key=key,
PartNumber=part_number,
UploadId=upload_id,
Body=data
)
return {'PartNumber': part_number, 'ETag': resp['ETag']}
<h1>启动线程池(不要超过 10,S3 对同一 upload_id 的并发 part 有限制)</h1><p>with ThreadPoolExecutor(max_workers=8) as executor:
futures = [
executor.submit(upload_part, (s3_client, bucket, key, upload_id, i+1, file_path, i*part_size, part_size))
for i in range(num_parts - 1)
]</p><h1>最后一块可能更小,单独处理</h1><pre class="brush:python;toolbar:false;">last_size = os.path.getsize(file_path) - (num_parts - 1) * part_size
futures.append(executor.submit(upload_part, (s3_client, bucket, key, upload_id, num_parts, file_path, (num_parts-1)*part_size, last_size)))
parts = [f.result() for f in futures]
上传失败后怎么续传
分片上传不是自动续传的。你得自己存 UploadId 和已成功上传的 PartNumber 列表(比如写进本地 .upload_state.json)。下次启动前先调 list_parts(),比对已传 part 和总 part 数,只补传缺失的。注意:
-
list_parts()返回的PartNumber是升序,但不保证连续——要自己算缺哪些 - 如果
UploadId已过期(S3 默认 7 天),list_parts()会报错NoSuchUpload,此时只能新建上传 - 别在没
complete或abort的情况下反复建新UploadId,否则 S3 里会堆积大量未完成上传,产生费用
比 s3transfer 自带的并发上传强在哪
boto3 底层的 s3transfer 确实支持并发(通过 Config(max_pool_connections)),但它把分片和线程调度全包了,你没法干预重试逻辑、监控每个 part 进度、或做自定义加密/压缩。真要细粒度控制(比如:只对 part >10MB 的启用 gzip、失败时降级到单线程、上传中动态调速),就必须绕过 upload_file(),直调 multipart API。这也意味着——你得自己处理 abort_multipart_upload 清理,不然中断后残留的 upload 会一直计费。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











