土法炼钢兴趣小组的算法知识备份

【存储工程】云对象存储内部架构

文章导航

分类入口
storage
标签入口
#cloud-object-storage#s3#object-storage-architecture#durability#storage-class#cross-az-replication

目录

一个对象写入 S3 后,AWS 承诺它的持久性是 99.999999999%(11 个 9)——这意味着如果你存了 100 亿个对象,统计上一年才可能丢一个。与此同时,S3 的容量几乎没有上限,单个桶可以存放无限数量的对象,总存储量已经超过百万亿(数百 EB)级别。一个分布式存储系统如何同时做到”几乎不丢数据”和”几乎无限容量”?靠的不是单一技术,而是元数据层、索引层、存储层三层架构的协同,加上跨可用区(Availability Zone,AZ)复制、纠删码(Erasure Coding)、持续数据校验等机制的叠加。

本文从 S3 的外部行为和 AWS 公开的架构信息出发,拆解云对象存储的内部架构设计——分层如何划分、持久性如何保障、一致性如何演进、存储类别在实现上有什么差异、成本模型背后的工程逻辑是什么。所有分析基于 AWS 官方文档、re:Invent 演讲、公开论文和工程经验推断,非一手源码分析,文中会明确标注推断部分。


一、对象存储的核心设计目标

1.1 与块存储、文件存储的本质区别

对象存储不是块存储加一层 HTTP 接口,也不是文件系统去掉目录树。三者的设计目标从根本上不同:

维度 块存储(EBS) 文件存储(EFS/NFS) 对象存储(S3)
访问接口 块设备(/dev/xvd*) POSIX 文件系统 HTTP REST API
寻址方式 LBA(逻辑块地址) 路径名(/dir/file) 桶名 + 键名(bucket/key)
修改方式 原地覆写任意偏移 原地覆写任意偏移 整对象替换(不支持部分修改)
一致性粒度 单块 单文件(close-to-open) 单对象(强一致)
扩展瓶颈 单卷容量有限(64 TiB) 元数据服务器 几乎无限(分片扩展)
延迟 亚毫秒 毫秒级 数十到数百毫秒
典型用途 数据库、OS 根盘 共享文件、HOME 目录 备份、数据湖、静态资源

对象存储为什么能做到”几乎无限容量”?关键在于它放弃了两个约束:第一,放弃原地修改——对象是不可变的(immutable),修改就是重新写一个完整对象;第二,放弃 POSIX 语义——没有目录树、没有文件锁、没有 seek(),键名空间是扁平的哈希映射。这两个放弃换来的是:元数据可以独立分片扩展,数据可以用纠删码分散到任意多节点,容量扩展不受单机或单卷限制。

1.2 S3 的设计约束

理解 S3 的架构设计,先理解它要满足的约束条件:

这些约束直接决定了架构选择:分层解耦、纠删码而非副本、跨 AZ 同步复制而非异步。


二、三层架构:元数据、索引与存储

S3 的内部架构可以拆解为三个逻辑层:元数据层(Metadata Tier)、索引层(Index Tier)和存储层(Storage Tier)。这不是猜测——AWS 在多次 re:Invent 演讲(如 2021 年 Andy Warfield 的 “Diving Deep on S3 Consistency”、2023 年 “S3 Internals” 等)中公开了这个分层模型。

graph TB
    subgraph 客户端
        A[应用 / SDK]
    end

    subgraph S3 前端
        B[S3 API 网关<br/>请求路由 / 认证 / 限流]
    end

    subgraph 元数据层
        C1[桶元数据<br/>ACL / 版本策略 / 生命周期]
        C2[对象元数据<br/>键名 → 存储位置映射]
    end

    subgraph 索引层
        D[分布式键值索引<br/>键名查找 / 版本管理 / 列举]
    end

    subgraph 存储层
        E1[AZ-1 存储节点<br/>纠删码分片]
        E2[AZ-2 存储节点<br/>纠删码分片]
        E3[AZ-3 存储节点<br/>纠删码分片]
    end

    A --> B
    B --> C1
    B --> C2
    C2 --> D
    D --> E1
    D --> E2
    D --> E3

上图展示了 S3 的分层结构:客户端请求经过 API 网关后,先经元数据层查询桶配置和对象位置信息,再通过索引层定位对象的物理存储分片,最后从存储层的多个 AZ 读取或写入纠删码分片。下面逐层拆解。

2.1 元数据层

元数据层负责两件事:管理桶级别的配置信息,以及维护对象键名到物理存储位置的映射。

桶元数据 包括桶名、所属区域(Region)、访问控制策略(ACL / Bucket Policy)、版本控制配置、生命周期规则、加密配置、事件通知配置等。桶元数据的读写频率远低于对象操作,但对一致性要求很高——一个桶策略的修改必须立即生效,不能出现”策略已更新但还能用旧策略访问”的窗口。

对象元数据 是更关键的部分。每个对象的元数据至少包括:

对象元数据的规模和对象数量成正比——S3 存储了超过 350 万亿个对象(2024 年 re:Invent 数据),元数据本身的存储和查询就是一个巨大的分布式系统问题。

元数据层的工程挑战在于:每个对象至少有一条元数据记录,假设每条记录 1 KB(键名、版本信息、位置映射等),350 万亿个对象的元数据就是 350 PB——这本身就是一个需要分布式存储的数据集。元数据层必须自己也做分片、复制和故障恢复,形成了一个”存储系统的存储系统”的递归结构。

