新的调研细节可参考:
使用
开源版本
1 2 3 4 5 6 7 |
$ 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
本地缓存会加速读写性能,但会导致数据的不一致。
官方商业版本
- 在官网上注册账号进入控制台:https://juicefs.com/console/
- 之后 创建文件系统
- 输入文件系统名称,选择公有云厂商,如阿里云
- 点创建
- 回到文件系统列表页,点文件系统名称,之后进入设置tab,可以看到客户端下载链接和挂载文件系统命令
- 元数据Redis不需要配置、维护,都有juicefs官方提供,用户无感知
- 对象存储由用户自己选择云厂商,挂载的时候提供ak、sk等信息,juicefs会帮你创建桶,基本上就是无感,操作挂载使用即可,非常便利
- 收费参考官网,收取的费用不包含对象存储容量费用的,对象存储费用用户自己出钱,跟juicefs无关
免费版本提供1T容量(仅juicefs本身软件费用,不含后端S3容量费用),可以注册试用下。
架构
关键流程
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连接等信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 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` } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
// 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 } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
// 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(对象名)
1 2 3 4 5 6 7 8 |
// 上传到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看下调用栈才行了。