GlusterFS元数据机制分析

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/liuben/article/details/100060564

​TaoCloud团队原创:微信公众号文章访问

   

GlusterFS作为一个免费开源的分布式文件系统,以其简约的架构设计,完善的协议支持,无中心节点、全局统一命名空间、高可用、高性能、横向扩展等特点,拥有着旺盛的生命力,在工业界受到极大的欢迎和使用。

就像所有的事情一样,都不可能是尽善尽美的,所以GlusterFS也存在不足的地方:因为其无中心的架构设计,在支持无限扩展,无单点故障,无性能瓶颈的同时,同样出现了元数据一致性处理困难,元数据操作性能低下等短板。

基于这样的背景,本文尝试着去分析GlusterFS的元数据机制,以期能从中找到突破口,为解决GlusterFS元数据的短板问题提供理论支持。 --------作者按

什么是元数据

任何文件系统中的数据分为数据和元数据。数据是指普通文件中的实际数据,而元数据指用来描述一个文件的特征的系统数据,诸如访问权限、文件拥有者以及文件数据块的分布信息等等。

例如我们有一个文件test.c,则其数据信息如下:

其元数据信息如下:

在分布式文件系统中,数据的分布信息包括:文件在磁盘上的位置以及磁盘在集群中的位置。用户需要操作一个文件必须首先得到它的元数据,才能获定位到文件的位置并且得到文件的内容。

在分析GlusterFs元数据机制之前,先介绍下分布式文件系统的元数据管理架构都有哪些种类,以此对比,说明GlusterFs元数据操作性能存在瓶颈的原因。  --------作者按

分布式文件系统下的元数据管理架构

中心控制节点架构(GFS)

如图1所示是google的GFS的架构模型。在该模型中master主要负责元数据的管理,client每次数据访问都要首先访问master获得数据所在的chunkserver以及元数据信息,然后再与相应的chunkserver通信获得数据。

图1:GFS架构模型

此类模型特点:元数据都由master节点统一管理。所以一致性问题处理起来相对容易。但是其存在单点故障和性能瓶颈。

完全无中心架构(GlusterFs)

如图 2所示是GlusterFs的架构模型。该类模型不存在master节点。在该模型中client(glusterfs)通过一致性hash算法计算出数据所在的brick(glusterfsd),然后与相应的glusterfsd通信获得元数据及数据。

图2:GlusterFs架构模型

此类模型特点:没有master统一管理元数据,所以不存在单点故障问题,也不会因为master存在性能瓶颈。但因其元数据分布在每一个brick上面,所以一致性处理起来相对困难,并且元数据相关操作性能较低。

通过两种元数据管理架构对比,可以看出,GlusterFs这种无元数据中心的架构模式,在元数据操作的时候,因为访问路径冗长(要从客户端访问到服务端,然后访问本地文件系统再返回),元数据分散(ls一个目录时,需要从所有brick获取元数据信息,然后聚合返回给用户)等特点,导致了元数据性能问题及一致性问题。

为了解决这些问题,下面将分别从GlusterFs系统架构,GlusterFs元数据的设计与使用两个方面去详细分析GlusterFs的元数据机制。

GlusterFs系统架构

首先,从用户角度去理解GlusterFs是如何进行一次数据的读写操作的。然后从分布式的角度去理解client与server之间的处理逻辑。--------作者按

数据访问流程

图3:GlusterFs数据访问流程

1. 通过Fuse将GlusterFs挂载到本地文件系统并提供一个挂载点。

2. 用户对挂载点进行操作。

3. Vfs接受到挂载点的操作请求,对操作进行一定的处理,并将posix标准化的操作提交给fuse。

4. GlusterFs client的fuse_bridge从/dev/fuse读取操作请求。

5. Client 经过一定的逻辑处理将操作及数据发送给指定的server。

6. Server经过posix将操作及数据在本地文件系统上进行实现,并返回。

如何实现分布式

从上面,我们了解到了一个访问流程是如何从用户到落盘的流程,但是我们只能看到一个简单的访问流程,并不能体现GlusterFs本身是个分布式的文件系统的特征,所以重点应该是在分布式上面,那么在这个访问流程中,GlusterFs是如何将文件进行分布式存储的呢?其主要部分就在于client与server之间,所以在这里着重说明下这部分的处理逻辑。

图4:client到server的逻辑

Client端到server端的分布式逻辑,主要是通过dht算法实现的。其根据文件名确定该文件所在的brick。