元数据层还承担了一个容易被忽略的职责:垃圾回收。 当对象被删除或被新版本覆盖时,旧数据的存储分片不能立即物理删除——可能还有正在进行的读取操作引用这些分片。元数据层需要维护引用计数或标记-清扫机制,在确认没有活跃引用后才安排物理删除。在万亿级对象规模下,垃圾回收本身就是一个持续运行的大规模后台任务。

2.2 索引层

索引层解决的核心问题是:给定一个桶名和键名,快速找到这个对象的元数据和数据位置。

S3 的索引不是一棵全局的 B 树或一个全局的哈希表——在万亿级对象规模下,任何单一的索引结构都无法支撑。索引按照桶进行分区,每个桶的键空间被进一步分片(Shard)。分片策略的核心是按键名的哈希或字典序范围做水平切分,使得每个分片管理的键数量在可控范围内。

索引层需要支撑三类操作:

  1. 点查(Point Lookup)。 GetObjectHeadObject 需要按桶名+键名精确查找。延迟要求最高,通常在个位毫秒级完成。
  2. 列举(List)。 ListObjectsV2 按前缀和字典序列举键名。这要求索引支持范围扫描,不能只用纯哈希。S3 的列举是按字典序排列的,返回结果按键名排序。
  3. 版本管理。 启用版本控制后,同一个键名可以有多个版本。索引需要维护版本链,支持按版本 ID 查询和按时间顺序列举版本。

索引层的分片和扩展是 S3 能够支撑高并发的关键。AWS 文档提到单前缀支撑每秒 5500 次 GET 和 3500 次 PUT——这实际上反映了单个索引分片的吞吐上限。使用多前缀(多分片)可以线性提升总吞吐。

索引层的另一个重要职责是支撑 S3 事件通知(Event Notification)。 当对象被创建、删除或还原时,索引层需要生成事件并投递到 SNS、SQS 或 Lambda。事件的生成必须和索引更新保持一致——如果索引已经更新但事件没有生成,下游的数据管道就会丢失触发信号。2020 年的强一致性改造让这个保证变得更可靠:PUT 返回成功意味着索引已更新、事件已生成(或即将生成),不会出现”对象已写入但 LIST 看不到、事件也没到”的情况。

2.4 写入路径的完整流程

把三层串起来,一次 PutObject 的内部流程大致如下:

sequenceDiagram
    participant C as 客户端
    participant GW as API 网关
    participant IDX as 索引层
    participant ST1 as 存储节点 AZ-1
    participant ST2 as 存储节点 AZ-2
    participant ST3 as 存储节点 AZ-3

    C->>GW: PUT /bucket/key(数据 + 元数据)
    GW->>GW: 认证、授权、请求校验
    GW->>IDX: 分配存储位置、生成分片计划
    IDX-->>GW: 返回分片目标节点列表

    par 并行写入分片
        GW->>ST1: 写入数据分片 1, 2, 5, 8
        GW->>ST2: 写入数据分片 3, 4, 6 + 校验分片 1
        GW->>ST3: 写入数据分片 7 + 校验分片 2, 3
    end

    ST1-->>GW: 写入确认
    ST2-->>GW: 写入确认
    ST3-->>GW: 写入确认

    GW->>IDX: 提交元数据(键名→分片位置映射)
    IDX-->>GW: 元数据提交成功
    GW-->>C: HTTP 200 OK + ETag

上图展示了 PUT 操作的关键步骤:API 网关先从索引层获取分片计划,然后并行向多个 AZ 的存储节点写入纠删码分片,等待足够的写入确认后,将对象的元数据(键名到分片位置的映射)提交到索引层,最后向客户端返回成功。这里有一个关键细节:元数据提交是在数据写入完成之后——这保证了一旦 PUT 返回成功,后续的 GET 一定能通过索引找到并读取数据。

2.3 存储层

存储层负责数据的物理存储和持久性保障。这一层的设计围绕一个核心目标:11 个 9 的持久性。

数据写入存储层时,经过以下处理:

  1. 分片。 对象数据被切分为多个分片(Shard / Chunk)。
  2. 纠删码编码。 对数据分片计算纠删码校验分片。例如,一种常见的配置是将数据编码为 N 个数据分片 + M 个校验分片(如 8+3 或 6+3),只需要 N 个分片即可恢复完整数据,最多允许 M 个分片同时丢失。
  3. 跨 AZ 分布。 分片被分散存储到同一区域内的多个可用区。标准存储类别至少跨 3 个 AZ 存储。
  4. 写入确认。 只有当足够数量的分片被持久化写入(写入磁盘并确认)后,PUT 操作才向客户端返回成功。

存储层的每个节点管理本地磁盘上的分片数据。节点间不做传统意义上的主从复制——纠删码本身提供了冗余,不需要额外的副本。这也是对象存储相比三副本块存储更节省空间的原因:三副本的存储开销是 3 倍,而 8+3 纠删码的开销只有 1.375 倍。


三、持久性保障机制

3.1 11 个 9 是怎么算出来的

11 个 9 的持久性(99.999999999%)是一个概率声明:在一年时间内,任意一个对象丢失的概率小于 10^-11。要理解这个数字怎么达成,需要拆解持久性模型的输入参数。

持久性模型的核心参数:

简化的概率模型:假设纠删码配置为 8+3(8 数据分片 + 3 校验分片,共 11 分片),允许同时丢失 3 个分片。数据丢失需要在修复窗口内同时丢失 4 个或更多分片。如果单分片年故障率为 1%,分片分布在不同 AZ 的不同服务器上(故障独立),修复时间为数小时,那么在修复窗口内同时有 4 个分片故障的概率极低——量级上可以达到 10^-11 甚至更低。

实际的持久性模型比这复杂得多,还要考虑静默数据损坏(Bit Rot)、软件 Bug 导致的批量丢失、整个 AZ 不可用等相关故障场景。AWS 用持续审计(Continuous Auditing)来应对这些额外风险。

3.2 纠删码 vs. 三副本

维度 三副本 纠删码(8+3)
存储开销 3 倍 1.375 倍
可容忍同时故障数 2(3 副本丢 2 个还有 1 个) 3(11 分片丢 3 个还够 8 个恢复)
读取时的计算开销 无(直接读副本) 需要解码(如果部分分片不可用)
修复开销 读一个完整副本,写一个新副本 读 N 个分片,计算并写入新分片
修复时的网络带宽 传输完整对象 传输部分分片(可以更分散)
适用规模 小规模、低延迟场景 大规模、成本敏感场景

S3 在标准存储类别中使用纠删码而非三副本,核心原因是成本——在 EB 级存储规模下,三副本的存储开销不可接受。纠删码的代价是读取和修复时需要额外的计算,但对于对象存储的访问模式(大对象、高吞吐、非亚毫秒延迟需求),这个代价可以接受。

3.3 跨 AZ 复制策略

S3 Standard 存储类别承诺跨至少 3 个 AZ 存储数据。这里的”跨 AZ”不是简单地把三个副本各放一个 AZ,而是把纠删码的分片分散到多个 AZ。

graph LR
    subgraph Region us-east-1
        subgraph AZ-1
            S1[分片 1]
            S2[分片 2]
            S5[分片 5]
            S8[分片 8]
        end
        subgraph AZ-2
            S3[分片 3]
            S4[分片 4]
            S6[分片 6]
            S9[校验分片 1]
        end
        subgraph AZ-3
            S7[分片 7]
            S10[校验分片 2]
            S11[校验分片 3]
        end
    end

上图是一种可能的分片分布示意(具体分布策略 AWS 未公开):8 个数据分片和 3 个校验分片被分散到 3 个 AZ。即使整个 AZ-3 不可用(丢失 3 个分片),剩余 8 个分片仍然足够恢复完整数据。

跨 AZ 分布策略的关键约束:

3.4 持续数据校验

纠删码能应对整块磁盘故障,但无法自动应对静默数据损坏——磁盘返回了错误数据但没有报告错误(Silent Data Corruption / Bit Rot)。S3 通过持续校验机制来发现和修复这类问题:

AWS 在 2023 年的 re:Invent 演讲中提到,S3 每天运行数十亿次完整性校验。这种持续审计(Continuous Auditing)是 11 个 9 持久性的重要保障——不是只在写入时做一次校验就完事,而是在整个生命周期内持续验证。

3.5 故障域隔离

除了纠删码和数据校验,S3 的持久性还依赖于故障域(Failure Domain)的隔离设计。分片的放置不是随机的——系统会确保同一个对象的分片分布在不同的故障域中:

故障域隔离的难点在于维护成本:当一个存储节点下线或新节点上线时,系统需要重新平衡分片分布,确保故障域隔离约束仍然满足。在 EB 级存储规模下,这种重平衡是一个持续运行的大规模编排任务。

3.6 防止软件 Bug 导致的批量丢失

硬件故障通常是局部的——一块磁盘坏了、一台服务器挂了。但软件 Bug 可能是全局的——一个有问题的代码版本部署到所有节点,可能同时损坏所有节点上的数据。这种相关故障(Correlated Failure)是纠删码无法应对的,因为纠删码假设分片故障是独立的。

AWS 采用的应对策略(基于公开信息推断):


四、一致性模型演进

4.1 最终一致性时代(2006-2020)

S3 在 2006 年发布时采用最终一致性(Eventual Consistency)模型。具体行为是:

这个模型给应用开发带来了大量问题。一个典型场景:数据管道先写入一个对象,然后通知下游来读——下游可能读到旧版本。开发者不得不在应用层加入重试逻辑、版本检查或使用 DynamoDB 做外部一致性协调。

4.2 为什么最初选择最终一致性

最终一致性不是设计缺陷,而是在当时的技术约束下做出的工程取舍:

说白了,在 2006 年的技术条件下,在万亿级对象规模上做跨 AZ 的强一致性索引更新,成本和复杂度都太高。

4.3 强一致性时代(2020 年 12 月至今)

2020 年 12 月,AWS 宣布 S3 支持所有操作的强一致性(Strong Consistency),即读后写一致性和列举一致性,且不额外收费、不牺牲性能。这是一次重大的架构升级。

升级后的行为:

4.4 强一致性的实现思路

