当前位置:首页 > 技术分析 > 正文内容

70%读写性能提升!基于UCloud对象存储US3的用户态文件系统设计

ruisui884个月前 (02-03)技术分析34

前言

为了解决在数据备份场景中的可靠性、容量、成本问题,越来越多的用户倾向于使用对象存储来进行备份。然而,有些场景下通过对象存储US3来备份还是不够方便,甚至不适用。比如在数据库备份场景下,如果直接使用对象存储备份,可能需要先把数据库通过mysqldump做逻辑备份,或者采用xtrabackup做物理备份到本地,然后使用基于对象存储的SDK的工具把备份文件上传到对象存储,备份过程繁琐。再例如服务的日志归档备份,为降低成本可以将日志存储到对象存储US3中,通过SDK或者工具来操作,不仅需要编写备份代码,而且管理复杂。如果能提供一种以POSIX接口远程访问对象存储的方式,就可以很好地解决上述问题。

开源方案实践

已经有一些开源的项目将对象存储中的bucket映射为文件系统,如s3fsgoofys等,在使用这些开源方案的时候,我们发现了一些问题。

s3fs

s3fs通过FUSE将s3和支持s3协议的对象存储的bucket挂载到本地(FUSE的介绍详见下文)。通过对s3fs进行测试后,我们发现其在大文件的写入方面性能特别差,研究其实现过程后,我们发现s3fs在写入时会优先写入本地临时文件,然后以分片上传的方式将并发的将数据写入到对象存储。如果空间不足,则会以同步的方式将分片上传,代码如下:

ssize_t FdEntity::Write(const char* bytes, off_t start, size_t size){ // no enough disk space if(0 != (result = NoCachePreMultipartPost())){ S3FS_PRN_ERR("failed to switch multipart uploading with no cache(errno=%d)", result); return static_cast(result); } // start multipart uploading if(0 != (result = NoCacheLoadAndPost(0, start))){ S3FS_PRN_ERR("failed to load uninitialized area and multipart uploading it(errno=%d)", result); return static_cast(result); }}

由于我们的主要使用场景为大文件的备份,基于云主机硬盘成本等方面的考虑,我们决定放弃这一方案。

goofys

goofys是用go实现的将s3以及部分非s3协议的对象存储挂载到linux的文件系统,测试后,我们发现goofys主要有三个问题:


?写入没有进行并发控制。在大文件的写入场景下,goofys同样将文件进行分片,然后每个分片开一个协程写入到后端存储。对象存储一般通过HTTP协议进行通信,由于请求是同步的方式,在不限制并发数的情况下会有大量的连接,消耗大量的内存等资源。


?读取采用同步方式,性能很差。FUSE有两种读取模式async和sync,通过挂载时的设置去选择,goofys强制使用了sync模式,并且预读的实现为乱序读取超过三次后停止预读,代码如下:

if !fs.flags.Cheap && fh.seqReadAmount >= uint64(READAHEAD_CHUNK) && fh.numOOORead < 3 { ... err = fh.readAhead(uint64(offset), len(buf)) ...}

fh.numOOORead为乱序读取的次数,FUSE模块会对超过128k的IO进行拆分,以128k对齐。简单介绍一下FUSE的同步读取和异步读取模式的区别。内核的读取一般入口是在底层文件系统的read_iter函数,然后调用VFS层的generic_file_read_iter,该函数内部实现会通过调用readpages进行预读。如果预读后没有对应的page则会调用readpage读取单页。由于goofys不支持该设置,我们通过对s3fs设置不同的配置来测试,然后抓取读取时的调用栈对比其中的区别。设置了异步读取模式的读堆栈如下所示:

fuse_readpages+0x5/0x110 [fuse]read_pages+0x6b/0x190__do_page_cache_readahead+0x1c1/0x1e0ondemand_readahead+0x1f9/0x2c0? pagecache_get_page+0x30/0x2d0generic_file_buffered_read+0x5a50xb10? mem_cgroup_try_charge+0x8b/0x1a0? mem_cgroup_throttle_swaprate+0x17/0x10efuse_file_read_iter+0x10d/0x130 [fuse]? __handle_mm_fault+0x662/0x6a0new_sync_read+0x121/0x170vfs_read+0x91/0x140

其中vfs_read是系统调用到vfs层的入口函数。之后会调用到readpages进行多页的读取。fuse_readpages将读请求发给用户态文件系统,进而完整整个读取流程。同步读取模式的堆栈如下所示:

fuse_readpage+0x5/0x60 [fuse] generic_file_buffered_read+0x61a/0xb10 ? mem_cgroup_try_charge+0x8b/0x1a0 ? mem_cgroup_throttle_swaprate+0x17/0x10e fuse_file_read_iter+0x10d/0x130 [fuse] ? __handle_mm_fault+0x662/0x6a0 new_sync_read+0x121/0x170vfs_read+0x91/0x140

和异步流程相同,依然是在generic_file_read_iter中进行读取,当读取之后没有对应的页,会尝试读取单页。相关代码如下,内核版本基于4.14

no_cached_page: /* * Ok, it wasn't cached, so we need to create a new * page.. */ page = page_cache_alloc_cold(mapping); if (!page) { error = -ENOMEM; goto out; } error = add_to_page_cache_lru(page, mapping, index, mapping_gfp_constraint(mapping, GFP_KERNEL)); if (error) { put_page(page); if (error == -EEXIST) { error = 0; goto find_page; } goto out; } goto readpage;

如果设置了同步方式进行读取,FUSE模块会无效内核的预读,转而进入到no_cached_page读取单页。所以同步模式下落到用户态文件系统的读IO有大块的readpagesIOreadpage4K单页IO,由于offset存在相同,goofys会判断为乱序的读取,超过3次后停止预读,由于每次和US3的交互都是4KGET请求,性能会比较差,难以满足用户的需求。


?分片上传的大小不固定,无法适配US3US3目前的分片大小固定为4M,而goofys的分片大小需要动态的去计算,并手动修改进行适配,代码如下:

func (fh *FileHandle) partSize() uint64 { var size uint64
if fh.lastPartId < 1000 { size = 5 * 1024 * 1024 } else if fh.lastPartId < 2000 { size = 25 * 1024 * 1024 } else { size = 125 * 1024 * 1024 }
...
}

同时,s3协议本身没有rename的的接口,s3fs和goofys的rename都是通过将源文件内容复制到目标文件,然后删除源文件实现的。


US3内部支持直接修改文件名,US3FS通过使用相关的接口实现rename操作,相比s3fs和goofys性能更好。同时s3fs和goofys挂载US3的bucket都需要走代理进行协议的转换,使用US3FS则减少了这一IO路径,性能上更有优势。


通过对s3fs和goofys的实践,我们发现两者在US3的备份场景上的性能有一些问题,同时适配的工作量也比较大,基于此,我们决定开发一款能够满足用户在数据备份场景需求的,依托对象存储作为后端的文件系统。


US3FS设计概述

US3FS通过FUSE实现部分POSIX API。在介绍US3FS实现之前,先简单介绍一下Linux的VFS机制和FUSE实现(有这部分基础的朋友可直接跳过)。


VFS

VFS,全称Virtual File System,是linux内核中一个承上启下的虚拟层,隶属于IO子系统。对上,为用户态应用提供了文件系统接口;对下,将具体的实现抽象为同一个函数指针供底层文件系统实现。


linux文件系统中的元数据分为dentry(directory entry)和inode,我们知道,文件名并不属于文件的元数据,为了优化查询,vfs在内存中建立dentry以缓存文件名和inode的映射以及目录树的实现。单机文件系统的实现,dentry只存在于内存中,不会落盘,当查找某个文件时内存没有对应的dentry,vfs会调用具体的文件系统实现来查找对应的文件,并建立起对应的数据结构。inode缓存了一个文件的元数据,如大小,修改时间等,会持久化到硬盘中,数据的读写通过地址空间找到对应的page和block device进行读写。


FUSE

FUSE,全称Filesystem in Userspace,用户态文件系统,我们知道,一般直接在内核态实现某个特性是比较痛苦的事情,通常内核的debug比较困难,而且稍不注意就会陷入到内核的各种细节而无法自拔。FUSE就是为了简化程序员的工作,将内核的细节隐藏起来,提供一套用户态的接口用于实现自己的文件系统,用户只需要实现对应的接口即可。内核态的FUSE模块和用户态的FUSE库的交互通过/dev/fuse进行通信,然后调用用户自己的实现。当然,缺点就是增加了IO路径以及内核态/用户态的切换,对性能有一定影响。

元数据设计

US3FS通过实现FUSE的接口,将US3中bucket的对象映射为文件,和分布式文件系统不同,没有mds(metadata server)维护文件元数据,需要通过HTTP向us3获取。当文件较多时,大量的请求会瞬间发出,性能很差。为了解决这一点,US3FS在内存中维护了bucket的目录树,并设置文件元数据的有效时间,避免频繁和US3交互。


这也带来了一致性的问题,当多个client修改同一bucket中的文件,其中的缓存一致性无法保证,需要用户自己取舍。为了提升检索的性能,文件并没有像对象存储以平铺的方式放在整个目录中,而是采用了传统文件系统类似的方式,为每一个目录构建相关数据结构来保存其中的文件,同时inode的设计也尽量简洁,只保存必要字段,减少内存的占用。


目前Inode中保存的字段有uid,gid,size,mtime等,通过US3的元数据功能在对象中持久化。例如下图所示,在US3的bucket中有一个名为"a/b/c/f1"的对象,在文件系统中,会将每一个“/"划分的前缀映射为目录,从而实现左边的目录树。

IO流程设计

对于数据的写入,US3支持大文件的分片上传。利用这一特性,US3FS通过将数据写入cache,在后台将数据以分片上传的方式,将数据以4MB的chunk写入到后端存储中。分片上传的流程如下图所示,通过令牌桶限制整个系统的写入并发数。每个分片写入的线程都会获取令牌后写入,通过当文件close时写入最后一个分片,完成整个上传流程。


文件的读取通过在US3FS的cache实现预读来提升性能。kernel-fuse自身对数据的读写进行了分片,在不修改内核的情况下,IO最大为128K。而大文件的读取场景一般为连续的大IO,这种场景下IO会被切成128K的片,不做预读的话,无法很好的利用网络带宽。US3FS的预读算法如下所示:

如图所示,第一次同步读取完成后,会往后进行当前长度的预读,并将预读的中点设置为下次触发预读的trigger。之后的读取如果不连续,则清空之前的状态,进行新的预读,如果连续,则判断当前读取的结束位置是否不小于触发预读的偏移,如果触发预读,则将预读窗口的大小扩大为2倍,直到达到设定的阈值。之后以新的窗口进行预读。如果未触发,则不进行预读。预读对顺序读的性能有很大提升。鉴于US3FS使用场景多为大文件的场景,US3FS本身不对数据进行任何缓存。在US3FS之上有内核的pagecache,当用户重复读取同一文件时pagecache能够很好的起作用。


数据一致性

由于对象存储的实现机制原因,当前大文件的写入,在完成所有的分片上传之前,数据是不可见的,所以对于US3FS的写入,在close之前,写入的数据都是不可读的,当close后,US3FS会发送结束分片的请求,结束整个写入流程,此时数据对用户可见。


对比测试

在并发度为64,IO大小为4M测试模型下,40G文件的顺序写和顺序读进行多次测试,平均结果如下:


测试过程中,goofys的内存占用比较高,峰值约3.3G,而US3FS比较平稳,峰值约305M,节省了90%内存空间。
s3fs表现相对较好,因为使用本地临时文件做缓存,所以内存占用比较少,但是写入文件比较大,硬盘空间不足时,性能会下降到表格中的数据。


在顺序读的测试中,测试结果可以验证我们的分析,goofys由于本身设计的原因,在这种场景下性能无法满足我们的要求。另外在测试移动1G文件的场景中,对比结果如下:


可见在移动需求场景下,特别是大文件居多的场景,通过US3FS能提升上百倍的性能。


总结

总而言之,s3fs和goofys在大文件的读写场景下各有优劣,相比之下,US3自研的 US3FS 无论是读还是写都有更好的性能,而且和US3的适配性更强,更易于拓展。

扫描二维码推送至手机访问。

版权声明:本文由ruisui88发布,如需转载请注明出处。

本文链接:http://www.ruisui88.com/post/1096.html

标签: nocache
分享给朋友:

“70%读写性能提升!基于UCloud对象存储US3的用户态文件系统设计” 的相关文章

gitlab常用命令大全

GitLab常用命令大全GitLab是一个基于Git的Web平台,它不仅提供代码托管,还集成了持续集成/持续交付(CI/CD)、代码审查、问题追踪等功能。在日常使用GitLab的过程中,我们常常需要使用一系列命令来管理代码仓库、处理分支和标签等。以下是GitLab常用的Git命令大全,并附上详细解释...

如何在GitLab上回退指定版本的代码?GitLab回退指定版本问题分析

在Git中,回退到指定版本并不是删除或撤销之前的提交,而是创建一个新的提交,该提交包含指定版本的内容。这意味着您需要将当前代码更改与指定版本之间的差异进行比较,并将其合并到一个新的提交中。如果您没有更新本地代码,并且您希望将 GitLab 仓库回退到指定版本,您可以使用以下命令:git fetchg...

代码分支规范

一.gitflow工作流说明:主分支:master,稳定版本代码分支,对外可以随时编译发布的分支,不允许直接Push代码,只能请求合并(pull request),且只接受hotfix、release分支的代码合并。gitlab上做限制。热修复分支:hotfix,针对现场紧急问题、bug修复的代码分...

高效使用 Vim 编辑器的 10 个技巧

在 Reverb,我们使用 MacVim 来标准化开发环境,使配对更容易,并提高效率。当我开始使用 Reverb 时,我以前从未使用过 Vim。我花了几个星期才开始感到舒服,但如果没有这样的提示,可能需要几个月的时间。这里有十个技巧可以帮助你在学习使用 Vim 时提高效率。1. 通过提高按键重复率来...

摄影后期必看 | PS插件camera raw 16.4教程 | 范围蒙版

范围蒙版Camera Raw 【蒙版】模块中提供了三个范围蒙版工具,可以通过特定的范围来创建蒙版。此次新增的【范围蒙版】大大加强了acr插件对局部调整的能力。点击下拉小箭头可以看到【颜色范围】,可用于快速选择想要编辑的颜色。快捷键:Shift + C【明亮度范围】,可用于快速选择想要调整的明亮度。快...

学前端,这30个CSS选择器,你必须熟记

你学会了基本的id,class类选择器和descendant后代选择器,然后就觉得完事了吗?如果这样,你就会错过许多灵活运用CSS的机会。虽然本文提到的许多选择器都属于CSS3,并且只能在现代的浏览器中使用,但学会这些是大有好处的。什么是CSS选择器呢?每一条css样式定义由两部分组成,形式如下:[...