juicefs使用及关键流程源码分析




新的调研细节可参考:

juicefs调研


使用

开源版本

还可以配置本地读写缓存,这里就不细说,参考链接:https://github.com/juicedata/juicefs/blob/main/docs/en/cache_management.md

本地缓存会加速读写性能,但会导致数据的不一致。

官方商业版本

  1. 在官网上注册账号进入控制台:https://juicefs.com/console/
  2. 之后 创建文件系统
  3. 输入文件系统名称,选择公有云厂商,如阿里云
  4. 点创建
  5. 回到文件系统列表页,点文件系统名称,之后进入设置tab,可以看到客户端下载链接和挂载文件系统命令
  6. 元数据Redis不需要配置、维护,都有juicefs官方提供,用户无感知
  7. 对象存储由用户自己选择云厂商,挂载的时候提供ak、sk等信息,juicefs会帮你创建桶,基本上就是无感,操作挂载使用即可,非常便利
  8. 收费参考官网,收取的费用不包含对象存储容量费用的,对象存储费用用户自己出钱,跟juicefs无关

免费版本提供1T容量(仅juicefs本身软件费用,不含后端S3容量费用),可以注册试用下。

架构

juice-arch

数据分片

关键流程

go-fuse

juicefs的fuse客户端是基于go-fuse开发的(https://github.com/hanwen/go-fuse/ ),文档可参考 https://pkg.go.dev/github.com/hanwen/go-fuse ,支持3种文件系统模式,juicefs用到的是RawFileSystem,全部操作需要自己实现,另外两种模式文档里有介绍。

通过https://github.com/urfave/cli这个go commandline库执行juicefs mount命令之后,会检查一堆参数,然后启动fuse server进程,主要代码路径:
cmd\main.go:main() –> cmd\mount.go:mount():这里会初始化各种连接(Redis、后端S3等)以及各种挂载点的配置如cache目录、写入策略WT或WB、预读取策略、内存buffer大小等,之后初始化vfs。之后进入–> cmd\mount_unix.go:mount_main()–>pkg\fuse\fuse.go:Serve()–>创建并启动go-fuse库fssrv.Serve(),初始化fuse.RawFileSystem,之后启动fuse server进程,创建挂载点,开始接受用户文件操作请求。

注册的文件操作请求处理函数是在pkg\fuse\fuse.go这个文件里,实际是通过继承fuse.RawFileSystem的fileSystem这个struct,然后通过其成员函数实现注册文件操作处理函数的,比如pkg\fuse\fuse.go:Open()、Read()、Create()等。

元数据管理

  • 文件组织方式:file–>chunk–>slice
  • 文件操作流程:redis<–Meta<–datawriter< -->filewriter< -->chunkwriter< -->slicewriter,先写数据再更新元数据
  • meta、attr、chunk、inode:
    • inode–>attr
    • ionde+offset/chunksize–>chunk+pos–>slices
    • meta:整个文件系统一个,包含打开的文件、Redis连接等信息

open

  • 特殊inode(位于挂载点根目录下): // pkg\vfs\internal.go
    • .accesslog:用来查看日志,用途可参考README_CN.md,处理函数:pkg\vfs\accesslog.go
    • .control:主要用来处理juicefs rmr命令(删除目录),应该可以加速删除目录下的文件? pkg\vfs\internal.go:handleInternalMsg()

关键代码流程:
pkg\fuse\fuse.go:Open() –> pkg\vfs\vfs.go:Open() –> pkg\meta\redis.go:Open():
r.openFiles[inode] = r.openFiles[inode] + 1

write

关键代码流程:
pkg\fuse\fuse.go:Write() –> pkg\vfs\vfs.go:Write() –> pkg\vfs\writer.go:(f *fileWriter) Write() –> pkg\vfs\writer.go:(f *fileWriter) writeChunk() –> pkg\vfs\writer.go:(f *fileWriter) findChunk() –> pkg\vfs\writer.go:(c *chunkWriter) findWritableSlice()

findChunk和findWritableSlice如果发现没有可用的就会自动生成新的chunk或者slice,如果slice已经用完,也会创建新的chunk,写入slice之前,要确保前面的slice已经刷盘以保证IO顺序(pkg\vfs\writer.go:(c *chunkWriter) commitThread()),之后写入缓存,可能是内存或者磁盘缓存:
pkg\vfs\writer.go:(s *sliceWriter) write() –> pkg\chunk\cached_store.go:(c *wChunk) WriteAt() –> pkg\chunk\disk_store.go:(c *diskFile) WriteAt(),写满一个chunk就flush到后端S3,否则就先放到缓存里面等待被动flush。

查找chunk和slice都是根据offset和length来确定的,offset可以确认要写入的chunk和slice,length则可以确认要写入多少个slice。

用nos作为后端存储,写入数据后nos的目录结构为:
– wang(文件系统)
– chunks(chunks)
– 0(chunk)
– 0(slices?)
– 18_1_4194304(对象名构成:chunkid_sliceid_size)
– 18_2_2097152(对象名)

fsync

下刷数据,
pkg\fuse\fuse.go:Fsync() –> pkg\vfs\vfs.go:func Flush() –> pkg\vfs\writer.go:(f *fileWriter) Flush() –> pkg\vfs\writer.go:(f *fileWriter) flush() –> pkg\vfs\writer.go:(s *sliceWriter) flushData() –> pkg\chunk\cached_store.go:(c *wChunk) Finish() –> pkg\chunk\cached_store.go:(c *wChunk) FlushTo() –> pkg\chunk\cached_store.go:(c *wChunk) upload() –> pkg\chunk\cached_store.go:(c *wChunk) syncUpload()(WT模式)或asyncUpload()(WB模式) –> pkg\chunk\cached_store.go:(c *wChunk) put() –> pkg\object\nos.go:(s *nos) Put() –> nos-sdk:client.PutObjectByStream()

read

pkg\fuse\fuse.go:(fs *fileSystem) Read() –> pkg\vfs\vfs.go:Read() –> 先把该inode未落盘数据flush掉 writer.Flush(ctx, ino) –> pkg\vfs\reader.go:(f *fileReader) Read() –> 预读 pkg\vfs\reader.go:(f *fileReader) readAhead() –> 分片、准备读请求 ,看到这里不知道往下怎么看了(估计是用了golang的黑科技编码方式),反向看起来是能对上的,但没想明白怎么会走到那个流程,要用go的调试工具dlv看下调用栈才行了。