高性能分布式文件系统JuiceFS解析(1)

1,231 阅读7分钟

JuiceFS 是一款面向云原生设计的高性能分布式文件系统,在 Apache 2.0 开源协议下发布。提供完备的 POSIX 兼容性,可将几乎所有对象存储接入本地作为海量本地磁盘使用,亦可同时在跨平台、跨地区的不同主机上挂载读写。

JuiceFS 采用「数据」与「元数据」分离存储的架构,从而实现文件系统的分布式设计。文件数据本身会被切分保存在对象存储(例如 Amazon S3),而元数据则可以保存在 Redis、MySQL、TiKV、SQLite 等多种数据库中,你可以根据场景与性能要求进行选择。

 大纲

一.JuiceFS架构

二.I/O存储栈

三.JuiceFS原理

四.go-fuse源码解析

一.JuiceFS架构

640.png JuiceFS 由三个部分组成

  • JuiceFS 客户端
  • 数据存储
  • 元数据引擎

JuiceFS 客户端(Client) :所有文件读写,乃至于碎片合并、回收站文件过期删除等后台任务,均在客户端中发生。可想而知,客户端需要同时与对象存储和元数据引擎打交道。客户端支持众多接入方式:

  • 通过 FUSE,JuiceFS 文件系统能够以 POSIX 兼容的方式挂载到服务器,将海量云端存储直接当做本地存储来使用。
  • 通过 Hadoop Java SDK,JuiceFS 文件系统能够直接替代 HDFS,为 Hadoop 提供低成本的海量存储。
  • 通过 Kubernetes CSI 驱动,JuiceFS 文件系统能够直接为 Kubernetes 提供海量存储。
  • 通过 S3 网关,使用 S3 作为存储层的应用可直接接入,同时可使用 AWS CLI、s3cmd、MinIO client 等工具访问 JuiceFS 文件系统。
  • 通过 WebDAV 服务,以 HTTP 协议,以类似 RESTful API 的方式接入 JuiceFS 并直接操作其中的文件。

数据存储(Data Storage) :文件将会切分上传保存在对象存储服务,既可以使用公有云的对象存储,也可以接入私有部署的自建对象存储。JuiceFS 支持几乎所有的公有云对象存储,同时也支持 OpenStack Swift、Ceph、MinIO 等私有化的对象存储。

元数据引擎(Metadata Engine) :用于存储文件元数据(metadata),包含以下内容:

  • 常规文件系统的元数据:文件名、文件大小、权限信息、创建修改时间、目录结构、文件属性、符号链接、文件锁等。
  • JuiceFS 独有的元数据:文件的 chunk 及 slice 映射关系、客户端 session 等。

所以JuiceFS的主体就是客户端,也是后面解析的主要内容.

二.I/O存储栈

当我们程序想将一段数据存储下来,我们需要有个存储路径,将数据可以存储到指定路径/data/path上.

以golang代码为例:

file, err := os.Create("/data/path") 
if err != nil { 
    fmt.Println(err) return 
} 
defer file.Close() 

data := []byte("{要写入的数据}") 
file.Write(data)

存储路径如何来的呢?

通过mount操作.将文件系统跟存储路径关联上,而底层不同的文件系统有各种不同的操作,可以是存在内存,存在磁盘,或者如JuiceFS的fuse将数据转到另一个程序(JuiceFS客户端)上面.

当程序执行了上面代码后,其实会触发底层的系统调用程序,系统调用会将数据传给VFS.

虚拟文件系统(Virtual File System,VFS)是一种允许多种文件系统的文件抽象层。它可以被视为一个中间层,将应用程序的文件操作请求路由到特定的文件系统。VFS允许应用程序使用相同的API来访问多种文件系统,而不必知道底层文件系统的详细信息。

在Linux中,VFS是Linux内核的一部分,并且它是Linux系统中所有文件访问的基础。它提供了与文件系统相关的系统调用,例如open()、read()和write(),并定义了文件系统对象的抽象概念,例如struct file和struct inode。

Pasted Graphic 2.png

如图,JuiceFS使用了一个叫fuse的文件系统,数据由程序通过系统调用传到VFS,接着传到文件系统fuse上,接着会被传到JuiceFS客户端上,由JuiceFS客户端将数据传给远程的对象存储上面.

640.png

(摘自:奇伢云存储公众号)

该图表达的意思有以下几个:

  1. 背景:一个用户态文件系统,挂载点为 /tmp/fuse ,用户二进制程序文件为 ./hello ;
  2. 当执行 ls -l /tmp/fuse 命令的时候,流程如下:
    • IO 请求先进内核,经 vfs 传递给内核 FUSE 文件系统模块;
    • 内核 FUSE 模块把请求发给到用户态,由 ./hello 程序接收并且处理。处理完成之后,响应原路返回;

三.JuiceFS客户端原理

Page Cache.png

