10.8 对象存储系统设计

一句话

对象存储把 “元数据” 与 “数据” 彻底分开:元数据进分布式索引,数据切成块进大容量存储,再用 桶 + 扁平 key 做路由——换来近乎无限的扩展,代价是 没有原地修改、没有目录语义、list 很贵。

三层架构

层 职责 关键设计
接入层 S3/HTTP API、鉴权、限流、分片上传 无状态,水平扩展
元数据层 桶、对象名、大小、ETag、版本、权限 按 bucket + hash(key) 分片,KV/索引存储
数据层 实际字节(chunk 4–64MB) 纠删码或多副本,按 chunk 分散
后台 GC、生命周期、副本修复、跨区复制 异步,不能阻塞前台 PUT

一次 PUT 的路径

1. 客户端 → 接入层:PUT /bucket/key(带 Content-MD5)
2. 接入层校验权限与配额,算 key hash → 定位元数据分片
3. 大对象先 multipart:每片单独上传、校验 ETag
4. 数据写 chunk(EC 编码或 3 副本),落盘后返回成功
5. 元数据最后提交(决定 “对象是否可见”)

顺序很关键:先写数据、后提交元数据。反过来的话,元数据可见而数据未落盘 = 读到空洞。

一致性:读写语义要写进契约

操作 常见保证 说明
新建后读 强一致(read-after-write) 元数据提交即可见
覆盖写后读 强一致(现代实现) 新版本提交后旧版本不再可见
删除后读 强一致 返回 404
list 可能最终一致 分页遍历期间的新增可能不出现
跨区域复制 最终一致(秒–分钟) RPO 由复制延迟决定

S3 在 2020 年底把 “覆盖写/删除” 也做到了强一致;自建系统若用异步元数据,务必在文档里写清 “哪个操作不是强一致”。

纠删码 vs 三副本

方案 空间开销 修复代价 适合
3 副本 3× 低(拷一份) 热数据、低延迟
EC 4+2 1.5× 中(读 4 块重建) 温/冷数据、大对象
EC 8+3 1.375× 高(CPU 与网络) 归档

EC 省空间但放大读放大与重建时间:修一块要读 k 块。所以常见组合是 “热数据三副本、冷数据 EC”。

小文件与 list:两个绕不开的坑

坑 现象 对策
大量小对象 元数据 QPS 打满、list 超时 打包成大对象(tar/parquet)+ 索引表
按前缀 list 扫桶 一次 list 几百万条,慢且贵 用外部索引(数据库/清单文件)
覆盖写频繁 每改一次重传整个对象 拆成多对象或改用数据库

落到数字:容量与请求的换算

项 量级 说明
单对象上限 5TB(S3 语义,常规实现) 超过必须 multipart
multipart 分片 5MB–5GB/片,最多 1 万片 分片大小决定重传成本
EC 4+2 空间开销 1.5× 对比 3 副本的 3×
重建 1 个 64MB 块 读 4 块 + 编码 CPU 大集群重建风暴的根源
list 单页 1000 个 key(常见) 遍历 100 万对象 = 1000 次请求
aws s3api head-object --bucket b --key k --query '{len:ContentLength,etag:ETag}'  # 校验大小与 ETag
curl -sI https://cdn.example.com/k.jpg | grep -iE "etag|age|x-cache"              # 边缘是否命中

反模式

反模式 后果
把对象存储当文件系统(目录/rename) 语义缺失,只能模拟,性能极差
高频覆盖同一对象 每次全量重传 + 版本堆积
用 list 做业务查询 分页 + 最终一致,结果不可靠
只用 3 副本存归档 存储成本 2–3 倍
元数据与数据同库 扩展性被单库锁死
用对象存储做“事务型更新” 无多对象事务,跨对象一致性靠自己保证

三条铁律

🔑 数据先落盘、元数据后提交;顺序反了就会读到 “可见但不存在” 的对象。

🔑 对象不可变:要改就写新版本,旧版本交给生命周期清理。

🔑 list 不是查询;业务检索要建自己的索引。

自检

Q1 为什么对象存储的 PUT 要 “先写数据再提交元数据”?

▸ 答案:因为元数据提交决定 “对象可见”。若先提交元数据再写数据,中间窗口里客户端能 GET 到这个对象,但数据还没落盘 → 读到空洞或报错。反过来先写数据、后提交元数据,最坏情况是 “数据写了但对象不可见”(由 GC 清理),不会产生错误读。

Q2 一个桶里 5 亿个小文件,最先崩的是哪一层?

▸ 答案:元数据层(索引 QPS 与前缀 list)。对象存储的容量层可以无限加盘,但元数据的分片、索引与 list 能力是瓶颈。解法是合并小对象(打包)并把 “按属性查询” 移到外部索引,而不是继续加存储节点。

Q3 EC 4+2 相比 3 副本省多少空间?代价是什么?

▸ 答案:空间从 3× 降到 1.5×(省 50%),代价是:① 读一个对象要读 4 块(读放大);② 重建一块要读 4 块并算编码(CPU + 网络);③ 小对象场景下 EC 的元数据开销占比更高。所以 EC 常用于大对象与冷数据,热数据仍用副本。

Q4 为什么 “覆盖写” 在对象存储里代价高?

▸ 答案:因为对象不可变:覆盖 = 写一个全新对象 + 提交新版本 + 旧版本等待 GC。频繁覆盖会产生大量写放大与版本垃圾。正确做法是 “追加小对象” 或 “把可变部分放到数据库/KV”,对象只存不可变的大块。

Q5 跨区域复制的 RPO 由什么决定?

▸ 答案:由复制方式与延迟决定:异步复制(常见)的 RPO ≈ 复制延迟(秒到分钟);同步复制可以做到 RPO=0,但写入延迟增加一个跨区 RTT。选择依据是业务能容忍丢多少数据(见 36.1 RTO / RPO)。

Q6 为什么 “重建 1 个块要读 4 块” 会在大集群里变成事故?

▸ 答案:因为重建是读放大:1 块坏 → 读 4 块 → 占用 4 倍网络与磁盘带宽。集群越大、单盘越大,同时坏盘的概率越高,重建流量会与前台流量抢带宽,导致 “修数据把业务拖慢”。对策:限速重建、优先重建热数据、监控重建队列。

关联