分布式存储的战争(二)对象存储的挑战 MinIO/SeaweedFS/Ceph

5 阅读24分钟

本系列文章分为五篇

  1. 分布式存储的战争(一)大数据的基石-HDFS的崛起
  2. 分布式存储的战争(二)对象存储的挑战 MinIO/SeaweedFS/Ceph
  3. 分布式存储的战争(三)存算分离的必然性-Alluxio/JuiceFS数据编排层
  4. 分布式存储的战争(四)AI的咆哮-GPFS/Deepseek 3FS 并行文件系统
  5. 创作中

本文为第二篇,书接上回

1. 新王的野心:对象存储的诞生

2006年Amazon发布了其云服务Amazon Web Service(AWS),Simple Storage Service(S3)作为其核心以及最受欢迎的产品,向世人展示了什么是对象存储。站在今天来看,S3几乎就是对象存储的定义者。

上一篇讲到HDFS的起源是Dong Cutting面临的问题是在搜索引擎场景下的“单机存不下,算不动”的问题,而S3起源于Amazon内部电商等业务需要存储商家和用户上传的照片和视频、以及内部的日志文件、静态网页文件等等,主要解决的是存储的扩展和运维问题。

为了解决这个问题,S3的设计哲学就是:无限扩展、扁平命名空间、简单API。S3的核心目标就是简化存储,它并没有类似HDFS的POSIX兼容性。你就存你的文件,然后调用PUT/GET/DELETE等接口,就这么简单。 这就是它为什么不被称作分布式文件系统(Distributed File System)而被称作对象存储(Object Storage),因为这里没有目录树(S3提供的目录树是模拟的),不兼容POSIX协议,你的文件就是一个Key-Value对象。

2. 王权根基:对象存储的技术基础

2.1 Erasure Coding 纠删码

丐版Demo

先不看公式,假如我们有数据内容为0713,要保证它可靠性,一种做法是存多份,另外一种做法就是先切分为两部分07和13,再存储他们的和 07+13 = 20。假如13丢了,我就能用 20 - 07 = 13 恢复出13,07丢了同理。这就是纠删码的超级简化版原理。

纠删码就是上面例子的扩充: 如果有K个数据,代表原始数据被切分成的块个数(在上面的例子中就是2个),然后再构建M个线性无关的方程,就得到M个方程结果,代表计算出的冗余块(上面的例子就是1个)。只要剩下 K个已知数(无论是原始值还是方程结果),我们就能解出剩下的未知数。由于最著名的算法是 Reed-Solomon (RS) 码,一般写作RS-K-M,实践中常用的编码是RS-6-3。

更复杂但直观的例子

举一个更复杂直观的例子深入理解。以RS-6-3举例,一个文件被切分成6个数据块,将 6 个数据块 [A, B, C, D, E, F] 看作一个向量,乘以一个9*6的生成矩阵。注意乘号左边是「生成矩阵」,右边是数据块,最终得出的结果是9个数据块。

这个「生成矩阵」中前 6 行是单位矩阵,所以结果的前 6 个就是原始数据本身,后 3 行是经过特殊设计的系数,计算结果生成了校验块P1, P2, P3。

假如我们丢失了A,B,P3,对应公式中的操作就是删除掉「生成矩阵」中的第1, 2, 9行,那么上述矩阵乘法可以写成:

为了计算原始块A和B,如下变换上述矩阵就可以计算得出。

由于是RS-6-3编码,我们最多只能丢3个块,再多丢一个块就无法还原A和B了。

本质

相比于HDFS简单粗暴三副本来保证数据可靠性来说,Erasure Coding纠删码的存储效率要高很多,但其本质是用计算换存储。除了Reed-Solomon算法外,还有更多优化版的算法,比如Low-Density Parity-Check Codes、Locally Repairable Codes等,篇幅原因不多做介绍。

2.2 存算分离

对于对象存储系统来说,它要解决的只有存储问题,所以它根本就不需要计算资源,不需要考虑类似HDFS那样的将计算移动到数据本地的需求,这也是和HDFS在设计上的本质区别。这就造成对象存储在扩容的时候成本要比HDFS低很多,只需要扩容存储资源即可。但这并不是没有代价的,在计算密集型场景下,只用对象存储的效率会比存算一体的HDFS要低。

