4.2 对象存储 COS:文件与模型存取

场景:服务器硬盘开始不够用了

工具箱上线一个月,服务器磁盘告警了。一看占用:用户上传的 Excel 和 PDF 堆了 8 个 G,RAG 用的 Embedding 模型文件 2 个 G,还有每天导出的分析报告。更麻烦的是我想加一台 worker 机器分压,发现文件都在第一台的本地硬盘上,迁移成本高得离谱。

显然该上对象存储了。腾讯云对象存储 COS 的思路很简单:文件放云端桶里,应用通过 SDK 存取,计算与存储分离,服务器磁盘只放代码。这一节记录我接入 COS 的过程:建桶、权限设计、SDK 封装、临时密钥分发,以及四个让我加班排查的坑。如果你的业务里有文件上传、下载、模型分发的需求,这套方案可以直接照抄。

方案:建桶与 SDK 封装

第一步是建桶。桶名全局唯一,我按 ai-toolkit-prod-1300000000 这种业务加 APPID 的格式命名,区域选与轻量服务器一致(都在广州),内网访问走快还不收公网流量费。权限直接选私有,文件一律不公开,需要时给临时签名 URL。

存储类型按访问频率选,COS 的常用类型对照:

存储类型 适用场景 单价 取回费用 我的建议
标准 频繁访问 高 无 用户上传文件、模型文件
低频 月访问几次 中 低 月度分析报告
归档 几乎不访问 低 高 半年前的原始数据
深度归档 合规留档 最低 最高 日志冷备

SDK 封装上,我给团队定了个铁律:所有 COS 操作走同一个工具类,禁止在业务代码里散落密钥。初始化用子账号密钥加 CAM 最小权限,只给所用桶的读写:

from qcloud_cos import CosConfig, CosS3Client
import os

def get_cos_client():
    config = CosConfig(
        Region=os.getenv("COS_REGION"),          # ap-guangzhou
        SecretId=os.getenv("COS_SECRET_ID"),
        SecretKey=os.getenv("COS_SECRET_KEY"),
        Scheme="https",
    )
    return CosS3Client(config)

def upload_file(key, local_path, bucket="ai-toolkit-prod-1300000000"):
    client = get_cos_client()
    client.upload_file(
        Bucket=bucket, Key=key, LocalFilePath=local_path,
        EnableMD5=True,
    )
    return f"https://{bucket}.cos.{os.getenv('COS_REGION')}.myqcloud.com/{key}"

下载和预签名 URL 是另一对常用操作。给前端用的下载链接我都走预签名,有效期默认半小时,过期即失效,比直接公开桶安全得多:

import time

def get_download_url(key, bucket="ai-toolkit-prod-1300000000", expires=1800):
    client = get_cos_client()
    url = client.get_presigned_url(
        Method="GET", Bucket=bucket, Key=key,
        Expired=expires, SignHost=False,
    )
    return url

分发大模型文件是最爽的一个场景。以前每台新机器都要从网盘下载 2G 的模型,网盘限速能让人等到崩溃;现在走 COS 内网,速度跑满带宽还免外网流量费。大文件用分块上传更稳,SDK 的 upload_file 已经内置了分块逻辑,断网中断后重试不会从头传:

def upload_big_model(key, local_path):
    # 5GB 以内 upload_file 自动分块;失败可断点续传
    client = get_cos_client()
    for attempt in range(3):
        try:
            client.upload_file(
                Bucket=BUCKET, Key=key, LocalFilePath=local_path,
                PartSize=8 * 1024 * 1024,   # 8MB 一块
                EnableMD5=True, Retry=2,
            )
            return
        except Exception as e:
            print(f"第 {attempt + 1} 次上传失败: {e}")
    raise RuntimeError("模型上传三次均失败,请检查网络")

命令行工具 COSCMD 也是好东西,coscmd upload ./models/bge-base ./models/ 一行同步,比写脚本快。批处理脚本与 COS 打交道的更多技巧,可以回看第二篇的文件批处理。

生命周期规则是省钱的兜底配置。我在桶上配了两条:历史版本的报告 30 天后转低频存储、180 天后转归档;未管理对象(没有任何标签和元数据的孤儿文件)90 天自动删除。规则在控制台几分钟就能配好,但生效是异步的,配完当天别盯着账单看变化。

还有一个关键设计:给前端直传用临时密钥。网页端上传 Excel 如果都经过我的服务器,带宽和内存 double 消耗。正确做法是后端通过 STS 换一把十分钟有效期的临时密钥给前端,前端直接 PUT 到 COS,服务器只落个元数据。临时密钥方案的细节与安全实践,在腾讯云实践里有更完整的说明,我们团队的安全评审就是照着过的。

踩坑与排查

坑一:跨区域访问慢到怀疑人生。 有台机器在上海,拉广州桶的文件只有几百 KB 每秒。原因一是跨区域走公网,二是客户端没用分块下载。解决:能同区域就同区域;必须跨区时用 coscmd -b <上海桶> 做一次区域间复制,应用读本地桶。顺带一提,跨区域复制是按量收费的功能,开之前看一眼价格。

坑二:403 签名错误。 前端拿临时密钥上传总报 SignatureDoesNotMatch。排查发现是 URL 里的 Key 包含中文未编码,以及临时密钥的 AllowPath 没覆盖到实际路径。教训:密钥授权路径要用通配符 folder/* 而不是只到 folder;客户端拼 URL 一律先 quote。这类签名问题的排查还有一招:把 SDK 的调试日志打开,对比实际签名串与文档示例的差异,通常一眼就能看出是哪一段没对齐。

坑三:删除文件残留计费。 业务里用户删除记录时只删了数据库字段,忘了删 COS 上的对象,一年后账单里多出一笔归档存储费。后来统一改为:删除数据库记录的同一事务里投递一条消息,由清理任务异步删 COS,并按生命周期规则给桶配了 30 天自动未管理对象删除兜底。对账技巧也分享一下:控制台的用量统计可以按前缀筛选,每月把业务前缀的存储量导出对一遍,孤儿文件无处遁形。

坑四:把 SecretKey 提交到了 Git。 某次 review 发现同事把 .env 传进了仓库,全组密钥重换。血泪经验:COS 配置只认环境变量,仓库根目录放 .gitignore 加 .env,CI 里用变量注入。换密钥那次我们还顺手复盘了权限,发现旧的 SecretId 有全桶读写权限,趁机改成只读写业务桶。

小结

对象存储接入的核心就三件事:存储与计算分离、最小权限加临时密钥、生命周期兜底清理。这套结构搭好之后,加机器、扩容、迁移全是配置问题,不再是搬砖问题。文件能存能取了,但外地同事反馈下载报告还是慢——静态资源分发是下一节CDN 加速的内容。大文件分发的其他玩法也可以翻翻全栈 Serverless 部署里的存储章节。想亲手搭一个 COS 桶试水,AI 办公助手活动页有新用户存储代金券可以领。

延伸阅读