负责实现fuse接口,将数据写到对象存储,元数据写到存储引擎。以下是客户端的模块:

  • fuse模块:负责和go-fuse对接,同时调用vfs模块接口
  • vfs模块:负责整个posix语义实现,数据操作会进行chunk粒度的拆分,调用chunk模块接口;元数据操作调用meta模块接口
  • chunk模块:负责数据上传下载,同时通过object模块适配不同的厂商。
  • meta模块:负责和redis交互。

juicefs-storage-format-new-fb61716d2baaf23335573504aa5f3bc7.png 任何存入 JuiceFS 的文件都会被拆分成一个或多个 「Chunk」(最大 64 MiB)。而每个 Chunk 又由一个或多个 「Slice」 组成。Chunk 的存在是为了对文件做切分,优化大文件性能,而 Slice 则是为了进一步优化各类文件写操作,二者同为文件系统内部的逻辑概念。Slice 的长度不固定,取决于文件写入的方式。每个 Slice 又会被进一步拆分成 「Block」(默认大小上限为 4 MiB),成为最终上传至对象存储的最小存储单元。

internals-write-f8d47b907ab19996af7ddf95f81025e9.png

四.go-fuse源码解析

在juicefs客户端的代码中可以看到,需要构建一个systemFile,并现实以下接口: Pasted Graphic 3.png

构建一个systemFile后,就可以提供给fuse,由fuse库去做一些底层的工作: Pasted Graphic 4.png

fuse 文件系统把这个 io 请求封装起来,打包成特定的格式,通过 /dev/fuse 这个管道传递到用户态

fuse有个loop函数,一直从pool获取出请求req: Pasted Graphic 5.png

在初始化的时候,就会将所有posix操作都存到一个map里面:

for op, v := range map[uint32]operationFunc{
   _OP_OPEN:            doOpen,
   _OP_READDIR:         doReadDir,
   _OP_WRITE:           doWrite,
   _OP_OPENDIR:         doOpenDir,
   _OP_CREATE:          doCreate,
   _OP_SETATTR:         doSetattr,
   _OP_GETXATTR:        doGetXAttr,
   _OP_LISTXATTR:       doGetXAttr,
   _OP_GETATTR:         doGetAttr,
   _OP_FORGET:          doForget,
   _OP_BATCH_FORGET:    doBatchForget,
   _OP_READLINK:        doReadlink,
   _OP_INIT:            doInit,
   _OP_LOOKUP:          doLookup,
   _OP_MKNOD:           doMknod,
   _OP_MKDIR:           doMkdir,
   _OP_UNLINK:          doUnlink,
   _OP_RMDIR:           doRmdir,
   _OP_LINK:            doLink,
   _OP_READ:            doRead,
   _OP_FLUSH:           doFlush,
   _OP_RELEASE:         doRelease,
   _OP_FSYNC:           doFsync,
   _OP_RELEASEDIR:      doReleaseDir,
   _OP_FSYNCDIR:        doFsyncDir,
   _OP_SETXATTR:        doSetXAttr,
   _OP_REMOVEXATTR:     doRemoveXAttr,
   _OP_GETLK:           doGetLk,
   _OP_SETLK:           doSetLk,
   _OP_SETLKW:          doSetLkw,
   _OP_ACCESS:          doAccess,
   _OP_SYMLINK:         doSymlink,
   _OP_RENAME:          doRename,
   _OP_STATFS:          doStatFs,
   _OP_IOCTL:           doIoctl,
   _OP_DESTROY:         doDestroy,
   _OP_NOTIFY_REPLY:    doNotifyReply,
   _OP_FALLOCATE:       doFallocate,
   _OP_READDIRPLUS:     doReadDirPlus,
   _OP_RENAME2:         doRename2,
   _OP_INTERRUPT:       doInterrupt,
   _OP_COPY_FILE_RANGE: doCopyFileRange,
   _OP_LSEEK:           doLseek,
} {
   operationHandlers[op].Func = v
}

上面获取的req解析以后,就可以通过获取到的key,从map里面获取到对应的执行函数,然后去执行: Pasted Graphic 6.png

总结

常见的NFS,CIFS等网络文件系统会有以下缺点:

  1. 客户端与存储端交互太多,特别是存在多级目录的情况下
  2. 一次数据访问需要多次访问磁盘
  3. 存储端无法通过横向扩展的方式来提升性能和容量

而Juicefs分布式文件存储,底层使用了对象存储,并且将元数据另外放到存储引擎中,这样会有以下优点:

  • 交互次数太多主要是协议造成,打开文件时,需要确定父目录和每个祖先目录的存在性.在这种情况下就需要多次向存储系统发送GETATTR命令.Juicefs元数据访问不采用本地文件系统,而是将元数据全部缓存到内存中,提高文件搜索速度.

  • 对象存储采用S3协议,将多次访问减少为一次.

  • 将横向拓展的问题扔给对象存储,恰恰对象存储相对文件存储更容易实现横向扩展.

粗略的讲了一下,一个程序是如何通过Juicefs去写数据到fuse的,希望对大家有用.