这也是对象存储适配云环境的核心设计:存储节点仅负责数据持久化。

2.3 其他

如果要对比HDFS的话,对象存储还有很多点可以讲,比如元数据和数据文件一起存放、去中心化完全无主架构、生命周期管理、跨区域复制、标签元数据等等,但这些术语并不是对象存储的专利,HDFS所属的分布式文件系统也可以实现这些能力,甚至前面讲到的Erasure Coding纠删码在HDFS 3.x版本就已经实现了。这里不对每一个点进行详细介绍,在下节的架构解析中会挑一些亮点进行介绍。

3. 王朝解构:典型对象存储架构解析

本节主要分析当前火热的对象存储的技术架构,包括MinIO、SeaweedFS、Ceph Rados Gateway,在进行典型系统的架构分析前,我们先思考一下,这个系统应该要满足什么条件。在我看来对象存储需要满足:

  • 避免单点瓶颈:不能有类似HDFS的NameNode单点瓶颈。
  • 数据丢失容错:任何节点故障都有对应的容错手段(Fault Tolarence),我们可以采用HDFS的多副本存储或者纠删码的条带化存储。
  • 数据存储负载均衡:数据要分布均匀,不能有过热过满节点。
  • 弹性扩容:只需添加部分节点即可扩容。

3.1 S3 (Simple Storage Service)

Amazon S3是公有云对象存储的典范,其API已成为对象存储的事实标准,几乎所有第三方对象存储均支持S3兼容API,核心优势在于生态完整性和规模化运维能力,可承载全球海量用户的非结构化数据存储需求。由于其非开源,这里就不对其架构做过多介绍。

3.2 MinIO/AIStor

MinIO是当下最火热的开源对象存储。(2025年12月宣布开源代码停止更新,并更名为AIStor)

下图是MinIO的架构图,MinIO采用元数据与数据一体化存储的设计理念,完全无主架构,所有节点都是平等的,避免出现单点瓶颈问题。

完全无主架构

MinIO的完全无主架构设计的非常有特色,它完全没有使用传统的环状一致性哈希,转而使用确定性哈希映射算法来管理数据位置。先来看看MinIO是如何组织节点的

  • Erasure Set(以下简称ES): 一个ES包含多个节点,节点下的磁盘个数必须和纠删码中的K+M之和相等。一个文件只会存储在一个ES中。比如纠删码的配置是6+3,那么节点的个数必须是9个。这是为了能够将文件切分后均匀的分配在节点之间。此外,在查找时也能够方便的获取文件的物理存储节点位置信息。如果将一个文件切分之后完全分散的存储在集群中的任意节点中,对于文件的物理位置元信息就比较复杂。
  • Pool:一个Pool包含多个ES,是一对多的关系。Pool是MinIO的最小扩容单元,一个集群通常只包含少量的Pool。Pool 的主要作用是实现物理隔离(如不同硬件类型、不同可用区),Pool 一旦创建,其规模和配置就是固定的,不能动态增删节点。

对于ES这样的设计,就导致一个ES对应的物理节点的磁盘大小要求是一致的,否则会有负载不均和空间浪费的情况。

读写流程

写入流程

  • 客户端会将对象传输给Load Balancer,然后路由到一个负载低的MinIO节点,这个节点成为Proxy节点。
  • Pool选择:Proxy节点根据每个Pool的负载,选择一个负载较低的Pool。这个负载信息是MinIO 节点之间使用 Gossip 协议来交换心跳和状态信息计算得出的。
  • Erasure Set选择:一个Pool中每个Erasure Set有对应的索引。写入时先计算index = hash(object) % num(erasure set list),将对象放入索引值为index的Erasure Set中。为了避免写入导致部分节点不均衡,写入起始节点是Hash(Object) % Num(erasure set)。
  • Proxy节点进行Object的切分和纠删码计算,然后将计算好的块写入对应的Erasure Set中。

