这一讲读起来"每句都懂、连起来不懂",因为它一路在给结论:
- 可靠性、可用性、持久性三个词换着用,但它们不是一回事
- “100 年丢一次” → “1 年丢一次” → “4 年丢一次” → “15 天丢一次”,中间的账没算
1 - [(1-p)^0.5]^4 ≈ 2p直接甩出来- “28 + 4”、“38%”、“纠删码” —— EC 是什么完全没说
- “Key 里的 / 只是一个普通字符” —— 这一句到底意味着什么
- “对 Key 做 Hash 或按 Range 分区” —— 真实的对象存储比这多一整层
- 而且它假定你已经懂:硬盘/机架这些硬件、RAID5、HDFS
怎么读这份带读
| 这一部分是 | 建议 | |
|---|---|---|
| 第一部分 · 地基(一~四) | 原文假定你已经会的东西 | 不熟就从这里开始,四节都不难 |
| 第二部分 · 原文(五~九) | 38 讲本身讲了什么,把省掉的推导补上 | 主体 |
| 第三部分 · 引申(十~十二) | 原文只给了一句话的工程实践 | 标了 ⚠ 非原文,想知道"实际怎么做"再看 |
只想搞懂某一件事的话:
对象存储凭什么取代文件系统 → 七 · 十二 | 那些公式 → 八 · 九 | 实际怎么分片 → 十 · 十一
这一讲的演示脚本(推荐按这个顺序跑):
| # | 脚本 | 演什么 | 配合 |
|---|---|---|---|
| 1 | 代码/RAID5是什么.py |
RAID 三种粘法 · RAID5 就是那个异或 · 重建为什么要 3 天 | 三 |
| 2 | 代码/HDFS是什么.py |
NameNode / DataNode 分工 · 账本有多大(日志 vs 图片) | 四 |
| 3 | 代码/没有目录的世界.py |
树 vs 扁平 dict · 接回 37 讲的分片函数 | 七 |
| 4 | 代码/纠删码是什么.py |
EC 其实就是异或 · 那笔 38% 的账 | 八 |
| 5 | 代码/持久性怎么算.py |
1-[(1-p)^0.5]^4 是怎么来的 · 三个场景一次算完 |
九 |
| 6 | 代码/对象存储怎么分片.py |
元数据层 / 数据层两层分片 · S3 的真实历史 | 十 |
| 7 | 代码/故障域和打散.py |
故障域 · 打散,用 9 块盘讲清楚 | 十一 |
第一部分 · 地基
这四节是原文假定你已经会的。它们不难,但缺了后面全是糊的。
一 · 一个机房长什么样:盘、机器、机架、机房
"盘"就是硬盘——一块实体硬盘,能拿在手里的那种。后面所有的"坏一块""能坏 4 块"说的都是它。
机房
├── 机架1(一个两米高的铁柜子)
│ ├── 服务器1 ← 一个机箱,里面插了 12 块硬盘
│ ├── 服务器2 ← 又 12 块
│ ├── ...
│ └── 服务器20
│ ★ 这 20 台共用【一个交换机】和【一路电】
├── 机架2
├── ...
└── 机架N
四层,每层的数量级:
| 层 | 是什么 | 一个里面装几个下一层 |
|---|---|---|
| 盘 | 一块硬盘,现在常见 10~20 TB | —— |
| 机器 | 一台服务器(一个机箱) | 插 12~36 块盘 |
| 机架 rack | 一个铁柜子 | 装 20~40 台机器 |
| 机房 | 一栋楼里的一个大厅 | 几百上千个机架 |
所以"1000 块盘的集群"翻译成实物大约是:80 台服务器、3~4 个机架、10 PB 左右容量。
不算大,一个中等公司的规模。
1.1 为什么全篇都在数"盘",而不是数"机器"
因为坏的最小单位就是一块盘。
机械硬盘里有高速旋转的盘片和悬在上面的磁头——它是整台服务器里唯一大量存在的活动部件,所以是最容易坏的零件。数据中心的经验值是每年约 1~2% 的盘会坏:
1000 块盘的集群 → 平均每个月坏一块
10000 块盘的集群 → 平均每周坏两三块
★ 坏盘不是事故,是日常。
这就是原文那句"服务端存储只要规模够大,很多看起来是小概率的事件,变成必然事件"的物理含义。
你不是在防范意外,你是在设计一个"每周都会坏几块盘、但用户永远察觉不到"的系统。
1.2 这四层就是后面说的"故障域"
先记住这张表,第十一节会用到它:
| 出了什么事 | 一次带走 | 频率 |
|---|---|---|
| 一块盘坏 | 1 块 | 天天发生 |
| 一台机器挂(电源、主板、系统崩) | 那台上的 12 块同时没了 | 常有 |
| 一个机架挂(交换机挂、这路电断) | 20台×12 = 240 块同时没了 | 偶尔 |
| 一个机房挂(断电、光缆被挖断) | 全部 | 罕见但会有 |
★ 关键:机器不是"各坏各的",它们成批地坏。 这一句会在第十一节掀翻"能坏 M 块"这个说法。
二 · 三个词:持久性 / 可用性 / 可靠性
原文一路混用这三个词,他自己也察觉了,写了句"可靠性,更严谨来说是持久性(Durability)问题"。不分清,后面半篇都是糊的。
| 词 | 问的是 | 关键性质 |
|---|---|---|
| 持久性 Durability | 数据还在不在 | 不可逆。丢了就是永久丢了 |
| 可用性 Availability | 现在能不能访问 | 可恢复。机器起来了就好了 |
| 可靠性 Reliability | 日常口语,两者的笼统合称 | 原文里出现的"可靠性",九成指的是持久性 |
对着具体情况看就清楚了:
| 发生了什么 | 持久性 | 可用性 |
|---|---|---|
| 一块盘坏了,数据在另外两个副本上 | ✓ 没事 | ✓ 没事(读别的副本) |
| 整个机房断电,盘都好好的 | ✓ 没事 | ✗ 全挂 |
| 网线断了(37 讲的分区) | ✓ 没事 | ✗ 或降级 |
| 3 个副本同时坏了 | ✗ 永久丢失 | ✗ |
★ 可用性能补救,持久性不能。
这就是 36 讲那条底线为什么排在"24 小时不间断"前面:宕机一小时用户骂你,丢了数据用户离开你。
第九节整节讨论的都是持久性——记住这点,那些公式就有归属了。
三 · RAID5 是什么
python3 代码/RAID5是什么.py
RAID = 把几块盘粘成"看起来像一块",操作系统只看到一块盘,底下几块、怎么摆由 RAID 控制器管。粘法有好几种:
| 名字 | 怎么粘 | 4 块 1TB 能用 | 能坏几块 |
|---|---|---|---|
| RAID0 | 首尾相接拼成一块 | 4 TB | 0(坏一块全完) |
| RAID1 | 两两存一模一样的 | 2 TB | 1(一半空间换冗余) |
| RAID5 | 3 块数据 + 1 块校验 | 3 TB | 1(只花一块盘的代价) |
3.1 RAID5 怎么只花一块盘就换到"能坏一块"
就是异或(第八节的纠删码是同一个东西,只是块数多):
盘0: b"AAAA" 盘1: b"BBBB" 盘2: b"CCCC"
盘3: 盘0 ^ 盘1 ^ 盘2 ← 校验块
# 盘2 烧了?
盘0 ^ 盘1 ^ 盘3 → b"CCCC" ← 换回来了
靠的是 a ^ b ^ b == a。
3.2 盘坏之后:降级 → 重建
坏一块后系统进入降级模式:
- ✓ 数据没丢(持久性完好)
- ✗ 每读一次都要把其余三块读一遍现算 —— 慢很多(可用性降级)
- ✗ 此刻一块冗余都不剩了,再坏一块就真没了
所以必须尽快换新盘、把丢的那块重建回去:新盘上每一小块 = 其余三块对应位置的异或。
3.3 ★ 原文那个"3 天"是这么来的
重建 = 把其余三块盘整盘读一遍:
单盘 3TB: 要读 9TB ÷ 100MB/s = 26 小时 ≈ 1.1 天
单盘 10TB: 要读 30TB ÷ 100MB/s = 87 小时 ≈ 3.6 天
正好对上原文:“假设 RAID5 的数据修复时间是 1 天”= 3TB 盘的年代;
“如果实际数据修复时间没那么理想,比如变成 3 天”= 10TB 大盘。
而这还是理想值——原文自己提醒过,这几块盘还得同时对外服务。
3.4 用本讲的语言给 RAID5 定位
| 作用范围 | M(能坏几块) | T0(修复窗口) | |
|---|---|---|---|
| RAID5 | 一台机器的几块盘 | 1 | 3 天,不随集群变短 |
| EC 28+4(第八节) | 整个集群几百台机器 | 4 | 随集群变大而变短 |
★ 同一个技术(算术冗余),换个层次做,命运完全不同。
"为什么 RAID5 加再多机器也快不了"这件事的机制在第十一节(打散)——那里才讲得清。
四 · HDFS 是什么
python3 代码/HDFS是什么.py
4.1 它想解决什么:一个文件太大,一台机器放不下
老套路:切开——跟 37 讲的分片是同一个动作,只不过切的不是"很多条记录"而是"一个大文件":
/logs/2026.log (1GB) → 按 64MB 切成 16 个 block → 散到几百台机器,每块 3 副本
于是产生一个新问题:谁记得这 16 块分别在哪台机器上? HDFS 的答案是两种角色:
| 角色 | 台数 | 存什么 |
|---|---|---|
| NameNode | 1 台 | 只存账本:目录树 + 每个文件由哪些 block 组成 + 每块在哪几台。整本账常驻内存,不存任何文件内容 |
| DataNode | 几百台 | 只存 block 的实际字节,甚至不知道自己手上这块属于哪个文件 |
读一个文件两步:先问 NameNode 要清单,再拿着清单直接去 DataNode 取。
★ 注意这个分工:数据走 DataNode(可以有几百台),但所有元数据只在一台机器上。瓶颈就藏在这。
4.2 ★ 账本有多大:同样 10TB,日志 vs 图片
大日志(每个 1GB) 文件 10,240 个 元数据 174,080 条 → 26 MB
小图片(每个 100KB) 文件 1.07 亿 个 元数据 2.15 亿 条 → 30 GB
差 1233 倍
数据本身两边都是 10TB,DataNode 毫无压力,加机器就行。撑不住的不是数据,是账本——而 NameNode 只有一台。
这就是原文"单 Master 结构,决定了它能够存储的元数据条目数有限"的全部含义。
4.3 ⚠ 澄清原文那句"不足 64M 也会占用 64M"
这句容易被读成"磁盘空间浪费 64 倍",但 HDFS 不是这么工作的:
block size 是上限,不是下限。 一个 100KB 的文件在 DataNode 的磁盘上就占 100KB,不会真的吃掉 64MB。
真正被吃掉的是 NameNode 的内存——就是上面那 30 GB。所以原文列的三条里,第①条和第②条其实是同一个瓶颈:
| 原文的三条 | 真正的机制 |
|---|---|
| ① block 64M,小文件浪费 | 元数据条目太多 → NameNode 内存撑爆 |
| ② 单 Master,元数据条目有限 | 同一件事:Master 是单机的,内存有上限 |
| ③ 沿用文件系统 API(有目录) | 有目录树 → Master 没法拆成分布式(第七节详解) |
而"调小 block"这个念头错得更狠:
| block 大小 | 1GB 日志切成几块 | 10TB 日志的元数据条目 |
|---|---|---|
| 64 MB | 16 | 174,080 |
| 16 MB | 64 | 665,600 |
| 1 MB | 1,024 | 10,496,000 |
block 越小 → 同一个文件切成越多块 → 元数据条目越多 → NameNode 死得更快。
原文"这是不正确的做法"的结论是对的,只是给的理由(省空间)不准——换成正确的理由之后,这个结论反而更硬。
4.4 为什么不把 NameNode 也做成分布式的
答案在第七节:NameNode 存的是目录树,而树没法分片。 一条因果链:
沿用文件系统 API(有目录)
→ 元数据是一棵树,拆不到多台机器
→ 只能单 Master,内存有上限
→ 元数据条目数有上限
→ 存不了海量小文件
对象存储怎么破的?把目录扔了。 key 一扁平,元数据就能按 key 分片,Master 就能是分布式的。
★ 所以 HDFS 不是"做得不好",是为另一个场景设计的:GFS 论文的背景是搜索引擎,
HDFS 的舒适区是大文件日志 + 批量分析——单文件 1GB 级、元数据条目极少、顺序整体读,64MB 大块正合适。
拿它存几百 K 的图片,是拿一个"条目数有上限"的系统去装一个"条目数无上限"的东西。
4.5 两个地基的病根是同一个
进第二部分之前把三、四节收一下——RAID5 和 HDFS 各自撞的墙,其实是同一堵:
| 被困在哪 | 后果 | |
|---|---|---|
| RAID5 | 重建只能在一组盘内完成 | T0 不随规模变短 → 机器越多越危险 |
| HDFS | 元数据只能在一台机器上 | 条目数有上限 → 小文件越多越撑不住 |
★ 都是"某件事只能在一个小范围内完成",所以加机器解决不了。
对象存储把两者都打散了:数据打散 → 修复能全集群并行(第十一节);
key 扁平 → 元数据能分片(第七、十节)。
这一讲后面所有内容,都是这一句的展开。
第二部分 · 原文这一讲
五 · 存储为什么复杂:异常处理才是它的业务逻辑
| 普通业务程序 | 存储系统 | |
|---|---|---|
| 精力花在 | 正常分支 | 各种异常分支 |
| 遇到意外 | 抛个错,告诉用户"你不该这么玩" | 必须兜住,把损失降到最低 |
“存储系统你需要花费绝大部分精力在各种异常情况的处理上,甚至你应该认为,
这些庞杂的、多样的错误分支处理,才是存储系统的"正常业务逻辑"。”
这正是 36 讲那四堵墙的另一种说法。 你自己写持久化会撞的墙(增量、索引、并发、崩溃、机器坏),全都是"异常处理"。存储中间件的价值就是把这堆异常分支从每个业务里搬走一次、写对一次、全世界共用。
六 · 单机文件系统的四条硬伤
| 问题 | 表现 | 对应第二节哪个词 |
|---|---|---|
| 伸缩性 | 单机容量有限 | —— |
| 性能瓶颈 | 文件数过临界点后性能崩塌 | —— |
| 持久性 | 单副本,坏盘即丢 | 持久性 |
| 可用性 | 单副本,宕机即不可读写 | 可用性 |
后两条是同一个原因(单副本)造成的两种不同后果——对着第二节那张表看就很明白了。
6.1 那笔"小概率变必然"的账
原文给了一串数字但没说怎么来的。其实只有一步除法——每台机器是一个独立的丢失源,概率线性叠加:
1 台机器 → 100 年丢一次
10 台机器 → 10 年丢一次
100 台机器 → 1 年丢一次
1000 台机器 → 37 天丢一次
★ 这就是原文那句"小概率事件变成必然事件"。
单机时代能靠"这事一百年才发生一次"糊弄过去,规模一上来就糊弄不了了。
(物理直觉见第 1.1 节:1000 块盘的集群本来就是每月坏一块。)
6.2 ⚠ 一处我算不出原文数字的地方
原文接着说:“修复时间变成 3 天,单机可靠性直降至 4 年丢失一次。”
按"丢数据概率与修复窗口成正比"这个最直接的模型:
1 天 → 3 天,窗口 ×3,那 100 年应该变成 33 年,不是 4 年。
(而从 4 年推到"100 台 15 天"那步是对的:4×365÷100 ≈ 15 天)
中间那 8 倍差距原文没解释。真实世界里大盘 RAID5 还有一个更狠的杀手:重建时要把其余每块盘整盘读一遍,而不可恢复读错误(URE)的概率随盘容量线性上升——10TB 的盘重建时几乎必然撞上一次。这才是"RAID5 在大盘时代不够用"的真正原因。
结论方向没问题(修复越慢越危险),但这几个具体数字别去背。
七 · 对象存储:扔掉目录,换来了什么
python3 代码/没有目录的世界.py
7.1 一句话:树 → 扁平 dict
# 文件系统:一棵树,/ 是父子关系的分隔符
文件系统 = {"照片": {"2026": {"a.jpg": ..., "b.jpg": ...}}}
文件系统["照片"]["2026"]["a.jpg"] # 一层层走下来
# 对象存储:一个扁平 dict,/ 就是个普通字符
对象存储 = {"照片/2026/a.jpg": ..., "照片/2026/b.jpg": ...}
对象存储["照片/2026/a.jpg"] # 一次直达
原文那句"Key 中出现的 / 字符,只是一个普通字符",意思就是:
它和a.jpg里的那个.地位完全一样,系统不认识它。
“照片/2026/” 这个目录并不存在,存在的只是几个 key 恰好都以它开头。
7.2 扔掉了什么
| 操作 | 文件系统(树) | 对象存储(扁平) |
|---|---|---|
| 建目录 | 必须 mkdir,否则往里写会报错 | 不需要,直接 put("照片/2027/d.jpg") |
| 列目录 | 取子节点,O(1) | 前缀扫描——S3 的 list 接口就是干这个的 |
| 目录改名 | 改一个节点,O(1),底下多少文件都不动 | 逐个改名,O(n) |
第三行是真正的代价:一个"目录"下有 1000 万个对象,改名就是 1000 万次操作。
所以 S3 干脆不提供"重命名目录"这个操作。 不是偷懒,是它做不到便宜。
7.3 换来了什么 —— 接回 37 讲那个函数
37 讲的结论:分片 = N 个小 dict + 一个「该找哪台(key)」的函数。
扁平 dict 天生适配这个函数:每个 key 独立算,互不相干。树不行,三个原因:
| # | 树为什么没法分片 |
|---|---|
| 1 | 形状由用户决定。「照片」下塞 1 亿个文件,那台机器撑爆、别的空着,你无能为力 |
| 2 | rename 变成跨分片事务。把子树挪到另一个顶层目录下 = 同时改两台机器,要么原子、要么留下半挂的子树 —— 正是 37 讲第七节那个最难的问题 |
| 3 | 查找要逐层走。a/b/c/d/e.jpg 要问 5 次,每层还可能在不同机器上;扁平 dict 一次哈希直达 |
★ 这就是第 4.4 节那条因果链的另一头,也是原文"在分布式系统中维护目录树结构会遭遇诸多难题"的全部含义。
对象存储用「目录改名从 O(1) 变成 O(n)」,换来了「每个 key 都能独立算出该找哪台机器」。
7.4 "去关系"又是什么意思
原文:
“NoSQL 数据库的名字其实并不恰当,它们更多的不是去 SQL,而是去关系。
有关系意味着有多个索引,也就是有多个 Key,而这对数据库转为分布式存储系统来说非常不利。”
接回 37 讲第一节:数据库的每一个索引,背后就是一个 KV。所以——
一张有 3 个索引的表 = 3 个 KV = 同一条数据在【3 个不同的 key 下】都有影子
按 主键 分片 → 主键KV 在 5 号机
按 邮箱 分片 → 邮箱KV 在 2 号机
改一条数据 → 要【同时】改 5 号机和 2 号机 → 又是跨分片事务
★ “去关系” = 让每条数据只有一个 key = 它只落在一台机器上 = 所有操作都退化成单机操作。
去目录(7.3)和去关系(7.4)是同一个动机的两面:都是为了让该找哪台(key)这个函数能存在。
(第十二节还有第三刀。)
7.5 S3 的接口有多小,以及七牛为什么不止于此
第一个公认的对象存储是 AWS S3,最基本接口就两个:
func PutObject(bucket, key string, object io.Reader) (err error)
func GetObject(bucket, key string) (object io.ReadCloser, err error)
两个函数。 对比文件系统的
open/read/write/seek/mkdir/rename/stat/chmod/...——
接口小到这个程度,正是"能水平扩展"的代价和证明。
而原文那个公式:
七牛云存储 = 对象存储 + 上传下载加速 + 多媒体处理
后面两项不是分布式存储的问题,是"用户在公网另一头"带来的新问题:
| 加的这一项 | 解决什么 |
|---|---|
| 上传加速 | 2G 弱网下怎么传得上去、几 TB 的大文件怎么传得完(分片上传、断点续传) |
| 下载加速(CDN) | 用户离机房很远 —— 成熟技术直接拿来用 |
| 多媒体处理 | 文件托管在你这儿,缩略图 / 转码的需求会自然长出来 |
八 · 纠删码:那个 “28 + 4” 和 “38%”
python3 代码/纠删码是什么.py
8.1 EC 就是异或(第三节 RAID5 那个的推广)
原文只说"切 28 份、算 4 份冗余",没说怎么算。最简单的形态 N+1 就是一次异或:
块 = [b0, b1, b2, b3] # 文件切 4 块,存 4 块盘
校验块 = b0 ^ b1 ^ b2 ^ b3 # 算出第 5 块,存第 5 块盘
# 第 3 块盘烧了?
b2 = b0 ^ b1 ^ b3 ^ 校验块 # 剩下的全异或一遍,就把它换回来了
8.2 从 N+1 到 N+M
一块校验块 = 一个方程: b0 ^ b1 ^ b2 ^ b3 = P
丢一块 → 一个未知数 → 解得出来
丢两块 → 两个未知数 → 一个方程解不出来
所以想能坏 M 块,就得有 M 个【互相独立】的方程 = M 块冗余。
真实系统用的是 Reed-Solomon 码(在有限域上做线性代数),但思路就这一句:M 块冗余 = M 个方程 = 能解 M 个未知数。
8.3 那笔 38% 的账
| 方案 | 存 1PB 要买 | 相当于 3 副本的 | 能同时坏 |
|---|---|---|---|
| 单副本 | 1.00 PB | 33% | 0 块 |
| 3 副本 | 3.00 PB | 100% | 2 块 |
| EC 28+4 | 1.14 PB | 38% | 4 块 |
32 ÷ 28 = 1.143,1.143 ÷ 3 = 38% —— 原文那个数字就是这么来的。
★ 这里有件反直觉的事:冗余度从 3 倍降到 1.14 倍(省 62% 的钱),容错却从"能坏 2 块"涨到"能坏 4 块"。
原文那句"冗余度降低不一定会伤害持久性和可用性,它们和冗余度不是正相关,而和集群的容错能力相关"说的就是这个。
为什么能又便宜又抗造?因为 3 副本是"复印"——存 3 份只换来 2 块容错;
而 EC 是"算术"——4 份冗余保住 28 份数据,摊到每份数据头上极便宜。
8.4 那 EC 的代价在哪(原文没讲)
| 代价 | 说明 |
|---|---|
| 读放大 | 读一个文件要凑齐 28 块、跨 28 台机器;3 副本随便找一台就整个读出来 |
| 修复更贵 | 坏一块盘要读 28 块才能算出来;3 副本只要复制一份 —— 修复越慢,第九节的 T0 越长 |
| 写要算 CPU | 写入前先算冗余块 |
所以真实系统常常是分层的:热数据 3 副本(读得快、修得快),冷下来之后转 EC(存得便宜)。
九 · 持久性的算式:T0 和 M
python3 代码/持久性怎么算.py
9.1 先搞清楚"丢数据"是怎么发生的
不是"坏一块盘就丢数据"——有冗余,坏一块能修回来。
一块盘坏了
├───────────── T0:修复窗口 ─────────────┤
└─ 这段时间里如果【又坏了 M 块】→ 数据真的没了
所以持久性只由两个数决定,原文说的就是这一句:
| 参数 | 是什么 | 方向 |
|---|---|---|
| T0 | 修复一块盘要多久 | 越短越安全 |
| M | 还能再扛几块(N+M 里的 M) | 越大越安全 |
9.2 那个吓人的公式,其实是一句话
原文直接甩了 1 - [(1-p)^0.5]^4 ≈ 2p。拆开只有两条规则:
p = 原来丢数据的概率,(1-p) = 不丢的概率
盘数变成 k 倍 → 指数 × k (更多盘 = 更多独立的出事机会)
修复窗口变成 t 倍 → 指数 × t (窗口更长 = 暴露更久)
新的丢失概率 = 1 - (1-p)^(k×t)
★ 把两个倍数乘起来当指数,就完了。
原文那个[(1-p)^0.5]^4,因为0.5 × 4 = 2,其实就是(1-p)²,展开约等于2p。
9.3 三个场景,一次算完
| 做法 | k(盘数倍数) | t(窗口倍数) | k×t | 结论 | 原文原话 |
|---|---|---|---|---|---|
| 单机多插一倍硬盘 | 1 | 1 | 1 | ≈ p | “对持久性几乎不会造成影响” |
| 单盘容量翻倍 | 0.5 | 4 | 2 | ≈ 2p | “对持久性的伤害还是比较大的” |
| 集群扩容一倍 | 2 | 0.5 | 1 | ≈ p | “一正一反两相抵消,大体可以忽略” |
每个 k 和 t 是怎么来的:
| 做法 | 推导 |
|---|---|
| 单机多插硬盘 | 集群总盘数没变 → k=1;修复速度没变 → t=1 |
| 单盘容量翻倍 | 总容量不变 → 盘数减半 → k=0.5; 要修的数据量 ×2,能参与修复的盘 ÷2 → t = 2×2 = 4 |
| 集群扩容一倍 | 盘数翻倍 → k=2;参与修复的盘多一倍 → t=0.5 |
中间那个 t=4 是全篇最容易漏的一处:原文写的是"修复时间和要修复的数据量成正比,和集群可用的磁盘数成反比"——两个因子要相乘,不是相加。
9.4 一个悬着的问题,留到第十一节
6.1 100 台 RAID5 → 丢数据快 100 倍 (越大越危险)
9.3 集群扩容一倍 → 持久性基本不变 (越大越安全,严谨算还偏正向)
同一件事怎么方向相反?
原文对"集群越大越可靠"给了个前提,他自己也写出来了:“修复速度和集群规模成正比”。
★ 但这个前提凭什么成立?为什么 RAID5 就不成立?
答案不在算式里,在数据摆在哪几块盘上——见第十一节。
第三部分 · 引申:实际是怎么做的
⚠ 这三节不是原文内容。 原文关于"对象存储怎么分片"只有一句话:
“既然对象存储是一个键值存储,就意味着我们可以通过对 Key 做 Hash,或者对 Key 按 Key Range 做分区,
都能够让请求快速定位到特定某一台存储机器上,从而转化为单机问题。”真实的对象存储比这多一整层,而且还有两条原文完全没提的放置规则。
十 · 对象存储实际怎么分片:其实是两层
python3 代码/对象存储怎么分片.py
10.1 一次 PutObject 发生了什么
put("photos/2026/a.jpg", 10MB 的字节)
① 数据层:把 10MB 切成 4MB 的块 —— 3 块,每块单独找地方存
块A → 机器17、机器53、机器88 (3 副本,散在不同机架)
块B → 机器 4、机器61、机器92
块C → 机器23、机器45、机器77
② 元数据层:记一条账
"photos/2026/a.jpg" → {大小:10MB, 块:[A,B,C], 每块的位置}
★ 对象存储有【两个互不相干的分片问题】:
① 元数据(key → 这个对象由哪些块组成、每块在哪)—— 按 key 分片
② 数据本体(块的字节)—— 不按 key 分片原文那句话只说了第①个。把两层分开,后面所有事情就顺了。
10.2 为什么必须分成两层
因为这两种东西的性质正好相反:
| 元数据 | 数据本体 | |
|---|---|---|
| 条目 | 很小很多(一条几百字节,几百亿条) | 很大很少(几 MB~几 GB,条目少几个数量级) |
| 要什么硬件 | SSD / 内存索引,高 IOPS 低延迟 | 大容量机械盘,顺序吞吐 |
| 可不可变 | 要改(改名、删、改权限) | 写完永不变(见第十二节) |
硬件需求相反,放一起两边都不好。所以真实系统一定是分开部署、分别分片。
10.3 元数据层:按 key 分片 —— 就是 37 讲那个函数
这一层是纯 KV,key 是 bucket/objectkey,直接套 该找哪台(key)。但你在 37 讲学的那个取舍在这里原样重演:
── 哈希分片 ──
✓ 点查一个 key:算一次哈希,直达
✗ list(prefix="photos/2026/"):同前缀的对象散在所有分片上
→ 【要问遍所有元数据分片再合并】
── 范围分片 ──
✓ list 只问一台(同前缀的 key 字典序相邻,必在同一片)
✗ 热点:所有 photos/ 开头的都压在一台上
★ S3 历史上走的是范围分片(按 key 字典序自动分裂)。
所以 AWS 曾经建议"给 key 加随机前缀"来打散热点——那其实就是让用户手工模拟哈希分片。
2018 年后 S3 改进了自动分裂,这个技巧不再需要。37 讲那张「哈希 vs 范围」的表,在这里是有名有姓的真实历史。
10.4 数据层:为什么不按 key 分片
一个很自然的想法:既然 key 能算出机器号,数据也照这个放不就行了?不行,三个原因:
| # | |
|---|---|
| 1 | 一个 10GB 的对象会整个压在一台机器上——那台机器的磁盘和网卡就是它的上限 |
| 2 | 读大对象只能从一台拉。切块后可以几十台并行拉,带宽差几十倍 |
| 3 | 加机器均衡时只能整对象搬。切块后可以一块一块挪,粒度细得多 |
★ 所以数据层的规矩是:
对象 → 切成固定大小的块(4MB / 8MB);
每块存哪儿由「放置策略」决定,和 key 没有关系。
位置记在元数据里(Ceph 则用 CRUSH 算法当场算出来,连表都不用存)。"放置策略"是什么 → 第十一节。
十一 · 放置策略:故障域与打散
python3 代码/故障域和打散.py
这一节回答两个问题:这些块具体不能放哪(故障域)、最好怎么放(打散)。
第二个问题顺便解开第 9.4 节那个悬案。
11.1 故障域:因为盘不是"各坏各的"
说"EC 28+4 能坏 4 块"的时候,脑子里想的是:
坏 1 块 … 又坏 1 块 … 又坏 1 块 … 又坏 1 块 ← 四件互不相干的事
但第 1.2 节那张表说了,机器是成批地坏的。
★ 故障域 = 会一起死的一组东西。 就这一句话,没有别的。
它一层套一层:盘(1块) ⊂ 机器(12块) ⊂ 机架(240块) ⊂ 机房(全部)
11.2 于是"能坏 M 块"要重新理解
9 块盘、3 个机架,存一个对象用 EC 4+2(共 6 块,M=2)。这 6 块摆哪儿,生死不同:
盘0 盘1 盘2 盘3 盘4 盘5 盘6 盘7 盘8
甲 甲 甲 乙 乙 乙 丙 丙 丙
✗ 坏摆法 ● ● ● ● ● · ● · · ← 甲架上放了 3 块
✓ 好摆法 ● ● · ● ● · ● ● · ← 每架 2 块
拉闸试试:
✗ 坏摆法: 甲断电 → 一次丢 3 块(M=2) ✗ 数据永久丢失
✓ 好摆法: 甲断电 → 一次丢 2 块(M=2) ✓ 还能救回来
乙断电 → 一次丢 2 块 ✓
丙断电 → 一次丢 2 块 ✓
★ 关键区别不在"坏了几块盘",在是不是一件事导致的。
M=2 保护的是"2 次互不相干的坏",它挡不住"1 件事一次带走 3 块"。
规则一:任何一个故障域里的块数,都不能超过 M。
- 想扛住"挂一个机架" → 32 块至少摊到 8 个机架(32÷8 = 4 ≤ M=4)
- 想扛住"挂一整个机房" → 得跨机房摆 —— 那就是 37 讲的"两地三中心"
- 反过来也说明 M 不是拍脑袋定的:M 该多大 = 你想同时容忍几个故障域挂 × 每个域里放几块
- "3 副本要放在不同机架"这条老规矩,就是 M=2 版本的同一条铁律
11.3 打散:先说清"摆法"是什么
换个场景:9 块盘,存 6 个对象,每个对象 3 副本。
★ “每个对象的 3 个副本,放在哪 3 块盘上”——这个选择,就叫摆法。
有两种截然不同的摆法:
摆法 A · 固定分组(先把 9 块盘绑成 3 组,对象只能整组挑)
盘0 盘1 盘2 │ 盘3 盘4 盘5 │ 盘6 盘7 盘8
对象a ● ● ● │ │
对象b ● ● ● │ │
对象c │ ● ● ● │
对象d │ ● ● ● │
对象e │ │ ● ● ●
对象f │ │ ● ● ●
→ 只有三种可能的组合。盘0 的搭档【永远】是盘1、盘2
摆法 B · 打散(每个对象自己挑 3 块,组合各不相同)
盘0 盘1 盘2 盘3 盘4 盘5 盘6 盘7 盘8
对象a ● ● ●
对象b ● ● ●
对象c ● ● ●
对象d ● ● ●
对象e ● ● ●
对象f ● ● ●
→ 六行六种组合,没有两行一样。注意盘0 出现两次,但【搭档完全不同】
11.4 盘0 坏了,谁能来帮忙重建
摆法 A
盘0 上有:对象a、对象b
对象a 的另外两份在 盘1、盘2
对象b 的另外两份在 盘1、盘2
→ 能供数据的盘:盘1、盘2 共 2 块
摆法 B
盘0 上有:对象a、对象b
对象a 的另外两份在 盘3、盘6
对象b 的另外两份在 盘4、盘7
→ 能供数据的盘:盘3、盘4、盘6、盘7 共 4 块
要写进新盘的数据量是一样的(都是盘0 上那些),但能同时读出来喂给它的来源不一样:
重建时间 = 要写的量 ÷ 能同时供数的速度 → B 快 2 倍
11.5 ★ 集群变大时,两种摆法命运完全不同 —— 解开 9.4 那个悬案
集群盘数 摆法 A 能供数 摆法 B 能供数
9 2 8
100 2 99
1000 2 981
摆法 A 永远是 2。 因为盘0 的搭档被钉死了就是盘1、盘2,机房里另外 997 块盘永远插不上手——它们的盘面上根本没有盘0 那些数据的任何副本,想帮也帮不上。
摆法 B 随集群线性增长。 因为盘0 上每个块的搭档都不一样,加起来几乎覆盖全集群。
★ RAID5 就是摆法 A。 这就是第 3.4 节欠下的那个解释:
RAID5 的重建只发生在那一组盘内部,所以 T0 和集群规模无关;机器越多、出事的组越多 → 纯粹变差。★ 而"修复速度和集群规模成正比"这个前提,靠的就是摆法 B。
它不是自动成立的——摆错了,集群再大也只是一堆 RAID5。这种摆法有个名字:declustered placement(去集群化放置)。
RAID = clustered(盘被绑成固定的组);对象存储 = declustered(没有固定的组,每个块自己挑伙伴)。
11.6 两条规则合起来
故障域 管【不能放哪】 —— 别让一件事一次带走超过 M 块 → 保命
打散 管【最好怎么放】—— 别让盘结成固定搭档 → 保修得快
真实系统是在故障域约束下随机:随机挑,但挑出来的必须分散在不同机架。
回头看 11.3 那个摆法 B —— 它每一行恰好是甲、乙、丙各一块,所以它同时满足了两条:
任一机架挂掉每个对象只丢 1 块(≤M),而且每行组合都不同(重建时全集群能帮忙)。
十二 · 最后一块拼图:对象是不可变的
上面这些能成立,全靠一个前提:
★ 对象存储没有"改一个对象的第 100 个字节"这种操作。
PutObject 是整体覆盖,不是就地修改。
改一个对象 = 写一批全新的块 + 把元数据指针指过去 + 旧块交给后台 GC。
12.1 不可变换来了什么
数据块一旦写完就永远不变,于是:
| 因为不变 | 所以 |
|---|---|
| 没有并发修改 | 不需要锁、不需要事务 —— 37 讲那套乐观锁、版本链、幻读,全都用不上 |
| 拷贝过程中它不会变 | 可以随便复制、随便迁移(第十一节那些搬迁、重建才敢那么随意) |
| 内容固定 | 校验和算一次永远有效;EC 编码算一次永远有效 |
而唯一需要原子修改的是元数据那一条记录——它是单个 key,只落在一台机器上,所以是个单机事务。
★ 37 讲第七节那个跨分片事务的噩梦,被绕过去了。
12.2 ★ 这才是对象存储能做到 EB 级的真正原因
数据库: 数据要就地改 → 必须有 MVCC、事务、锁 → 难以水平扩展
对象存储: 数据不可变 → 只有元数据那一条要原子改 → 随便扩
它把最难的那件事(并发就地修改)从需求里删掉了。
12.3 三刀,砍在三个地方
回头看,对象存储相对文件系统 / 数据库,一共砍了三刀,每一刀都是为了同一个目的:
| 砍掉什么 | 换来什么 | 在哪一节 |
|---|---|---|
| 去目录(没有 mkdir、没有 rename 目录) | 元数据能按 key 分片 | 七 · 十 |
| 去关系(一条数据只有一个 key) | 每条数据只落一台机器 | 七 |
| 去可变(Put 是整体覆盖) | 不需要分布式事务 | 十二 |
★ 三刀都是同一个手法:把做不到的需求砍掉,剩下的就能无限扩展。
这也是 38 讲那句大话的具体含义——“对象存储的出现,是服务端体系架构和桌面操作系统分道扬镳的开始”。
分家分的不是技术,是服务对象:文件系统:为【人】设计 —— 人需要目录来手工整理,需要随时改文件的任意一段 对象存储:为【机器】设计 —— 机器需要扁平 key、不可变的块,才能水平扩展
验收
地基(一~四)
| # | 问题 | 答案在 |
|---|---|---|
| 1 | "盘"是什么?为什么全篇数盘而不数机器? | 一 |
| 2 | 持久性和可用性差在哪?为什么"绝不丢数据"排在"不宕机"前面? | 二 |
| 3 | RAID5 是什么?它和 EC 28+4 是什么关系? | 三 · 八 |
| 4 | HDFS 存小文件的真正瓶颈是什么?为什么"调小 block"是错的? | 四 |
原文(五~九)
| # | 问题 | 答案在 |
|---|---|---|
| 5 | 对象存储里的 / 是什么?"重命名目录"为什么在 S3 里没有?扔掉目录换回了什么? |
七 |
| 6 | "去关系"是什么意思?它和"去目录"是什么关系? | 七 |
| 7 | 纠删码 28+4 凭什么能坏 4 块?为什么比 3 副本又便宜又抗造?代价在哪? | 八 |
| 8 | 1-[(1-p)^0.5]^4 ≈ 2p 怎么来的?为什么"单盘容量翻倍"的 t 是 4 不是 2? |
九 |
引申(十~十二)
| # | 问题 | 答案在 |
|---|---|---|
| 9 | 对象存储的两层分片各分什么?为什么数据层不按 key 分? | 十 |
| 10 | "故障域"指什么?为什么它会让"能坏 M 块"这句话失效? | 十一 |
| 11 | "摆法"指什么选择?为什么摆法 A 的集群加再多机器,重建也快不了? | 十一 |
| 12 | 对象不可变,省掉了哪些东西?三刀分别砍在哪? | 十二 |
答案
1. 盘 = 一块实体硬盘。 数盘是因为坏的最小单位就是一块盘——机械硬盘是整台服务器里唯一大量存在的活动部件,年坏盘率 1~2%。1000 块盘的集群平均每月坏一块,坏盘是日常不是事故。
2. 持久性 = 数据还在不在(不可逆);可用性 = 现在能不能访问(可恢复)。 机房断电,可用性全挂但持久性毫发无伤,来电就好;三副本同时坏,数据永久没了,谁也救不回来。能补救的排在不能补救的后面。
3. RAID5 = N+1 的纠删码,做在一台机器的几块盘之间——3 块数据 + 1 块校验,校验块就是前三块异或,坏一块靠"剩下的全异或一遍"换回来。和 EC 28+4 是同一个技术在不同层次上做:RAID5 是 M=1、范围是几块盘、T0 不随集群变短;EC 28+4 是 M=4、范围是几百台、T0 随集群变短。
4. 真瓶颈是元数据,不是磁盘空间——block size 是上限不是下限,100KB 的文件在磁盘上就占 100KB。但 NameNode 把每个文件、每个 block 的元信息常驻内存(约 150 字节/条),同样 10TB,日志 17 万条(26MB)vs 图片 2.15 亿条(30GB),而 NameNode 只有一台。"调小 block"错得更狠:不省空间,却让每个文件的 block 数变多、元数据更多。
5. / 就是一个普通字符,系统不认识它;“照片/2026/” 这个目录并不存在,存在的只是几个 key 恰好以它开头。所以"重命名目录"是逐个改名 O(n),1000 万个对象就是 1000 万次操作,S3 干脆不提供。换回的是 37 讲那个「该找哪台(key)」的函数能存在——树不行,因为①形状由用户决定②rename 变跨分片事务③查找要逐层走。
6. 有关系 = 有多个索引 = 有多个 key;而每个索引背后就是一个 KV,所以同一条数据在多个 key 下都有影子,分片后散在不同机器上,改一条要跨机器同时改。“去关系” = 每条数据只有一个 key = 只落一台机器。 和"去目录"是同一个动机的两面:都为了让 该找哪台(key) 能存在。
7. M 块冗余 = M 个独立的方程 = 能解 M 个未知数(N+1 就是一次异或,靠 a^b^b==a)。便宜是因为 EC 是"算术"不是"复印"——4 份冗余保住 28 份数据,32/28/3 = 38%。代价三条:读一个文件要跨 28 台、坏一块要读 28 块才能修(T0 更长)、写要算 CPU。所以真实系统常分层:热数据 3 副本,冷数据转 EC。
8. 两条规则:盘数 ×k → 指数 ×k;修复窗口 ×t → 指数 ×t,合起来 1-(1-p)^(k×t)。单盘容量翻倍时 k=0.5、t=4,0.5×4=2,所以是 1-(1-p)² ≈ 2p。t 是 4 不是 2,因为两个因子要相乘:要修的数据量 ×2(盘更大),能参与修复的盘数 ÷2(盘更少)。
9. ① 元数据(key → 由哪些块组成、每块在哪)按 key 分片;② 数据本体(块的字节)不按 key 分片。数据层不按 key 分的三个原因:①一个 10GB 对象会整个压在一台机器上②读大对象只能从一台拉,切块后能几十台并行③均衡时只能整对象搬,切块后粒度细得多。每块存哪儿由放置策略决定,和 key 无关。
10. 故障域 = 会一起死的一组东西(同一台机器、同一个机架、同一个机房)。因为 M 保护的是"M 次互不相干的坏",而一个故障域挂掉是一件事一次带走一批——只要那一批超过 M 块,M 就白设了。规则:任何一个故障域里的块数都不能超过 M。
11. 摆法 = "这个对象的几个副本/块,具体放在哪几块盘上"这个选择。 摆法 A 把盘绑成固定的组,盘0 的搭档永远是盘1、盘2;集群里其他盘上根本没有盘0 那些数据的任何副本,想帮也帮不上,所以加机器 T0 不变。摆法 B 让每个块自己挑伙伴,组合各不相同,加起来几乎覆盖全集群 → T0 随集群规模缩短。RAID5 就是摆法 A。
12. 不可变省掉了:锁、事务(没有并发修改)、迁移时的一致性顾虑(拷贝过程中不会变)、重复校验/重复编码(算一次永远有效)。唯一要原子改的是元数据那一条,是单机事务。三刀:去目录(元数据能分片)、去关系(一条数据只落一台机器)、去可变(不需要分布式事务)——都是把做不到的需求砍掉,剩下的就能无限扩展。
下一步
39 讲:内存缓存——存储中间件清单上的下一项。
回看:
36 讲 · 存储中间件的由来(四堵墙 · 存储即数据结构)
带读 11 · 37 讲(「该找哪台」那个函数 · 哈希 vs 范围 · 跨分片事务——本讲第七、十节全靠它)