AWS 首席工程师 Andy Warfield 在 2021 年 re:Invent 演讲 “Diving Deep on S3 Consistency” 中透露了实现思路。核心改造发生在索引层:

  1. 索引层引入”见证者”(Witness)机制。 每次写入操作不仅更新主索引分片,还通过一个分布式协议确保足够多的索引节点确认了更新。
  2. 读取路径增加一致性检查。 GET 和 LIST 操作在返回结果前,会检查结果的新鲜度——确认它反映了所有已确认的写入。
  3. 消除读取缓存的过期问题。 不是简单地关掉缓存,而是让缓存条目携带版本信息,读取时可以验证缓存是否足够新。

这里的技术核心是一种”缓存一致性”问题的解法。传统的缓存失效有两种策略:主动失效(写入时通知所有缓存节点)和被动失效(设置 TTL 过期)。前者在全球分布的节点间代价太高,后者无法保证强一致。S3 的做法更接近”乐观读 + 版本验证”:读取时先查本地缓存,但在返回前向权威索引节点验证版本号是否是最新的。如果版本号匹配,直接返回缓存结果(无需等待);如果不匹配,重新从权威节点读取。这样在大多数情况下(写入不频繁时)读取延迟不受影响,只有在写入刚发生时需要额外的验证往返。

这个改造的关键约束是:不能增加延迟、不能降低吞吐、不能额外收费。Andy Warfield 在演讲中强调,S3 团队花了数年时间做这个改造,重写了索引层的核心逻辑。

从工程角度看,这里有一个容易忽略的问题:强一致性让应用开发变简单了,但它不是免费的午餐——系统内部的协调开销增加了,只是 AWS 选择自己承担这个开销而不是转嫁给用户。在极高并发写入场景下,强一致性的索引更新可能成为吞吐瓶颈。AWS 通过索引分片的自动拆分和扩展来缓解这个问题。


五、存储类别的实现差异

S3 提供多种存储类别(Storage Class),价格和性能差异很大。这些差异不是简单地改一个参数——背后是不同的物理存储介质、不同的纠删码策略和不同的数据放置策略。

5.1 存储类别总览

存储类别 持久性 可用性 SLA 最小存储期 首字节延迟 跨 AZ 典型用途
Standard 11 个 9 99.99% 毫秒级 3+ AZ 频繁访问数据
Intelligent-Tiering 11 个 9 99.9% 毫秒级 3+ AZ 访问模式不确定
Standard-IA 11 个 9 99.9% 30 天 毫秒级 3+ AZ 不频繁访问
One Zone-IA 11 个 9 99.5% 30 天 毫秒级 1 AZ 可重建的低频数据
Glacier Instant 11 个 9 99.9% 90 天 毫秒级 3+ AZ 归档但需即时访问
Glacier Flexible 11 个 9 99.99% 90 天 分钟到小时 3+ AZ 归档
Glacier Deep Archive 11 个 9 99.99% 180 天 12-48 小时 3+ AZ 长期归档

5.2 Standard vs. Standard-IA:实现差异

Standard 和 Standard-IA(Infrequent Access,不频繁访问)的持久性相同(11 个 9),跨 AZ 策略相同,首字节延迟也相同。那么 IA 便宜在哪?贵在哪?

存储成本更低,访问成本更高。 Standard-IA 的每 GB 存储价格约为 Standard 的一半,但每次 GET 请求的价格是 Standard 的 10 倍(以 us-east-1 为例,Standard 的 GET 价格为 $0.0004/千次,IA 为 $0.001/千次),且有 128 KB 的最小计费对象大小。

从实现角度推断(AWS 未完全公开),IA 类别可能使用以下策略来降低存储成本:

5.3 One Zone-IA:单 AZ 的代价

One Zone-IA 只在一个 AZ 内存储数据。存储价格比 Standard-IA 再低约 20%,但代价是:如果那个 AZ 发生物理灾难(如火灾、洪水),数据就丢了。

这不意味着 One Zone-IA 的持久性低——在 AZ 内部,纠删码和数据校验机制和 Standard 一样,磁盘故障导致数据丢失的概率同样极低。11 个 9 的持久性声明是在”AZ 正常运行”的前提下成立的。但 AZ 级别的灾难不在这个概率模型内。

One Zone-IA 适用于可以从其他来源重建的数据——例如从原始数据重新计算的中间结果、从其他区域复制过来的副本等。

5.4 Glacier 系列:从热到冷的存储介质变化

Glacier 的名字来源于”冰川”,暗示数据像冰川一样”冻住”——不经常移动,但非常持久。Glacier 系列的关键实现差异:

Glacier Instant Retrieval: 存储价格比 Standard-IA 低约 68%,但 GET 请求价格更高。首字节延迟仍然是毫秒级——说明数据仍然在在线磁盘上,只是通过更高的纠删码比例和更高密度的存储介质来降低成本。

Glacier Flexible Retrieval(原 Glacier): 首字节延迟在分钟到小时级别。这意味着数据不在随时可读的在线存储上。AWS 未公开 Glacier 的具体存储介质,但业界普遍推断使用了以下一种或多种策略:

不同的取回选项对应不同的处理优先级:

取回操作的内部流程是异步的:调用 RestoreObject API 发起取回请求,S3 在后台从冷存储介质读取数据并写入在线存储层,完成后对象在指定天数内可通过标准 GET 读取。

# 从 Glacier Flexible 取回对象,标准优先级,取回后保留 7 天
aws s3api restore-object \
  --bucket my-archive-bucket \
  --key "backup/2024-01-full.tar.gz" \
  --restore-request '{"Days": 7, "GlacierJobParameters": {"Tier": "Standard"}}'

