OBS Python SDK 大文件分段上传与断点续传实践

举报
ack 发表于 2026/11/03 10:23:06 2026/11/03
【摘要】 场景:5GB 备份文件,一次 put 传不上去 我在华为云 ECS(Ubuntu 22.04)上有一个每日全量备份脚本,产物是一个约 5GB 的数据库导出文件,需要定期归档到 OBS 桶。最开始用的是最直观的写法: pOBS Python

场景: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 内部对超过阈值的大文件自动转分段),但断点续传仍然要自己实现上面这套逻辑。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。