Alluxio FUSE 实现原理

Posted Alluxio

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Alluxio FUSE 实现原理相关的知识,希望对你有一定的参考价值。

Alluxio FUSE服务通过提供Unix/Linux下的标准POSIX文件系统接口,让应用程序(比如Tensorflow,PyTorch等)在不修改代码的前提下,以访问本地文件系统的方式访问 Alluxio分布式文件系统中的数据。本文介绍Alluxio FUSE的实现原理,现状和未来发展方向。
在这里插入图片描述

FUSE(Filesystem in user space),顾名思义,是用户空间文件系统框架。FUSE允许在不重新编译操作系统内核的前提下,在用户态提供一个自定义的文件系统实现。大致来说,FUSE包含一个内核模块和一个用户空间 FUSE 服务进程,将应用对VFS的调用传递给这个用户空间FUSE服务程序来处理。

Alluxio FUSE就是一个实现了用户态文件系统的挂载操作(mount),以及VFS调用方法的具体实现。通过Alluxio FUSE,所有的VFS定义的调用方法实现,都会转化成与 Alluxio的通信,比如将Alluxio文件系统中的文件信息或文件内容返回,从而最终用户态应用程序可以通过普通的POSIX API读取Alluxio中的文件。

1、两种底层实现:JNI-FUSE vs JNR-FUSE

Alluxio FUSE最早是由来自IBM研究院的工程师基于一个第三方FUSE库JNR-FUSE 实现的。JNR-FUSE 是一个基于JAVA语言实现的一个 FUSE 服务程序的框架,因为其不用涉及本机代码(native),特别适合快速原型实现。但由于项目维护活跃度和性能等诸多因素,Alluxio社区决定基于JNI和JNR-FUSE重新实现了一个FUSE框架:JNI-FUSE。利用JNI-FUSE同样可以使用JAVA语言,快速实现一个FUSE服务程序,同时获得更好的性能。

1.1 JNI-FUSE 与 JNR-FUSE 对比

那么,JNI-FUSE 与 JNR-FUSE 存在着哪些差异呢? 下表做出了详细的对比。
在这里插入图片描述

1.2 JNI-FUSE 性能更好的原因

首先,JNI比JNR性能要好。看起来,JNR使得开发者不再需要编写JNI的胶水代码,但是JNR-FUSE需要引入非常多的依赖,使得整体调用流程变得冗长臃肿。
在这里插入图片描述如下图所示,JNR的组件众多,JNR-FUSE的代码量也要比JNI-FUSE多出若干倍,项目架构复杂,并且仅仅是一个个人项目,因此代码中存在的问题,也难以及时得到修复。JNR-FUSE,对回调函数支持不是很好,比如每次native线程调用JVM的时候,都会先 attach到JVM,这个过程开销会比较昂贵。
在这里插入图片描述

2、FUSE 选项

Alluxio 后续可能会逐渐去掉JNR-FUSE,专注维护JNI-FUSE。此外,通过调整以下 FUSE配置项,可以优化性能。
在这里插入图片描述

3、 Alluxio FUSE 的主要流程

Alluxio FUSE是基于JNR-FUSE或JNI-FUSE框架实现的FUSE服务程序。Alluxio FUSE首先需要主动通过JNR-FUSE或JNI-FUSE调用libfuse的fuse_main_real方法,注册挂载点和每个fuse operation回调函数,然后Alluxio FUSE会打开/dev/fuse,返回文件描述符fd,等待读取来自FUSE设备的请求。fd在进程内共享,在进程间不共享,因此FUSE支持multi-thread模式,也支持多个FUSE服务程序,创建多个相互隔离的挂载点。

在这里插入图片描述当一个应用程序,比如mkdir调用 syc_mkdir 想要在FUSE挂载点之下创建文件夹时,内核态VFS发现这个挂载点对应的是FUSE类型,则构造一个FUSE_MKDIR的 fuse_req,投递到pending队列里。