# 查看取回状态
aws s3api head-object \
  --bucket my-archive-bucket \
  --key "backup/2024-01-full.tar.gz" \
  --query "Restore"
# 输出示例:ongoing-request="false", expiry-date="Sun, 27 Oct 2025 00:00:00 GMT"

Glacier Deep Archive: 首字节延迟 12-48 小时,存储价格最低(约 $0.00099/GB/月,是 Standard 的约 1/23)。这一级别几乎可以确定使用了磁带存储——只有磁带才能在如此低的成本下提供 11 个 9 的持久性。磁带库的写入是顺序的、批量的,读取需要装载磁带、定位数据,延迟在小时级别完全合理。

关于磁带存储的工程特点:现代磁带技术(如 LTO-9)单盘容量 18 TB(压缩后 45 TB),顺序读写速度 400 MB/s,持久性可达 30 年以上。磁带不耗电(只在读写时才需要驱动器),是长期归档的理想介质。但磁带的随机访问延迟极高(分钟级的装带+定位),完全不适合在线访问。这正是 Glacier Deep Archive 取回延迟长达 12-48 小时的原因——不是技术上做不到更快,而是批量调度磁带读取比逐个处理更经济。

5.5 Intelligent-Tiering:自动分层

Intelligent-Tiering 不是一种独立的存储介质方案,而是一个自动化的分层策略。它监控每个对象的访问频率,自动在多个层之间移动:

Intelligent-Tiering 的代价是每个对象每月有一笔监控和自动化费用(每千个对象 $0.0025)。对于小文件较多的场景,这笔费用可能不划算。

Intelligent-Tiering 的内部实现(基于公开行为推断):S3 为每个启用 Intelligent-Tiering 的对象维护一个”最后访问时间戳”。后台进程定期扫描这些时间戳,将超过阈值的对象标记为移入下一层。移入冷层时,对象的数据可能被重新编码(使用更高比例的纠删码)或迁移到更高密度的存储介质上;当对象被访问时,自动移回频繁访问层,数据重新迁移到热存储介质。

这个设计的工程含义是:Intelligent-Tiering 对每个对象都有状态追踪开销。如果桶中有数十亿个小文件,仅监控费就可能每月达到数千美元——这时候不如根据已知的访问模式直接指定存储类别。Intelligent-Tiering 最适合的场景是”访问模式确实不可预测”的数据集。


六、成本模型分析

6.1 S3 定价的四个维度

S3 的费用不只是”存储费”——理解成本模型需要看四个维度:

  1. 存储费(Storage)。 按每 GB 每月计费,不同存储类别价格不同。
  2. 请求费(Requests)。 按请求次数计费,PUT/COPY/POST/LIST 和 GET/SELECT/其他 分别定价。
  3. 数据传输费(Data Transfer)。 从 S3 传出到互联网按 GB 计费;S3 到同区域 AWS 服务免费或低价。
  4. 管理与分析费。 包括 Intelligent-Tiering 的监控费、S3 Analytics、S3 Inventory 等。

6.2 各存储类别的成本对比

以 us-east-1 区域为例(价格可能变动,以下为撰文时的参考值):

存储类别 存储费(\(/GB/月) | PUT 费(\)/千次) GET 费(\(/千次) | 取回费(\)/GB)
Standard 0.023 0.005 0.0004
Standard-IA 0.0125 0.01 0.001 0.01
One Zone-IA 0.01 0.01 0.001 0.01
Glacier Instant 0.004 0.02 0.01 0.03
Glacier Flexible 0.0036 0.03 0.0004 0.01-0.03
Deep Archive 0.00099 0.05 0.0004 0.02-0.052

6.3 成本模型分析示例

下面用一个具体场景来分析存储类别选择:

场景: 1 TB 日志数据,每天写入一次(365 次 PUT/年),前 30 天每天读取 10 次(300 次 GET/年),之后一年内偶尔读取(约 12 次 GET/年)。数据保留 1 年。

# S3 存储类别年度成本估算(简化模型)

storage_gb = 1024  # 1 TB = 1024 GB
puts_per_year = 365
gets_first_30d = 300
gets_rest_year = 12
total_gets = gets_first_30d + gets_rest_year

# Standard
std_storage = storage_gb * 0.023 * 12           # 存储费/年
std_put = (puts_per_year / 1000) * 0.005        # PUT 费/年
std_get = (total_gets / 1000) * 0.0004          # GET 费/年
std_total = std_storage + std_put + std_get
print(f"Standard:    存储 ${std_storage:.2f} + 请求 ${std_put + std_get:.4f} = 总计 ${std_total:.2f}/年")

# Standard-IA
ia_storage = storage_gb * 0.0125 * 12
ia_put = (puts_per_year / 1000) * 0.01
ia_get = (total_gets / 1000) * 0.001
ia_retrieval = storage_gb * total_gets * 0.01 / 1000  # 取回费(简化)
ia_total = ia_storage + ia_put + ia_get + ia_retrieval
print(f"Standard-IA: 存储 ${ia_storage:.2f} + 请求+取回 ${ia_put + ia_get + ia_retrieval:.4f} = 总计 ${ia_total:.2f}/年")