1. Client端解析路径及文件名,经过DHT计算hash值,然后根据父目录hash范围确定hash子卷。

2. 在确定hash子卷之后,根据ec/afr/stripe等确定数据在该子卷中各个brick的分布,然后将数据及操作发给对应brick所在的server进程。

3. Server进程对本地文件系统进行具体的操作。

DHT定位文件位置示例如图5所示:

图5:DHT-定位文件

由图5可知,要定位一个文件,首先要计算该文件的hash-value以及知道其父目录的hash-layout。然后根据hash-value及hash-layout确定其hashed-subvol。然后确定该hashed-subvol对应的brick,并与其glusterfsd通信。

GlusterFs元数据设计与使用

由dht算法描述可知,其定位文件过程中用到的元数据信息至少包含:文件本身的hash-value,父目录的hash-layout,subvol到brick的映射关系,该brick对应glusterfsd服务的ip port, 文件在本地文件系统的绝对路径,文件在本地文件系统的元数据信息。

综合来看,可以将GlusterFs的元数据信息分为两个类:

1. 集群管理相关元数据:主要管理了集群公共信息和brick相关信息。

2. 本地文件系统元数据信息:本地文件系统相应数据的的元数据信息。

--------作者按

集群管理相关元数据信息

GlusterFs集群管理相关元数据信息是持久化到本地/var/lib/glusterd/目录下的配置文件当中的,当服务启动时,加载到服务进程内存当中。主要包含如下:

Peers:集群可信池中所有的ip/hostname

Volume:该vol下所有brick

Bricks:每一个brick对应的ip/hostname, port, brick-id=vol-name-client-i real_path等信息

通过以上信息,可以确定subvol与brick的映射关系,brick对应glusterfsd的进程ip/hostname, port,以及real_path等信息,即可通信获得数据。  

本地文件系统元数据信息

本地文件系统元数据信息可分为两部分:

1.  Inode, dentry等本地文件系统元数据信息,用于根据绝对路径获取数据及属性信息。

2.   文件扩展属性信息:用于保存文件的gfid,hash-layout等信息

由上例可知,每次访问文件时,都需要获取其父目录的hash-layout信息,然后计算该文件的hash-value以确定其hashed-subvol。其hash-layout信息就保存在目录的扩展属性当中。

GlusterFs元数据设计、实现与使用

经过对GlusterFs元数据的分析,能够看出,GlusterFs并没有一个单独的管理单元去维护系统的元数据信息。这样看似对元数据的处理简单了许多,但是,正因为没有单独的管理单元去维护元数据信息,所以也就造成了元数据信息的使用十分麻烦。下面将从代码设计的角度去了解下GlusterFs是如何维护这些分散在各个brick上面的元数据信息的。

GlusterFs对元数据的设计借鉴了传统文件系统元数据的设计模式,主要通过inode及dentry两个结构体来维护一个文件或者目录的元数据信息。

由inode_table来维护全局inode,dentry的使用。其关系如图6所示。

图6:inode_table、inode、dentry关系图

如图6所示,inode_table作为一个全局使用的结构体,其通过inode_hash和name_hash两个hash表来记录和查找全局所有在使用中的inode及dentry。       

并通过active, lru, pruge三个链表来维护了inode的使用状态。

而每个文件的唯一标识符gfid及元数据信息都存储在inode结构体当中。通过这样的方式,去管理整个系统中的元数据信息。

下面将对GlusterFs对inode_table,inode, dentry的实现做下分析。--------作者按

inode_table  

数据结构:

Inode_hash: 保存inode的hash表,其key对应inode->gfid的hash值。

Name_hash: 保存dentry的hash表,其key对应dentry->name的hash值。

Active: 处于使用状态的inode。

Lru: 处于热点状态,但暂时未使用的inode。

Purge:未使用状态,并且nlookup=0的inode。等待inode_destroy()销毁该inode。

生命周期:

Inode_table_new(size_t lru_limit, xlator_t *xl)

服务启动时,fuse, nfs, libgfapi, protocol/server调用inode_table_new函数,初始化一个inode_table。该inode_table作为其对应服务的的全局变量。 

Inode_table_destroy(inode_table_t *inode_table)

服务退出时,fuse, nfs, libgfapi, protocol/server调用inode_table_destroy函数,销毁inode_table。

作用:

Inode_table结构体主要包含inode_hash和name_hash两个成员,其中记录了系统中部分常用或者最近使用的inode信息。

