简介:这份PDF综述面向大数据与云计算方向的学习者、后端开发及存储架构初学者,系统梳理主流分布式存储技术的架构脉络与选型逻辑,帮助读者理解文件存储、块存储、对象存储三类形态的差异与适用边界。压缩包内为1个PDF文件,大小约1.39MB,内容按概述、经典系统剖析、发展历程回顾等模块组织,便于通读与检索。已有403人学习下载。文中以GFS的主从架构、64MB数据块与三副本机制为起点,对照HDFS的容错恢复、顺序写入特性及在MapReduce批处理中的定位,并补充MinIO基于RESTful API与S3兼容接口的对象存储用法;同时将块、文件、对象存储按服务对象与数据管理方式加以区分,指出块存储适合低延迟数据库场景、文件存储适合层级目录与权限共享、对象存储适合海量非结构化数据扩展。资源还回顾从网络文件系统、共享SAN文件系统、面向对象并行文件系统到云文件系统的四阶段演进,并提及SSD、NVMe、纠删码与计算存储融合等演进方向,适合用作技术选型参考与面试复习材料。
1. 到底什么是分布式存储:从一台插满硬盘的服务器说起
一台服务器插满硬盘,容量总归有上限,硬盘一坏数据就可能全没。分布式存储要解决的就是这个天花板:把几十上百台普通机器的硬盘拼成一个整体,对外看起来像一块超大硬盘、一个超大目录,或者一个能按名字取件的大仓库,同时在好几块盘同时坏掉的情况下还能把数据捞回来。它不是把硬盘堆在一起那么简单,真正难的地方在于"谁管账、账本放哪、数据放几份、坏了怎么补"。搞懂这几个问题,市面上各种分布式存储系统的差异基本就能一眼看穿。
2. 为什么会有三套存储:块、文件、对象的分野从哪来
很多人第一次接触这块内容时都会疑惑:不都是存数据吗,为什么要有三种?答案藏在一个很容易被忽略的角度里——用数据的"人"不是同一个。
2.1 块存储:只认字节,不认内容的裸地皮
块存储最典型的形态,就是操作系统里看到的 C 盘、D 盘,或者一块挂载上来的云盘。它的特点是:数据按字节访问,块设备本身对你存进去的是什么内容、什么格式一无所知。
打个比方,块存储像一块已经打好格子、编好号的地皮。你可以在地皮上盖任何房子,地皮不问用途,只管把编号对应的那块空间给你。这种"什么都不管"恰恰是它的优势——没有额外解释开销,读写路径最短,性能最高。数据库这类对响应时间极其敏感的系统,几乎都跑在块存储上。
代价也很明确:它不知道数据之间的关系。想要共享、想要权限、想要目录结构,全得靠上面那一层自己去实现。
2.2 文件存储:带目录树的档案室
文件存储的表现形式大家最熟,就是路径加文件名。数据按目录和文件组织,有层级、有名字、有属性。
它像一间档案室:进门先找柜子,再找抽屉,再找文件夹,最后拿到那张纸。档案室的好处是管理能力强——可以给某个文件夹单独设权限,可以说清楚这个文件谁建的、什么时候改的,也可以让好几个人同时来查阅。再加上网络文件协议成熟,文件存储天生就适合共享,用途最广。
它的天花板在于那个"目录树"本身。层级越深、文件越多,查找和遍历的负担就越重。当成千上万的客户端同时来这间档案室找东西,柜子前面的那条走廊就会变成瓶颈。
2.3 对象存储:把数据和标签打包成快递包裹
对象存储的形态完全不同:你拿到的不是一个路径,而是一串标识符。数据和它的元数据被打包成一个整体,塞进一个巨大的池子里。你只要报出这串标识,系统就能立刻把它找出来。
这就像快递柜:每个包裹上贴着一张单子,写着收件人、重量、寄出时间等等。你取件时报单号,柜子不需要知道包裹里装的是什么,也不需要让包裹按字母顺序排好。正因为不需要维护复杂的层级结构,对象存储从设计之初就能非常容易地横向扩展到超大规模,特别适合数据量大、增速又快的图片、视频、日志、备份这类非结构化内容。
代价是:对象通常作为一个整体被访问,想在里面改几个字节再存回去,往往做不到,只能整个换掉。
| 维度 | 块存储 | 文件存储 | 对象存储 |
|---|---|---|---|
| 使用者 | 能读写块设备的软件系统 | 自然人 | 其他软件程序 |
| 组织方式 | 定长卷,按字节访问 | 目录树,按路径访问 | 扁平池,按标识访问 |
| 典型优势 | 延迟低、性能高 | 共享方便、权限清晰 | 扩展简单、容量几乎无上限 |
| 典型短板 | 无共享、无结构 | 元数据压力大 | 整体读写、难随机修改 |
提示:这三种形态不是替代关系,而是分层关系。很多对象存储的底层,本身就是跑在块存储之上的。
2.4 同一条赛道上的四次代际更替
回头看这条路,大致能分成四段。上世纪八十年代,以太网兴起,研究重点是让网络另一头的文件能被当成本地文件用,解决的是"能不能共享"。九十年代,存储开始从计算机系统里独立出来,专门的存储网络出现,问题变成"能不能扩展"。两千年之后,高速网络普及,容量和并发同时吃紧,元数据怎么高效管理、访问怎么并行成了主线,对象存储的思想也是这时候成型的。到了近十几年,数据量爆炸式增长,弹性扩展、高可用、多租户、服务质量保证被一起摆上台面,复制、纠删码、去重、压缩、分层、计算存储融合这些手段开始密集出现。
有意思的是,这条线上真正被反复讨论的架构其实只有两种,剩下的都是在这两种上打补丁。这就要说到元数据了。
3. 元数据放在哪里:中心化与无中心两条路线的分野
数据本身好办,多存几份、分散放就行。难的是那本"账"——哪个文件由哪些块组成、每块在哪台机器上、谁有权限动它。这本账就是元数据。分布式存储系统的架构差异,九成以上可以从"账本放哪"这个问题上推出来。
3.1 中心化架构:一个账本管所有,简单但怕瓶颈
中心化架构会专门设一个元数据服务,所有客户端先问它,拿到位置再去对应的数据节点读写。早期那套主从式文件系统设计就是典型代表:一个主节点管全量元数据,一堆数据节点存实际的数据块,文件被切成固定大小的块,每块有个全局唯一且不可修改的编号,默认存三份。
好处是结构极其清晰。集群怎么扩、数据怎么搬、副本怎么补,主节点一句话就能调度;而且因为数据节点不需要知道全局信息,完全可以拿便宜的商用机器来堆,成本压得很低。
坏处也同样清晰:主节点就是那个单点。它一挂,整个集群虽然数据还在,但没人知道数据在哪;它的内存一满,文件数量就到顶了。这也是后来所有改良的出发点。
3.2 无中心架构:人人都是账本,省了单点却累了目录
另一条路干脆不要独立的元数据服务。用一致性哈希一类的算法,直接通过计算得出某个数据应该落在哪台机器上。每个节点都知道规则,谁也不用问谁。
这条路省掉了单点,扩缩容时只需搬动环上相邻的一小段数据,理论上可以线性扩展。但代价在元数据操作上——目录遍历、重命名、权限检查这类操作,全得靠扫节点算出来,而日常使用中元数据操作占的比例极高,往往超过一半。更麻烦的是故障应对不够灵活:节点进出的每一次变化都会引起哈希重算,进而触发数据迁移,迁移期间业务 IO 就受影响。规模小的时候还好,上百个节点之后就很难管了。
# 一致性哈希的极简示意:虚拟节点如何缓解数据倾斜 import hashlib class HashRing: def __init__(self, replicas=150): self.replicas = replicas # 每个物理节点映射出的虚拟节点数 self.ring = {} # 哈希值 -> 物理节点名 self.sorted_keys = [] def add_node(self, node): for i in range(self.replicas): key = self._hash(f"{node}#{i}") self.ring[key] = node self.sorted_keys.append(key) self.sorted_keys.sort() def _hash(self, s): return int(hashlib.md5(s.encode()).hexdigest(), 16) def get_node(self, data_key): if not self.ring: return None h = self._hash(data_key) # 顺时针找到第一个不小于 h 的虚拟节点 for k in self.sorted_keys: if h <= k: return self.ring[k] return self.ring[self.sorted_keys[0]] # 绕回环首 ring = HashRing(replicas=150) for n in ["node-a", "node-b", "node-c"]: ring.add_node(n) print(ring.get_node("photo-2024-001.jpg"))这段代码里有三个参数值得留意。replicas是每个物理节点在环上映射出的虚拟节点数量,设得太小,节点在环上分布不均,数据就会倾斜到某几台机器上;设成一百以上通常能压得比较平。_hash用的是什么算法并不重要,重要的是同一个 key 每次算出来的值必须一致。get_node里的绕回逻辑处理的是"哈希值超过环上最大值"的情况,少了这一句,部分数据就会找不到落点。
3.3 中心化架构的两个改良方向
单点问题并不是无解的。主流做法有两条。
第一条是把元数据分组。不再让一个服务管全量,而是拆成若干组,每组只负责一部分命名空间,组与组之间互不干扰。这样一来,单组的规模上限被摊薄了,坏一组也只影响一部分数据。
第二条是把元数据塞进分布式键值系统。不再自己造一套元数据存储,而是直接借用成熟的 KV 引擎来承载。这个改动带来的收益比想象中大:KV 引擎天然带持久化和批量写入优化,元数据不必整体常驻内存,文件数量级的天花板一下子抬高了几个数量级。
还有一条更彻底的思路是把命名空间和数据块管理拆成两个独立服务,各管各的。原来一个服务身兼两职,任何一边吃紧都会拖垮另一边;拆开之后,命名空间服务可以单独扩容,块管理服务也能独立演进。
3.4 副本还是纠删码:可靠性这道题的两种解法
确定架构之后,下一个问题是怎么保证不丢数据。最直觉的答案是复制:每份数据存三份,坏一份补一份。简单、恢复快、读的性能也好,因为可以从任意一份读。但成本是实打实的三倍,存一 TB 要占三 TB 空间。
另一种解法是纠删码。把数据切成 N 块,再算出 M 块校验块,总共 N+M 块分散存放。只要丢失的块数不超过 M,就能靠剩下的块把原始数据算回来。比如 8+3 的配置,冗余开销只有 37.5%,远低于三副本的 200%。
| 方案 | 空间开销 | 恢复代价 | 适用场景 |
|---|---|---|---|
| 三副本 | 200% | 低,直接从副本拷 | 热数据、低延迟要求 |
| 纠删码 8+3 | 37.5% | 高,需读取多块计算 | 冷数据、归档、大容量 |
注意:纠删码的恢复过程会大量占用网络和 CPU,写入时也要先算校验,延迟明显高于副本。用它省空间,就得接受性能上的让步,通常配合冷热分层一起用。
4. 同一条赛道上的六种取舍:小文件、超算、容器各走各的路
看过架构和可靠性这两层,再去看具体的系统,会发现它们的差异其实都来自"优先服务谁"。没有哪个系统能在所有维度上做到最好,能把一两个场景吃透就已经很成功了。
4.1 面向海量小文件:合并成大块再存
有一类业务的数据特点是:单个文件极小,通常不超过 1 MB,但总量巨大。图片站点、商品详情页素材、社交内容配图,都是这个形态。
直接在通用文件系统上存这类数据会很痛。每个文件都要占一份元数据,几亿个小文件就能把元数据服务的内存吃干。更要命的是寻址时间——为了读 10 KB 的内容,先要花几十毫秒找到它在哪,读取本身反而成了零头。
针对这个形态,一类系统采取的做法是把大量小文件合并进一个大块里。大块通常按 64 MB 左右切分,每个块有集群内唯一的编号,由元数据服务在创建时分配并维护块与存储节点的对应关系。块内部再给每个小文件分配一个块内唯一的编号,把所有小文件在块中的位置信息写进一个索引文件,索引文件一般整体加载进内存。
这样的结构带来两个直接好处:元数据服务只需要管"块"这个量级,文件数量再涨,账本的条目数也是按块计的;读取时一次定位到块,再靠内存里的索引瞬间算出偏移,寻址开销被压到极低。
代价是通用性。这类系统往往不提供标准的文件访问接口,客户端必须走专有 SDK,也没法直接挂载成普通目录给任意程序用。它的定位更像"基于文件的服务",而不是"文件系统"。
4.2 面向超算与大带宽:并行读写与元数据分片
超级计算机上的负载是另一个极端:文件不多,但每个都极大,而且要求极高的聚合带宽。这类场景催生了并行文件系统。
并行文件系统的核心思路是把一个文件切成很多条带,分散到多个存储节点上,多个客户端同时从不同节点读同一个文件的不同部分。为了让这种并发不出乱子,它需要一套分布式的锁管理机制来做并发控制,同时把元数据和数据的通信链路分开管理——元数据走一条路,数据走另一条路,互不干扰。
一个反直觉的设计是:这类系统通常不做冗余存储。原因在于,复制和其他冗余机制都会引入固有的延迟和带宽开销,而超算环境里任何额外开销都会直接降低应用可用带宽、拖慢整体效率、延长出结果的时间。可靠性被交给了下层硬件和上层调度去兜。
这类系统对硬件也有要求,往往需要专门的网络和存储设备支持,元数据服务的分布式管理在早期版本里甚至是缺失的。
4.3 面向容器与云原生:把元数据搬进内存与 KV
容器环境的需求很具体:容器销毁了数据还得在,多个容器要能同时访问同一个文件,存储资源要在不同服务之间共享。这三条合起来,等于要求一个可持久化、高并发、可共享的存储层。
为这种场景设计的系统,在架构上做了几处明显调整。一是把命名空间管理和块管理彻底拆开,变成两个独立服务,各自做高可用。二是数据节点不再以单个数据块为管理单位,改成以"容器"为单位,一个容器管一批块,容器内部用共识协议在副本之间做复制和一致性保证。三是单机存储引擎换成 KV 引擎,元数据不再整体堆在内存里。
还有一个容易忽略但影响很大的变化:文件不能修改,也不能追加,只能新增和删除。这听起来像是退步,其实是对象存储接口的必然要求——对象是整体读写的,允许局部修改反而会破坏语义。
这条路线的短板在于生态和成熟度。接口对标准文件语义的覆盖往往不全,重命名、文件锁这些常用操作可能还有问题;元数据虽然搬进了 KV,但每条记录包含的布局信息依然不小,规模上去后内存压力仍在;新节点加入后的数据均衡、磁盘预留这类运维能力,也常常是后来才补上的。
4.4 面向新硬件:绕开内核的那一次赌博
前面几类系统的假设都是"硬盘慢、网络慢",所以大量设计围绕减少 IO 次数展开。但新一代固态盘把这个假设推翻了:单盘每秒几十万次 IO、延迟一百微秒左右,更新的介质甚至能压到十微秒级。
硬件快到一定程度,软件本身就成了瓶颈。操作系统内核里的文件系统栈、网络协议栈,这些原本可以忽略的开销,现在成了主要延迟来源。于是有一类系统选择直接绕过内核:用用户态驱动接管设备,用低延迟网络接口做节点间通信,用轮询完成队列代替中断通知,连线程切换的开销都要省掉。
这套做法的收益是延迟大幅下降,代价则是三个"不":依赖特定硬件,不支持标准文件接口(只能走用户态接口),不支持跨可用区部署(跨区带来的延迟会抵消掉全部优化)。它适合的是对延迟极度敏感的数据库场景,而不是通用存储。
5. 最常见的五组易混概念:这些词看着像,用起来差很远
概念一多,边界就容易糊。下面这五组是踩坑率最高的。
5.1 分布式存储不等于把硬盘堆在一起
最容易混的点,是把"多台机器各存一部分"当成分布式存储。实际上,单纯的分散存储连最基本的问题都没解决:某台机器突然下线,上面那份数据怎么办?新机器加进来,已有数据怎么匀过去?两份副本同时写,以谁为准?
真正的分布式存储至少要同时回答三个问题:数据怎么分布、副本怎么保持一致、故障怎么自动恢复。少了任何一个,它就只是"一堆独立的硬盘"。判断方法很简单——把一台机器直接断电,看系统是自动补齐还是需要人工介入。需要人工的,本质上还是手工运维的集群。
5.2 副本多不等于数据一定安全
三副本听起来很稳,但它防的是"硬件坏",防不了"写错了"。如果客户端把一份错误数据写进去,三份副本会忠实地一起变错。副本数只影响可用性,不影响正确性。
真正的数据安全还需要另一层:校验。常见做法是给每份数据算校验和,读的时候顺带验一遍,发现对不上就用其他副本修复。这也是为什么有些系统敢说"丢一半硬盘还能恢复"——靠的不只是副本数量,还有纠删码加校验和这套组合。
区分方法:问一句"如果写进去的数据本身是坏的,系统能发现吗"。答不上来的,副本再多也只是心理安慰。
5.3 对象存储不是"没有目录的文件系统"
不少对象存储提供类似"前缀"的路径写法,看起来像目录,于是一个常见的误解就产生了:对象存储只是把目录结构简化了。
这是两回事。那个路径写法只是对象名的一部分,系统并不真的维护目录这个实体。所以对象存储里没有"空目录"这个概念,重命名一个"文件夹"往往要逐个改写里面所有对象的名字,目录级别的权限继承也无从谈起。
区分方法:看它能不能 O(1) 地重命名一个含十万个对象的"目录"。做不到的,说明目录只是名字里的斜杠,不是结构。
5.4 一致性哈希不等于数据一定均匀
一致性哈希经常被当成"自动负载均衡"的同义词。它确实能在节点增减时减少数据迁移量,但并不保证数据在节点上分布均匀。
原因在于哈希本身的随机性。节点少的时候,环上的分布很容易出现明显倾斜,某台机器扛了 40% 的数据,另一台只有 10%。解决办法是引入虚拟节点——每个物理节点在环上映射出上百个虚拟位置,靠数量把随机性摊平。
区分方法:看系统的配置里有没有虚拟节点数这个参数,以及它默认给的数值。给个位数的,基本可以判断分布不会太均匀。
5.5 可扩展不等于性能线性增长
"理论上性能会随磁盘数量线性增长"这句话,在宣传材料里出现频率极高,但它成立的前提非常苛刻:元数据不能成为瓶颈、网络不能成为瓶颈、数据分布必须足够均匀。
现实中,扩到一定规模后通常会撞上三个墙中的一个。元数据服务先撑不住,因为文件数涨得比磁盘数快;或者网络先撑不住,因为跨机架的流量把交换机打满;或者分布开始倾斜,新增的容量集中在已经被写热的节点附近。
区分方法:不看理论曲线,看有没有公开的规模化实测数据,尤其是元数据操作在集群扩大后的延迟变化。
6. 什么时候该用哪一套:一条能落地的边界线
讲到这里,其实可以给出一条相当朴素的判断路径:先看数据形态,再看访问模式,最后看规模。
数据是海量小文件、且主要给自家应用读写,选那类把小文件合并成大块的系统,专有接口换来的是极高的元数据效率。数据是超大文件、要求极高聚合带宽、跑在专用集群上,选并行文件系统,别指望它给你标准的共享目录体验。数据是非结构化的图片视频日志、增长快、要能被各种程序直接读,选对象存储,接受"整体读写"这个前提。数据要跑数据库、要挂虚拟机磁盘、要极致延迟,选块存储,别指望它自带共享。既要文件接口又要块接口还要对象接口,那就得上统一存储平台,同时做好运维复杂度翻倍的准备。
有几条边界是明确不该越过的。
不要在对象存储上跑需要频繁随机改写的业务。写放大和整体替换的开销会把你所有的性能预算吃干净。
不要在超小规模(比如总共几十 TB)时上重型分布式系统。元数据服务的运维成本、共识协议的心跳开销、监控告警的复杂度,在这个规模下都是纯负担,一台带冗余的普通服务器加定期备份反而更省心。
不要为了省空间,在热数据上直接用纠删码。延迟和恢复期的带宽占用,往往会在业务高峰给你上一课。
也不要把"能扩"当成"该扩"。扩之前先确认瓶颈到底在哪一层,是元数据、是网络、还是磁盘。我见过一个团队花了三个月做集群扩容,最后发现问题出在客户端连接池配置上,扩容带来的收益几乎为零。
选型这件事,最忌讳的是单点最优的思路。一台机器要同时做到容量最大、延迟最低、成本最省、运维最简,这在工程上不成立。真正可用的方案,永远是在几个指标之间找一个能接受的平衡点,然后把这个平衡点写进架构文档,让后来的人知道当初为什么这么选。
从那以后我每次碰到选型讨论,都会强制自己先写一行字:"这套系统在什么情况下会变得不合适。"写不出来,说明还没想清楚。希望帮到你。
本文还有配套的精品资源,点击获取