设计一个对象存储系统(S3 类)

本文系统设计一个 S3 类对象存储系统:需求澄清与容量估算、元数据与数据面分离架构、对象分片与多副本/纠删码、一致性哈希数据分布、分段上传与版本控制、生命周期冷热分层、跨机房容灾,并给出架构图、元数据表、分片放置伪代码与副本/纠删码一致性权衡对比。

对象存储(Object Storage)是几乎所有大规模系统的「数据底座」:图片、视频、备份、日志、数据湖文件,最终都落在 S3 这类服务上。像 视频流媒体平台 的源片、设计一个日志检索系统 的冷日志,底层都是对象存储。它和文件系统、块存储最大的区别在于——对象一旦写入就不可原地修改,只能整体覆盖或删除,接口只有 PUT/GET/DELETE/LIST 这几个动词。这个「简单」的约束反而让水平扩展、多副本、纠删码变得极其自然。本文按照系统设计面试的标准答题结构,设计一个支持海量小文件与超大文件、跨多机房容灾的 S3 类对象存储系统。

一句话:对象存储的核心是「元数据与数据分离」——控制面管 Key→分片映射与权限,数据面管不可变的字节块;把两者解耦,才能各自独立地水平扩展。

一、需求澄清与量级估算

1.1 需求澄清

面试官给出题目后,先用提问收敛边界:

  • 接口范围:只需要 PUT/GET/DELETE 与 List,还是要支持 S3 兼容的完整 API(分段上传、版本控制、预签名 URL、生命周期规则)?
  • 对象大小:是图片/文档这类 KBMB 的小对象,还是视频/备份这类 GBTB 的大对象?
  • 一致性要求:读己之写(read-after-write)?列表强一致?还是最终一致可接受?
  • 访问模式:读多写少(CDN 回源)还是写多读少(备份/数据湖)?
  • 容灾级别:单机房多副本,还是跨 3 可用区 / 跨地域容灾?

明确假设(面向面试的合理假设):

需求项假设
对象大小小到 1KB 缩略图,大到 5TB 备份,允许分段上传
一致性新对象读己之写;覆盖与删除最终一致(1 秒内收敛)
冗余热数据 3 副本;冷数据纠删码(EC 10+4)
容灾跨 3 个可用区;关键桶跨地域复制
访问读多写少,峰值 QPS 百万级 GET

1.2 量级估算

指标估算值推导
总对象数1000 亿假设 1 亿用户,人均 1000 个对象
日均 PUT10 亿次假设日活 5000 万,人均 20 次上传
峰值 GET QPS~500 万日均读 100 亿次 / 86400 ≈ 11.5 万,再乘 40 倍峰值系数
单对象平均大小500 KB小对象为主,被少量大对象拉高
总容量~50 PB1000 亿 × 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 分片)  │   │   分片块文件 + 校验和         │
              └─────────────────────────────┘   └────────────────────────────┘

整体拆为四层:

  1. 接入层:S3 兼容网关,负责 SigV4 验签、限流、路由到元数据或数据面。
  2. 控制面(元数据服务):管理 Key→分片位置、桶策略、版本、生命周期。
  3. 数据面(数据服务):把对象切成块,按副本/纠删码放置到存储节点。
  4. 依赖设施:元数据库(分布式 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+41.4x高(需读 10 片重建)冷数据、大对象、归档
EC 4+21.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 冷热分层

存储类别介质取回延迟成本典型场景
StandardSSD/HDD 3 副本毫秒高热图、在线业务
Infrequent (IA)HDD 副本毫秒中月度备份
ArchiveHDD EC分钟~小时低合规归档
Deep Archive磁带/蓝光数小时极低长期留存

分层靠生命周期规则自动迁移,迁移是后台异步任务,迁移期间对象仍可读(先复制后删源)。短视频转码流水线 的原始素材和成片就常用 Standard/IA 两层,设计一个视频转码平台 也依赖对象存储做中转。

四、数据模型

表/结构用途分片键说明
object_metaKey→对象映射hash(bucket,key)千亿行主表
object_version版本列表hash(bucket,key)版本控制开启时使用
shard_map分片→节点object_id分片放置结果
bucket_policy桶策略/ACLbucket权限与生命周期
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%+ 成本。

容量与热点

  • 单存储节点挂 1224 块 HDD,单机裸容量 200400TB;10 万台节点支撑 EB 级。
  • 热点对象(爆款视频)用多级 CDN + 边缘缓存卸载,对象存储只做回源。
  • 千亿元数据行:单行约 200 字节 → 约 20TB 元数据,必须分片 + SSD。

八、权衡与备选

决策点本文选型备选权衡说明
冗余方式热 3 副本 / 冷 EC全 EC全 EC 省空间但读延迟高、重建慢;分层最均衡
元数据库分布式 KV + Raft单机 MySQLKV 易分片、水平扩展;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)挂在嘴边,而不是堆组件。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「design」更多文章

  1. 设计一个视频会议系统(WebRTC SFU)
  2. 设计一个 A/B 测试与实验平台
  3. 设计一个分布式锁服务