当一个文件/目录被创建,或者第一次lookup时,将会生成相应的inode及dentry信息,并保存在inode_table的inode_hash及name_hash中。

当下次使用该inode时,可以通过gfid或者name直接从inode_table中获取相应的inode信息。

对于inode_hash中已经存在的inode,inode_table将其状态分为active, lru, pruge三种,并分别用active, lru, pruge三个链表进行维护。

其状态转换关系图如图7所示:

图7:Inode状态转换关系图

当一个inode创建时,首先从inode_table->inode_pool中申请出来,进入lru链表,然后当该inode被引用时,从lru链表中移除,并进入active链表。当active链表中某个inode不再被引用时,判断该inode的nlookup是否为0,若为0,则将该inode放入purge链表中,准备回收,若不为0,则将inode放入lru链表中等待下次使用。当inode_table中lru_limit被修改变小时,则将inode中最久未被使用的inode从lru链表中移除,并放入purge链表中,准备回收。

 Inode

数据结构:

Table: 该inode所属的inode_table。

Gfid: 作为glusterFs文件系统内,文件的全局唯一id,不会出现重复现象。

Fd_list: 在该inode上面打开的所有文件描述符链表。

Dentry_list: 该inode对应的dentry列表。正常情况下,一个inode对应一个dentry, 若有硬链接情况,则出现一个inode对应多个dentry的情况。

Hash: 通过该值,去定位inode_table中的inode_hash。

List: 确定该inode是在inode_table内的哪个链表中(active/lru/pruge), 如图7所示。

_ctx: 该成员为一个数组。数组的每个成员对应一个xlatory的私有信息。例如:在md-cache xlator中,该ctx对应struct md_cache结构体。其存储的信息为该inode的属性信息及扩展属性信息。

生命周期:

Inode_new(inode_table_t *table)

当创建一个文件/目录/符号链接时,或者lookup一个inode_table中不存在的inode时,会调用inode_new()函数,创建一个inode,并加入inode_table的inode_hash及lru中。

当有引用时,将该inode从lru链表中移动到active链表中。

Inode_unlink(inode_table_t *table)

当删除一个文件/目录时,会调用inode_unlink()对inode及dentry进行回收。

作用:

Inode作为维护文件元数据信息的结构体。每个服务进程中,一个inode和一个文件/目录一一对应。其维护了一个文件/目录在一个ops生命周期内,用到的所有数据,元数据信息及其他一些ops生命周期内需要的信息。

当某一个文件/目录第一次被使用时,在fuse_bridge层创建该inode信息,并放入inode_table中,然后一层层向下传递,并在每个translator中初始化该inode所需的私有信息并保存在inode->_ctx[xl_id]中。例如md-cache中保存了该inode的属性信息及扩展属性信息,Dht层保存了该inode的hashed-subvol和cached-subvol信息。

以后对该inode进行操作时,即可直接从inode中获取所需私有信息(不失效情况),而不需要重复获取此类信息。

Dentry

数据结构

Inode_list: 与inode->dentry连接, 主要实现inode到dentry的映射关系。如图6所示。

Hash: 为name的hash值,通过该值,定位inode_table->name_hash; 如图6所示。

Inode: 指向该dentry所属的inode,获得inode信息。

Name: 该文件/目录的名字。

Parent: 该文件/目录的父目录的inode。

生命周期:

__dentry_create(inode_t *inode, inode_t *parent, const char *name)

当创建一个文件/目录/符号链接或者一个硬链接时,调用dentry_create()函数。并根据其name的hash值放入inode_table的name_hash表中。然后与对应的inode建立相应的连接。

__dentry_unset(dentry_t * dentry)

当删除一个文件/目录时,inode_unlink会调用__inode_unlink然后调用__dentry_unset()函数,销毁dentry。

作用:

Dentry保存了文件名/目录名到inode之间的映射关系。目的是为了在用户知道文件名的情况下,获得其inode信息。在内核知道其inode的情况下,输出其文件名/目录名信息。

所以,由此来看,dentry是作为inode的辅助信息,主要完成name到inode的映射关系。

为了方便文件操作及操作安全,GlusterFs仿照传统文件系统,设计了文件描述符fd结构体。这样每次文件操作,只需要打开一个fd句柄,并持有该句柄,就可以一直在该句柄上面进行各种操作,而不是每次都需要查找对应的inode、dentry等信息。--------作者按

Fd

数据结构:

Pid: 打开该fd的进程号。

Refcount: 该fd上面的引用数。