# Glacier Instant Retrieval
gi_storage = storage_gb * 0.004 * 12
gi_put = (puts_per_year / 1000) * 0.02
gi_get = (total_gets / 1000) * 0.01
gi_retrieval = storage_gb * total_gets * 0.03 / 1000
gi_total = gi_storage + gi_put + gi_get + gi_retrieval
print(f"Glacier IR:  存储 ${gi_storage:.2f} + 请求+取回 ${gi_put + gi_get + gi_retrieval:.4f} = 总计 ${gi_total:.2f}/年")

这个脚本展示了成本分析的基本思路:存储费按月累计,请求费和取回费按次数和数据量累计。实际分析还需要考虑最小计费对象大小(IA 的 128 KB 最小计费)、最小存储期限的提前删除费用、以及数据传输费。

关键结论:

6.4 常见的成本陷阱

  1. 小文件的 IA 陷阱。 Standard-IA 对每个对象按最小 128 KB 计费。如果实际对象只有 1 KB,存储费按 128 KB 算,相当于被放大了 128 倍。大量小文件用 IA 可能比 Standard 更贵。

  2. 最小存储期的删除罚款。 Standard-IA 的最小存储期是 30 天,Glacier Instant 是 90 天,Deep Archive 是 180 天。在最小期限内删除或转移对象,仍然按最小期限收费。举个例子:一个对象存入 Glacier Instant 后 10 天就删除了,你仍然要支付 90 天的存储费。

  3. LIST 操作的隐性成本。 ListObjectsV2 按请求次数计费($0.005/千次),每次最多返回 1000 个键。列举一个包含 1 亿个对象的桶需要 10 万次请求,费用 $0.50——不算贵,但如果频繁列举就不便宜了。一些应用框架(如 Spark 读取 S3 数据时)会做大量 LIST 操作来发现文件,这个成本容易被忽略。

  4. 跨区域传输费。 数据从一个区域传输到另一个区域按 GB 计费($0.02/GB),1 TB 的跨区域传输费 $20,可能超过一个月的存储费。

  5. 版本控制的存储膨胀。 启用版本控制后,每次覆盖写入都会保留旧版本。如果一个 1 GB 的对象每天更新一次,一个月后桶里有 30 个版本,存储费是 30 GB 而非 1 GB。必须配合生命周期规则清理旧版本(NoncurrentVersionExpiration)。

  6. 分段上传的未完成碎片。 分段上传如果中途失败,已上传的分段不会自动清理,会持续产生存储费。建议配置生命周期规则自动清理超期的未完成分段上传:

{
  "Rules": [
    {
      "ID": "abort-incomplete-multipart",
      "Status": "Enabled",
      "Filter": {},
      "AbortIncompleteMultipartUpload": {
        "DaysAfterInitiation": 7
      }
    }
  ]
}

七、访问模式与存储类别匹配

7.1 访问模式分类

对象存储中的数据访问模式大致分为四类:

热数据(Hot Data): 频繁读写,延迟敏感。例如网站静态资源、CDN 源站、实时数据管道的中间结果。适用 Standard 类别。

温数据(Warm Data): 偶尔读取,可以容忍稍高的访问成本。例如 30 天以上的日志、非活跃用户的文件、历史报表。适用 Standard-IA 或 Intelligent-Tiering。

冷数据(Cold Data): 很少读取,但需要在分钟级延迟内取回。例如合规归档、季度审计数据、灾备数据。适用 Glacier Instant Retrieval。

冰数据(Frozen Data): 几乎不读取,取回可以等待数小时。例如法规要求保留 7 年的交易记录、历史备份、监控视频归档。适用 Glacier Flexible 或 Deep Archive。

7.2 生命周期策略实战

S3 生命周期规则(Lifecycle Rules)可以自动将对象在存储类别之间转移。下面是一个日志数据的典型生命周期配置:

{
  "Rules": [
    {
      "ID": "log-lifecycle",
      "Status": "Enabled",
      "Filter": {
        "Prefix": "logs/"
      },
      "Transitions": [
        {
          "Days": 30,
          "StorageClass": "STANDARD_IA"
        },
        {
          "Days": 90,
          "StorageClass": "GLACIER_IR"
        },
        {
          "Days": 365,
          "StorageClass": "DEEP_ARCHIVE"
        }
      ],
      "Expiration": {
        "Days": 2555
      }
    }
  ]
}

这个配置的含义:logs/ 前缀下的对象,写入 30 天后转为 Standard-IA,90 天后转为 Glacier Instant Retrieval,1 年后转为 Deep Archive,7 年后自动删除。

使用 AWS CLI 应用这个配置:

aws s3api put-bucket-lifecycle-configuration \
  --bucket my-log-bucket \
  --lifecycle-configuration file://lifecycle.json

7.3 生命周期策略的注意事项

7.4 访问模式分析工具

S3 提供了 Storage Lens 和 S3 Analytics(Storage Class Analysis)来帮助分析访问模式:

我认为在实际工程中,最有效的存储类别优化不是靠工具自动分析,而是靠对业务数据生命周期的理解。例如:日志数据的访问模式几乎 100% 可预测——写入后几天内频繁查询、一周后偶尔查询、一个月后几乎不查询。这种确定性的模式直接用生命周期规则比 Intelligent-Tiering 更划算。Intelligent-Tiering 和 Analytics 更适合那些访问模式不确定的数据——例如用户上传的文件,你不知道用户什么时候会再次下载。


八、实战:多场景下的 S3 架构选型

