相比本地文件存储, 分布式文件存储系统是一种能够将文件数据分布式地存储在多台服务器上的系统。它采用了多台服务器的集群化部署方式,有效地提高了数据的可靠性、容灾性和访问性能。
常见的分布式文件系统:HDFS、Ceph、Swift、GlusterFS、FastDFS 等。
01 基础架构分布式存储的基本架构主要分为中心化控制和去中心化结构。
去中心化架构的出现正是为了解决中心化架构的性能瓶颈问题。实际上,无论是分布式存储还是其他分布式系统,采用中心化架构都会面临性能瓶颈,因此,越来越多的系统正向去中心化架构发展,其中最常用的方法就是哈希映射思想。
1、中间控制节点架构(HDFS)
分布式存储最早由谷歌提出,旨在通过廉价服务器解决大规模、高并发场景下的 Web 访问问题。
下图展示了谷歌分布式存储系统(HDFS)的简化模型。在该架构中,服务器被分为两类:一种是名为 Namenode 的节点,负责管理数据的元数据;另一种是名为 Datanode 的节点,负责实际数据的存储和管理。
如果客户端需要读取某个文件的数据,首先会从 Namenode 获取该文件的位置(即具体在哪个 Datanode 上存储),然后从相应的 Datanode 读取具体数据。
Namenode 通常采用主备部署,而 Datanode 则由大量节点构成一个集群。由于元数据的访问频率和访问量相对于实际数据较小,因此 Namenode 通常不会成为性能瓶颈,而 Datanode 集群则能够分散客户端的请求。通过这种架构,可以通过横向扩展 Datanode 的数量来提升系统的承载能力,实现动态横向扩展。
2、完全无中心化架构 Ceph
Ceph 存储系统的架构与 HDFS 的不同之处在于 Ceph 没有中心节点。客户端通过设备映射关系计算出数据的存储位置,从而可以直接与存储节点通信,这样避免了中心节点可能带来的性能瓶颈问题。
核心组件包括 Mon 服务、OSD 服务和 MDS 服务等。对于块存储类型,只需 Mon 服务、OSD 服务和客户端软件即可。
其中,Mon 服务用于维护存储系统的硬件逻辑关系,主要管理服务器和硬盘的在线状态,并通过集群方式保证其高可用性。OSD 服务负责磁盘的管理,执行实际的数据读写操作,通常一个磁盘对应一个 OSD 服务。
客户端访问存储的大致流程如下:
客户端启动后,会首先从 Mon 服务拉取存储资源的布局信息。然后,基于该布局信息和写入数据的名称等信息,计算出数据的预期位置(包括具体的物理服务器和磁盘信息)。最后,客户端根据该位置信息直接与存储节点通信,以读取或写入数据。
02 原理概览1、数据定位
在分布式存储系统中,客户端需要根据文件名快速定位数据并获取文件内容。当前主流的分布式存储系统在解决数据定位问题时,通常采用两种方式:计算哈希值和查询元数据信息。
计算哈希值这种方法通过对数据的某一特征(如文件名)进行哈希计算,将哈希值与存储节点中的磁盘建立映射关系。
哈希算法的优势在于不需要记录元数据,任何节点只需了解哈希函数的计算方法即可确定数据的存储位置。
当然,哈希算法也存在一些问题。最主要的问题是,当集群中增加或减少节点时,部分数据需要重新分配到新的节点或从旧节点迁移,这可能导致数据迁移的开销和复杂性增加。
查询元数据信息查询方式则是将文件的映射关系存储起来,通过查询的方式来定位文件的位置。
但这种方式有一个潜在的风险,即随着集群规模的增长,元数据服务器容易成为性能瓶颈。因此,通常采用多个元数据服务来分布式存储元数据,以解决这个问题。
2、存储引擎
存储引擎决定了数据最终以何种形式存储在单机系统上。大多数分布式文件系统的底层存储形式依赖于本地文件系统接口,如 Swift、Ceph 等。这是因为分布式文件系统本身已经非常复杂,从上层到底层都自行实现存储引擎难度极大。而本地文件系统经过多年的发展已经相当成熟和完善,因此,大部分分布式文件系统选择依赖本地文件系统来实现底层存储。
不同分布式文件系统在单机上的存储格式各不相同。以 Swift 为例,它将数据以单个文件的形式存储在本地文件系统中,即一个文件在单机上对应一个实际文件(忽略对象存储层面的大文件映射关系)。而另一类分布式文件系统则在单机文件系统中将多个小文件合并存储为一个大文件,同时记录每个文件的操作日志。这种方式可以理解为对小文件的合并存储。
这两种存储方式各有优劣,并适用于不同的场景。日志文件系统通过合并文件来存储,虽然增加了文件二次定位的问题,但明显提升了小文件的读写性能。而 Swift 采用的未合并存储方式,虽然实现相对简单,但在处理大量小文件时,磁盘读写性能可能成为瓶颈。选择哪种方式,取决于具体的应用需求和系统设计目标。
3、多副本
副本(replica/copy)的存在是为了确保分布式系统中数据的冗余性,即在不同节点上持久化同一份数据。当某个节点上的数据丢失时,可以从副本中恢复,从而避免数据丢失。这是分布式系统解决数据丢失问题的主要手段。
对于对可靠性要求较高的数据,通常会采用三副本存储,甚至会要求副本跨分区存储;而对于可靠性要求较低的数据,两副本存储即可满足需求。然而,随着数据量的增加,多副本存储会导致存储成本显著增加。为了解决这一问题,引入了纠删码技术,这种技术能够在大幅降低存储成本的同时提升数据的可靠性。
然而,多副本存储也带来了一些需要解决的关键问题,例如副本数据的一致性、如何确保副本数量和位置的正确性等。
4、数据一致性
一致性协议是分布式文件系统中的核心问题之一,它涉及如何保持副本内容的一致性。常见的三种一致性模型如下:
强一致性:在更新操作成功执行后,所有后续的读操作都能够立即获取到最新的数据。弱一致性:在数据更新后,用户可能需要等待一段时间才能读到最新的数据。最终一致性:这是一种特殊形式的弱一致性。它无法保证在数据 XXX 更新后,所有后续的操作都能立即看到新数据,而是在经过一定时间后,最终能保证数据一致性。在此时间段内,数据可能处于不一致的状态。在多个副本节点没有主从之分的分布式系统中,数据一致性的保证往往由客户端保证,比如 Swift 的仲裁协议。
(1)定义:N:数据的副本总数;W:写操作被确认接受的副本数量;R:读操作的副本数量
(2)强一致性:R+W>N,以保证对副本的读写操作会产生交集,从而保证可以读取到最新版本;如果 W=N,R=1,则需要全部更新,适合大量读少量写操作场景下的强一致性;如果 R=N,W=1,则只更新一个副本,通过读取全部副本来得到最新版本,适合大量写少量读场景下的强一致性。
(3)弱一致性:R+W<=N,如果读写操作的副本集合不产生交集,就可能会读到脏数据;适合对一致性要求比较低的场景。
Swift 针对的是读写都比较频繁的场景,所以采用了比较折中的策略,即写操作需要满足至少一半以上成功 W >N/2,再保证读操作与写操作的副本集合至少产生一个交集,即 R+W>N。
Swift 默认配置是 N=3,W=2>N/2,R=1 或 2,即每个对象会存在 3 个副本,这些副本会尽量被存储在不同区域的节点上;W=2 表示至少需要更新 2 个副本才算写成功;当 R=1 时意味着某一个读操作成功便立刻返回,此种情况下可能会读取到旧版本(弱一致性模型);当 R=2 时,需要通过在读操作请求头中增加 x-newest=true 参数来同时读取 2 个副本的元数据信息,然后比较时间戳来确定哪个是最新版本(强一致性模型);如果数据出现了不一致,后台服务进程会在一定时间窗口内通过检测和复制协议来完成数据同步,从而保证达到最终一致性。
在多副本系统中,如果副本之间有主从节点之分,数据一致性通常由主节点保证。客户端的写请求会被发送到主节点,主节点成功更新后,会将请求转发至从节点,并在收到所有从节点的成功响应后再返回成功响应给客户端(强一致性模型)。
5、数据恢复
在分布式文件系统中,数据恢复的实现方式在有中心控制节点和无中心控制节点的系统中有显著差异。
有中心节点的系统:数据恢复通常由中心节点负责控制和调度。中心节点掌握存储节点和存储介质的全局信息,因此可以协调各个节点执行数据恢复任务。存储节点的作用通常是等待中心节点的指令,并根据其调度执行具体的数据恢复操作。无中心节点的系统:数据恢复由各个存储节点自行负责。例如,在 Swift 系统中,每个节点根据 ring 的信息来获取副本的位置。数据恢复进程由各个节点发起和执行,以维持副本数量和位置的正确性。这样做可以在没有中心节点的情况下,确保系统的弹性和数据的完整性。