对象存储(Object Storage)是几乎所有大规模系统的「数据底座」:图片、视频、备份、日志、数据湖文件,最终都落在 S3 这类服务上。像 视频流媒体平台 的源片、设计一个日志检索系统 的冷日志,底层都是对象存储。它和文件系统、块存储最大的区别在于——对象一旦写入就不可原地修改,只能整体覆盖或删除,接口只有 PUT/GET/DELETE/LIST 这几个动词。这个「简单」的约束反而让水平扩展、多副本、纠删码变得极其自然。本文按照系统设计面试的标准答题结构,设计一个支持海量小文件与超大文件、跨多机房容灾的 S3 类对象存储系统。
一句话:对象存储的核心是「元数据与数据分离」——控制面管 Key→分片映射与权限,数据面管不可变的字节块;把两者解耦,才能各自独立地水平扩展。
一、需求澄清与量级估算
1.1 需求澄清
面试官给出题目后,先用提问收敛边界:
- 接口范围:只需要 PUT/GET/DELETE 与 List,还是要支持 S3 兼容的完整 API(分段上传、版本控制、预签名 URL、生命周期规则)?
- 对象大小:是图片/文档这类 KB
MB 的小对象,还是视频/备份这类 GBTB 的大对象? - 一致性要求:读己之写(read-after-write)?列表强一致?还是最终一致可接受?
- 访问模式:读多写少(CDN 回源)还是写多读少(备份/数据湖)?
- 容灾级别:单机房多副本,还是跨 3 可用区 / 跨地域容灾?
明确假设(面向面试的合理假设):
| 需求项 | 假设 |
|---|---|
| 对象大小 | 小到 1KB 缩略图,大到 5TB 备份,允许分段上传 |
| 一致性 | 新对象读己之写;覆盖与删除最终一致(1 秒内收敛) |
| 冗余 | 热数据 3 副本;冷数据纠删码(EC 10+4) |
| 容灾 | 跨 3 个可用区;关键桶跨地域复制 |
| 访问 | 读多写少,峰值 QPS 百万级 GET |
1.2 量级估算
| 指标 | 估算值 | 推导 |
|---|---|---|
| 总对象数 | 1000 亿 | 假设 1 亿用户,人均 1000 个对象 |
| 日均 PUT | 10 亿次 | 假设日活 5000 万,人均 20 次上传 |
| 峰值 GET QPS | ~500 万 | 日均读 100 亿次 / 86400 ≈ 11.5 万,再乘 40 倍峰值系数 |
| 单对象平均大小 | 500 KB | 小对象为主,被少量大对象拉高 |
| 总容量 | ~50 PB | 1000 亿 × 500KB,含副本膨胀约 150 PB 裸容量 |
| 元数据条目 | 1000 亿行 | 每对象至少 1 行 Key→位置映射 |
一句话:小文件场景的瓶颈在元数据规模(千亿行 Key 映射),大文件场景的瓶颈在数据面带宽——两条链路必须分开扩容,这是对象存储架构的第一性原则。
二、高层架构设计
┌──────────────────────────────────────┐
│ 客户端 SDK / S3 API / CLI │
└───────────────────┬──────────────────┘
│ HTTPS (SigV4 签名)
┌──────────────────────▼──────────────────────┐
│ 接入网关 (Gateway) │
│ 鉴权 / 限流 / 签名校验 / 路由 / 预签名校验 │
└───────┬───────────────────────────┬──────────┘
│ 元数据操作 (PUT/DELETE/List) │ 数据读写
┌──────────────▼──────────────┐ ┌─────────▼──────────────────┐
│ 元数据服务 (Control) │ │ 数据服务 (Data Plane) │
│ ┌──────────┐ ┌──────────┐ │ │ ┌──────────┐ ┌──────────┐ │
│ │ Key 索引 │ │ 桶策略/ACL│ │ │ │ 分片管理器 │ │ 副本/EC │ │
│ └──────────┘ └──────────┘ │ │ └──────────┘ └──────────┘ │
│ ┌──────────┐ ┌──────────┐ │ │ ┌──────────┐ ┌──────────┐ │
│ │ 版本表 │ │ 生命周期 │ │ │ │ 校验和 │ │ 冷热分层 │ │
│ └──────────┘ └──────────┘ │ │ └──────────┘ └──────────┘ │
└──────────────┬──────────────┘ └─────────┬──────────────────┘
│ │
┌──────────────▼──────────────┐ ┌─────────▼──────────────────┐
│ 分布式 KV / NewSQL 元数据库 │ │ 存储节点 (HDD/SSD 池) │
│ (Etcd/TiDB/Cassandra 分片) │ │ 分片块文件 + 校验和 │
└─────────────────────────────┘ └────────────────────────────┘
整体拆为四层:
- 接入层:S3 兼容网关,负责 SigV4 验签、限流、路由到元数据或数据面。
- 控制面(元数据服务):管理 Key→分片位置、桶策略、版本、生命周期。
- 数据面(数据服务):把对象切成块,按副本/纠删码放置到存储节点。
- 依赖设施:元数据库(分布式 KV)、存储节点池、冷归档(磁带/对象冷层)。
2.1 为什么元数据与数据分离
- 扩容解耦:元数据是「小而多」的随机点查,适合内存/SSD + 分布式 KV;数据是「大而顺序」的流式读写,适合 HDD 池。两者硬件需求完全不同,混在一起必然一方拖累另一方。
- 故障隔离:数据节点宕机只影响它承载的分片,元数据仍可响应 Key 查询与 List。
- 小文件友好:把多个小对象打包进一个「块文件」(如 64MB),元数据只记「块文件 ID + 偏移 + 长度」,用一次磁盘寻道读多个对象,缓解小文件 IO 放大。
一句话:控制面与数据面分离,等价于把「查字典」和「搬货」拆给两组人,各自按自己的节奏扩容,互不阻塞。
三、核心组件设计
3.1 对象模型与 Key 设计
对象由三部分组成:
- Key:
bucket + "/" + object_key,全局唯一;bucket是命名空间,object_key支持/但不是目录(只是前缀)。 - Value:对象字节流,不可变。
- Metadata:用户自定义元数据(
x-amz-meta-*)+ 系统元数据(大小、ETag/MD5、创建时间、存储类别、版本 ID)。
bucket: photos
object_key: 2026/10/user-42/avatar.jpg
→ 元数据行: (bucket, key) → { version_id, size, etag, shard_map, storage_class, acl }
List 用前缀扫描实现「伪目录」:prefix=2026/10/ 返回所有以该前缀开头的 Key,用元数据库的有序索引范围扫描即可。
3.2 数据分片与放置
大对象切成分片(chunk,默认 8MB),每个分片独立冗余;小对象合并进块文件。分片放置用一致性哈希 + 虚拟节点决定落到哪些存储节点:
def place_chunks(object_id, size, storage_class):
chunks = split(object_id, size, CHUNK=8 << 20)
placements = []
for idx, chunk in enumerate(chunks):
# 用对象ID+分片序号做哈希,保证同一分片始终落到同一组节点
ring_key = f"{object_id}#{idx}"
if storage_class == "STANDARD": # 3 副本
nodes = ring.pick(ring_key, n=3, distinct_zone=True)
placements.append(("REPLICA", nodes))
else: # 纠删码 10+4
nodes = ring.pick(ring_key, n=14, distinct_zone=True)
placements.append(("EC:10+4", nodes))
return placements
副本 vs 纠删码的取舍:
| 方案 | 空间开销 | 恢复成本 | 适用 |
|---|---|---|---|
| 3 副本 | 3.0x | 低(直接复制) | 热数据、小对象、低延迟读 |
| EC 10+4 | 1.4x | 高(需读 10 片重建) | 冷数据、大对象、归档 |
| EC 4+2 | 1.5x | 中 | 小集群折中 |
一句话:副本用「空间换延迟和可靠性」,纠删码用「CPU 与网络换空间」;热层用副本、冷层用 EC,是业界通行做法。
3.3 分段上传(Multipart Upload)
上传 5TB 大对象不能一次性 PUT,必须分段:
1. CreateMultipartUpload(bucket, key) → 返回 upload_id
2. UploadPart(upload_id, part_number, bytes) → 每段独立上传并返回 ETag
(可并发、可重试、可断点续传;段号 1..10000,每段 ≥5MB 除最后一段)
3. CompleteMultipartUpload(upload_id, [parts]) → 服务端按段号拼接,生成最终对象
4. AbortMultipartUpload(upload_id) → 放弃并清理已传段
关键设计:
- 段暂存:各段先落在「临时分片区」,Complete 时只写一条元数据指向段列表,不做物理拷贝(用逻辑拼接)。
- 幂等:
upload_id唯一,重复 Complete 返回同一结果;未 Complete 的段由生命周期规则在 N 天后自动回收,避免垃圾堆积。 - 断点续传:客户端
ListParts查出已成功段,只补传失败段。
3.4 版本控制与删除
- 版本控制:开启后每次 PUT 生成新
version_id,旧版本保留;DELETE 写入一条 delete marker(删除标记),GET 返回 404 但旧版本仍在,可恢复。 - 生命周期规则:按前缀 + 天数自动「转存储类别 / 过期删除 / 清理未完成分段 / 清理非当前版本」,用后台任务扫描元数据执行。
- 软删除:未开启版本控制时,DELETE 直接移除元数据行,数据分片由异步 GC 延迟回收(给误删留恢复窗口)。
3.5 冷热分层
| 存储类别 | 介质 | 取回延迟 | 成本 | 典型场景 |
|---|---|---|---|---|
| Standard | SSD/HDD 3 副本 | 毫秒 | 高 | 热图、在线业务 |
| Infrequent (IA) | HDD 副本 | 毫秒 | 中 | 月度备份 |
| Archive | HDD EC | 分钟~小时 | 低 | 合规归档 |
| Deep Archive | 磁带/蓝光 | 数小时 | 极低 | 长期留存 |
分层靠生命周期规则自动迁移,迁移是后台异步任务,迁移期间对象仍可读(先复制后删源)。短视频转码流水线 的原始素材和成片就常用 Standard/IA 两层,设计一个视频转码平台 也依赖对象存储做中转。
四、数据模型
| 表/结构 | 用途 | 分片键 | 说明 |
|---|---|---|---|
| object_meta | Key→对象映射 | hash(bucket,key) | 千亿行主表 |
| object_version | 版本列表 | hash(bucket,key) | 版本控制开启时使用 |
| shard_map | 分片→节点 | object_id | 分片放置结果 |
| bucket_policy | 桶策略/ACL | bucket | 权限与生命周期 |
| multipart_upload | 未完成分段 | upload_id | 断点续传与 GC |
| object_tag | 对象标签 | object_id | 用于生命周期筛选 |
字段规范:Key 用 UTF-8,最大 1024 字节;ETag 单段为 MD5,多段为「段 MD5 拼接后再 MD5」+ -段数 后缀;大小用 BIGINT;版本 ID 用单调递增或 ULID。
五、关键流程
5.1 一次 PUT(小对象)时序
Client Gateway 元数据服务 数据服务 存储节点
│ PUT /key │ │ │ │
├──────────────▶│ 验签+限流 │ │ │
│ ├───────────────▶│ 分配 object_id │ │
│ │ ├───────────────▶│ 切块+算分片位置 │
│ │ │ ├──────────────▶│ 写副本/EC
│ │ │ │◀──────────────┤ 校验和OK
│ │ │◀───────────────┤ shard_map │
│ │◀───────────────┤ 写元数据(含位置) │ │
│◀──────────────┤ 200 + ETag │ │ │
要点:先写数据、后写元数据。若数据写成功但元数据写失败,对象「不可见」由 GC 回收,不会出现「元数据指向不存在的分片」的悬挂引用。
5.2 一次 GET 与 CDN 回源
Client → CDN(命中则直接返回) → 未命中回源 Gateway
Gateway → 元数据服务: 查 (bucket,key) → shard_map
→ 数据服务: 按 shard_map 就近读取分片 → 校验和比对 → 流式返回
- 就近读取:分片在 3 个可用区,选网络延迟最低的副本。
- Range 请求:支持
Range: bytes=a-b,只读部分分片(视频拖拽、断点下载)。 - 校验和:返回前比对 ETag/MD5,防止静默损坏(bit rot)。
六、可靠性与一致性
6.1 一致性模型
- 新对象:写元数据用强一致的分布式 KV(如 etcd/Raft 组),保证 read-after-write。
- 覆盖/删除:允许最终一致——更新元数据后异步失效边缘缓存,通常 1 秒内收敛。
- List:最终一致,因为有序索引的跨分片扫描难以强一致,S3 也如此。
6.2 数据可靠性
- 校验和:写入时算 CRC32C/MD5 存元数据;后台**擦洗(scrubbing)**任务定期读回校验,发现坏块用副本/EC 重建。
- 反熵:副本间定期比对 Merkle 树,发现不一致用多数派修复。
- 跨机房容灾:副本跨 3 AZ 打散;跨地域复制用异步复制 + 版本向量解决冲突(对象不可变,冲突只需按时间戳取新)。
6.3 元数据高可用
元数据库按 hash(bucket,key) 分片,每分片用 Raft 组做多副本。分片数固定(如 4096),扩容时只迁移部分分片(虚拟分片),避免全量重哈希。
一句话:数据面用「校验和 + 擦洗 + EC 重建」保证不丢字节;控制面用「Raft 分片 + 虚拟分片」保证元数据不丢行、扩容不搬山。
七、性能与扩展
- 元数据缓存:热点 Key 映射缓存在网关本地 LRU,命中率高的桶可显著降元数据库压力。
- 小文件合并:块文件(64MB)内打包多个小对象,元数据只存「块 ID + 偏移」,减少寻道。
- 大文件直传:分段上传让数据面直接对存储节点写,网关只转发控制指令,避免网关成带宽瓶颈。
- 纠删码编码加速:EC 编解码用 ISA-L 等 SIMD 库,或交给支持 EC 的硬件/DPU。
- 冷数据下沉:生命周期把 90 天未访问对象自动转 Archive,节省 70%+ 成本。
容量与热点
- 单存储节点挂 12
24 块 HDD,单机裸容量 200400TB;10 万台节点支撑 EB 级。 - 热点对象(爆款视频)用多级 CDN + 边缘缓存卸载,对象存储只做回源。
- 千亿元数据行:单行约 200 字节 → 约 20TB 元数据,必须分片 + SSD。
八、权衡与备选
| 决策点 | 本文选型 | 备选 | 权衡说明 |
|---|---|---|---|
| 冗余方式 | 热 3 副本 / 冷 EC | 全 EC | 全 EC 省空间但读延迟高、重建慢;分层最均衡 |
| 元数据库 | 分布式 KV + Raft | 单机 MySQL | KV 易分片、水平扩展;MySQL 简单但难扛千亿行 |
| 一致性 | 写强一致、List 最终一致 | 全强一致 | 全强一致 List 代价极高,S3 也选最终一致 |
| 小文件 | 块文件合并 | 每对象一文件 | 合并省寻道但 GC 复杂;直接存简单但 IO 放大严重 |
| 拼接 | 逻辑拼接(不拷贝) | 物理合并 | 逻辑拼接秒级完成、省带宽;物理合并慢但读取简单 |
关键取舍
- 一致性 vs 可用性:元数据走 Raft(CP),牺牲极端分区下的写可用性换元数据不丢;数据面走多副本(AP),分区时仍可读本地副本。
- 成本 vs 延迟:冷数据用 EC + 磁带,延迟从毫秒升到小时,换来 5~10 倍成本下降。
- 自研 vs 用开源:起步可直接用 MinIO/Ceph 兼容 S3,规模大了再自研控制面(元数据是差异化重点)。
九、扩展场景与面试追问
9.1 支持对象锁与合规留存
- 引入 WORM(Write Once Read Many):对象写入后 N 年内不可删除,用于金融/医疗合规。
- 实现:元数据加
retain_until字段,DELETE 时校验未到期则拒绝,且根账号也不能绕过。
9.2 跨地域复制与冲突
对象不可变,跨地域复制天然无写冲突,只需按 version_id 去重。难点在复制延迟与带宽:用异步复制 + 增量传输,只传新版本分片。
9.3 面试常见追问
| 追问 | 关键回答 |
|---|---|
| 为什么对象不可变? | 不可变让缓存、副本、CDN 失效策略都变简单,也天然支持版本与并发读 |
| 千亿元数据怎么查得快? | 按 hash(bucket,key) 分片 + 有序索引 + 网关本地缓存,点查 O(1) |
| 小文件怎么优化? | 块文件合并 + 元数据只存偏移,减少寻道与元数据条数 |
| 数据坏了怎么发现? | 写入校验和 + 后台擦洗读回比对,坏了用副本/EC 重建 |
| 大文件上传断了怎么办? | 分段上传 + ListParts 续传 + 生命周期清理未完成分段 |
十、总结
| 模块 | 关键设计 | 一句话记忆 |
|---|---|---|
| 分层架构 | 控制面/数据面分离 | 查字典和搬货分开扩容 |
| Key 设计 | 前缀即目录 | 用有序索引范围扫描实现 List |
| 数据放置 | 一致性哈希 + 虚拟节点 | 分片位置由对象 ID 决定,可重建 |
| 冗余 | 热副本冷 EC | 空间与延迟按温度分配 |
| 上传 | 分段 + 逻辑拼接 | 大对象靠并发段,拼接不拷贝 |
| 可靠性 | 校验和 + 擦洗 + 反熵 | 不信磁盘,定期读回自检 |
一句话:对象存储的面试核心是讲清楚「为什么要把元数据与数据分离、如何用一致性哈希放置分片、热副本冷 EC 怎么分层、以及分段上传与校验和如何保证大对象可靠」,把容量估算(千亿对象、50PB)挂在嘴边,而不是堆组件。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。