火山引擎对象存储 TOS 入门到上手

TOS 是什么

TOS(Volcengine Object Storage) 是火山引擎提供的对象存储服务,对标 AWS S3、阿里云 OSS、腾讯云 COS。最大特点是完全兼容 S3 协议——意味着你能用任何 S3 SDK 直接接 TOS,迁移成本几乎为零。

一句话定位:存海量非结构化数据(图片、视频、日志、备份、模型权重),按量计费、几乎无限容量、不挂本地磁盘。

💡
什么时候用 TOS
  • 用户上传的图 / 视频 / 文件,需要一个能直接 URL 访问的地方
  • 跨服务、跨地域共享同一份数据
  • 想脱开 ECS 本地盘做备份 / 归档

三个核心概念

桶 / 对象 / 键 Mermaid
概念含义注意点
Bucket命名空间,全局唯一名字创建后不可改名;区域不可迁移
Object桶里的一个文件 + 它的元数据没有「文件夹」概念,a/b/c.jpga/b/ 只是 key 的一部分
Key对象在桶里的路径不超过 1KB,建议用 / 分层便于前缀查询
⚠️
桶名一旦建好就改不了

桶名 = 全局命名空间。如果你在 cn-beijing 建了 my-prod,那么这个桶名在所有区域都被占用。起名时请带环境前缀,如 my-prodmy-stagingmy-archive-2026

AccessKey 三件套

TOS 用 AccessKey ID + Secret AccessKey 鉴权。永远别把这对密钥硬编码进前端。

凭证类型用途能干啥
主账号 AK全部权限只在服务端初始化时用一次,平时禁用
子账号 AK受限权限给业务服务用,配 IAM 策略
STS 临时凭证短期、动态前端 / 移动端发 Presigned URL 走这个
🚫
前端不能用 SecretAccessKey

任何暴露在前端代码里的 AK 都是公开密钥。前端只能拿 STS 临时凭证(默认 1 小时过期),或者直接拿后端签好的 Presigned URL。

控制台三步上手

  1. 火山引擎控制台 → 对象存储 TOS → 创建桶
    • 选区域(离用户最近的那个)
    • 选存储类型:标准 / 低频 / 归档(归档取回慢、便宜)
    • 权限:私有读写(默认)/ 公有读 / 公有读写(不要用公有读写
  2. 访问控制 IAM → 用户 → 创建 AccessKey
    • 只授予这一个桶的读写权限(最小权限原则)
    • 立即下载 AK 凭据,关闭页面就再也看不到 SecretAccessKey 了
  3. 桶设置 → 跨域 CORS / 防盗链 / 生命周期规则

Node.js 上传(官方 SDK)

npm install @volcengine/sdk
# 或用 AWS SDK(协议兼容):
# npm install @aws-sdk/client-s3

最小可跑上传:

import { TosClient } from "@volcengine/sdk";

const client = new TosClient({
  accessKeyId: process.env.TOS_AK!,
  accessKeySecret: process.env.TOS_SK!,
  region: "cn-beijing",
  endpoint: "tos-cn-beijing.volces.com",
});

await client.putObject({
  bucket: "my-prod",
  key: `uploads/${Date.now()}-${file.name}`,
  body: file.stream(),
  contentType: file.type,
});
💡
服务端直传:前端先问后端拿签名 URL,再 PUT 到 TOS

经典模式:前端永远不持 SecretAccessKey。用户上传文件时:

  1. 前端 → 后端:POST /api/upload/sign,带上文件名 / 大小 / 类型
  2. 后端 → TOS:生成 Presigned PUT URL(含签名)
  3. 后端 → 前端:返回 URL
  4. 前端 → TOS:直接 fetch(url, { method: 'PUT', body: file }),流量绕开后端、不吃后端带宽

Presigned URL(最常用的对接姿势)

Presigned URL = 把一次操作(PUT / GET / DELETE)的鉴权签名直接打到 URL 里,持 URL 的人就有权限,过期即失效。

import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

// 上传 URL(5 分钟有效)
const uploadUrl = await getSignedUrl(
  client,
  new PutObjectCommand({ Bucket: "my-prod", Key: key, ContentType: mime }),
  { expiresIn: 300 }
);

// 下载 URL(私有文件也能直接给浏览器用)
const downloadUrl = await getSignedUrl(
  client,
  new GetObjectCommand({ Bucket: "my-prod", Key: key }),
  { expiresIn: 3600 }
);
用途过期时间建议
上传5–15 分钟
下载几分钟到 24 小时,看业务
分享配合防盗链 referer,别让 URL 一传十十传百

大文件断点续传

超过 100MB 的文件用分段上传:

分段上传流程 Mermaid
  • 分片大小:5MB–5GB,推荐 5–20MB
  • 片数限制:最多 10000 片,超过就要放大单片
  • 断点重传:客户端记录已成功片的 partNumber + ETag,下次续传直接跳过成功的片
  • 失败清理:服务端用 lifecycle 规则自动清理未完成的分段上传(见下)

生命周期规则(省钱 + 自洁)

桶级规则,按前缀匹配批量处理:

动作触发条件用途
转低频创建 30 天后访问频率下降,自动省钱
转归档创建 90 天后几乎不读,归档存储最便宜
删除创建 365 天后日志 / 临时文件自动清
清理未完成分段14 天未完成防上传中断留垃圾、吃存储费
{
  "Rules": [
    {
      "ID": "log-auto-clean",
      "Filter": { "Prefix": "logs/" },
      "Status": "Enabled",
      "Expiration": { "Days": 365 }
    },
    {
      "ID": "kill-aborted-uploads",
      "Filter": { "Prefix": "" },
      "Status": "Enabled",
      "AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 14 }
    }
  ]
}

CORS(前端直传必开)

桶的「跨域设置」加:

AllowedOrigin:  https://your-site.com
AllowedMethod:  PUT, GET, HEAD, POST
AllowedHeader:  *
ExposeHeader:   ETag, Content-Length
⚠️
别把 AllowedOrigin 写成 `*` 一刀切

如果同时打开 AllowedHeader: * 和允许自定义 header,浏览器会拒绝带凭证的请求。要么用具体 origin,要么用具体 header 白名单,不要双宽。

最小权限 IAM 策略

{
  "Version": "1",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "tos:GetObject",
        "tos:PutObject",
        "tos:DeleteObject",
        "tos:InitiateMultipartUpload",
        "tos:ListMultipartUploadParts",
        "tos:UploadPart",
        "tos:CompleteMultipartUpload",
        "tos:AbortMultipartUpload"
      ],
      "Resource": ["trn:tos:::my-prod/*", "trn:tos:::my-prod"]
    }
  ]
}

