说说分布式文件存储系统-基本架构

大家所熟知的几种分布式文件存储系统,如GFS、TFS、Swift、Ceph等等,在系统架构的设计上大同小异,在目前的知识边界内,主要从以下两点展开吧。其一,是有无中心管理节点;其二,存储节点是否有主从之分。

分布式存储系统可以理解为多台单机存储系统的各司其职、协同合作,统一的对外提供存储的服务。所以无论是存储非结构化数据的分布式文件系统,存储结构化数据的分布式数据库,还是半结构化数据的分布式KV,在系统的设计上主要需要满足以下需求(但不仅限于):基本读写功能、性能、可扩展性、可靠性、可用性。

1. 有中心管理节点 AND 存储节点有主从

此种架构的系统以GFS为代表以以下系统为例说明,整体设计和GFS类似,最近发现小米的分布式KV系统Pegasus用的是同样的架构设计方案。



从基本的读写功能来说,分布式文件系统主要提供文件的读/写/删功能,那么从哪里读写?怎么读写?以什么形式存储?也就是在上篇所提到的数据定位和存储引擎的问题

Master

在有中心控制节点的分布式文件系统,从哪里读写的任务基本是由中心控制节点完成的,即图中的Master节点。为了能完成这一项核心的任务,Master节点需要做两件比较重要的事情:

1. 存储节点信息和状态

对于分布式文件系统,从文件到某一台单机上的一块磁盘的某个位置,会有一个逻辑的拓扑结构便于分区扩展或者数据隔离等等。拓扑结构从上至下存储池、分区、服务器、磁盘、文件目录等。

Master节点需要保存整个集群的全局视图,为了提高性能,这个逻辑拓扑结构一般会缓存在内存中,定期地持久化在Master节点的磁盘上。

Master节点需要监听系统的所有数据节点和磁盘状态,如节点的上下线等,并对事件作出相应的处理来保证系统状态的正确性。

同时为了使得系统的数据分布和资源使用更均衡,Master节点可以获取数据节点的容量、负荷等状态,供读写调度模块有策略的去分配可写的资源。

2. 文件读写的调度策略

由于没有采用类似Hash算法这种静态计算读写位置的方式,中心控制节点就需要担任起调度的角色。当客户端发起写请求时,第一步会到Master节点获取文件ID,Master节点根据客户端读写文件的大小、备份数等参数以及当前系统节点的状态和权重,选择合适的节点和备份,返回给客户端一个文件ID,而这个文件ID包含了该文件多个副本位置信息。这样的好处是不用将每个文件的映射关系都存储下来。而对于对象存储系统因为功能的需要,这个对象和文件的映射关系是必需要保存的。

Master节点除了完成以上两种主要功能,还需承担维持副本数量和内容正确性的责任,因为毕竟它知道太多了,数据恢复调度的任务也是只能是它的,这里就先不展开了。

3.存储节点选主

对于存储节点有主从之分的系统,每个备份的主从节点的选取也需要Master节点来控制。

StorageNode

数据节点除了负责文件在单机系统上如何进行存储,对于有主从之分的存储节点,各自还承担如何保持备份数据一致性的任务。归纳为以下几个要点:

1. 单机存储引擎的实现

主从存储节点在单机存储引擎的实现上几乎没有差异,解决的是文件如何存储的问题。对于不同的系统还有个区别是一个存储引擎负责一台机器所有磁盘的存储,还是一个磁盘一个存储引擎,我们这里以一个存储引擎负责一个磁盘的设计说明

分布式文件系统的存储引擎大多都是在单机文件系统之上,数据最终以文件的形式存在。而单机文件系统以目录的形式将文件组织起来。该系统基本沿用了这一思想,是以目录为单位进行副本划分,而不是节点,和Swift一样称作为Partition,是备份的基本单位。以三备份为例,中心节点会根据策略选择三个不同的物理磁盘上创建同一个ID的Partition,在不同物理磁盘的同一个PartitionID下存储的文件是一样的。

这里还有另外一个问题,文件存储到单机文件系统上是否合并的问题。简单直接的做法,就是以一个个文件直接存储在指定的某个Partition目录下。这样的方式带来的问题也是直接可见的,直接受到文件系统随机读写的性能的制约,对于小文件读写比较多的场景来说,磁盘常常会成为整个系统的瓶颈,然后不得不通过增加节点来进行吞吐能力的扩展。

所以该系统将多个文件追加合并写在一个相对较大且大小固定的文件里,将随机写转为了顺序写,实验表明可以大大提高单机存储的吞吐能力。这样的解决方式带来的性能提升是明显,但是在实现上增加了一定的复杂性,合并必然就会带来定位文件的最终位置需要二次映射,即是在合并文件中的偏移。

2. 保证备份一致性

