加载中...

这一讲读起来"每句都懂、连起来不懂",因为它一路在给结论:

  • 可靠性、可用性、持久性三个词换着用,但它们不是一回事
  • “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.1431.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)² ≈ 2pt 是 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 范围 · 跨分片事务——本讲第七、十节全靠它)

公告栏
这是我的个人知识库。
记录技术,也记录生活 —— 读过的、试过的、想明白的,都堆在这儿。
最新文章
网站资讯
文章数目 :
5
已运行时间 :
本站总字数 :
15.7k
本站访客数 :
本站总访问量 :
最后更新时间 :
全局知识图谱
当前页面 已访问 文章 标签
ESC 关闭 · 滚轮缩放 · 拖拽移动 · Ctrl+G 开关