要点:

  • 只授权业务实际用到的桶
  • 只给操作对象的权限,不给 tos:ListAllMyBuckets
  • 如果只是「前端签名」,后端只需要 tos:PutObject 之类;ListBuckets / DeleteBucket 这种管理类 API 不下发

防盗链

私有桶 + Presigned URL 已经能挡 99% 的盗链。公有读桶才需要 referer:

Referer 白名单:  https://your-site.com
空 Referer 行为: 拒绝
ℹ️
referer 不防爬虫

Referer 是「客户自觉」的,curl / 爬虫都能伪造。防盗链靠短过期 + 签名而不是靠 Referer。

计量计费口径

怎么算
存储按 GB × 天 × 存储类型单价
请求PUT / GET / LIST 各有单价(万次为单位)
流量公网下行按 GB;内网下行免费,CDN 回源另算
数据取回低频 / 归档取回要额外付费

省钱三招:

  1. 同地域用内网:服务在 cn-beijing 就建 cn-beijing 的桶,公网流量 = 钱
  2. 挂 CDN:TOS 直接做源站,CDN 发静态内容,回源流量便宜一个数量级
  3. 归档化冷数据:超过 90 天不读就丢归档

常见坑

前端用了主账号 AK,导致密钥泄露
TOS 控制台能一眼看到「这个 AK 是不是主账号」,泄露 = 全桶可读可写。永远用子账号 + STS。
桶跨区域建错了,迁移要走跨区域复制
桶建在哪就在哪,没法直接迁。提前确认 ECS / 函数 / CDN 的区域。
归档桶被用户请求取回,半天没反应
归档取回需要先发起「解冻」请求,标准取回几十分钟、批量取回要几小时。归档只放「绝对不读」的数据。
分段上传到一半服务挂了,留了一堆孤儿 part
没有 lifecycle 清理规则 → 桶越来越大还收存储费。建桶第一天就把 abort 规则打开。
把 SecretAccessKey 写进前端 .env,发到 git
前端环境变量是公开的。能用 Presigned URL 就用 Presigned URL,必须在前端用的就只放 STS 临时凭证。

速查表

场景用什么
服务端上传PutObject
浏览器直传Presigned PUT URL(5–15 分钟过期)
私密文件下载Presigned GET URL
大文件(>100MB)分段上传 + 断点续传
前端不能用 AKSTS 临时凭证 / Presigned URL
清理垃圾lifecycle AbortIncompleteMultipartUpload
省钱冷数据转低频 / 归档
跨服务共享同区域 + 内网 Endpoint
静态站加速TOS 做源站 + CDN
TOS 用起来像 S3,费用像水电,密钥一旦泄露像裸奔——前三件事做好了,后面就是白嫖。

目录

图表