libfuse启动的读取到fuse设备的请求后,会执行fuse_lib_mkdir操作,这个操作会回调到mount时给定的回调函数里,再由JNI-FUSE转而回调到AlluxioJNIFuseFilesystem中的mkdir方法,最后,mkdir方法体中会通知Alluxio的master创建文件夹,当完成操作后,会回复ok给fuse设备,这样fuse_mkdir则会返回给调用者,文件夹创建成功。

4、JNI-FUSE 的实现原理

Alluxio Fuse启动以后,会经历下图中的流程。
在这里插入图片描述首先,AlluxioFuse会通过调用native函数fuse_main_real, 进行文件系统挂载。Native 代码会调用libfuse中的fuse_main_real方法,向kernel注册挂载点以及回调函数指针。

此时,如果一个mkdir操作发生在 这个FUSE挂载点,则AlluxioFuse会收到这个通知,相应的挂载函数,jnifuse_oper_mkdir指向的mkdir_wrapper会被调用。最后这个mkdir 请求会反向调用JAVA代码中相应的FS实现函数mkdir,执行真正的实现逻辑。对于 Alluxio,就是向Alluxio master发送mkdir rpc请求。

JNI-FUSE需要把libfuse或kernel定义的数据结构返回给Java程序,这个过程,是通过DirectByteBuffer进行Native和Java之间的数据通信。
在这里插入图片描述如上图所示,当Java程序需要获取fuse_context时,会通过调用native方法fuse_get_context,将请求发送给native实现代码,native代码会调用libfuse中实现的函数 fuse_get_context,返回一个struct fuse_context类型的结构体函数指针。通过创建一个指向这块地址的DirectByteBuffer并返回给Java端的调用者,完成了Native端的使命。Java端利用JNI-FUSE对DirectByteBuffer的解析并构造出FuseContext。

5、Alluxio FUSE 多线程并发安全

FUSE服务程序在启动时,会维护一个默认大小为10 (libfuse3.x 可以设置选项 max_idle_threads 指定)的线程池,处理来自FUSE设备的请求,转而调用用户注册的回调函数执行相应的操作。因为FUSE可能会打开一个文件,使用多个线程,并发读取文件的不同部分,因此,Alluxio FUSE的read过程需要考虑并发安全。
在这里插入图片描述当Alluxio FUSE接收到一个open请求时,会打开对应的Alluxio FS中的文件,构造输入流,并且为当前调用者返回一个唯一的文件ID,当多个线程用同一个文件ID对文件进行读取时,实际上在使用同一个输入流进行读取的,但是输入流对象本身不是线程安全的,是有状态的,比如输入流的当前pos,如果一个线程正在读,另一个线程执行了seek或reset操作修改了pos,则会出现数据错误,数据流关闭等严重错误。

Alluxio采用的办法,是根据当前读请求提供的文件ID,找到对应的输入流,利用输入流对象作为monitor,确保对于同一个输入流对象,不会同时被多个读线程读取数据,实现了线程安全控制。

6、Alluxio FUSE 缓存

Alluxio FUSE 为了避免每个请求都执行 shell 命令获取用户 id 和组 id,利用 Guava 提供的 LoadingCache,实现了 groupId 和 userId 的缓存,目前缓存的数量是 100 个。

此外,Alluxio 还缓存了高频请求 path 到 AlluxioURI 对象的转换,因为每一个对 FUSE 挂载路径的请求都需要把路径转换成 Alluxio 路径。目前默认最大缓存 500个目录映射。

7、限制

Alluxio FUSE 目前由于 Alluxio 的设计限制,无法支持随机写。

8、未来工作

8.1 JNI-FUSE独立模块

JNI-FUSE功能,包含C代码,因此需要为Alluxio仓库引入C代码编译,但不是每一个 Alluxio开发者都需要编译JNI-FUSE,因此,把JNI-FUSE独立为一个单独的模块,不需要或不具备C代码编译环境的 Alluxio 开发者则不需要编译JNI-FUSE模块的C代码。

同时,JNI-FUSE模块的输出文件jar内包含着linux和macosx的libjnifuse共享库,使用 JNI-FUSE 时,无需指定 java.library.path 参数,对 libjnifuse 文件透明。