Inode_list: 连接inode->fd_list,标书该inode上面打开的fd。

Inode: 该fd所属的inode。

_ctx: 该成员同inode->_ctx相同的用法,都是一个数组,该数组的每一个成员对应一个xlator子卷。每一个成员都保存了该xlator子卷的私有信息。例如:该fd对应在其所在brick上面打开的本地文件系统文件描述符就保存在其xlator子卷所对应_ctx[xl_id]成员当中。

生命周期:

Fd_create(inode_t *inode, pid_t pid)

当create或者open一个文件/目录时,调用fd_create()函数创建fd句柄。并返回给用户。

Fd_destroy(fd_t *fd, gf_boolean_t bound)

当一个fd的引用数为0时,调用fd_destroy()函数,销毁该fd。

作用:

每一个文件描述符与一个打开的文件对应,当打开一个文件描述符fd后,其fd与inode建立联系,之后在该fd上面的所有文件操作,都将基于此fd操作。

所以,打开fd之后可以直接获得inode信息,而不需要再去查询。并且该fd->_ctx成员中保存了每一个xlator子卷的私有信息。当具体文件操作时,可以直接从里面获取,而不需要每次都进行创建销毁。

例如,具体对brick进行文件操作时,只需要从fd->_ctx中拿到本地文件系统的fd即可进行操作,而不需要每次都进行动态的创建销毁。

因为现在的GlusterFs采用的是无元数据中心的架构模式,所以在对元数据操作的时候,将会有很长的访问路径,从用户经过网络再访问磁盘,最后返回,并且有时候可能需要访问多个磁盘,这样的话,不但很大程度的延长了元数据操作的路径和操作的节点数,并且还要对每个brick挂载的磁盘进行io操作,所以就会造成元数据操作效率十分低下的情况。

这种情况,虽然在GlusterFs的架构之中属于合理情况,但是对于使用者来说,这种元数据操作的高延迟是不可忍受的。并且在一定情况下,即使非纯元数据的操作,例如create,但是因为在操作过程中需要用到元数据,所以也会造成一定的不必要的延时。

--------作者按

现存问题及现有解决方案

 Ls

 Ls操作,当要读取一个目录时,需要对该系统的所有子卷都进行遍历,然后才能获得该目录下的所有目录项。

而这个操作,因无元数据中心的原因存在以下瓶颈点:

1. 路径冗长,需要经过网络及磁盘io。

2. 每次readdir的size较小,需要很多次readdir才能完成一次ls。

3. Readdirp时,需要获取目录下的属性及扩展属性,要进行磁盘io,极其耗时。

4. Ls需要获取所有子卷上的目录项,聚合之后才能完成一次完整的ls。所以当集群过大,子卷过多时,会导致ls性能严重下降。

小文件

众所周知,小文件问题一直是学术界及工业界的难点问题,而在GlusterFs中小文件问题表现得尤为严重。因小文件操作的特性就是元数据操作频率过高,无法发挥磁盘io性能。性能瓶颈基本是由元数据操作导致的,而GlusterFs又正好是元数据操作性能低下,这样的情况下,就会严重拉低小文件操作的性能。

面对这样的困境,社区方面做出了很多努力,如md-cache, readdir-ahead等。其中最典型的代表为md-cache translator。其主要作用是对文件属性信息及扩展属性信息进行缓存,当用户lookup, getattr, getxattr等请求在md-cache中缓存命中的时候,就可以直接返回,而不需要继续向下继续发送操作请求。若缓存命中率足够高的话,那么,将大幅度提升元数据操作的性能。下面将介绍下md-cache的具体实现,了解下GlusterFs现有优化方案的解决思路。--------作者按

md-cache

md-cache在GlusterFs中的位置及作用如图8所示。

图8:md-cache在GlusterFs中的位置

下面将从md-cache translator在系统中的位置,作用,实现三方面去介绍。

Md-cache的在GlusterFs中的位置及作用

如上图所示,md-cache在GlusterFs 的translator架构中,是属于比较上层的,并且也在客户端部分。这样用户一个关于文件属性或者扩展属性的请求来之后,能尽快的查看是否在缓存命中了,减少了许多不必要的translator操作,并且也大幅度的减少了服务端对brick查询及rpc通信的情况,大幅度的提高了获取文件属性信息及扩展属信息的性能。

Md-cache的实现

下面我们将从如何实现去分析下md-cache。