8.1 场景一:数据湖的原始层

数据湖(Data Lake)通常把原始数据(Raw Data)存入 S3 作为”真相之源”(Source of Truth),然后通过 ETL 管道生成加工层数据。

# 使用 aws s3 sync 将本地数据同步到 S3 数据湖
aws s3 sync /data/raw/ s3://my-data-lake/raw/ \
  --storage-class STANDARD \
  --metadata '{"x-amz-meta-source":"production-db","x-amz-meta-pipeline":"v2"}' \
  --exclude "*.tmp"

架构要点:

8.2 场景二:多区域灾备

跨区域复制(Cross-Region Replication,CRR)可以把一个桶的对象自动复制到另一个区域的桶中。这在灾备场景下常用,但需要理解其实现特点:

8.3 场景三:合规归档

金融、医疗等行业有法规要求数据保留 N 年。S3 Object Lock 提供了 WORM(Write Once Read Many,一次写入多次读取)功能:

# 创建启用 Object Lock 的桶
aws s3api create-bucket \
  --bucket compliance-archive \
  --object-lock-enabled-for-object-lock

# 设置默认保留策略:合规模式,保留 7 年
aws s3api put-object-lock-configuration \
  --bucket compliance-archive \
  --object-lock-configuration '{
    "ObjectLockEnabled": "Enabled",
    "Rule": {
      "DefaultRetention": {
        "Mode": "COMPLIANCE",
        "Years": 7
      }
    }
  }'

合规模式(Compliance Mode)下,任何用户(包括 root 账户)都无法在保留期内删除或修改对象。这和 Glacier Vault Lock 的效果类似,但 Object Lock 可以用在任何存储类别上。


九、性能优化与限制

9.1 S3 的性能基线

S3 在单前缀维度的性能基线(AWS 文档公开数据):

注意这是单前缀的限制。如果数据分布在多个前缀下,总吞吐可以线性增长。例如,使用 100 个不同前缀,理论上可以达到每秒 55 万次 GET。

9.2 提升吞吐的关键做法

使用随机化前缀。 如果所有对象都在 data/ 一个前缀下,请求会集中到同一组索引分片,成为瓶颈。把键名设计为 data/{hash-prefix}/object-name,利用哈希前缀分散请求到多个分片。

从 2018 年开始,AWS 改进了 S3 的自动分片机制——即使不使用随机前缀,S3 也会根据请求模式自动拆分索引分片。但在突发流量场景下,自动拆分可能来不及。如果能预知高并发需求,提前使用分散的前缀仍然是更可靠的做法。

使用分段上传。 大文件(100 MB 以上)应使用分段上传(Multipart Upload),每个分段独立上传,上传完成后合并。好处是: - 各分段可以并行上传,利用多连接带宽 - 单个分段失败只需重传该分段,不需要重传整个文件 - 分段上传支持暂停和恢复

分段上传的实际操作示例:

# 对 5 GB 文件使用分段上传
# aws s3 cp 默认对大文件自动启用分段上传
aws s3 cp large-dataset.parquet s3://my-bucket/data/large-dataset.parquet \
  --expected-size 5368709120

# 手动控制分段大小(每段 100 MB)
aws configure set default.s3.multipart_threshold 100MB
aws configure set default.s3.multipart_chunksize 100MB

# 使用低级 API 手动分段上传(更精细的控制)
# 1. 初始化分段上传
aws s3api create-multipart-upload \
  --bucket my-bucket \
  --key "data/large-dataset.parquet" \
  --query "UploadId" --output text

# 2. 上传各分段(使用返回的 UploadId)
# 3. 完成分段上传

使用 S3 Transfer Acceleration。 通过 CloudFront 边缘节点中转数据,利用 AWS 骨干网络加速跨地域传输。适用于客户端距离 S3 桶所在区域较远的场景。

使用 S3 Express One Zone。 2023 年 re:Invent 发布的新存储类别,基于目录桶(Directory Bucket)设计,提供个位毫秒的延迟和每秒数十万次请求的吞吐。它使用单 AZ 部署(不跨 AZ),存储价格高于 Standard 但请求费用大幅降低(PUT 和 GET 费用只有 Standard 的约 50%)。适用于机器学习训练数据、实时分析等对延迟敏感的场景。Express One Zone 的架构和传统 S3 有显著差异——它不使用扁平键名空间,而是引入了真正的目录结构,更接近文件系统的组织方式。

9.3 延迟特征

S3 的延迟特征和块存储完全不同:

操作 典型延迟 影响因素
PUT(小对象 < 1 MB) 50-200 ms 跨 AZ 写入确认、索引更新
PUT(大对象分段上传) 取决于带宽和分段并行度 网络带宽、分段数
GET(小对象 < 1 MB) 20-100 ms 索引查找、数据读取
GET(首字节,大对象) 20-100 ms 同小对象
HEAD 10-50 ms 只查索引,不读数据
LIST(1000 个键) 50-200 ms 索引扫描范围
DELETE 30-100 ms 索引标记删除

S3 的延迟主要来自网络往返(客户端到 S3 端点)和内部索引查找,而非磁盘 I/O。这也解释了为什么 S3 的延迟优化主要靠减少请求次数(批量操作、预签名 URL、CDN 缓存),而不是像块存储那样优化 I/O 路径。

9.4 请求限流与重试策略

