最近研究文件系统,把近期比较火的JuiceFS代码翻出来看了一下,研究为啥其性能要比CephFS要好。
参考资源:https://mp.weixin.qq.com/s/HvbMxNiVudjNPRgYC8nXyg
FUSE 是一个用来实现用户态文件系统的框架,这套 FUSE 框架包含 3 个组件:
背景:一个用户态文件系统,挂载点为 /tmp/fuse ,用户二进制程序文件为 ./hello ;当执行 ls -l /tmp/fuse 命令的时候,流程如下: IO 请求先进内核,经 vfs 传递给内核 FUSE 文件系统模块; 内核 FUSE 模块把请求发给到用户态,由 ./hello 程序接收并且处理。处理完成之后,响应原路返回;
JuiceFS 由三个部分组成:
JuiceFS 依靠 Redis 来存储文件的元数据。Redis 是基于内存的高性能的键值数据存储,非常适合存储元数据。与此同时,所有数据将通过 JuiceFS 客户端存储到对象存储中。
任何存入 JuiceFS 的文件都会被拆分成固定大小的 "Chunk",默认的容量上限是 64 MiB。每个 Chunk 由一个或多个 "Slice" 组成,Slice 的长度不固定,取决于文件写入的方式。每个 Slice 又会被进一步拆分成固定大小的 "Block",默认为 4 MiB。最后,这些 Block 会被存储到对象存储。与此同时,JuiceFS 会将每个文件以及它的 Chunks、Slices、Blocks 等元数据信息存储在元数据引擎中。
使用 JuiceFS,文件最终会被拆分成 Chunks、Slices 和 Blocks 存储在对象存储。因此,你会发现在对象存储平台的文件浏览器中找不到存入 JuiceFS 的源文件,存储桶中只有一个 chunks 目录和一堆数字编号的目录和文件。不要惊慌,这正是 JuiceFS 高性能运作的秘诀!
补充一下源码中,每个blocks的命名规则定义,也就是最终存储在对象存储系统中的对象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))
}
从命名规则里面也能看出,数据是支持按partition进行分区存储的,也就是说最终存储数据的bucket可以是多个,这样有助于提高并发能力,特别是AWS S3每个bucket是有TPS性能上限的。
文件系统定义核心数据结构
type FileSystem struct {
conf *vfs.Config
reader vfs.DataReader
writer vfs.DataWriter
m meta.Meta
logBuffer chan string
}
下图为个人理解所画的抽象接口结构图
JuiceFS支持的数据列表:
https://github.com/juicedata/juicefs/blob/main/docs/zh_cn/databases_for_metadata.md
JuiceFS元数据redis与MySQL性能对比测试:
https://github.com/juicedata/juicefs/blob/main/docs/en/metadata_engines_benchmark.md
下图是源码中对ChunkStore的抽象接口定义,通过NewReader和NewWriter生成对应的Reader读取抽象和Writer抽象,实现数据的读写相关原子接口。
任何厂家的对象存储产品,只要实现下面的接口抽象,即可实现与JuiceFS的对接。
下图是数据读取抽象接口的继承组合关系
最终的数据读取关联到rChunk这个struct的相关method方法。
下图是数据写入抽象接口的继承组合关系
最终的数据读取关联到wChunk这个struct的相关method方法。
至此,基本理清了JuiceFS的基本构架和核心数据结构,下节开始对其读写具体接口流程进行剖析。
打个小广告,团队持续招聘存储开发和运维同学,感兴趣的可以看看之前公众号网易游戏招聘相关内容。