根据IDC预测2020年全球数据量将达到44ZB,其中80%来自于非结构化数据的贡献。随着云计算、大数据、物联网、AI、5G等技术的发展应用,可快速扩展的基础架构成为必需,这些需求推动了软件定义存储(SDS)的增长。 2018年中国软件定义存储市场需求场景中,文件存储仍然是主力,占比高达62.3%。
在诸多分布式文件系统中,GlusterFS以其简约的架构设计,完善的协议支持,无中心节点、全局统一命名空间、高可用、高性能、横向扩展等特点,拥有着旺盛的生命力,在工业界受到极大的欢迎和使用。然而因其无中心的架构设计,导致其目录ls性能一直被用户所诟病。
基于这样的背景,本人及所在TaoCloud团队针对GlusterFS目录ls性能低的问题进行分析研究,提出了几种优化方案。根据方案的相关性及复杂度制定出了我们的研发计划,并已完成了阶段性目标,极大的提升了GlusterFS目录ls的性能。
问题分析
与中心控制节点架构的分布式文件系统不同,GlusterFS的元数据是分散存储于每一个brick上面的,所以其目录ls操作无法直接从元数据节点获取所需信息,而是需要分别向所有brick下发请求以获得元数据信息。
对GlusterFS目录ls流程分析,其存在以下影响时延的因素:
1. 无元数据中心,导致每次ls要分别向所有brick发起readdir请求。
2. Ls操作经过vfs解析变成size为4k的多次连续readdir请求。
3. 访问路径冗长,要经过多次内核态与用户态的交互及网络通信。
4. GlusterFS内部锁机制(如EC卷的eager-lock)。
GlusterFS社区对目录ls操作已经进行了一些优化,例如增加了readdir-ahead和parallel-readdir功能,可将每次readdir请求size设置为128k,有效的减少了client与server之间的网络通信次数,并且预读下一个readdir(next_off, size),当fuse下发下一个readdir时,可从readdir-ahead层的缓存中直接返回。针对EC的eager-lock,增加了other-eager-lock参数,用于分离读写与ls之间的锁等。
然而,即使这样,GlusterFS的目录ls操作时延,在许多情况下依然超过用户的容忍极限。
在此基础上,我们讨论出以下几种优化方案:
1. 在GlusterFS之上增加一个通用型的元数据中心,作为GlusterFS之上的插件使用。该方案不依赖与GlusterFS,作为一个独立的系统架构。可供其他需要元数据管理的系统使用。该方案原型设计复杂度较高,工作量较大,项目周期较长。但效果显著,能解决包括ls在内的大多数元数据操作性能问题,且可作为插件供其他系统使用,通用性较高。
2. 在GlusterFS的Client端增加元数据中心,用于保存GlusterFS的元数据,当请求下发到Client时,直接从元数据中心获取所需信息。该方案基于GlusterFS系统本身实现,原型设计复杂度没有方案1那么高,但多Client一致性问题处理起来较为复杂,且可能对其他操作造成一定的性能影响,项目周期也较长,但ls性能优化效果明显,可解决包括ls在内的大多数元数据操作性能问题。
3. 在GlusterFS的Server端增加元数据缓存,各Server端进程只缓存该brick的目录项及元数据,不用考虑多Client一致性问题。该方案基于GlusterFS系统本身实现,在Server端增加一个translator用以缓存目录项及元数据。用户readdir(p)请求下发到此translator时,直接根据缓存信息处理请求并返回,而无需继续下发到本地文件系统,这样以访问高速缓存替代低速磁盘,并减少一次用户态与内核态交互的方式,来减少ls的时延。该方案优化效果虽不如前两种方案明显,但亦有极大的提升,且项目周期较短,适合作为突破口。
经过实验及深入分析,发现在readdir过程中,GlusterFS在Server端posix-translator向本地文件系统发起readdir请求耗时较多,约占整个访问流程时延的2/3,且在磁盘读写压力大的情况下,该操作时延增加明显。综合用户需求及项目周期等因素,尽量做到短期内对ls性能有较大的提升效果,最终选择方案3来实现GlusterFS目录ls性能优化,同时继续进行对方案1,2的调研工作,并作为下一步工作重点及优化方向。
·方案设计
软件架构:
相关模块功能说明如下:
· GlusterFS Client:GlusterFS客户端进程,负责处理/dev/fuse操作请求(如readdir操作),并与GlusterFS Server进程交互,完成对应用请求的处理。其中,已有的readdir-ahead功能位于客户端进程中,用于预读目录项及其元数据信息,并可设置rda-request-size为128k,减少请求次数。而parallel-readdir功能包含在readdir-ahead功能,可以通过参数设置启用或禁用。
· GlusterFS Server:GlusterFS服务端进程,负责接收并处理GlusterFS客户端进程的请求,对外提供基于POSIX协议的存储服务。其中,如图2所示,在已有posix层之上,我们引入一个dcache层(即dcache translator),用于缓存和处理brick端的目录结构和文件属性和扩展属性元数据信息。
Dcache设计
Dcache设计目的是为了解决用户目录ls时延高的问题,所以该方案中Dcache缓存了目录项及元数据信息。用于处理并返回readdir(p)的请求,而无需继续下发的posix访问磁盘。对于其他获取元数据的请求(如lookup),并不在dcache中处理。
考虑到服务器内存限制,Dcache方案并未将目录项及元数据信息全部保存于内存当中。而是仅将目录结构信息保存于内存当中,而将元数据保存到嵌入式KV数据库levelDB当中。
如图3,GlusterFS在Server端处理readdir(p)请求时,Dcache在步骤2取代了虚线部分posix层访问本地文件系统的操作,以高速缓存代替低速磁盘访问,并减少一次用户态与内核态交互的方式,减少readdir(p)时延。
如图4,为目录在Dcache内存中的组织结构。
GlusterFS中每个目录项对应一个dentry。dentry的data信息中保存了该dentry的gfid, name, pargfid, d_type等信息。
通过目录项的gfid,采用hash算法,定位到dentry_table->gfid_hash[hv]。并通过gfid,name, pargfid信息确定该目录项对应的dentry。若该dentry为目录,则其child_list链表下链接的为该目录下所有目录项。该内存结构即可满足readdir请求所需的信息。
DB元数据信息
DB以每个dentry的gfid为key, 属性及扩展属性为value的方式保存了所有目录项的元数据信息。
通过Dcache的内存结构,可获取其目录结构,即可返回readdir请求。而若是readdirp请求,则需要该目录下每个目录项的元数据信息(属性及扩展属性信息)。此时需要以目录项的gfid为key访问KV数据库levelDB获取元数据信息。
·方案实现
Dcache缓存目录项及元数据的设计采用了非持久化的设计方案,每次进程启动都需要重新加载目录项及元数据到缓存,而不依赖于GlusterFS Server进程上次运行缓存的结果,从而很大程度的减少了程序设计与实现的难度与复杂度。
Dcache实现主要分为三部分:预加载模块,任务处理模块,readdir(p)相关模块。
1. 预加载模块在存储节点启动时,加载brick上已有的目录项及其元数据信息到内存目录结构及DB中。
2. 任务处理模块为处理与元数据相关的ops在dcache中的逻辑。
3. Readdir(p)相关模块处理了opendir, readdir(p), releasedir的逻辑。
预加载模块
该模块从根目录开始,采用深度优先算法遍历所有目录项。如图5所示,为获取其中一个目录项的处理步骤:
1. 从磁盘获取目录项及属性和扩展属性信息。
2. 为该目录项创建相应的内存结构
3. 以该目录项的gfid为key, 属性和扩展属性信息为value存入数据库当中。
重复以上步骤,直到遍历结束。根据测试,加载过程耗时不多(每个brick上500万目录项2min左右),且加载阶段不影响系统对外提供服务,只是该阶段内dcache未生效,不对外提供ls加速效果。
任务处理模块
分析了GlusterFS所有ops,从而筛选出与元数据处理相关的ops在Dcache中进行处理。为尽量减少增加dcache后对正常读写等逻辑的性能影响,可将ops分为两类:
1. 目录项增删相关的ops: create, mkdir, mknod, link, rmdir, unlink, symlink, rename。
2. 目录项元数增删改相关ops: open(O_TRUNC), writev, (f)truncate, (f)setattr, (f)setxattr, (f)removexattr, zerofill, discard, (f)xattrop, fallocate。
其中目录项增删相关的ops操作采用同步处理机制,以便目录ls时,能够获取到最新的结果。目录项元数据增删改相关的ops操作采用异步聚合处理机制,以最大限度的减少此类操作对正常逻辑的性能影响(如小粒度的追加写操作会频繁的更新元数据)。经测试,增加dcache后,对系统正常读写逻辑几乎没有影响。
·目录项增删处理
为保证目录ls时能够及时获取到最新的结果,以避免出现刚创建的目录项查看不到,或者查看到已删除的目录项的情况,故目录项增删相关的ops采用同步处理机制。
如图6所示,在一个ops流程中,当posix在本地文件系统执行完成,向上回调时,dcahe在回调函数中,根据posix的处理结果,执行相应操作,以保证dcache中目录项及元数据与本地文件系统一致。具体ops的处理细节略有不同,此处不进行代码实现细节的赘述。
·目录项元数据增删改处理
为避免dcache操作内存及数据库影响系统对外提供服务的性能,将及时性要求不是特别高的目录项元数据增删改操作采用异步聚合处理机制。
该模块中需要在初始化时创建一个后台线程worker thread,用于异步处理ops操作生成的任务。
如图7所示,对于目录项元数据的增删改对应的ops,在dcache层回调时,根据不同的ops生成相应的任务(task), 并将该任务的必要信息保存到task中,然后加入队列(queue)中等待后台线程(worker thread)处理。
Worker thread线程,会顺序的从queue中取task,并根据task中的信息,进行相应的逻辑处理,具体ops对应的处理逻辑略有不同,此处不进行代码实现细节的赘述。
此处有两点需要特别注意:
异步处理逻辑的目的就是为了尽量减少元数据更新操作对系统性能的影响,所以,在生成task并加入queue中时,此处做了查重处理,对于同一个目录项的同类型任务进行聚合,最后只进行一次处理,大幅度减少了后台线程元数据操作的次数,减轻系统负载。如:同一文件的大量连续的小粒度追加写操作,可聚合为一次元数据更新。
因异步处理元数据信息,导致,若该task未处理,而有readdirp请求需要获取该目录项的元数据信息,此时,从DB中获取到的元数据信息可能不是最新的。若用户的后续操作依赖于该readdirp获取到的元数据信息,则会出错。所以,针对这种情况,在readdir(p)模块进行了特殊处理,以避免这种问题的出现。
Readdir(p)模块
如问题分析中所述,一个ls请求会被分为许多次连续的readdir(off, size)请求。其中off记录了上次请求的位置,size为本次请求的大小。
如图4:内存目录结构所示,该目录结构设计中并无off相关的设计,所以无法记录上次readdir的位置,且若同一个目录被多用户ls时,每个用户都会持有自己的off,且在readdir过程中有目录项的增删,都会对off造成影响。
针对这种情况,设计了游标结构dfd。用于标记readdir的位置,其在opendir时,创建该游标结构dfd,并将该游标保存于opendir的fd结构当中。游标dfd随着readdir移动。当游标dfd移动到尾部时,readdir返回结束标志。收到releasedir时,释放该游标。
·Readdir(p)逻辑
如图8所示,当opendir时dfd指向其要打开目录的dentry。
Readdir(p)逻辑:
当off为0时,以游标dfd指向的dentry->child_list为head,向后遍历该链表,以size计算该次遍历结束的标志。此时,dfd也随着dentry的遍历而移动,off也随之进行加操作。
当off不为0时,以游标dfd指向的dentry->brother为head,向后遍历该链表,以size为该次遍历结束的标志。此时,dfd也随着dentry的遍历而移动,off也随之进行加操作。
若遇到dentry->brother的下一个为打开目录的dentry,则此次遍历结束,并且返回readdir结束的标志。
每遍历一个dentry,则可从其data信息中获取到readdir所需的所有信息,若为readdirp请求,则以data中的gfid为key,访问DB,获取其value并填充属性就扩展属性信息。然后返回readdirp请求。
注:若该目录下有新增dentry的操作,则新增dentry加入队列尾部,不影响后续的readdir。若该目录下有删除dentry的操作,则将指向被删除的dentry的dfd向前移动一位,指向上一个dentry,亦不影响后续的readdir操作。
·特殊处理
在4.2.2中已经提到过,因目录项元数据更新采用异步处理机制,所以其readdirp获取到的目录项的元数据可能不是最新的。针对这种情况,在readdir(p)模块进行了特殊处理。
在dcache设计时,因考虑到性能的问题,对于目录项元数据相关ops并未采用同步处理机制。但也不能完全不考虑一致性的问题。
所以,在这里采用了一种相对折中的方式,当opendir时,下发一个强制执行queue中属于该目录下目录项的任务。由一个后台处理线程process thread去执行该任务。
当收到readdir(p)请求时,判断强制执行任务是否执行完成,若完成,则可进行readdir(p)操作,未完成则等待。因queue中任务进行了聚合操作,所以一般情况下,任务都是很少的,基本不会出现等待时间过长的情况。
·测试环境与方法
测试环境
硬件环境:
软件环境:
OS: CentOS Linux release 7.6.1810 (Core)
Gluster: glusterfs-3.12.15-1.el7.x86_64
工具:vdbench
集群环境:
EC:3 * (2+1)
双副本: 3 * 2
测试方法
测试均在开启readdir-ahead, parallel-readdir, readdir-optimize, nl-cache的情况下进行。
静态无读写情况测试:
1. 原生系统,目录下不同数量级的文件个数ls耗时。
2. Dcache方案,目录下不同数量级的文件个数ls耗时。
动态有读写(1个压力客户端进行vdbench 64线程随机读写(6:4),一个测试客户端进行ls测试):
1. 原生系统,目录下不同数量级的文件个数ls耗时。
2.Dcache方案,目录下不同数量级的文件个数ls耗时。
·结果与结论
测试结果
1 EC卷测试结果
2 副本卷测试结果
·结果与结论
根据测试结果,可得出如下结论:
EC模式
1. 在静态无读写情况下,增加dcache方案,ls性能约提升1-2倍。
2. 在动态有读写情况下,原生系统ls性能表现出不稳定的特点,受系统压力及磁盘状态影响严重。
3. 在动态有读写情况下,dcache方案表现稳定,基本不受读写压力的影响。
4. 在动态有读写情况下,增加dcache方案,ls性能约提升7-57倍,且随着读写压力增大,提升愈加明显。
副本模式
1. 在静态无读写情况下,增加dcache方案,ls性能约提升2-5倍。
2. 在动态有读写情况下,原生系统ls性能表现出不稳定的特点,受系统压力及磁盘状态影响严重。
3. 在动态有读写情况下,dcache方案表现稳定,基本不受读写压力的影响。
在动态有读写情况下,增加dcache方案,ls性能约提升15-82倍,且随着读写压力增大,提升愈加明显。
总结与展望
综合以上测试结果及结论,可以看出dcache方案对ls提升效果明显,且表现稳定,不随系统压力或磁盘状态而出现严重影响ls时延的现象。
在实际应用场景中,这种ls不受系统压力影响,且时延随文件个数近似线性增长的现象,能够给用户以合理的时间预期,而不会出现ls时延超出用户容忍极限或者死等待的情况,极大的提升了用户体验。
该方案虽然对ls性能有很大提升,但其还存在许多不足之处:
1. 访问路径冗长、交互次数过多、需要访问所有brick的问题依然存在。
2. 无读写状态下,ls性能优化效果有限。
3. 仅解决了目录ls的性能问题,并不能解决其他元数据操作的性能问题(如:lookup, fstat等)。
由此可见,GlusterFS目录ls性能还存在很大的优化空间,且GlusterFS元数据性能问题也有待解决,所以,我们以此为突破口,计划下一步做位于GlusterFS之上的通用型元数据中心,从根本上解决GlusterFS元数据性能问题,并希望可以将其应用到其他需要元数据管理的系统当中。
参考文献
1. https://mp.weixin.qq.com/s/MtXTIPDwBZ1ZfUExK0mdnA《GlusterFs元数据机制分析》
2. https://blog.csdn.net/liuhong1123/article/details/8126451《Glusterfs之小文件优化 》
3. https://blog.csdn.net/inevity/article/details/24873165《Glusterfs目录ls性能优化方案分析》
(TaoCloud团队原创)