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




新的调研细节可参考:

juicefs调研


使用

开源版本

  $  wget https://github.com/juicedata/juicefs/releases/download/v0.11.0/juicefs-0.11.0-linux-amd64.tar.gz
  $  tar xf juicefs-0.11.0-linux-amd64.tar.gz
  $  ./juicefs format --access-key ${AK} --secret-key ${SK} --storage nos --bucket https://wpjuicefs.nos-eastchina1.126.net localhost wang
  $  ./juicefs mount -d localhost ~/jfs
  $ df -h
  JuiceFS:wang    1.0P   64K  1.0P   1% /root/jfs

还可以配置本地读写缓存,这里就不细说,参考链接: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连接等信息
// pkg\meta\redis.go:
type redisMeta struct {
    sync.Mutex
    conf    *RedisConfig
    rdb     *redis.Client
    txlocks [1024]sync.Mutex // Pessimistic locks to reduce conflict on Redis

    sid          int64
    openFiles    map[Ino]int
    removedFiles map[Ino]bool
    compacting   map[uint64]bool
    symlinks     *sync.Map
    msgCallbacks *msgCallbacks

    shaLookup string // The SHA returned by Redis for the loaded `scriptLookup`
}
// pkg\meta\interface.go:

const (
    TypeFile      = 1 // type for regular file
    TypeDirectory = 2 // type for directory
    TypeSymlink   = 3 // type for symlink
    TypeFIFO      = 4 // type for FIFO node
    TypeBlockDev  = 5 // type for block device
    TypeCharDev   = 6 // type for character device
    TypeSocket    = 7 // type for socket
)

// 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
}

// Entry is an entry inside a directory.
type Entry struct {
    Inode Ino  // uint64,用redis控制单调递增
    Name  []byte
    Attr  *Attr
}

// 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
}

func (r *redisMeta) inodeKey(inode Ino) string {
    return "i" + inode.String()
}

func (r *redisMeta) entryKey(parent Ino) string {
    return "d" + parent.String()
}

func (r *redisMeta) chunkKey(inode Ino, indx uint32) string {
    return "c" + inode.String() + "_" + strconv.FormatInt(int64(indx), 10)
}

func (r *redisMeta) sliceKey(chunkid uint64, size uint32) string {
    return "k" + strconv.FormatUint(chunkid, 10) + "_" + strconv.FormatUint(uint64(size), 10)
} // 这个从代码里看起来是合并chunk的时候为了区分slice所属chunk才用到的,我测试环境的redis里面没看到有这个key

// 用法示例:
pipe.Set(ctx, r.inodeKey(inode), r.marshal(&attr), 0)
pipe.HSet(ctx, r.entryKey(parent), name, r.packEntry(_type, ino))
pipe.RPush(ctx, r.chunkKey(inode, uint32(old/ChunkSize)), w.Bytes())  // inode->chunk-->slices

// redis key信息:
127.0.0.1:6379> KEYS *
 1) "c8_0"
 2) "d1"
 3) "nextinode"
 4) "i1"
 5) "c9_0"
 6) "nextchunk"
 7) "mykey" // 测试key,忽略
 8) "i2"
 9) "i8"
10) "totalInodes"
11) "i9"
12) "c2_0"
13) "c6_0"
14) "c7_0"
15) "i6"
16) "nextsession"
17) "setting"
18) "i7"
19) "usedSpace"
20) "sessions"
127.0.0.1:6379> get totalInodes
"5"
127.0.0.1:6379> get i2
"\x00\x11\xa4\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00`Z\xe7`)>\xb6\xb1\x00\x00\x00\x00`Z\xe7\xe2![\x81\xb7\x00\x00\x00\x00`Z\xe7\xe2![\x81\xb7\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00"
127.0.0.1:6379> get d1
(error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> get c2_0
(error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> get i6
"\x00\x11\xa4\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00`Z\xe7\xf7\x00\x00\x00\x00\x00\x00\x00\x00`Z\xe7\xc0\x00\x00\x00\x00\x00\x00\x00\x00`Z\xe7\xf7\x17\x89n\xa8\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00"
127.0.0.1:6379>

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(对象名)

// 上传到s3的key生成方法:
func (c *rChunk) key(indx int) string {
    if c.store.conf.Partitions > 1 {
        return fmt.Sprintf("chunks/%02X/%v/%v_%v_%v", c.id%256, c.id/1000/1000, c.id, indx, c.blockSize(indx))
    }
    return fmt.Sprintf("chunks/%v/%v/%v_%v_%v", c.id/1000/1000, c.id/1000, c.id, indx, c.blockSize(indx))
}

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看下调用栈才行了。