S3 在检测到单个桶或前缀的请求速率过高时,会返回 HTTP 503(Service Unavailable)或 HTTP 429(Too Many Requests)。这不是 Bug,而是保护机制——防止单个租户的突发流量影响其他租户。

应对策略:

# AWS SDK 的重试配置示例(boto3)
import boto3
from botocore.config import Config

config = Config(
    retries={
        'max_attempts': 10,
        'mode': 'adaptive'  # 自适应重试,根据错误类型调整策略
    }
)

s3 = boto3.client('s3', config=config)

十、架构设计的取舍与思考

10.1 不可变性的代价

对象存储的”整对象替换”语义是其可扩展性的基础——不需要处理并发写入同一对象的不同部分、不需要维护文件锁、不需要管理块级别的一致性。但这个设计带来了工程上的限制:

这些限制不是 Bug,而是深思熟虑的设计选择——对象的不可变性让纠删码编码一次即可、让缓存无需考虑一致性失效、让版本管理可以简单地保留所有旧版本。如果需要原地修改、追加写入、文件锁,应该选择块存储(EBS)或文件存储(EFS),而不是试图在对象存储上模拟这些语义。

实际工程中常见的变通方案:

10.2 扁平命名空间的工程影响

S3 没有真正的”目录”概念。photos/2025/vacation/beach.jpg 中的 / 只是键名的一部分,不是目录分隔符。S3 控制台显示的”文件夹”是通过 ListObjectsV2PrefixDelimiter 参数模拟出来的。

这个设计的工程影响:

10.3 强一致性后的架构简化

2020 年之后 S3 的强一致性让很多之前需要在应用层处理的一致性问题消失了:

这是一个值得注意的架构演进方向:云服务提供商在底层基础设施中承担更多的复杂性,让应用层更简单。但工程师仍然需要理解底层实现——在极端场景下(如每秒数万次写入同一前缀),强一致性的内部协调开销可能导致性能不如预期,这时候需要回到索引分片的层面去理解和优化。

10.4 对象存储的适用边界

我认为选择对象存储时,最重要的判断标准不是”数据量大不大”,而是”访问模式是否符合对象存储的语义”。具体来说:

对象存储不是万能的,但在它适用的场景里,几乎没有替代方案能同时提供这样的持久性、容量和成本效率。


十一、与其他云厂商的对比

S3 定义了云对象存储的事实标准接口,但各家云厂商在实现细节上有差异:

维度 AWS S3 Google Cloud Storage Azure Blob Storage
持久性 11 个 9 11 个 9 11 个 9(LRS: 11 个 9,GRS: 16 个 9)
一致性 强一致(2020 年起) 强一致(发布即支持) 强一致
存储类别数 7 4(Standard / Nearline / Coldline / Archive) 4(Hot / Cool / Cold / Archive)
最低归档延迟 12-48 小时(Deep Archive) 小时级(Archive) 小时级(Archive)
最低存储价格 ~$0.001/GB/月 ~$0.0012/GB/月 ~$0.00099/GB/月
S3 兼容 API 原生 提供互操作 XML API 不兼容(独立 API)

Google Cloud Storage(GCS)在设计上选择了从一开始就提供强一致性——GCS 没有经历 S3 那样从最终一致到强一致的演进。这并不意味着 GCS 的技术更先进,更可能是因为 GCS 发布时(2010 年)分布式一致性的工程实践已经更成熟,而且 GCS 的初始规模远小于 S3,做强一致性的代价更低。

Azure Blob Storage 在存储类别的命名上和 S3 不同(Hot / Cool / Cold / Archive),但底层逻辑类似。Azure 的一个独特特性是提供了”更改 Blob 层”的 API,可以在不复制数据的情况下改变 Blob 的访问层(类似于 S3 的生命周期规则转移)。


十二、总结

S3 的 11 个 9 持久性和近乎无限的容量,不是靠某一个单一技术实现的,而是多层架构协同的结果:

存储类别的差异不只是”价格表上的数字不同”——Standard 和 IA 可能在纠删码配置和存储介质密度上有区别,Glacier 系列则涉及从在线磁盘到近线磁带的介质切换。理解这些实现差异才能做出正确的存储类别选择,而不是单纯比较每 GB 的价格。

成本优化的核心不是”选最便宜的存储类别”,而是”让访问模式和存储类别匹配”——热数据用 Standard,温数据用 IA,冷数据用 Glacier Instant,冰数据用 Deep Archive,不确定的用 Intelligent-Tiering。生命周期规则把这个匹配过程自动化,但前提是你理解每种类别的最小存储期、最小计费大小和取回费用。


参考资料

官方文档

re:Invent 演讲

论文与书籍


上一篇:【存储工程】云块存储架构

下一篇:【存储工程】计算存储分离实践

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2025-10-06 · storage

【存储工程】数据持久性工程

深入分析数据持久性的工程计算——故障率模型、多副本与纠删码的持久性推导、相关故障的影响、实际数据丢失案例与持久性计算器实现

2025-09-25 · storage

【存储工程】S3 API 深度解析

全面剖析 S3 API 的工程细节——Multipart Upload、S3 Select、生命周期策略、跨区域复制、性能优化与 boto3 高级用法

2026-04-22 · db / storage

数据库内核实验索引

汇总本站数据库内核与存储引擎实验文章,重点覆盖从零实现 LSM-Tree 及其工程权衡。


By .