10.2 对象存储语义 (S3 API 的语义细节)

一句话

S3 API 看起来只是 “PUT/GET/DELETE”,但真正决定系统行为的是语义细节:整体替换、条件写、ETag 与分片的含义、list 的最终一致与分页——不理解这些,就会写出 “看着能跑、并发下错乱” 的上传逻辑。

六个必须搞清的语义

语义 规则 常见误解
PUT 是整体替换 同名 PUT 覆盖为新对象(旧版本由版本控制/生命周期处理) 以为可以 “部分更新”
条件写 If-None-Match: * 实现 “不存在才创建”(防覆盖) 以为并发 PUT “后者覆盖” 可接受
ETag 普通 PUT 通常是 MD5(或实现定义);分片上传的 ETag 是 “分片 MD5 + 分片数” 的派生值 以为分片 ETag = 整文件 MD5
分片上传 每片 5MB–5GB、最多 1 万片;完成后合并 用小于 5MB 的分片(除最后一片)
list 语义 按 key 字典序分页(常见 1000/页),强一致实现下新对象可见 把 list 当 “实时索引/查询”
删除 DELETE 幂等(不存在也返回成功) 以为能区分 “删成功” 与 “本来就没有”

容量与限制(S3 语义,各实现略有差异)

项 限制
单对象上限 5TB
单次 PUT 上限 5GB(超过必须分片)
分片大小 5MB–5GB(最后一片可更小)
分片数 ≤ 10,000
单次 list 常见 1,000 个 key
请求计费 GET/PUT/LIST 分别计费(小文件场景请求费可能超过容量费)

代码:三种 “安全上传” 的写法

# ① 不存在才创建(防并发覆盖)
aws s3api put-object --bucket b --key k.json --body k.json \
  --if-none-match '*'                    # 已存在则 412 PreconditionFailed

# ② 大文件分片(并发上传 + 失败只重传一片)
aws s3 cp big.iso s3://b/big.iso --expected-size 10737418240   # 自动 multipart

# ③ 校验:本地 MD5 与 ETag 对比(仅非分片对象成立)
md5sum k.json; aws s3api head-object --bucket b --key k.json --query ETag
# 幂等写入:先写临时 key,再用 copy 原子替换(部分实现支持)
s3.put_object(Bucket=b, Key=f"tmp/{uid}", Body=data)
s3.copy_object(Bucket=b, Key=f"final/{uid}", CopySource={"Bucket": b, "Key": f"tmp/{uid}"})
s3.delete_object(Bucket=b, Key=f"tmp/{uid}")

⚠️ 上述命令为真实 API 用法,但未在本机对真实 S3 执行(无凭证);语义以各实现文档为准。

反模式

反模式 后果
用 PUT 做 “追加写” 每次整对象重传,写放大与请求费翻倍
并发 PUT 同一 key 不设条件 后写覆盖先写,丢更新
用分片 ETag 当整文件 MD5 校验 校验永远失败或永远通过(不可靠)
小文件(<5MB)硬做分片 请求数暴涨,费用上升
用 list 做业务查询 分页 + 字典序,无法按属性过滤
删除后立即依赖 “对象不存在” 版本控制/生命周期下可能仍存在旧版本

三条铁律

🔑 对象不可变:改就是整体替换,不要设计 “原地修改”。

🔑 并发写要条件化(If-None-Match / 版本号),否则必丢更新。

🔑 list 不是查询;检索靠外部索引。

自检

Q1 为什么分片上传的 ETag 不能当整文件 MD5 用?

▸ 答案:因为分片 ETag 是 “各分片 MD5 的哈希 + 分片数” 派生出来的,不是对整文件内容算的 MD5。用它与本地 md5sum 比较必然不一致(或在小文件单分片时偶然一致),会误判校验失败/通过。

Q2 If-None-Match: * 解决什么问题?

▸ 答案:解决并发创建覆盖:两个客户端同时创建同名对象,只有一个成功,另一个收到 412。没有它,“后写覆盖先写” 会静默丢数据。它也是实现 “分布式锁/CAS” 的基础(见 48.3 分布式锁)。

Q3 为什么 “小文件 + 分片上传” 反而更贵?

▸ 答案:因为分片上传会带来额外的 Initiate/Complete/ListParts 请求,小文件本来一次 PUT 就够,分片后请求数翻几倍;按请求计费时,费用上升而收益为零。

Q4 list 的 “最终一致” 会带来什么坑?

▸ 答案:分页遍历期间的新增/删除可能不出现或重复出现:你用 list 做 “全量同步” 时,可能漏掉刚写入的对象。对策是 “用清单/版本号/时间戳增量”,而不是依赖 list 的实时性。

Q5 删除为什么是幂等的?这对业务有什么影响?

▸ 答案:因为 DELETE 不存在也返回成功(语义上 “保证不存在”),所以无法用它判断 “到底有没有删到”。业务要区分 “删了” 与 “本来没有”,必须先用 HEAD/GET 查,或依赖版本号/审计日志。

Q6 为什么对象存储 “不适合做数据库”?

▸ 答案:因为它没有原地更新、事务、范围索引:每次修改都是整体替换,查询只能按 key,一致性模型也不支持多对象事务。它适合 “一次写多次读” 的大对象,不适合频繁更新的结构化数据(见 10.1 块存储 / 文件存储 / 对象存储)。

Q7 选对象存储时,除了价格还要看什么?

▸ 答案:四件事:一致性模型(是否强一致 list/覆盖写)、请求计费(小文件/高频访问成本)、分片与校验语义(ETag 行为)、生命周期与版本(能否自动降冷/清理)。只看 “每 GB 单价” 会低估真实成本。

关联