更重要的,有了 JNI-FUSE 模块,其它模块或项目也可以使用 JNI-FUSE,可以为JNI-FUSE提供更多应用场景。目前apache ozone在使用的fuse程序hcfsfuse,就在使用 JNI-FUSE。
Alluxio Github Issue 12852

8.2 Alluxio FUSE & Alluxio Worker整合

在机器学习场景下,很多情况 Alluxio FUSE 和 Alluxio Worker 是部署在一起的,但 Alluxio FUSE 到 Alluxio Worker 的本地RPC请求可能性能瓶颈,因此,Alluxio 提供了在 Alluxio Worker 进程内部启动 Alluxio FUSE,这样 Alluxio FUSE 就可以直接在 Worker 进程内操作 Block,减少 GRPC 的传输损耗。

Alluxio Github Issue 12830

8.3 JNI-FUSE 支持 libfuse3.x

libfuse 3.2以上,提供了MAX_IDLE_THREADS参数。Alluxio目前还不支持libfuse 3.x,是因为libfuse 3.x和libfuse 2.x里有部分数据结构不一样。MAX_IDLE_THREADS默认大小是10,在libfuse 2.x是无法指定的。在高并发场景,调大MAX_IDLE_THREADS可以有效降低fuse worker线程的频繁创建销毁,从而可以显著提升性能。

下表,是在1个节点,8个进程,8个线程的读测试,对比max_idle_threads为10和80两种情况下FUSE 创建线程(worker)数量。
在这里插入图片描述
当Alluxio FUSE接收到一个open请求时,会打开对应的Alluxio FS中的文件,构造输入流,并且为当前调用者返回一个唯一的文件ID,当多个线程用同一个文件ID对文件进行读取时,实际上在使用同一个输入流进行读取的,但是输入流对象本身不是线程安全的,是有状态的,比如输入流的当前pos,如果一个线程正在读,另一个线程执行了seek或reset操作修改了pos,则会出现数据错误,数据流关闭等严重错误。

Alluxio采用的办法,是根据当前读请求提供的文件ID,找到对应的输入流,利用输入流对象作为monitor,确保对于同一个输入流对象,不会同时被多个读线程读取数据,实现了线程安全控制。

6、Alluxio FUSE 缓存

Alluxio FUSE 为了避免每个请求都执行 shell 命令获取用户 id 和组 id,利用 Guava 提供的 LoadingCache,实现了 groupId 和 userId 的缓存,目前缓存的数量是 100 个。

此外,Alluxio 还缓存了高频请求 path 到 AlluxioURI 对象的转换,因为每一个对 FUSE 挂载路径的请求都需要把路径转换成 Alluxio 路径。目前默认最大缓存 500个目录映射。

7、限制

Alluxio FUSE 目前由于 Alluxio 的设计限制,无法支持随机写。

8、未来工作

8.1 JNI-FUSE独立模块

JNI-FUSE功能,包含C代码,因此需要为Alluxio仓库引入C代码编译,但不是每一个 Alluxio开发者都需要编译JNI-FUSE,因此,把JNI-FUSE独立为一个单独的模块,不需要或不具备C代码编译环境的 Alluxio 开发者则不需要编译JNI-FUSE模块的C代码。

同时,JNI-FUSE模块的输出文件jar内包含着linux和macosx的libjnifuse共享库,使用 JNI-FUSE 时,无需指定 java.library.path 参数,对 libjnifuse 文件透明。

更重要的,有了 JNI-FUSE 模块,其它模块或项目也可以使用 JNI-FUSE,可以为JNI-FUSE提供更多应用场景。目前apache ozone在使用的fuse程序hcfsfuse,就在使用 JNI-FUSE。
Alluxio Github Issue 12852

8.2 Alluxio FUSE & Alluxio Worker整合

