JuiceFS 设计
分布式文件系统的发展
Google GFS 的发表应该是分布式文件系统发展的里程碑,Meta + Data 的架构影响了后来的分布式系统设计。随后的 HDFS 是大数据时代最重要的存储系统之一,现在分布式文件系统最流行的2种接口协议是 POSIX 和 HDFS 接口协议。
随着云原生和对象存储的发展,分布式系统的设计趋势是把数据放到对象存储,比如 autoMQ 把 Kafka 的数据放到对象存储,AWS 刚发布的基于对象存储的向量数据库。
对分布式文件系统的需求也有变化,相比大数据时代,AI 训练存储的更多是中小文件,所以文件系统的元数据更多。
分布式文件系统的难点
文件系统的元数据组成一个 DAG,分布式文件系统的难点在于:
- 怎么存储这个 DAG?
- 怎么高并发的读写这个 DAG?
几种思路:
- 整个 DAG 存储在单个节点,缺点是单节点容量有限,DAG size 成为系统瓶颈。
- 转化为 KV/Relation 存储在 KV/Relational DB。
- 设计一个可扩展的存储 DAG 的系统,把 DAG 划分存储到多个节点,目前没有这样做的开源项目(?),但是有相关的论文,应该有公司实现了/在做这个。
- 不采用 Meta + Data 架构,把 Meta 和 Data 混合存储。
JuiceFS
JuiceFS 的元数据采用第2种思路,支持 TiKV/MySQL 等多个系统作为 Meta Service,甚至还支持 Redis (在一致性要求不高的场景使用),数据放在对象存储。
架构:
app go-fuse --> juicefs --> local meta cache --> meta service
| | |
syscall fd --> local data cache --> object storage service
| |
vfs --> fuse
存储文件数据
当写文件时,元数据放在 Meta Service,数据放在对象存储。如果一个文件作为一个对象去存储,当读一部分/修改文件时,会有严重的读写放大,特别是大文件,所以需要把文件划分成多个对象去存储。
JuiceFS 设计了3个概念:chunk, slice, block:
- 文件逻辑上划分成多个 chunk,chunk size = 64MB
- chunk 会有一次或多次写,对 chunk 的每次写为一个 slice,slice = (chunk id, offset, size, data) 存储在 local data cache,多次写可能有重叠,所以还要记录 slice 顺序,以最新的为准。跨 chunk 的写会拆分成多个 slice
- 把 local data cache 的 slice 存储到对象存储时,把一个 slice 作为一个对象去存储?为了提高 slice 写到对象存储的速度,把 slice 划分成多个 block (block size 最大为 4M) 并发写入
所以对象存储的对象是一个个 <= 4M 的 block。
对象名:${fsname}/chunks/${hash}/${basename}
:
- fsname: 用户定义的文件系统名字
- hash: hash_func(basename),为了做隔离
- basename: 对象的有效名字,格式是
${slice_id}_${index}_${block_size}
,index 是 block 在 slice 的序号,范围 [0,15]
多个写的重叠会产生垃圾 slice -> 产生垃圾 block,需要删除,还可以做合并碎片等设计。
上面说每次写是一个 slice,实际上由于有 local data cache,JuiceFS 做了一些优化,比如一次写后,slice 还没有上传到对象存储,此时有新的写操作和它重叠或连续,会直接修改它,不创建新 slice。
元数据结构设计
JuiceFS 元数据:
- 系统元数据
- 各种计数值:下个可用的 inode id, slice id, session id, &c
- session: client id, info 和超时时间
- inode attr:type, nlink, uid, parent inode id, &c
- file inode data (chunk): (inode id, index) -> []sliceMeta
- dir inode data (DAG edge):(parent inode id, name) -> (type, inode id),即(目录 inode id, 子 inode 名字) -> (子 inode type, id)
- plock: (inode, session id, owner) -> []plock record
- &c
JuiceFS 需要设计合理的数据结构,把元数据放到 TiKV/MySQL/Redis 等。
Redis:
- 系统元数据:string KV 方式:
setting
-> json string - 各种计数值:string KV 方式:
nextInode
-> num,nextSliceId
-> num, &c - session
- session timeout:
allSessions
-> a sorted set {member: client id, score: timeout timestamp} - session info:
sessionInfos
-> a hash table {client id -> json string}
- session timeout:
- inode attr: string KV:
i${inode_id}
-> 二进制编码的 attr struct - chunk:
c${inode_id}_{chunk_index}
-> list {二进制编码的 sliceMeta struct} - edge:
d${inode_id}
-> a hash table {name -> 二进制编码的 (type, inode_id)} - &c
读写流程
root inode id = 1, 给定一个路径,我们能够找到 inode_id/attr。
前面提到文件数据 block 名字的设计:${fsname}/chunks/${hash}/${basename}
:
- fsname: 用户定义的文件系统名字
- hash: hash_func(basename),为了做隔离
- basename: 对象的有效名字,格式是
${slice_id}_${index}_${block_size}
,index 是 block 在 slice 的序号,范围 [0,15]
读文件流程:
- 根据路径找到 inode_id, size(in attr)
- 计算要读的 chunk_index,
c${inode_id}_{chunk_index}
获取 slice meta list - 计算要读的位置在哪些 slices,在 slice 内的 index,
${slice_id}_${index}_${size}
读取 block,得到数据,拼接
WIP: 上面讲了 JuiceFS 的总体设计,主要是为了我自己能够梳理一下,看看从总体上有没有可以改进的点。有时间再接着补充详细的设计。