当用户create操作时,在mdc_create_cbk中设置属性及扩展属性。此时inode->ctx[mdc_id]中的信息还是空的,所以在mdc_create_cbk中mdc_inode_iatt_set时,因第一次使用,将会创建md_cache结构体,该结构体用于保存文件的属性信息及扩展属性信息。创建之后,将该md_cache与inode->ctx[mdc_id]建立连接,所有对该inode的属性信息及扩展属性信息的更新或查询,都会通过inode->ctx[mdc_id]中去操作。

Mdc_create_cbk图解如图9:

图9:mdc_create_cbk

之后所有相关操作,都可直接从该缓存中查询,若存在且有效,则直接给用户返回,无需再下发其他translator进行处理。其图解如图10:

图10:md-cache命中缓存

当有信息变更相关操作时,将在其回调函数中,更细缓存信息,其图解如图11:

图11:md-cache更新缓存

md-cache的一致性问题

当有文件的属性或扩展属性信息有更新时,将会通过upcall translator的

this->notify()函数触发通知,然后md-cache的notify机制将会收到通知,并调用mdc_invalidate将缓存设置我失效状态。

图12:md-cache一致性

当下次需要获取该inode的属性或扩展属性信息时,重新获取并填充,使其生效。

基于GlusterFs本身对元数据问题进行优化

Server端优化

在glusterfsd上加一层缓存,将该brick上面所有元数据信息都放在内存中。这样在进行元数据操作的时候,就避免了磁盘访问时间,要知道磁盘访问是十分耗时的。

在设计这种缓存的时候,有多种模式可供选择。

例如:

1. 内存足够的情况下,可以将目录结构及所有元数据信息(属性及扩展属性)全部缓存在内存中。

2. 当内存不足时,可以采用mmap的办法,使用本地磁盘来扩展内存,以达到全缓存的目的。

3. 采用lru算法,对目录结构采用全缓存,但是对于元数据信息(属性及扩展属性)采用lru算法,只缓存热点数据。

4. 采用内存型数据库来缓存目录结构及元数据信息(属性及扩展属性)。

5. 将目录结构及元数据信息存储在一个ssd磁盘当中。

6. 等等。

 Client端优化

Client端实现,如果在client端实现元数据的优化,那么将大幅度提高元数据操作的性能,因其不需要经过网络及磁盘io,可以直接在client端的内存中获取元数据信息。

其具体实现有以下几种方法:

1. 可以是对现有的md-cache进行修改,使得md-cache的命中率更高,失效时间延长,失效时机减少等。

2. 也可以是重构md-cache,将md-cache做成一个全缓存的模式,将所有元数据都加载进来,当然,由于内存的限制,可能无法将所有元数据都加载进内存,所以此时是否可以借助一些本地数据库或者本地磁盘缓存的方式,来逻辑上扩大内存,使得所有元数据都加载入内存,当然同时也要考虑一致性等问题。

3. 采用alluxio等内存高速虚拟分布式存储系统,设计一个全缓存的md-cache,可供所有client公用。

采用额外机制对GlusterFs元数据问题进行优化

增加元数据中心

单独的增加一个元数据中心,将所有的元数据操作都汇聚到元数据中心上进行操作。这样能够最大程度的优化元数据操作的延时,即减少了元数据操作的路径,也避免了非必要的磁盘io操作。当然如何去增加一个元数据中心的方法有很多,可以完全独立实现,也可以借助一些现有的机制去完成(如mongodb等),并且当元数据中心宕机之后,原有系统还能继续为用户提供服务。

也可以在GlusterFs之上设计一个通用型的元数据中心模型,对于用户的所有操作,都先经过该元数据中心模型。元数据中心模型对元数据进行操作之后,再将具体操作下发给GlusterFs。

其他

例如:

1. 增加缓存服务器,对经常使用的数据及元数据进行缓存。

2. 改为更加快速的固态盘。

参考文献

1. 《什么是 metadata(元数据)》https://blog.csdn.net/liuchangjie0112/article/details/47321415

2. 《GlusterFs inode详解》http://www.360doc.com/content/15/0121/16/20873288_442604816.shtml

3. 《深入理解GlusterFS之POSIX接口》 https://blog.csdn.net/liuaigui/article/details/77776621

4. 《一篇文章讲透分布式存储》

https://baijiahao.baidu.com/s?id=1623275115956708740&wfr=spider&for=pc

5. https://staged-gluster-docs.readthedocs.io/en/release3.7.0beta1/Developer-guide/datastructure-inode/

猜你喜欢

转载自blog.csdn.net/liuben/article/details/100060564