读取流程

  • Pool探测:Proxy节点通过Gossip协议存储了整个集群的地图,比如Pool下面的Erasure Set,每个Erasure Set下的ip信息。由于是完全无主架构,Proxy节点会向所有Pool的发出请求来进行询问。由于我们在写入时采用了hash来选择对应的Erasure Set,那么在读取时也是一样采用hash来定位。这样只需要探测与Pool个数相等的Erasure Set即可,避免请求集群所有节点。
  • Erasure Set下的所有节点收到请求后,检查自己存储的数据,发现包含目标对象,则回复Proxy节点。Proxy节点收到半数以上的节点说文件存在时,则确认文件确实存在于这个Erasure Set中。不请求单个节点的原因是因为单个节点的数据不一定权威,可能网络延迟或者正在恢复数据中。
  • Proxy节点请求目标Erasure Set获取所有Chunk下的Block,然后在本地组装数据,返回给(Load Balancer),最终返回给客户端。

此外对于ListFile这种请求,由于MinIO是完全无主架构,所以需要依赖全局广播请求,来收集全局文件信息。这会有潜在的性能瓶颈问题。

在写入时,由于是按照确定性哈希来决定一个文件的存储位置,就有可能会导致文件在Erasure Set之间分布不均。但MinIO官方说这种随机性本身会解决不均的问题。

单点故障

MinIO所有节点之间会建立连接,并对等发送周期心跳以探测节点是否故障。MinIO的节点故障只能是数据节点故障,即一个ES中的某个节点故障。

任意一个ES中只要不超过M个磁盘宕机,数据就是安全的,这也是MinIO最核心的职责。对于写入来说,MinIO会把要写入到目标宕机节点的数据暂存到本地,然后回复客户端写入成功。等有新节点加入之后,就写入到目标节点上。

无主架构下的锁

对于有主架构,锁通常由中心节点管理。无主架构的MinIO则采用了自己实现的 dsync 分布式锁机制。其原理是节点向集群内所有节点广播锁请求,只有获得超过半数(N/2+1)节点同意时,锁才能获取成功。对于节点宕机导致的锁无人释放问题,MinIO采用了HDFS相同的策略,即租约。锁请求节点需要不断进行对锁“续约”。

这就要求MinIO的节点数量不能过多,这种广播机制决定了单个 MinIO 集群的规模不能无限扩张。MinIO中的节点之间都会建立连接,节点数量过多会导致连接风暴。一般推荐是32个,否则会出现锁竞争带来的性能损耗。节点数量的限制就限制了一个MinIO的集群规模,所以MinIO的一个节点通过挂载多个磁盘来实现大容量。

Load Balancer的瓶颈问题

在早期的版本中,文件的上传和下载都必须通过Load Balancer,即使Load Balancer只工作在OSI 4层TCP网络,这样仍然会导致Load Balancer本身出现瓶颈。

为此,MinIO推出了SideKick,其原理类似于一个sidecar,通过这个sidecar路由到MinIO集群中的节点,从而让客户端直接与MinIO集群中的节点进行读写数据。

弹性扩容

由于MinIO的架构限制,扩容只能通过新增Pool的方式进行扩容。无法像其他对象存储那样扩容仅仅需要往集群里面简单丢几台机器,而是需要放入完整的一个Pool。

3.3 SeaweedFS

有主架构

  • Master Server:负责分配 File ID、管理 Volume 的生命周期(创建、迁移),是集群的 “调度中心”;Master Server是有状态服务,维护Volume的元信息;通常部署多个节点,通过Raft协议选主;
  • Volume Server: 实际存储文件数据块,每个Volume 30GB;物理节点和Volume是一对多的关系;
  • Filer:核心元数据服务,通常接收客户端所有文件操作请求,维护文件的元数据(包括文件名、对应 File ID、文件大小 / 权限 / 时间戳等),元数据存储在 Redis/MySQL 等数据库中;Filer本身是无状态服务,将元数据存储在数据库;

其中File ID可以简单看作是包含了VolumeID + offset。

读写流程

  • 写入
    • Filer对文件进行切分和EC编码计算,对结果数据块向Master申请File ID;
    • Master会根据集群中的Volume Server负载,选择最佳的Volume和可用的File ID;
    • 将数据块写入到Volume中,并将元数据持久化至数据库中。
  • 读取
    • Filer读取元数据数据库获取文件对应的File ID。
    • Filer向Volume Server读取文件,在Volume Server内,直接通过offset定位文件并读取;获取数据后在本地完成拼接。