在保证备份一致性上,主从存储节点的角色就有一些区别了。对于Master节点选出的主存储节点,它需要根据主从一致性协议将数据推送到其它从节点,一般采用存储节点分主从的系统都会选择强一致协议,即主节点采用将数据发送给从节点收到响应成功后,才会将数据持久化到本地,返回用户成功,这样用户读到的数据始终是一致的。当然整个过程不是那么简单的,后续再一致性协议部分再展开。

3. 汇报消息,听从调度

对于存储节点需要保持和Master节点的心跳信息,同时将自己当前的容量和资源使用情况汇报给Master节点。从控制系统的角度来说,这才形成了一个负反馈系统。

同时,存储节点处于待命状态,等待Master节点的派遣任务,比如说数据备份的恢复迁移等。

Client

Client一般作为分布式文件系统的接入层,对写操作,接受用户数据流,将数据写入存储节点;对于读操作,从多副本中随机选择副本来读取。同时为了提高系统整体性能和可用性,该系统的Client一般还会负责额外的功能

  • 集群信息缓存:主要为了减少与Master节点的交互,提高写的性能。可以从Master节点获取副本位置信息,缓存在本地,设置缓存过期时间。
  • 异常处理:Client在提高系统可用性上扮演这重要的角色,在性能损耗可容忍的情况下,通过简单的重试超时方式即可解决Master、Storage节点不可用的异常,最大限度地保证系统可用性。

从性能角度来看,Master节点是不会成为系统瓶颈的,毕竟现在的服务器处理能力是很高的,无论是Master节点缓存的集群信息占用的内存,还是对于一个几万台机器的集群的调度,单机Master节点都是可以扛得住的。说道这里,在做系统性能测试时,对于分布式存储系统中的管理节点的性能,即管理流的场景要模拟覆盖到的。实验证明这样场景确实能发现了不少的性能问题。

那么对于有中心管理节点,数据节点有主从之分的系统,它的性能瓶颈是在哪里?根据理论分析,假设单机存储系统参数不当或代码实现导致的性能问题都已排除解决,即纵向的性能优化基本符合预期。这样一个系统,对于小文件的读写场景,在有限的磁盘数量和文件数量,如果数据分布的不是很分散,那么主要的性能瓶颈在于集群中某些磁盘的吞吐能力,而在大文件的读写场景,系统的瓶颈在主节点的出口网卡流量。以三备份为例,主节点需要往两个从节点写数据,这时候主节点的出口网卡流量是入口网卡的两倍,如果出口入口均为千兆网卡那么入口网卡的流量始终只能跑满到网卡最大流量的一半。所以在设计时才会以Partition为单位作为备份的基本单位,这样一来每个节点上面都有主从,每个节点的流量基本都可以跑满。

性能分析先收住,分布式存储系统的性能测试和优化是很重要的,涉及到的知识和内容也比较多,有机会再展开。

从可用性的角度来看,如果不做高可用的设计,Master节点就是系统的单点。消除系统单点的方法很多,一般分布式系统中常直接使用Zookeeper来保证节点的高可用。所以其实中心控制节点的单点问题并没有那么严重的。相比而言,中心控制节点的调度策略显得更为重要,因为数据分布的是否均衡直接影响到系统对外服务的性能。


2. 无中心管理节点 AND 存储节点无主从

以Swift为例说明,从其基本架构可以看出,Swift 采用了完全对称、所有组件都可以扩展的分布式系统架构设计,系统无单点存在。去掉Proxy-Server层对象映射的逻辑,可被看作为要讨论的分布式文件系统,从另一个角度Proxy-Server也可以看做是分布式文件系统的客户端,只是在实现上和对象存储的逻辑耦合相对比较紧密。

此外,对象存储逻辑层的认证服务、缓存服务忽略暂不讨论,Container-Server和Account-Server和Object-Server的设计基本一致,所以仅讨论Object-Server。

以下为Swift的基本架构图,包含主要各个组件


Client

Swift的对象业务逻辑和底层的分布式文件系统耦合是比较近的,为了便于拆分理解,这里可以将Proxy-Server节点看做为分布式文件系统的Client端

抛开Proxy节点作为对象服务功能不谈,作为无中心控制节点系统Client端,它承担了比较重要的两个角色

1. 备份一致性的保证

Swift 采用 Quorum 协议,关于此协议内容就不赘述了。Quorum协议对于一致性的保证很灵活。Swift默认选择的是写完三个备份才算写成功,也可通过配置参数选择写完2个副本即返回,剩余副本有Object-Replicator保证副本数量的正确性。对于读操作,Swift默认选择任意读其中一个备份数据即返回,此种弱一致性模型是很可能读到旧版本数据的,不过它支持用户在读操作请求头中增加 x-newest=true 参数来同时读取 2 个副本的元数据信息,然后比较时间戳来确定哪个是最新版本,返回给用户最新数据。

对于Swift选用的最终一致性模型实现,光靠Client数据流来保证是不可靠的,所以在StorageNode上会有进程定时检查备份是否一致,在Object-Replicator部分会说道。

