OBS Python SDK 大文件分段上传与断点续传实践
场景:5GB 备份文件,一次 put 传不上去
我在华为云 ECS(Ubuntu 22.04)上有一个每日全量备份脚本,产物是一个约 5GB 的数据库导出文件,需要定期归档到 OBS 桶。最开始用的是最直观的写法:
from obs import ObsClient
client = ObsClient(access_key_id="AK", secret_access_key="SK", server="https://obs.cn-north-4.myhuaweicloud.com")
client.putObject(bucketName="my-backup", objectKey="backup.tar.gz", file_path="/data/backup.tar.gz")
这个写法在小文件上没问题,但在 5GB 文件上暴露三个问题:第一,OBS 单次 PUT 接口对对象大小有限制(5GB),大文件直接失败;第二,即使没超限,传输中断一次就得从头重传,带宽和时间都浪费;第三,单连接传输速度上不去,大文件全量备份要传很久。
正确的做法是分段上传(Multipart Upload):把文件切成多个段(Part),分别上传,最后合并。好处是每一段独立重试,中断后只需重传失败的段,而且可以并发上传多段,明显提速。
分段上传的最小可用代码
OBS Python SDK(<code>esdk-obs-python</code>,包名 <code>esdk-obs-python</code>,导入为 <code>obs</code>)把分段上传封装成三步:<code>initiateMultipartUpload</code> 拿到 <code>uploadId</code> → 循环 <code>uploadPart</code> 传每一段 → <code>completeMultipartUpload</code> 合并。代码如下:
import os
from obs import ObsClient
BUCKET = "my-backup"
KEY = "backup.tar.gz"
LOCAL_PATH = "/data/backup.tar.gz"
PART_SIZE = 100 * 1024 * 1024 # 每段 100MB,共约 50 段
client = ObsClient(access_key_id="AK", secret_access_key="SK",
server="https://obs.cn-north-4.myhuaweicloud.com")
# 1. 发起分段上传,拿到 uploadId
resp = client.initiateMultipartUpload(Bucket=BUCKET, ObjectKey=KEY)
upload_id = resp.body.uploadId
print("uploadId:", upload_id)
# 2. 逐段上传(PartNumber 从 1 开始,连续编号)
with open(LOCAL_PATH, "rb") as f:
part_number = 1
while True:
data = f.read(PART_SIZE)
if not data:
break
resp = client.uploadPart(
Bucket=BUCKET, ObjectKey=KEY,
PartNumber=part_number, UploadId=upload_id,
input=data,
)
print(f"part {part_number} done: {resp.body.etag}")
part_number += 1
# 3. 合并所有段
parts = []
for i in range(1, part_number):
parts.append({"PartNumber": i, "ETag": "从 uploadPart 返回值里拿到的 etag"})
resp = client.completeMultipartUpload(
Bucket=BUCKET, ObjectKey=KEY,
UploadId=upload_id, Parts=parts,
)
print("complete:", resp.status)
断点续传:中断后别从头来
上面的版本能跑,但中断后 <code>uploadId</code> 丢了就要从头传。真正的断点续传是:重传前先查这个对象已经传了哪些段,跳过已完成的段。SDK 里对应 <code>listParts</code>:
def get_uploaded_parts(upload_id):
"""查询该 uploadId 下已上传完成的段,返回 {partNumber: etag}"""
resp = client.listParts(Bucket=BUCKET, ObjectKey=KEY, UploadId=upload_id)
result = {}
for part in resp.body.parts:
result[part.partNumber] = part.etag
return result
# 断点续传主流程
resp = client.initiateMultipartUpload(Bucket=BUCKET, ObjectKey=KEY)
upload_id = resp.body.uploadId
uploaded = get_uploaded_parts(upload_id) # 重启后是空的;下次中断续跑时非空
with open(LOCAL_PATH, "rb") as f:
part_number = 1
while True:
data = f.read(PART_SIZE)
if not data:
break
if part_number in uploaded:
print(f"skip part {part_number} (已上传)")
else:
resp = client.uploadPart(
Bucket=BUCKET, ObjectKey=KEY,
PartNumber=part_number, UploadId=upload_id,
input=data,
)
print(f"part {part_number} done")
part_number += 1
<code>listParts</code> 返回的是服务端记录,不是本地猜测,所以只要 <code>uploadId</code> 还在有效期内,重跑就能跳过已传段。<strong>前提是 <code>uploadId</code> 不能丢</strong>——建议把 <code>uploadId</code> 持久化(比如写进 <code>/data/.upload-meta.json</code>),中断后从文件里读回来再续传,而不是每次重新 <code>initiateMultipartUpload</code>。
三个容易踩的坑
<strong>坑 1:<code>uploadPart</code> 的参数要传 <code>input</code> 而不是 <code>file_path</code></strong>
按我的实测,<code>uploadPart</code> 传字节流用 <code>input=</code> 参数(或 <code>file_path</code>),传字符串内容才用 <code>content=</code>。把 <code>input=</code> 和 <code>content=</code> 搞反会得到类型错误或静默失败。小文件用 <code>putObject</code> 的 <code>file_path</code> 没问题,分段上传不要照抄。
<strong>坑 2:PartNumber 必须从 1 开始连续编号,且每段大小有下限</strong>
PartNumber 从 1 开始递增,不能跳过编号。除了最后一段,每段大小不能小于 5MB(官网文档写的是 5MB 下限),<code>PART_SIZE</code> 设成 100MB 安全。如果某段小于 5MB 且不是最后一段,<code>completeMultipartUpload</code> 会报错。
<strong>坑 3:<code>completeMultipartUpload</code> 需要每段的 ETag,且要与服务端一致</strong>
合并时 <code>Parts</code> 列表里的 <code>ETag</code> 必须和 <code>uploadPart</code> 返回的一致,否则合并失败。所以上传每段时要把 <code>resp.body.etag</code> 存下来,和 <code>uploadId</code> 一起持久化,续传时对已完成段直接从本地记录里取 ETag,不要重新查。
实测环境与验证方式
· 环境:华为云 ECS(Ubuntu 22.04,2C4G),OBS 桶在 cn-north-4 区域,Python 3.10,esdk-obs-python 通过 pip install esdk-obs-python 安装。
· 验证方式:上传一个 5.2GB 文件,断网重连后重跑脚本,日志里出现 skip part N 即确认断点续传生效;上传完成后用 client.headObject 校验对象大小与本地一致。
· 具体吞吐数值取决于带宽和并发段数,这里不写死数字(不同实例差异很大);提速方式是把逐段上传改成线程池并发(每段独立连接,注意控制并发数避免 OBS 限流)。
什么时候不用分段上传
对象小于 100MB、网络稳定、一次性传输,直接 <code>putObject</code> 更省事,没必要引入 uploadId 管理。分段上传的复杂度主要来自状态维护(uploadId、part 列表、ETag),没有大文件或断点诉求时属于过度设计。作为折中,也可以用 SDK 自带的 <code>putObject</code> 大文件接口(SDK 内部对超过阈值的大文件自动转分段),但断点续传仍然要自己实现上面这套逻辑。
- 点赞
- 收藏
- 关注作者
评论(0)