单点故障

  • Filer单点故障:Filer本身是无状态的,故障时可快速拉起一个新的Filer。但其元数据是存储在数据库中的,数据需要做额外的容灾,一般大厂内都有对应的数据库,或者直接使用云服务厂商提供的数据库。
  • Master Server单点故障:Master是有状态的,在主节点宕机后进行通过Raft协议进行重新选主。
  • Volume单点故障:在纠删码模式下,本身就可以容灾;在多副本模式下,一个Volume会有对应的副本Volume。

虽然没有单点故障,但类似于所有的有主架构一样,都有主节点的瓶颈问题。比如Filer/元数据库/Master Server。

弹性扩容

相比于MinIO来说,Seaweed得益于有主模式,文件可以任意分布在集群中的Volume Server中,所以Seaweed在扩容时,可以直接向集群中加入机器。

天然适合小文件存储

SeaweedFS 被公认为小文件存储的最优解之一,主要得益于以下几个方面:

  • Master不会遭受小文件问题困扰:因为Master 仅管理 Volume 级元数据,完全不care文件级的元数据。
  • Volume 按 “块组” 存储小文件:
  • 传统的存储一般将文件直接存储在XFS/EXT4等文件系统中,这一方面导致在文件寻址中会有额外的操作,另一方面文件元数据也会有额外的开销(尤其是小文件)。
  • Seaweed聪明的做法在于,直接使用裸块存储,不依赖底层文件系统的磁盘块。一个 Volume 会将海量小文件(Seaweed中称作Needle针,很形象)的二进制数据连续存储在物理磁盘中。当写入大量小文件时,SeaweedFS 会将其直接追加到当前可用的 Volume 中,形成顺序写,无磁盘碎片浪费;
  • 这样的做法的收益是:无额外寻址开销(offset直接定位),避免元数据的额外开销,写入直接转换为磁盘顺序写,存储无碎片浪费。

根据实际测试,SeaweedFS 在处理小文件时,读写速度比传统方案快 3-5 倍,存储空间利用率也能提升 30%-50%。

3.4 Ceph Rados Gateway

Ceph是一个比较复杂的系统,其术语就能把初学者绕晕,后续会单开一篇来讲,这里仅用于和MinIO/SeaweedFS做对比。如下图所示,Object Gateway是网关层,接收客户端请求,RADOS是Ceph的统一存储层。其中MON维护了集群的状态,File指代存储对象;Objects指代被切分后的块;PGs被称作放置资源组,是资源虚拟单位,映射至多个OSD;OSD是真实的物理存储单位。其中PG和OSD是多对多的关系。

先简单解释下写入流程

  • 元数据获取:网关层向Rados MON 集群发送请求,获取最新的集群映射表(OSDMap/CRUSHMap/PGMap);
  • 文件分块:一个大文件首先会被切分为块,可以简单认为一个块对应一个oid;
  • 简单哈希算法选择PG:对于一个块来说,通过hash(oid)%pg_num来选择pg,其中pg_num是集群固定的;
  • CRUSH哈希算法选择OSD:(CRUSH哈希算法是一个变种一致性哈希,它在一致性哈希的基础上额外考虑了集群结构、节点权重、要求的节点数量等因子)。将计算出的 PG 编号传入 CRUSH 算法,结合 CRUSHMap(包含集群拓扑、故障域、副本策略),计算出该 PG 对应的OSD 组(纠删码的多个数据 / 校验 OSD),同时确定主 OSD(PG 的读写主节点)。
  • 文件纠删码计算并存入物理磁盘:PG下的主OSD会进行纠删码计算,并将shard存储到OSD中。一个PG会存储不同文件的oid。

有主无主混合架构

  • 有主:MON通常由多个节点组成,通过Raft算法选主,用于管理Rados集群的元信息。
  • 无主:底层的OSD节点是无主的;Ceph通过CRUSH哈希映射,将数据分块最终存储到OSD中。MinIO的缺点(比如集群硬件配置一致、分布不均、弹性扩容困难等问题)被Ceph的复杂设计给解决了。