在机器学习场景下,很多情况 Alluxio FUSE 和 Alluxio Worker 是部署在一起的,但 Alluxio FUSE 到 Alluxio Worker 的本地RPC请求可能性能瓶颈,因此,Alluxio 提供了在 Alluxio Worker 进程内部启动 Alluxio FUSE,这样 Alluxio FUSE 就可以直接在 Worker 进程内操作 Block,减少 GRPC 的传输损耗。

Alluxio Github Issue 12830

8.3 JNI-FUSE 支持 libfuse3.x

libfuse 3.2以上,提供了MAX_IDLE_THREADS参数。Alluxio目前还不支持libfuse 3.x,是因为libfuse 3.x和libfuse 2.x里有部分数据结构不一样。MAX_IDLE_THREADS默认大小是10,在libfuse 2.x是无法指定的。在高并发场景,调大MAX_IDLE_THREADS可以有效降低fuse worker线程的频繁创建销毁,从而可以显著提升性能。
下表,是在1个节点,8个进程,8个线程的读测试,对比max_idle_threads为10和80两种情况下FUSE 创建线程(worker)数量。
在这里插入图片描述
可以得出结论,增加空闲worker数量能够显著减少线程的频繁创建,这也是增大max_idle_threads 能够优化性能的原因。

Alluxio Github Issue 12758

9、Alluxio FUSE 的评测

如下测试结果是在1台机器上,分别调整测试程序的进程数和线程数,达到增大并发的目的。在同一个并发数下,分别测试AlluxioFUSE,AlluxioJniFUSE,JNR-FUSE,JNI-FUSE,FUSE的读性能。
其中,AlluxioFUSE和AllluxioJniFuse是测试数据在Alluxio分布式系统中的真实的全链路测试,而JNR-FUSE和JNI-FUSE的测试,是使用一个测试目的的单机内存文件系统,替代 Alluxio,只为评估FUSE框架的能力,最后一个FUSE测试,是测试程序直接使用 libfuse,代表着理论上限。图中单位为 MiB/s 。
在这里插入图片描述下面的折线图可以更直观的对比出JNI-FUSE比JNI-FUSE有更好的性能,AlluxioJniFuse比AlluxioFUSE性能更优,尤其在并发数增大后,差距更为明显。
在这里插入图片描述此外,利用 hcfsfuse, FIO, fstest, vdbench, FileBench, LTP, iozone 以及 Alluxio FUSE 提供的 stackFS 都可以进行 Alluxio FUSE 的功能、性能评测。

在使用Alluxio FUSE是,当发现Alluxio FUSE性能没有达到预期,或者运行失败报错,需要判断问题出现在通信链路的哪一段,利用hcfsfuse可以把 FUSE 服务程序的远端存储,由Alluxio更换为localfs(本地文件系统),如果更换为localfs性能变化很大,或者问题不再出现,可以判断问题出现在FUSE服务程序与远端存储通信的链路,反之,问题与后端存储无关。利用StackFS也可以纯测试Alluxio FUSE的服务能力,因为StackFS不与后端存储 Alluxio 通信。

鸣谢

感谢Alluxio范斌和邱璐,阿里巴巴车漾在Alluxio FUSE的贡献,以及本文章测试数据的贡献。

引用

  • https://docs.alluxio.io/os/user/edge/en/api/POSIX-API.html#configure-mount-point-options
  • https://github.com/opendataio/hcfsfuse
  • https://developer.aliyun.com/article/765736
  • https://www.youtube.com/watch?v=DMkxu_v-9vA
  • https://www.oracle.com/technetwork/java/jvmls2013nutter-2013526.pdf
  • https://sudonull.com/post/97408-Writing-a-Wrapper-for-FUSE-in-the-Java-Native-Runtime
  • https://segmentfault.com/a/1190000016793722

以上是关于Alluxio FUSE 实现原理的主要内容,如果未能解决你的问题,请参考以下文章

Rust 中“fuse”背后的词源或软件原理是啥?

2min速览:从设计实现和优化角度浅谈Alluxio元数据同步

2min速览:从设计实现和优化角度浅谈Alluxio元数据同步

FUSE文件系统介绍

FUSE文件系统介绍

Presto on Alluxio By Alluxio SDS 单节点搭建