分布式存储的软件架构与案例解析
前文我们知道开源的对象存储系统有很多种实现,比如Ceph、Minio、SeaWeedFS和Swift等。这些对象存储软件架构不同,实现的语言也有差异。比如Ceph是用C++开发的,Swift是用Python开发的、Minio和SeaWeedFS则是用Go语言开发的。
在软件架构和核心算法上也有很大的差异,本文介绍一下分布式存储系统的主流软件架构,主要集中在原理层面,不会涉及太多代码实现的内容。同时,本文结合一些开源对象存储的实现对软件架构进行进一步的解释。
在分布式系统中,对于存储集群端主要有两种类型的架构模式,一种是有中心控制节点的分布式架构,比如Ceph和GFS等,另外一种对等的分布式架构,也就是没有中心控制节点的架构,比如Minio和Swift等。本节我们首先简要介绍一下这两种架构模式。
我的地盘我做主---中心架构
所谓有中心架构是指在存储集群中有一个(或多个)中心节点,中心节点维护着整个分布式系统的元数据,为客户端提供统一的命名空间。在实际生产环境,中心节点通常是多于一个的,其主要目的是为了保证系统的可用性和可靠性。
在中心架构中,集群节点的角色分为两种:一种是前文所述的中心节点,又称为控制节点或者元数据节点,该种类型的节点只存储文件系统的元数据信息;另外一种是数据节点,该种类型的节点用于存储文件系统的用户数据。
如上图是一个基于中心架构的示意图,在该示意图中包含1个控制节点和3个数据节点。当客户端访问对象的时候,首先会访问控制节点,控制节点会进行元数据的相关处理,然后给客户端应答。客户端得到应答后与数据节点交互,在数据节点完成数据访问。
在分布式文件系统中,控制节点除了存储元数据外其实还有很多其他功能。以分布式文件系统为例还包括访问权限的检查、文件锁的特性和文件的扩展属性等其实都是存储在控制节点的。
为了保证系统的可用性和可靠性,控制节点通常不止一个。比如GFS文件系统的控制节点是主备的方式,其中一个作为主节点对外提供服务。在主节点故障的情况下,业务会切换到备节点。由于该种架构的控制节点同一时间只有一个节点对外提供服务,因此也称为单活控制节点。
单活控制节点有个明显的缺点就是元数据管理将成为性能瓶颈。因此有些分布式文件系统实现了控制节点的横向扩展,也就是多活控制节点。其中比较出名的就是CephFS了,在CephFS中包含多个活动的控制节点(MDS),通过一种称为动态子树的方式实现元数据的分布式管理和负载均衡。
大家都是兄弟---对等架构
对等架构是没有中心节点的架构,集群中并没有一个特定的节点负责文件系统元数据的管理。在集群中所有节点既是元数据节点,也是数据节点。在实际实现中,其实并不进行角色的划分,只是作为一个普通的存储节点,也就是大家都一样。
由于在对等架构中没有中心节点,因此代理服务来通过数据特征计算数据应该存储的位置,也就是数据应该放置在哪个服务器的哪个硬盘。代理服务并不一定就是一个独立的服务,他可以是一个独立的服务或者模块。比如在Swift中就是一个独立的服务,而在Minio中,Minio只有一个服务,因此相关的功能其实是和存储服务集成在一起的。
在类似算法中常用的一种算法称为一致性哈希算法(Consistent Hashing)。一致性哈希建立了文件特征值与存储节点的映射关系。当客户端访问文件时,根据特征值可以计算出一个数值,然后根据这个数值可以从哈希环上找到对应的设备,具体如下图所示。
在上图中,当客户端访问某一数据时,根据特征得到的位置如图所示。然后按照顺时针规则确定为节点3。再然后,客户端会直接与节点3交互,实现数据访问。
接下来我们以OpenStack Swift为例简要介绍一下其架构特点。如图所示,Swift的软件分为代理服务(Proxy Service)和存储服务(Storage Service),存储服务又分为账户(Account)、容器(Container)和对象(Object)三类。其中账号的概念类似公有云中租户(Tenant)的概念,容器的概念则与公有云中桶(Bucket)的概念一致。在具体部署时两个服务可以部署在不同的物理节点上,也可以部署在相同的物理节点上。
在Swift中代理服务是对象存储的入口,也是分布式算法实现的地方。通过代理服务,Swift为用户提供了统一的命名空间,屏蔽了底层分布式的架构。对于用户来说,Swift就好像是一个单机版存储系统一样。
虽然代理服务是对象存储系统的入口,但是代理服务并不会成为系统的性能瓶颈,因为代理服务是可以横向扩展的。也就是代理服务可以由一个或者多个实例提供服务。在代理服务的前端可以添加一个负载均衡器,这样来自客户端的请求可以均匀的分发给多个代理服务,架构如下图所示。
既然Swift的代理服务可以是多个,那么Swift的代理服务是如何为用户提供统一命名空间呢?其基本原理就是通过一个ring文件,这个ring文件的核心内容本质上是一个一致性哈希的数据布局实现,他将物理设备分布在一个非常大的哈希环上。由于每个代理服务中的ring文件内容是一样的,因此相同的数据必然会被路由到相同的位置。这就实现了分布式环境的统一命名空间。
一致性哈希算法是分布式存系统中常用的一种建立数据与物理设备映射关系的算法。一致性哈希算法通常需要满足均衡性(Balance)、单调性(Monotonicity)、 分散性(Spread)和负载(Load)四个特性。简单而言,一致性哈希算法主要保证数据要均匀的落在集群的所有节点上,而且在新增或者删除节点的时,数据的映射关系变化最小。
为了保证数据在物理节点的均衡性,一致性哈希通常并不直接建立键值与物理设备的映射,而是通过虚节点的方式增加数据的均衡性。所谓虚节点就是假想的节点,虚节点与物理节点之间有一个对应关系,比如一个物理节点对应100个虚节点。由于虚节点比物理节点多的多,因此就比较容易均匀的分布在哈希环上。关于一致性哈希的更多细节,我们在《一致性哈希算法及案例解析》中会详细介绍。
在Swift中,ring文件的实现要比上述一致性哈希复杂一些,但本质上仍然是一致性哈希。以3副本的配置为例,在ring哈希环的每一个虚拟节点(Swift中称为分区-Partition)上有3个ID信息。这里面每个ID对应着一个服务器中的一块硬盘。在ring文件中,通过这个ID可以找到服务器的IP地址和硬盘的挂载路径等信息。
基于上述信息,当通过客户端访问Swift中的一个对象时,根据其特征可以计算出一个哈希值,进而可以知道应该由哪一个分区来处理这个请求。也就是可以确定具体由哪几个节点来处理这个请求。
除了数据定位外,另外有重要的特性是要保证数据的可靠性。在Swift中每个分区包含3个节点,也就是数据会同时发送到三个节点,并通过Quorum算法保证数据的可靠性和一致性。关于这部分内容,我们后面会详细介绍。
另外一个对等架构的分布式对象存储系统是Minio。Minio是后起之秀,发布的比较晚,作者是原来GlusterFS的一些开发人员。虽然Minio发布的比较晚,但是由于其架构简单、性能比较高以及对云原生的支持,很快成为比较受欢迎的对象存储系统。
前面我们也简要介绍过Minio,使用Minio是非常简单的。通过下面这条命令就可以启动一个单节点的对象存储服务。
minio server --console-address ":9090" /data/minio/data
如果想启动一个分布式对象存储系统也不复杂,只需要在每个物理机节点上运行相同的命令就可以启动一个集群。以6个节点,每个节点包含6块硬盘的系统为例,我们可以通过在每个节点上运行如下命令其启动一个对象存储集群。
minio server https://node{1...6}.example.net/mnt/drive{1...6}
为了能够比较方便的使用Minio的对象存储服务,并且实现多个节点间的负载均衡,通常在节点前面安装一个负载均衡器。这样对象存储系统就可以通过一个统一的入口来提供服务了,对于用户而言就是统一的命名空间。
数据请求通过负载均衡器被路由到任意一个节点上,由于节点是对等的,因此可以通过任何一个节点访问这个分布式对象存储系统,得到的数据是一致的。也就是说,Minio通过多个节点提供了一个统一的命名空间。
在Minio的官网提供了一个计算器,可以通过该计算器快速计算存储系统的有效存储容量和上面的命令行参数。该计算机的界面如下图所示。
前文说了Swift通过多副本来保证数据的可靠性,而Minio采用纠删码的方式保证数据的可靠性(其实目前Swift也支持纠删码)。纠删码是一种通过校验数据保证数据可靠性的方法,其有效数据要比副本方式高很多,但需要比较多的计算资源。
说到开源对象存储不得不介绍一下Ceph了,Ceph作为开源分布式存储大佬级的存储系统,本身是一个统一存储系统,也就是同时包含分布式块存储、分布式文件系统和分布式对象存储三种不同类型的存储系统。虽然Ceph在协议层面提供了三种不同形态的存储,但Ceph的底层实现其实就是一个对象存储系统,也就是其RADOS(Reliable Autonomic Distributed Object Store)。
基于Ceph的块存储、文件系统和对象存储都是构建在RADOS基础上的。本文首先介绍一下RADOS的架构,在RADOS中有两种类型的组件,分别为OSD和Monitor。OSD组件是一个独立的服务,用于实现对磁盘的管理和分布式数据的备份和恢复。OSD是数据层面的核心组件,通常每个硬盘有一个对应的OSD服务。Monitor(简称MON)组件是Ceph的中心节点(集群),该组件维护这Ceph集群的状态信息,比如集群的硬件情况、资源映射关系和存储池情况等等。MON最少需要一个,通常需要多个,其目的是为了保证可用性,如下是组件关系示意图。虽然Ceph RADOS是中心架构,但是管理节点很少被访问到,因此不太容易成为性能瓶颈。
计算节点如果想访问Ceph集群需要安装客户端软件,我们称为librados客户端。客户端与Ceph集群建立连接后会从Monitor节点拉取集群状态信息。后续创建对象、读写对象等操作将不再访问MON节点,而是直接与对应的OSD交互。
librados是位于客户端的软件,该软件为上层服务提供访问Ceph集群的接口,同时在该软件内会根据元数据信息进行数据位置的计算,并且实现数据的分发。如下图所示,当创建一个对象时,位于客户端的librados会根据对象信息计算出其所位于的PG,在PG中包含着副本信息,根据PG的信息将请求发送到主副本所在的OSD。需要注意的是下图中的虚线是组织关系,而非IO请求顺序。
虽然RADOS天然是对象存储,但是并不方便直接对外提供对象存储服务。其原因在于访问rados基于私有协议实现,与现有公有云S3或者Swift协议不兼容。为了能够更好的对外提供对象存储服务,Ceph中实现了一个对象存储网关,也就是radosrgw。可以将radosrgw理解成一个Web服务,其功能包括对不同协议访问请求的解析,并将请求转换为对RADOS集群的操作。
介绍完Ceph,我们再介绍一下SeaWeedFS,虽然从名称上来看似乎是一个文件系统,但其本质更倾向于是一个分布式对象存储系统。在介绍SeaWeedFS之前我们有必要介绍一下FaceBook的HayStack,因为SeaWeedFS其实是FaceBook Haystack的一个开源实现。
HayStack的整体架构如下图所示,包含三个服务,目录服务、缓存和存储服务。缓存与主要是为了提升性能,与存储系统的业务逻辑关系不密切,我们重点关注目录服务和存储服务。
这里的目录服务可以理解为HayStack的中心节点,通常采用一个集群实现。目录服务中维护着整个对象存储系统的元数据信息,其主要作用包括:
1) 提供逻辑卷到物理卷的映射
2) 提供负载均衡,可以实现写操作的跨逻辑卷负载均衡和读操作的跨物理卷负载均衡
3) 确定请求走CDN还是直接发送到Cache服务
4) 识别某些逻辑卷是否为只读状态。
上面描述其实涉及很多概念,我们暂时不做过多介绍。我们需要知道的是,当创建对象时,目录服务为对象构建一个URL(步骤1-4)。URL稍微有点复杂,一个访问CDN的URL格式如下:
http://<CDN>/<Cache>/<Machine id>/<Logical volume, id>
从该URL可以看出,该路径包含CDN地址信息、缓存信息、存储节点信息和逻辑卷及对象ID信息等内容。所以,通过这个信息就可以定位到具体服务器的具体卷的具体位置。需要注意的是,虽然HayStack也是中心架构,但与Ceph不同。Ceph的中心节点集群维护着存储节点的关系,这种关系通常是静态的。客户端从中心节点拉取关系后,发送请求时是通过上述关系计算出具体位置的。而HayStack并不需要客户端感知这种关系,每次访问对象必须从目录服务路径信息。
SeaWeedFS是对HayStack的模仿,其服务分为master服务和卷服务。其中master服务对应HayStack的目录服务,也就是中心节点。卷服务则对应HayStack的存储服务,负责存储数据。SeaWeedFS的部署也是非常简单的,首先在一个节点启动master节点。
./weed master -mdir="/seaweedfs_data/node1" -ip=192.168.2.100
然后一个或者多个卷服务,具体命令如下。通过命令行中的选项可以其中包含master的IP地址信息。通过该IP地址实现了卷服务与master服务的关联。
./weed volume -max=100 -mserver="192.168.2.239:9333" -ip=192.168.2.239 -ip.bind=0.0.0.0 -dir="/seaweedfs_data/node1"
完成部署后,我们就可以访问该对象存储了。具体流程与HayStack相仿。SeaWeedFS也是基于HTTP协议的,因此我们可以借助curl工具实现对其访问。以创建对象为例,首先我们需要向master申请(命令为dir/assign),此时master节点会返回一个fid和URL信息。通过上述信息就可以创建对象了。
curl http://10.0.20.46:9333/dir/assign
如下是执行上述命令后返回的信息,其中包括创建对象的URL和fid。在fid中包含3部分内容,其中逗号前面的7为卷ID,后面看似一部分的内容其实是两部分01是文件的键值,后面94ab7162是文件的cookie值。
{"fid":"7,0194ab7162","url":"192.168.2.246:8080","publicUrl":"192.168.2.246:8080","count":1}
我们可以多执行几次dir/assing,获取信息列表如下,可以看到可以看到卷ID和文件键值都在变化。这也是为了实现卷数据之间的负载均衡。
{"fid":"7,0194ab7162","url":"192.168.2.246:8080","publicUrl":"192.168.2.246:8080","count":1}
{"fid":"6,02dfdeea10","url":"192.168.2.246:8080","publicUrl":"192.168.2.246:8080","count":1}
{"fid":"2,03c87ef261","url":"192.168.2.246:8080","publicUrl":"192.168.2.246:8080","count":1}
{"fid":"3,04ac8ca7d4","url":"192.168.2.246:8080","publicUrl":"192.168.2.246:8080","count":1}
{"fid":"6,05843b5c7d","url":"192.168.2.246:8080","publicUrl":"192.168.2.246:8080","count":1}
当获取对象的路径信息后,我们就可以上传数据了。依然借助curl工具进行数据上传,具体命令如下。这里我们上传一个文本文件,其内容为5行“HelloWorld”,大小为55字节。
curl -F file=@/root/helloworld.txt http://192.168.2.246:8080/7,0194ab7162
上传成功后可以看到返回的信息如下,其中包含文件名称和大小等信息。
{"name":"helloworld.txt","size":55,"eTag":"fa733316","mime":"text/plain"}
到此,本文简要介绍了一下开源分布式存储系统的架构模式,也就是中心架构和对等架构两种不同的架构模式。为了让大家更加形象的理解两种架构的特点,我们又分别结合两个比较热门的开源项目进行分析。本文对架构的介绍相对简要,后面会详细分析上述开源产品的更多细节以及IO处理的逻辑。
如果大家觉得写的还可以,请帮忙点一个赞,点赞量到30时,我将及时更新后面的内容。后面的内容包括如下主题。
《云原生对象存储Minio深入详解,从架构到IO路径》
《统一存储典范Ceph,RADOS架构的实现细节》
《海量小文件的处理之道,聊聊HayStack与SeaWeedFS》
《云时代对象存储Swift的前世今生》