对于OSD节点的存活性检测,Rados采用了两层心跳检测。第一层是无主探测,一组OSD节点之间会互相发送心跳,探测是否存活,频率较高,6s一次;第二层有主上报,OSD会定期向MON上报自己的心跳,频率较低,30s一次。

无主架构下的锁

Ceph RGW的锁粒度是oid粒度的,即一个文件的切分粒度。当要加锁时,oid对应的PG下的主OSD会作为主协调者,避免了像MinIO那样要像集群所有节点发起请求以获得半数同意的请求风暴。Ceph RGW得益于这个设计,其集群可以任意扩展,没有节点数量的限制。

单点故障

OSD由于是无主架构,OSD宕机只影响数据可靠性,对集群并无影响,而数据可靠性由纠删码来容错。MON集群是由多个集群组成的Leader-Follower节点,当Leader宕机后,Follower会被选举为新节点。网关层是无状态的,可以部署多个节点。

弹性扩容

Ceph支持向集群中添加任意配置的节点。当向集群中添加一个OSD之后,这个OSD会根据其配置进行权重计算(通常基于磁盘容量),这时Ceph的CRUSH Map会更新,意味着集群的形状发生了变化,Ceph会重新计算数据的最佳分布并进行重迁移(backfill)。得益于CRUSH是一种类似一致性哈希的机制,在扩容时,只有大约 1/N(N 为 OSD 总数)的数据需要移动,而不是全部数据洗牌。

3.5 对比总结

这里我不会列出一个表格来进行对比上述对象存储系统来帮助你选型,更多想谈一下这些对象存储系统的设计哲学以及其取舍。

主从架构的选择

  • 主从架构(Leader-Follower)某种程度上会有单组件瓶颈问题,但换取的是数据一致性和分布式协调效率;
  • 无主架构(Shared-Nothing)与之相对,牺牲了分布式协调效率和数据一致性,但完全消除了单组件瓶颈问题,在高可用和扩展性上一般更具优势。

从上述三个对象存储来说,MinIO选择完全无主架构,没有统一的中心节点;SeaweedFS选择有主架构,由Master来管理集群Volume元信息,以及由后端数据库来管理集群的文件元信息;而Ceph Rados Gateway则是有主无主混合架构,在Rados层面完全无主,将体积庞大的文件元数据存储在数据节点中,具有很好的扩展性,将集群非常少量的元信息保存在MON节点,保证MON节点负载可控,不会因为客户端的增加而导致MON节点负载成比例增加。

数据分布的选择

MinIO采用简单哈希来进行数据分布;SeaweedFS采用索引机制来分布数据(后端数据库的数据即为索引);而Ceph则采用类一致性哈希分布数据。简单哈希通常对于增删节点很不友好,需要大规模迁移数据,所以MinIO规定不允许直接增删节点。Ceph OSD的类一致性哈希让其可以采用无主架构来避免单点瓶颈,但代价是增加节点需要进行数据搬迁。

总体来看

MinIO是期望在高配置且配置一致的机器上建立一个可靠的对象存储系统,所以其可以在架构上设计得足够简单,但也因此其架构扩展性就比较差。Ceph则是假设底层物理机器千差万别,需要在一个‘凹凸不平’的地面上建造一座‘摩天大楼’,那么其在架构上就必须复杂,通过CRUSH算法等机制去‘填平这些坑坑洼洼’,以此换取在异构硬件上的极致适应性和企业级稳定性。Ceph通过复杂的设计规避了很多问题,使得其扩展性非常好。

从上述列出的数据一致、分布式协调效率、高可用、扩展性上来说,我认为Ceph更胜一筹。但trade-off总是无处不在,Ceph的运维可谓是地狱级,运维门槛非常高,参数调优非常复杂。

没有绝对的对错,只有适合的场景。这就看企业的需求了,对于中小型SeaweedFS/MinIO更简单,但会有瓶颈;大型企业可能会选择Ceph来保证业务发展不被技术组件卡脖子。

4. 攻城略地:对象存储的闪电战