2. 系统可靠性的保证

对于一个无中心的分布式文件系统来说,系统可靠性的保证不是某一个节点能全权负责的。Proxy节点主要是在处理用户数据流的时候最大程度的去保证用户请求能处理成功。但当用户发送一个上传对象的请求时,Proxy节点发现一个存储节点故障了,应该如何处理?

Swift引入了handoff节点,可理解为备用节点。其中,Swift的Ring对象的get_more_nodes方法,针对partition会返回一组handoff节点,如何挑选handoff节点先不展开,总体思想是尽量选择跟故障节点不在一个逻辑域和物理域,然后开始重试,重试次数由Proxy节点配置文件request_node_count 参数决定,默认是备份数的2倍。

StorageNode

Swift的StorageNode由以下几个组件组成。

Object Server,即单机的存储引擎实现,提供文件的元数据和内容存储。每个文件的的内容会以文件的形式存储在文件系统中,而元数据会作为文件属性来存储。每一个文件以生成的时间戳作为名字。

Object-Replicator主要用于保证副本数量和位置的正确性以及一致性。在Swift的实际使用中,有几个场景都会导致副本的数量和位置不正确。先在这里简单举例,后面在说到数据恢复时会详细说明。如上文提到的临时写到handoff节点,Replicator会根据ring计算出的信息,把数据搬到正确位置,再如因为添加了磁盘或机器导致ring发生变化,再入Proxy节点配置了写亲缘性,Proxy节点会将数据优先写到某个分区,最后由Replicator把数据跨分区复制到正确的位置。在某些节点故障或者用户选择只写两备份就返回的场景下,副本会处于少备份的情况,这也是Replicator要做的事情。

Object-Replicator同时还会保证副本的一致性,以Partition/Suffix为基本单位(Suffix为Partition下的子目录),以每个文件时间戳计算的hash值保存在Partition目录下的hashes.pkl文件中,通过请求远端备份的Object-Server获取hashes.pkl和本地值对比,发现不一致时会采用推的方式更新远程副本,使用远程文件拷贝工具 rsync 来做数据的同步,rsync带 ignore-existing参数对于文件名相同的文件会忽略拷贝,这样就避免了全量拷贝这样在远端备份同一个文件名的目录下可能会存储在不同版本的数据文件,这个Object-Server在处理读操作的时候会选择时间最新的数据返回。从这个角度来看,Object-Replicator对保证副本的一致性起到了作用。

Object-Auditor:主要用于检测副本数据是否正确,当发现比特级的错误,文件将被隔离,这样远程Object-Replicator检测到副本缺少时会将正确的副本推送过来。

Object-Updater:主要是解决对象存储的元数据的更新问题,简要说明下,文件成功创建后会将对象名更新到Container-Server,当Object-Server由于高负载的原因而无法立即更新时,任务将会被保存到本地文件,以便服务恢复后进行异步更新;Updater服务主要就是负责系统恢复正常后扫描任务做相应的更新处理。

此篇只对架构进行了简要的说明,而对于Swift的核心关键Ring的实现暂时还没展开。Ring作为数据定位的关键实现,再详细说明。

没有中心控制节点,所以Swift的可靠性的保证落在各个节点上,责任分工在上文也有所描述,这同时也需要每个节点都要拥有整个集群的配置信息,也就是Ring文件才能开展工作。所以,一旦Ring发生改变时候需要同步到集群的所有节点,否则系统就没法正常工作了。虽然对于Swift系统来说只有管理员在做节点增减等运维操作时候才会改变Ring结构,然后同步到各个节点,但是常常在生产使用中,还是最好是需要一个第三方监控进程,对比各个节点Ring文件的MD5,保证集群中所有节点的Ring文件始终保持一致.

从性能的角度来说,对于无中心管理节点,且数据节点业务主从之分的系统,同样也需要考虑集中典型的场景,来分别考虑其性能瓶颈。同样也需要假设单机存储系统参数不当或代码实现导致的性能问题都已排除解决,即纵向的性能优化基本满足需求。

对于小文件的读写场景,在有限的磁盘数量和文件数量,性能瓶颈也会受限集群中某些磁盘的吞吐能力,再加上如果没有对小文件做合并处理,性能会更受影响。Swift作为对象存储系统,存在的性能瓶颈和问题不仅仅于此,有机会再展开描述。

而在大文件的写场景,系统的瓶颈在Proxy节点的出口网卡流量。以三备份为例,Proxy节点默认需要写完三个备份才返回成功,Proxy节点出口网卡流量是入口网卡的三倍,在同样的机器硬件条件下,性能大大受限,只能通过扩展网卡或机器的方式才能达到同样的性能。

以上可以看出,分布式文件系统的架构比较难适合于所有的场景,如何进行选择和设计,常常优先围绕需求来选择和实现,留有扩展余地再不断地优化系统。

编辑于 2017-07-11