JuiceFS 是一款面向云原生设计的高性能分布式文件系统,在 Apache 2.0 开源协议下发布。提供完备的 POSIX 兼容性,可将几乎所有对象存储接入本地作为海量本地磁盘使用,亦可同时在跨平台、跨地区的不同主机上挂载读写。
JuiceFS 采用「数据」与「元数据」分离存储的架构,从而实现文件系统的分布式设计。文件数据本身会被切分保存在对象存储(例如 Amazon S3),而元数据则可以保存在 Redis、MySQL、TiKV、SQLite 等多种数据库中,你可以根据场景与性能要求进行选择。
大纲
一.JuiceFS架构
二.I/O存储栈
三.JuiceFS原理
四.go-fuse源码解析
一.JuiceFS架构
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。
如图,JuiceFS使用了一个叫fuse的文件系统,数据由程序通过系统调用传到VFS,接着传到文件系统fuse上,接着会被传到JuiceFS客户端上,由JuiceFS客户端将数据传给远程的对象存储上面.
(摘自:奇伢云存储公众号)
该图表达的意思有以下几个:
- 背景:一个用户态文件系统,挂载点为
/tmp/fuse,用户二进制程序文件为./hello; - 当执行
ls -l /tmp/fuse命令的时候,流程如下: -
- IO 请求先进内核,经 vfs 传递给内核 FUSE 文件系统模块;
- 内核 FUSE 模块把请求发给到用户态,由
./hello程序接收并且处理。处理完成之后,响应原路返回;
三.JuiceFS客户端原理
负责实现fuse接口,将数据写到对象存储,元数据写到存储引擎。以下是客户端的模块:
- fuse模块:负责和go-fuse对接,同时调用vfs模块接口
- vfs模块:负责整个posix语义实现,数据操作会进行chunk粒度的拆分,调用chunk模块接口;元数据操作调用meta模块接口
- chunk模块:负责数据上传下载,同时通过object模块适配不同的厂商。
- meta模块:负责和redis交互。
任何存入 JuiceFS 的文件都会被拆分成一个或多个 「Chunk」(最大 64 MiB)。而每个 Chunk 又由一个或多个 「Slice」 组成。Chunk 的存在是为了对文件做切分,优化大文件性能,而 Slice 则是为了进一步优化各类文件写操作,二者同为文件系统内部的逻辑概念。Slice 的长度不固定,取决于文件写入的方式。每个 Slice 又会被进一步拆分成 「Block」(默认大小上限为 4 MiB),成为最终上传至对象存储的最小存储单元。
四.go-fuse源码解析
在juicefs客户端的代码中可以看到,需要构建一个systemFile,并现实以下接口:
构建一个systemFile后,就可以提供给fuse,由fuse库去做一些底层的工作:
fuse 文件系统把这个 io 请求封装起来,打包成特定的格式,通过 /dev/fuse 这个管道传递到用户态
fuse有个loop函数,一直从pool获取出请求req:
在初始化的时候,就会将所有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里面获取到对应的执行函数,然后去执行:
总结
常见的NFS,CIFS等网络文件系统会有以下缺点:
- 客户端与存储端交互太多,特别是存在多级目录的情况下
- 一次数据访问需要多次访问磁盘
- 存储端无法通过横向扩展的方式来提升性能和容量
而Juicefs分布式文件存储,底层使用了对象存储,并且将元数据另外放到存储引擎中,这样会有以下优点:
-
交互次数太多主要是协议造成,打开文件时,需要确定父目录和每个祖先目录的存在性.在这种情况下就需要多次向存储系统发送GETATTR命令.Juicefs元数据访问不采用本地文件系统,而是将元数据全部缓存到内存中,提高文件搜索速度.
-
对象存储采用S3协议,将多次访问减少为一次.
-
将横向拓展的问题扔给对象存储,恰恰对象存储相对文件存储更容易实现横向扩展.
粗略的讲了一下,一个程序是如何通过Juicefs去写数据到fuse的,希望对大家有用.