对比HDFS,对象存储有几点优势:

  • 低成本:得益于其在架构上的存算分离设计,使其完全不考虑计算资源;在云服务上还可以用完即走,用多少付多少钱;并且最早大规模应用纠删码,相比3副本存储的存储成本就要少一半。粗略来看,HDFS存储1GB的数据需要0.6~1元/月,而存储1GB数据到S3只需要0.15~0.2元/月。可以看到对象存储的价格只是HDFS单价的1/3左右。
  • 无限扩展:其设计上规避了HDFS NameNode的小文件问题,使得对象存储即使存储海量小文件,也能无限扩展。
  • 高可用:一方面早先应用纠删码来换取数据层面的高可用,另一方面,大多数对象存储在设计时都避免了类似HDFS的NameNode单点瓶颈问题。AWS S3对外宣称的可用性是99.999999999% 的持久性和99.99% 的可用性。

在上面的优势下,HDFS在海量小文件存储、冷数据归档等场景下失守阵地,对象存储迅速占领了非结构化的存储市场,典型的应用场景是在云原生的场景下,将对象存储挂载在K8s的container上,来进行日志文件、编译构建产物的存储。在结构化数据的存储上,当前数据湖技术也在以对象存储为底座构建大数据表格存储。

5. 阿喀琉斯之踵:新王的软肋

5.1 不支持追加写

对象存储采用不可变模型,文件写入不可再修改,也不支持追加写入。相比之下,HDFS是支持追加写的。追加写在很多场景下都很实用,比如直播视频流的存储,或者是HBase的WAL日志写入。不支持追加写就让直播视频流的存储业务代码很复杂,或者是HBase无法完全采用对象存储来实现。

为什么对象存储不支持追加写呢?并不是技术上不可行,而是一种技术上的trade-off。对象存储在设计上期望保持简单,比如它的扁平存储结构,不支持追加写可以让对象存储的实现更简单,支持机制的弹性扩缩容。而支持追加写则会引入锁进行协调、失败恢复、分布式不一致等等问题,这会引入更多的复杂性。

上面费劲圆了半天,但还是有例外,阿里云OSS对象存储支持了追加写能力,阿里技术还是牛逼(没有恰饭)。所以还是技术上可行,就是实现复杂。

5.2 元数据能力弱

对象存储由于是完全的KeyValue存储,缺乏真正的层级目录结构,所以其在文件list、rename场景下操作都更昂贵;其次由于部分系统的元数据通常都是和数据文件一起保存,对于元数据的获取成本更高。

6. 旧王未死,新王有瑕:存储范式转移

文章的结尾,我们再回头看看对象存储对HDFS的挑战。

HDFS脱胎于Google GFS,延续了“文件系统思维”,为分布式场景下的大数据分析计算而生,本质是将本地文件系统延伸至集群维度。

对象存储则彻底抛弃了这一思维定势。在云原生时代,云原生的哲学是无状态,容器可任意销毁并重建,对象存储以其API友好(不像HDFS那样专有客户端,鉴权复杂)、弹性伸缩,低成本(存算分离)完美地解决了无状态容器的状态持久化问题,比如持久化用户上传文件、系统日志等数据。如此,对象存储成为了支撑微服务、DevOps、弹性伸缩等云原生特性的底层数据基石。

那为什么HDFS就不配成为云原生的基石呢?是因为HDFS没有采用纠删码吗?还是没有解决NameNode单点瓶颈呢?都不是。

本质是设计目标不同。HDFS为大数据批处理优化,对象存储则为极致的弹性存储而生。他们为解决不同问题而生,他们都有其适用的场景。对象存储与HDFS的对抗,本质上是“对象存储范式”与“文件系统范式”的代际更替。

而目前的现实格局是“旧王未死,新王有瑕”,HDFS并未被完全淘汰,而是退守至其优势场景——如大数据分析、频繁追加写的场景,在Hadoop生态中仍占据重要地位;对象存储虽成为非结构化数据存储的“新王”,但也并不完美。随着时代的发展,AI等应用场景对大规模分布式存储提出了更高的要求(如高速文件读写),下一篇,我们来看大规模分布式存储在AI时代下如何发展。

欢迎大家评论区交流想法~ 欢迎不一样的看法~