1.基本组件介绍
JuiceFS Client:支持多种Client端的接口,比如兼容POSIX文件系统的接口,以此你可以将它挂载到系统上当文件系统使用,且可以为k8s提供存储使用,用ks8s的csi driver进行接入。同时也支持S3协议,开发了对应的S3网关进行支持;
Data Storage:对象存储服务,用以存储具体数据的,可以类比文件系统里的block数据保存,支持多种后端存储;
Metadata Engine:元数据服务,用以存储文件元数据信息的,比如文件名、目录信息、文件inode等信息,可以类比文件系统里的inode数据管理,支持多种元数据存储;
2.快速部署
我们使用docker部署一个minio作为对象存储服务,用docker部署一个redis作为元数据服务。
git下载juicefs代码并进行编译生成juicefs二进制可执行文件:
git clone https://github.com/juicedata/juicefs.git
cd juicefs && make
安装docker和下载redis和minio/minio镜像。
部署元数据服务和对象存储服务:
sudo docker run -d --name redis -v /data/redis-data:/data -p 6379:6379 --restart unless-stopped redis redis-server --appendonly yes
sudo docker run -d --name minio -v /data/minio-data:/data -p 9000:9000 --restart unless-stopped minio/minio server /data
进行format和挂载:
mkdir /data/ussfs
./juicefs format --storage minio --bucket http://127.0.0.1:9000/test123 --access-key minioadmin --secret-key minioadmin redis://127.0.0.1:6379/10 test123
./juicefs mount -d redis://127.0.0.1:6379/10 /data/ussfs
3.挂载过程分析
format过程:
format的作用是将一些元数据信息先注册好,在mount时进行获取作为配置参数,比如对象存储的相关信息,bucket的相关信息等。
流程说明:
(1)接收客户端执行的命令,如果是mount的命令,则进入mount逻辑;
(2)通过format给定的url来判断是哪种元数据服务,并初始化元数据服务对象;
(3)构建format结构体对象,结构体里包含对象存储服务信息,block size等信息;
(4)根据format的信息初始化对象存储服务并测试对象存储服务是否可增删改操作,确保对象存储服务可用;
(5)持久化format信息,在redis为元数据服务时,即是将格式化后的format数据保存到redis的setting这个key中;
(6)创建inode号为1的第一个文件,文件类型为目录,作为后续创建文件的父目录,并持久化到元数据中。
mount过程:
mount就是将自定义的文件系统挂载到指定目录下,可供符合POSIX的接口进行调用,由mount中开启的server服务进行文件操作请求接收并处理。
流程说明:
(1)获取mount中的命令行参数,获取到元数据信息url;
(2)创建元数据服务连接实例,从元数据服务中获取之前保存的format信息;
(3)根据format信息创建对象存储服务连接实例storage,创建store对象,store对象是对对象存储数据进行管理,store对象属性里包含了storage对象和对cache的管理;
(4)初始化vfs对象,vfs是一层虚拟文件系统对象,它包含了对meta和storage的管理,创建了读写对象和文件句柄管理;
(5)如果命令行参数中是有用-d指定了mount进程后台运行,则调用makeDaemon函数将fork出一个进程作为daemon进程后台运行;
(6)通过读取挂载目录文件属性来检查挂载目录是否可进行挂载;
(7)创建本次挂载session,生成session信息保存到元数据服务中;
(8)创建自定义的文件系统类型,通过用户态fuse(用户空间实现文件系统)库来进行实现,启动服务来接收fuse的请求信息并封装成request,解析request找到对应的处理函数进行处理并返回。
启动的server接收fuse请求大致流程图:
4.元数据保存key含义解析
主要介绍在redis为元数据服务时,保存的各个key的含义,方便下面讲解读写流程。
这里我们先列出redis中保存的所有key:
可以看到,大概分成好几种key,有i开头的,有d开头的,有c开头的,还有其它的一些固定字符串的key。
setting:保存的format信息的key,对应的结构体:
type Format struct { Name string UUID string Storage string Bucket string AccessKey string SecretKey string `json:",omitempty"` BlockSize int Compression string Shards int Partitions int Capacity uint64 Inodes uint64 EncryptKey string `json:",omitempty"` }
i1:表示记录的inode号为1的文件的属性信息,同理i2为inode号为2的文件属性key,保存的值对应的结构体为:
// Attr represents attributes of a node. type Attr struct { Flags uint8 // reserved flags Typ uint8 // type of a node Mode uint16 // permission mode Uid uint32 // owner id Gid uint32 // group id of owner Atime int64 // last access time Mtime int64 // last modified time Ctime int64 // last change time for meta Atimensec uint32 // nanosecond part of atime Mtimensec uint32 // nanosecond part of mtime Ctimensec uint32 // nanosecond part of ctime Nlink uint32 // number of links (sub-directories or hardlinks) Length uint64 // length of regular file Rdev uint32 // device number Parent Ino // inode of parent, only for Directory Full bool // the attributes are completed or not KeepCache bool // whether to keep the cached page or not }
d1:当文件为目录类型时,就会对应一个d开头的key,1表示该文件夹是inode号为1的,它是一个hash类型的键,它的每个field key是文件名字,field key对应的值是文件类型和对应的inode号,field key的值保存着文件类型和文件inode号。这样的话就可以方便的找到某个目录下的某个文件名的inode号及该文件的类型,同理d3就是inode号为3的文件夹记录着它文件夹下的文件元信息。
c14_0:c表示chunk的意思,我们先来看下这个数据结构图
可以看到一个文件数据会被拆分成chunk来进行保存的,如果是大文件,那么会被按指定大小拆分成多个chunk,这个c14的14就表示这个chunk是归属于inode14这个文件的,c14_0的0则表示是第一个chunk,该key是列表类型的键,它的每个元素是一个slice,slice不是固定大小的,slice会根据block size大小拆分成多个block进行存储,比如block size是4M,如果slice是6M,则拆分成2个block来进行存储,计算出对应的两个key把键值往对象存储服务中存储。比如生成35_0_16384和35_1_8192,35是表示这个slice的id(Slice结构体中的Chunkid字段),0和1表示两个block,16384和8192分别表示block的大小,分别为4M和2M。
chunk的结构体:是slices列表的数据结构,[]meta.Slice
Slice结构体:
// Slice is a slice of a chunk. // Multiple slices could be combined together as a chunk. type Slice struct { Chunkid uint64 Size uint32 Off uint32 Len uint32 }
nextinode:它是一个自增的key,作用是分配新的inode;
nextchunk:它也是自增的,用来分配给slice的chunkid的;
nextsession:它也是自增的,用来给session结构体分配session id的。
totalInodes和usedSpace是记录当前一共有多少个inode和当前空间已使用量的。
5.读写文件过程分析
5.1.创建文件夹
创建文件夹命令示例:mkdir /data/ussfs/testdir
流程图:
流程说明:
(1)先获取根文件属性信息,然后通过文件路径和文件名获取指定文件属性,获取的方式就是一层层获取,比如/a/b/c,先是在a目录下找b,然后在b目录下找c,这是通过调用doLookup函数来查询获取;
(2)如果要创建的文件夹不存在则进行创建,创建调用doMkdir函数进行创建,该函数又调用mknode函数来创建文件信息(文件夹也是一个文件);
(3)从元数据服务器中获取一个新的inode号,如果是redis元数据服务,则是由一个自增的nextinode来保存当前分配的inode号;
(4)设置新文件的文件属性信息,比如权限、创建时间等信息;
(5)向父目录中添加该新文件,当redis为元数据服务时,则是向d开头的key里比如d1里添加一条该新文件信息;
(6)更新父目录文件属性信息、新文件属性信息和文件系统的一些总体使用信息。
创建文件跟创建文件夹类似,先调用的doCreate,然后也会调用mknode来生成inode和保存文件属性信息。
5.2.写入数据到文件
写入文件命令示例:echo "123456789" > /data/ussfs/testdir/testfile
流程图:
流程说明:
(1)跟先前类似,先通过文件路径找到该文件,获取该文件属性信息,如果文件不存在先创建文件;
(2)调用doopen函数打开文件,主要是初始化文件handle,创建文件读写对象,返回文件描述符;
(3)调用dowrite,传入偏移位置和写入数据,通过偏移位置计算出要写入到第几个chunk中去,如果是跨chunk(一个chunk默认64M),则先写入一部分数据到一个chunk,然后再写剩下的数据到下一个chunk;
(4)在一个chunk中查找合适的slice进行写入,比如改变的数据是在中间部分的,那其实只要更新那一个slice数据即可,其它slice可以不变更,我们目前这场景是找不到一个合适的slice,它会创建一个slice,然后通过该slice进行数据上传;
(5)通过偏移量进行block的计算,每个block会生成对应的key,然后调用对象存储的put方法进行key value的上传来存储数据;
(6)保存slice元信息到元数据服务中。
5.3.读取文件数据
读取文件命令示例:cat /data/ussfs/testdir/testfile
流程图:
流程说明:
(1)跟先前类似,先通过文件路径找到该文件,获取该文件属性信息,如果文件不存在先创建文件;
(2)调用doopen函数打开文件,主要是初始化文件handle,创建文件读写对象,返回文件描述符;
(3)分配存储数据的page数据结构,从元数据服务获取所有slice列表;
(4)遍历每个slice,取出slice对应的所有block信息保存到page对象中;
(5)如果block有缓存则直接从缓冲中获取,否则从对象存储中重新获取,并进行缓存。