Aggregator
CVE-2021-40760 | Adobe After Effects up to 18.4.1 m4a File buffer overflow (apsb21-79 / Nessus ID 209422)
CVE-2021-40756 | Adobe After Effects up to 18.4.1 null pointer dereference (apsb21-79 / Nessus ID 209422)
CVE-2021-40761 | Adobe After Effects up to 18.4.1 null pointer dereference (apsb21-79 / Nessus ID 209422)
The HackerNoon Newsletter: 13 Cybercrime Facts That Will Give You Chills (10/26/2024)
直观解读 JuiceFS 的数据和元数据设计(一):看山是山(2024)
本系列分为三篇文章,试图通过简单的实地环境来直观理解 JuiceFS 的数据(data)和元数据(metadata)设计。
Fig. MinIO bucket browser: one object was created ({volume}/juicefs_uuid) on a new juicefs volume creation.
水平及维护精力所限,文中不免存在错误或过时之处,请酌情参考。 传播知识,尊重劳动,年满十八周岁,转载请注明出处。
- 直观解读 JuiceFS 的数据和元数据设计(一):看山是山(2024)
- 直观解读 JuiceFS 的数据和元数据设计(二):看山不是山(2024)
- 直观解读 JuiceFS 的数据和元数据设计(三):看山还是山(2024)
- 1 JuiceFS 高层架构与组件
- 2 搭建极简 JuiceFS 集群
- 3 将 JuiceFS volume 挂载到本地路径
- 4 在 JuiceFS volume 挂载的本地路径内读写
- 5 总结
- 参考资料
本篇首先快速了解下 JuiceFS 架构和组件,然后将搭建一个极简 JuiceFS 集群, 并以 JuiceFS 用户的身份来体验下它的基本功能。
1 JuiceFS 高层架构与组件JuiceFS 的高层架构和组件,
Fig. JuiceFS cluster initialization, and how POSIX file operations are handled by JuiceFS.
三大组件:
- 元数据引擎:存储文件元数据,例如文件名、权限等。JuiceFS 支持多种元数据引擎,比如 TiKV、sqlite、redis 等。
- 对象存储:存储文件本身。JuiceFS 支持多种对象存储,比如 MinIO、AWS S3、阿里云 OSS 等。
- JuiceFS 客户端:将 JuiceFS volume 挂载到机器上,提供文件系统视图给用户。
更多架构信息,见 [1]。
2 搭建极简 JuiceFS 集群接下来搭建一个极简 JuiceFS 环境,方便我们做一些功能测试。 按上一节提到的,只需要搭建以下 3 个组件:
- 元数据引擎,这里我们用 TiKV;
- 对象存储,这里我们用 MinIO;
- JuiceFS 客户端。
对于功能测试来说,使用哪种元数据引擎都无所谓,比如最简单的 sqlite 或 redis。
不过,本系列第二篇会介绍 TiKV 相关的一些设计,所以本文用的 TiKV 集群作为元数据引擎, 相关的搭建步骤见社区文档。
本篇假设搭建的是三节点的 TiKV 集群,IP 地址分别是 192.168.1.{1,2,3}。
2.2 搭建对象存储(MinIO)这里我们用 MinIO 搭建一个对象存储服务,主要是空集群方便观察其中的文件变化。
2.2.1 启动 MinIO serverMinIO 是一个兼容 S3 接口的开源对象存储产品,部署非常简单,就一个可执行文件,下载执行就行了。
也可以用容器,一条命令启动:
$ sudo docker run -p 9000:9000 -p 8080:8080 \ quay.io/minio/minio server /data --console-address "0.0.0.0:8080"访问 http://localhost:8080/ 就能看到 MinIO 的管理界面了。默认账号密码都是 minioadmin。
2.2.2 创建 bucket通过 MinIO 管理界面创建一个 bucket,这里我们命名为 juicefs-bucket,
Fig. MinIO bucket list: an empty bucket.
可以看到现在里面一个对象也没有,已使用空间也是 0 字节。
2.3 下载 juicefs 客户端从 https://github.com/juicedata/juicefs/releases 下载一个可执行文件就行了,
$ wget https://github.com/juicedata/juicefs/releases/download/v1.2.1/juicefs-1.2.1-linux-amd64.tar.gz $ tar -xvf juicefs-1.2.1-linux-amd64.tar.gz $ chmod +x juicefs 2.4 创建 JuiceFS volume接下来就可以创建一个 JuiceFS volume 了,这里命名为 foo-dev。
2.4.1 创建/格式化 volume:juicefs format $ juicefs format --storage minio --bucket http://localhost:9000/juicefs-bucket \ --access-key minioadmin \ --secret-key minioadmin \ tikv://192.168.1.1:2379,192.168.1.2:2379,192.168.1.3:2379/foo-dev \ foo-dev <INFO>: Meta address: tikv://192.168.1.1:2379,192.168.1.2:2379,192.168.1.3:2379/foo-dev [interface.go:504] <INFO>: Data use minio://localhost:9000/juicefs-bucket/foo-dev/ [format.go:528] <INFO>: Volume is formatted as { "Name": "foo-dev", "UUID": "3b4e509b-a7c8-456f-b726-cb8395cf8eb6", "Storage": "minio", "Bucket": "http://localhost:9000/juicefs-bucket", "AccessKey": "minioadmin", "SecretKey": "removed", "BlockSize": 4096, "UploadLimit": 0, "DownloadLimit": 0, ... } 2.4.2 查看 MinIO bucket:多了一个 juicefs_uuid 文件再查看 MinIO bucket,会发现多了一个 object,
Fig. MinIO bucket browser: one object was created on a new juicefs volume creation.
点进去,发现是一个叫 juicefs_uuid 的文件,
Fig. MinIO bucket browser: one object was created after juicefs format.
可以把这个文件下载下来,其内容就是上面 juicefs format 命令输出的 uuid 信息,也就是说 juicefs client 会把 volume 的 uuid 上传到对象存储中。
3 将 JuiceFS volume 挂载到本地路径这么我们将这个 volume 挂载到本地路径 /tmp/foo-dev,
$ ./juicefs mount --debug --backup-meta 0 \ tikv://192.168.1.1:2379,192.168.1.2:2379,192.168.1.3:2379/foo-dev /tmp/foo-dev [INFO] [client.go:405] ["[pd] create pd client with endpoints"] [component=tikv] [pid=2881678] [pd-address="[192.168.1.1:2379,192.168.1.2:2379,192.168.1.3:2379]"] [INFO] [base_client.go:378] ["[pd] switch leader"] [component=tikv] [pid=2881678] [new-leader=https://192.168.1.3:2379] [old-leader=] [INFO] [base_client.go:105] ["[pd] init cluster id"] [component=tikv] [pid=2881678] [cluster-id=7418858894192002550] [INFO] [client.go:698] ["[pd] tso dispatcher created"] [component=tikv] [pid=2881678] [dc-location=global] <INFO>: Data use minio://localhost:9000/juicefs-bucket/foo-dev/ [mount.go:650] ...进入目录:
$ cd /tmp/foo-dev $ ls -ahl -r-------- 1 root root 0 Oct 26 10:45 .accesslog -r-------- 1 root root 2.9K Oct 26 10:45 .config -r--r--r-- 1 root root 0 Oct 26 10:45 .stats dr-xr-xr-x 2 root root 0 Oct 26 10:45 .trash可以看到几个隐藏文件,
- 这些是 JuiceFS 的元数据文件,在 [1] 系列文章中有过详细介绍。
- 这些都是 volume 本地文件,不会上传到 MinIO。此时,MinIO juicefs-bucket 里面还是只有一个 uuid 文件。
接下来进行一些 POSIX 操作测试。
4.1 创建和写入文件创建三个文件,一个只有几十字节(但命名为 file1_1KB), 一个 5MB,一个 129MB,
$ cd /tmp/foo-dev $ echo "Hello, JuiceFS!" > file1_1KB $ dd if=/dev/zero of=file2_5MB bs=1M count=5 5+0 records in 5+0 records out 5242880 bytes (5.2 MB, 5.0 MiB) copied, 0.0461253 s, 114 MB/s $ dd if=/dev/zero of=file3_129MB bs=1M count=129 129+0 records in 129+0 records out 135266304 bytes (135 MB, 129 MiB) copied, 0.648757 s, 209 MB/s 4.2 查看文件属性 $ ls -ahl file* -rw-r----- 1 root root 16 file1_1KB -rw-r----- 1 root root 5.0M file2_5MB -rw-r----- 1 root root 129M file3_129MB $ file file2_5MB file2_5MB: data 4.3 读取和追加文件 $ cat file1_1KB Hello, JuiceFS! $ echo "Hello, JuiceFS!" >> file1_1KB $ cat file1_1KB Hello, JuiceFS! Hello, JuiceFS! 4.4 查找文件 $ find /tmp -name file1_1KB /tmp/foo-dev/file1_1KB 4.5 删除文件直接用 rm 删除就行了,不过这几个文件我们还有用,先不删。
4.6 目录操作目录的创建、移动、修改权限、删除等待也是一样的,大家可以自己试试,这里不再赘述。
4.7 小结根据以上测试,在 JuiceFS 挂载路径里创建/读写/查找/删除文件,都跟本地目录没什么区别 —— 这也正是「分布式“文件系统”」的意义所在 —— 兼容 POSIX 语义,用户无需关心数据存在哪, 当本地目录使用就行了(性能另当别论)。
5 总结本篇中,我们作为 JuiceFS 用户对它进行了一些最基本的功能测试,结论是和本地文件系统没什么区别。
对于普通用户来说,了解到这一层就够了; 但对于高阶用户以及 JuiceFS 的开发/运维来说,这只是表象,必有第二重境界等着他们。
参考资料直观解读 JuiceFS 的数据和元数据设计(二):看山不是山(2024)
本系列分为三篇文章,试图通过简单的实地环境来直观理解 JuiceFS 的数据(data)和元数据(metadata)设计。
Fig. JuiceFS object key naming and the objects in MinIO.
水平及维护精力所限,文中不免存在错误或过时之处,请酌情参考。 传播知识,尊重劳动,年满十八周岁,转载请注明出处。
- 直观解读 JuiceFS 的数据和元数据设计(一):看山是山(2024)
- 直观解读 JuiceFS 的数据和元数据设计(二):看山不是山(2024)
- 直观解读 JuiceFS 的数据和元数据设计(三):看山还是山(2024)
上一篇从功能的角度体验了下 JuiceFS,这一篇我们深入到背后,看看 JuiceFS 分别在数据和元数据上做了哪些设计,才给到用户和本地文件系统一样的体验的。
2 对象存储中 JuiceFS 写入的文件本篇以 MinIO 为例,来看 JuiceFS 写入到对象存储中的文件是怎样组织的。 其他云厂商的对象存储(AWS S3、阿里云 OSS 等)也都是类似的。
2.1 Bucket 内:每个 volume 一个“目录”可以用上一篇介绍的 juicefs format 命令再创建两个 volume,方便观察它们在 bucket 中的组织关系,
Fig. MinIO bucket browser: volume list.
如上图所示,bucket 内的顶层“目录”就是 JuiceFS 的 volumes,
我们这里提到“目录”时加双引号,是因为对象存储是扁平的 key-value 存储,没有目录的概念, 前端展示时模拟出目录结构(key 前缀一样的,把这个前缀作为一个“目录”)是为了查看和理解方便。 简单起见,后文不再加双引号。
2.2 每个 volume 的目录: {chunks/, juicefs_uuid, meta/, ...}每个 volume 目录内的结构如下:
{volume_name}/ |-chunks/ # 数据目录,volume 中的所有用户数据都放在这里面 |-juicefs_uuid |-meta/ # `juicefs mount --backup-meta ...` 产生的元数据备份存放的目录 2.2.1 juicefs_uuid:JuiceFS volume 的唯一标识可以把这个文件下载下来查看内容,会发现里面存放的就是 juicefs format 输出里看到的那个 uuid, 也就是这个 volume 的唯一标识。
删除 volume 时需要用到这个 uuid。
2.2.2 meta/:JuiceFS 元数据备份如果在 juicefs mount 时指定了 --backup-meta,JuiceFS 就会定期把元数据(存在在 TiKV 中)备份到这个目录中, 用途:
- 元数据引擎故障时,可以从这里恢复;
- 在不同元数据引擎之间迁移元数据。
详见 JuiceFS 元数据引擎五探:元数据备份与恢复(2024)。
2.2.3 chunks/Fig. MinIO bucket browser: files in a bucket.
chunks/ 内的目录结构如下,
{volume_name}/ |-chunks/ | |-0/ # <-- id1 = slice_id / 1000 / 1000 | | |-0/ # <-- id2 = slice_id / 1000 | | |-1_0_16 # <-- {slice_id}_{block_id}_{size_of_this_block} | | |-3_0_4194304 # | | |-3_1_1048576 # | | |-... |-juicefs_uuid |-meta/如上,所有的文件在 bucket 中都是用数字命名和存放的,分为三个层级:
- 第一层级:纯数字,是 sliceID 除以 100 万得到的;
- 第二层级:纯数字,是 sliceID 除以 1000 得到的;
- 第三层级:纯数字加下划线,{slice_id}_{block_id}_{size_of_this_block},表示的是这个 chunk 的这个 slice 内的 block_id 和 block 的大小。
不理解 chunk/slice/block 这几个概念没关系,我们马上将要介绍。
2.3 小结通过以上 bucket 页面,我们非常直观地看到了一个 JuiceFS volume 的所有数据在对象存储中是如何组织的。
接下来进入正题,了解一下 JuiceFS 的数据和元数据设计。
3 JuiceFS 数据的设计 3.1 顶层切分:一切文件先切 chunk对于每个文件,JuiceFS 首先会按固定大小(64MB)切大块, 这些大块称为「Chunk」。
- 这是为了读或修改文件内容时,方便查找和定位。
- 不管是一个只有几字节的文本文件,还是一个几十 GB 的视频文件, 在 JuiceFS 中都是切分成 chunk,只是 chunk 的数量不同而已。
Fig. JuiceFS: split each file into their respective chunks (with max chunk size 64MB).
3.1.2 对象存储:不存在 chunk 实体结合上一节在对象存储中看到的目录结构,
{volume_name}/ |-chunks/ | |-0/ # <-- id1 = slice_id / 1000 / 1000 | | |-0/ # <-- id2 = slice_id / 1000 | | |-1_0_16 # <-- {slice_id}_{block_id}_{size_of_this_block} | | |-3_0_4194304 # | | |-3_1_1048576 # | | |-... |-juicefs_uuid |-meta/- Chunk 在对象存储中 没有对应任何实际文件,也就是说在对象存储中没有一个个 64MB 的 chunks;
- 用 JuiceFS 的话来说,Chunk 是一个逻辑概念。暂时不理解没关系,接着往下看。
chunk 只是一个“框”,在这个框里面对应文件读写的,是 JuiceFS 称为「Slice」 的东西。
- chunk 内的一次连续写入,会创建一个 slice,对应这段连续写入的数据;
- 由于 slice 是 chunk 内的概念,因此它不能跨 Chunk 边界,长度也不会超 max chunk size 64M。
- slice ID 是全局唯一的;
根据写入行为的不同,一个 Chunk 内可能会有多个 Slice,
- 如果文件是由一次连贯的顺序写生成,那每个 Chunk 只包含一个 Slice。
- 如果文件是多次追加写,每次追加均调用 flush 触发写入上传,就会产生多个 Slice。
Fig. JuiceFS: chunks are composed of slices, each slice corresponds to a continues write operation.
拿 chunk1 为例,
- 用户先写了一段 ~30MB 数据,产生 slice5;
- 过了一会,从 ~20MB 的地方重新开始写 45MB(删掉了原文件的最后一小部分,然后开始追加写),
- chunk1 内的部分产生 slice6;
- 超出 chunk1 的部分,因为 slice 不能跨 chunk 边界,因此产生 chunk2 和 slice7;
- 过了一会,从 chunk1 ~10MB 的地方开始修改(覆盖写),产生 slice8。
由于 Slice 存在重叠,因此引入了几个字段标识它的有效数据范围,
// pkg/meta/slice.go type slice struct { id uint64 size uint32 off uint32 len uint32 pos uint32 left *slice // 这个字段不会存储到 TiKV 中 right *slice // 这个字段不会存储到 TiKV 中 } 3.2.2 读 chunk 数据时的多 slice 处理:碎片化和碎片合并Fig. JuiceFS: chunks are composed of slices, each slice corresponds to a continues write operation.
对 JuiceFS 用户来说,文件永远只有一个,但在 JuiceFS 内部,这个文件对应的 Chunk 可能会有多个重叠的 Slice,
- 有重叠的部分,以最后一次写入的为准。
- 直观上来说,就是上图 chunk 中的 slices 从上往下看,被盖掉的部分都是无效的。
因此,读文件时,需要查找「当前读取范围内最新写入的 Slice」,
- 在大量重叠 Slice 的情况下,这会显著影响读性能,称为文件「碎片化」。
- 碎片化不仅影响读性能,还会在对象存储、元数据等层面增加空间占用。
- 每当写入发生时,客户端都会判断文件的碎片化情况,并异步地运行碎片合并,将一个 Chunk 内的所有 Slice 合并。
跟 chunk 类似,在对象存储中 slice 也没有 没有对应实际文件。
{volume_name}/ |-chunks/ | |-0/ # <-- id1 = slice_id / 1000 / 1000 | | |-0/ # <-- id2 = slice_id / 1000 | | |-1_0_16 # <-- {slice_id}_{block_id}_{size_of_this_block} | | |-3_0_4194304 # | | |-3_1_1048576 # | | |-... |-juicefs_uuid |-meta/ 3.3 Slice 切分成固定大小 Block(e.g. 4MB):并发读写对象存储为了加速写到对象存储,JuiceFS 将 Slice 进一步拆分成一个个「Block」(默认 4MB),多线程并发写入。
Fig. JuiceFS: slices are composed of blocks (4MB by default), each block is an object in object storage.
Block 是 JuiceFS 数据切分设计中最后一个层级,也是 chunk/slice/block 三个层级中唯一能在 bucket 中看到对应文件的。
Fig. MinIO bucket browser: objects in a bucket.
- 连续写:前面 Block 默认都是 4MB,最后一个 Block 剩多少是多少。
- 追加写:数据不足 4MB 时,最终存入对象存储的也会是一个小于 4M 的 Block。
从上图的名字和大小其实可以看出分别对应我们哪个文件:
- 1_0_16:对应我们的 file1_1KB;
- 我们上一篇的的追加写 echo "hello" >> file1_1KB 并不是写入了 1_0_16, 而是创建了一个新对象 7_0_16,这个 object list 最后面,所以在截图中没显示出来;
- 换句话说,我们的 file1_1KB 虽然只有两行内容,但在 MinIO 中对应的却是两个 object,各包含一行。
- 通过这个例子,大家可以体会到 JuiceFS 中连续写和追加写的巨大区别。
- 3_0_4194304 + 3_1_1048576:总共 5MB,对应我们的 file2_5MB;
- 4_*:对应我们的 file3_129MB;
格式:{volume}/chunks/{id1}/{id2}/{slice_id}_{block_id}_{size_of_this_block},对应的代码,
// pkg/chunk/cached_store.go func (s *rSlice) key(blockID int) string { if s.store.conf.HashPrefix // false by default return fmt.Sprintf("chunks/%02X/%v/%v_%v_%v", s.id%256, s.id/1000/1000, s.id, blockID, s.blockSize(blockID)) return fmt.Sprintf("chunks/%v/%v/%v_%v_%v", s.id/1000/1000, s.id/1000, s.id, blockID, s.blockSize(blockID)) } 3.5 将 chunk/slice/block 对应到对象存储最后,我们将 volume 的数据切分和组织方式对应到 MinIO 中的路径和 objects,
Fig. JuiceFS object key naming and the objects in MinIO.
3.6 小结:光靠对象存储数据和 slice/block 信息无法还原文件至此,JuiceFS 解决了数据如何切分和存放的问题,这是一个正向的过程:用户创建一个文件,我们能按这个格式切分、命名、上传到对象存储。
对应的反向过程是:给定对象存储中的 objects,我们如何将其还原成用户的文件呢? 显然,光靠 objects 名字中包含的 slice/block ID 信息是不够的,例如,
- 最简单情况下,每个 chunk 都没有任何 slice 重叠问题,那我们能够根据 object 名字中的 slice_id/block_id/block_size 信息拼凑出一个文件, 但仍然无法知道这个文件的文件名、路径(父目录)、文件权限(rwx)等等信息;
- chunk 一旦存在 slice 重叠,光靠对象存储中的信息就无法还原文件了;
- 软链接、硬链接、文件属性等信息,更是无法从对象存储中还原。
解决这个反向过程,我们就需要文件的一些元数据作为辅助 —— 这些信息在文件切分和写入对象存储之前,已经记录到 JuiceFS 的元数据引擎中了。
4 JuiceFS 元数据的设计(TKV 版)JuiceFS 支持不同类型的元数据引擎,例如 Redis、MySQL、TiKV/etcd 等等,每种类型的元数据引擎都有自己的 key 命名规则。 本文讨论的是 JuiceFS 使用 transactional key-value(TKV)类型的元数据引擎时的 key 命名规则。
更具体地,我们将拿 TiKV 作为元数据引擎来研究。
4.1 TKV 类型 key 列表这里的 key 是 JuiceFS 定义元数据 key,key/value 写入元数据引擎; 请注意跟前面提到的对象存储 key 区别开,那个 key/value 是写入对象存储的。
key 是一个字符串,所有 key 的列表,
// pkg/meta/tkv.go setting format C{name} counter A{8byte-inode}I inode attribute A{8byte-inode}D{name} dentry A{8byte-inode}P{8byte-inode} parents // for hard links A{8byte-inode}C{4byte-blockID} file chunks A{8byte-inode}S symlink target A{8byte-inode}X{name} extented attribute D{8byte-inode}{8byte-length} deleted inodes F{8byte-inode} Flocks P{8byte-inode} POSIX locks K{8byte-sliceID}{8byte-blockID} slice refs Ltttttttt{8byte-sliceID} delayed slices SE{8byte-sessionID} session expire time SH{8byte-sessionID} session heartbeat // for legacy client SI{8byte-sessionID} session info SS{8byte-sessionID}{8byte-inode} sustained inode U{8byte-inode} usage of data length, space and inodes in directory N{8byte-inode} detached inde QD{8byte-inode} directory quota R{4byte-aclID} POSIX acl在 TKV 的 Keys 中,所有整数都以编码后的二进制形式存储 [2]:
- inode 和 counter value 占 8 个字节,使用小端编码
- SessionID、sliceID 和 timestamp 占 8 个字节,使用大端编码
setting 是一个特殊的 key,对应的 value 就是这个 volume 的设置信息。 前面的 JuiceFS 元数据引擎系列文章中介绍过 [3],这里不再赘述。
其他的,每个 key 的首字母可以快速区分 key 的类型,
- C:counter,这里面又包含很多种类,例如 name 可以是:
- nextChunk
- nextInode
- nextSession
- A:inode attribute
- D:deleted inodes
- F:Flocks
- P:POSIX lock
- S:session related
- K:slice ref
- L: delayed (to be deleted?) slices
- U:usage of data length, space and inodes in directory
- N:detached inode
- QD:directory quota
- R:POSIX acl
需要注意的是,这里是 JuiceFS 定义的 key 格式,在实际将 key/value 写入元数据引擎时, 元数据引擎可能会对 key 再次进行编码,例如 TiKV 就会在 key 中再插入一些自己的字符。 前面的 JuiceFS 元数据引擎系列文章中也介绍过,这里不再赘述。
4.2 元数据引擎中的 key/value 4.2.1 扫描相关的 TiKV keyTiKV 的 scan 操作类似 etcd 的 list prefix,这里扫描所有 foo-dev volume 相关的 key,
$ ./tikv-ctl.sh scan --from 'zfoo-dev' --to 'zfoo-dew' key: zfoo-dev\375\377A\000\000\000\020\377\377\377\377\177I\000\000\000\000\000\000\371 key: zfoo-dev\375\377A\001\000\000\000\000\000\000\377\000Dfile1_\3771KB\000\000\000\000\000\372 key: zfoo-dev\375\377A\001\000\000\000\000\000\000\377\000Dfile2_\3775MB\000\000\000\000\000\372 ... key: zfoo-dev\375\377SI\000\000\000\000\000\000\377\000\001\000\000\000\000\000\000\371 default cf value: start_ts: 453485726123950084 value: 7B225665727369...33537387D key: zfoo-dev\375\377U\001\000\000\000\000\000\000\377\000\000\000\000\000\000\000\000\370 key: zfoo-dev\375\377setting\000\376 default cf value: start_ts: 453485722598113282 value: 7B0A224E616D65223A202266...0A7D 4.2.2 解码成 JuiceFS metadata key用 tikv-ctl --decode <key> 可以解码出来,注意去掉最前面的 z,得到的就是 JuiceFS 的原始 key,看着会更清楚一点,
foo-dev\375A\000\000\000\020\377\377\377\177I foo-dev\375A\001\000\000\000\000\000\000\000Dfile1_1KB foo-dev\375A\001\000\000\000\000\000\000\000Dfile2_5MB foo-dev\375A\001\000\000\000\000\000\000\000Dfile3_129MB foo-dev\375A\001\000\000\000\000\000\000\000I foo-dev\375A\002\000\000\000\000\000\000\000C\000\000\000\000 foo-dev\375A\002\000\000\000\000\000\000\000I foo-dev\375A\003\000\000\000\000\000\000\000C\000\000\000\000 foo-dev\375A\003\000\000\000\000\000\000\000I foo-dev\375A\004\000\000\000\000\000\000\000C\000\000\000\000 foo-dev\375A\004\000\000\000\000\000\000\000C\000\000\000\001 foo-dev\375A\004\000\000\000\000\000\000\000C\000\000\000\002 foo-dev\375A\004\000\000\000\000\000\000\000I foo-dev\375ClastCleanupFiles foo-dev\375ClastCleanupSessions foo-dev\375ClastCleanupTrash foo-dev\375CnextChunk foo-dev\375CnextCleanupSlices foo-dev\375CnextInode foo-dev\375CnextSession foo-dev\375CtotalInodes foo-dev\375CusedSpace foo-dev\375SE\000\000\000\000\000\000\000\001 foo-dev\375SI\000\000\000\000\000\000\000\001 foo-dev\375U\001\000\000\000\000\000\000\000 foo-dev\375setting从上面的 keys,可以看到我们创建的三个文件的元信息了, 这里面是用 slice_id 等信息关联的,所以能和对象存储里的数据 block 关联上。
可以基于上一节的 key 编码规则进一步解码,得到更具体的 sliceID/inode 等等信息,这里我们暂时就不展开了。
5 总结这一篇我们深入到 JuiceFS 内部,从数据和元数据存储中的东西来 反观 JuiceFS 切分数据和记录元数据的设计。 站在这个层次看,已经跟前一篇的理解程度全然不同。
如果说第一篇是“见自己”(功能如所见),这第二篇就是“见天(元数据引擎)地(对象存储)”, 那必然还得有一篇“见众生”。
参考资料- 官方文档:JuiceFS 如何存储文件, juicefs.com
- 官方文档:JuiceFS 开发:内部实现, juicefs.com
- JuiceFS 元数据引擎初探:高层架构、引擎选型、读写工作流(2024)
直观解读 JuiceFS 的数据和元数据设计(三):看山还是山(2024)
本系列分为三篇文章,试图通过简单的实地环境来直观理解 JuiceFS 的数据(data)和元数据(metadata)设计。
Fig. JuiceFS object key naming and the objects in MinIO.
水平及维护精力所限,文中不免存在错误或过时之处,请酌情参考。 传播知识,尊重劳动,年满十八周岁,转载请注明出处。
- 直观解读 JuiceFS 的数据和元数据设计(一):看山是山(2024)
- 直观解读 JuiceFS 的数据和元数据设计(二):看山不是山(2024)
- 直观解读 JuiceFS 的数据和元数据设计(三):看山还是山(2024)
对于一个给定的 JuiceFS 文件,我们在上一篇中已经看到两个正向的过程:
- 文件本身被切分成 Chunk、Slice、Block,然后写入对象存储;
- 文件的元数据以 inode、slice、block 等信息组织,写入元数据引擎。
有了对正向过程的理解,我们反过来就能从对象存储和元数据引擎中恢复文件: 对于一个给定的 JuiceFS 文件,
- 首先扫描元数据引擎,通过文件名、inode、slice 等等信息,拼凑出文件的大小、位置、权限等等信息;
- 然后根据 slice_id/block_id/block_size 拼凑出对象存储中的 object key;
- 依次去对象存储中根据这些 keys 读取数据拼到一起,得到的就是这个文件,然后写到本地、设置文件权限等等。
但这个恢复过程不是本文重点。本文主要看几个相关的问题,以加深对 JuiceFS 数据/元数据 设计的理解。 更多信息见官方文档 [2]。
1.2 juicefs info 查看文件 chunk/slice/block 信息JuiceFS 已经提供了一个命令行选项,能直接查看文件的 chunk/slice/block 信息,例如:
$ ./juicefs info foo-dev/file2_5MB foo-dev/file2_5MB : inode: 3 files: 1 dirs: 0 length: 5.00 MiB (5242880 Bytes) size: 5.00 MiB (5242880 Bytes) path: /file2_5MB objects: +------------+--------------------------------+---------+--------+---------+ | chunkIndex | objectName | size | offset | length | +------------+--------------------------------+---------+--------+---------+ | 0 | foo-dev/chunks/0/0/3_0_4194304 | 4194304 | 0 | 4194304 | | 0 | foo-dev/chunks/0/0/3_1_1048576 | 1048576 | 0 | 1048576 | +------------+--------------------------------+---------+--------+---------+和我们在 MinIO 中看到的一致。
2 如何判断 {volume}/chunks/ 中的数据是否是合法bucket 中的数据是 JuiceFS 写入的,还是其他应用写入的呢? 另外即使是 JuiceFS 写入的,也可能有一些数据是无效的,比如 size 为 0 的 block、超出所属 slice 范围的 block 等等。 我们来看看基于哪些规则,能对这些非法数据进行判断。
2.1 原理准备工作:
- 从 JuiceFS 的元数据引擎中读取所有 slice size,这对应的是元数据信息;
- 从 object storage 中读取所有 object key,这对应的数据信息。
接下来,根据几条标准,判断 bucket 中 {volume}/chunks/ 内的数据是否是合法的 JuiceFS 数据:
- 如果 object 不符合命名规范 {volume}/chunks/{slice_id/1000/1000}/{slice_id/1000}/{slice_id}_{block_id}_{block_size}, 那么这个 object 就不是 JuiceFS 写入的;
- 如果符合以上命名规范,,那么这个 object 就是 JuiceFS 写入的,接下来,
- 如果 object 大小为零,那可以清理掉,因为这种 object 留着没意义;
- 如果 object 大小不为零,根据元数据内记录的 slice/block 信息计算这个 block 应该是多大,
- 如果大小跟 object 一致,那这个 object 就是一个合法的 JuiceFS 数据(Block);
- 否则,说明这个 object 有问题。
这个过程是没问题的,但需要对所有 object 和所有元数据进行遍历和比对,效率比较低。 有没有更快的方法呢?
2.2 改进:pending delete slices回忆上一篇,在元数据引擎中其实已经记录了待删除的 slice/block 信息, 这里“待删除”的意思是 JuiceFS 中已经把文件删掉了(用户看不到了,volume usage 统计也不显示了), 但还没有从对象存储中删掉,
- D 开头的记录:deleted inodes
- 格式:D{8bit-inode}{8bit-length},
这种记录是 JuiceFS 在从 object storage 删除文件之前插入到元数据引擎中的, 所以扫描所有 D 开头的记录,可以找到所有待删除的 slice/block 信息。
2.3 工具:juicefs gc结合 2.1 & 2.2,就可以快速判断 bucket 中的数据是否是 JuiceFS 合法数据,不是就删掉; 基于 juicefs 已有的代码库,就可以写一个工具 —— 但用不着自己写 —— JuiceFS 已经提供了。
2.3.1 核心代码完整代码见 pkg/cmd/gc.go。
从元数据引擎 list 所有 slice 信息 func (m *kvMeta) ListSlices(ctx Context, slices map[Ino][]Slice, delete bool, showProgress func()) syscall.Errno { if delete m.doCleanupSlices() // 格式:A{8digit-inode}C{4digit-blockID} file chunks klen := 1 + 8 + 1 + 4 result := m.scanValues(m.fmtKey("A"), -1, func(k, v []byte) bool { return len(k) == klen && k[1+8] == 'C' }) for key, value := range result { inode := m.decodeInode([]byte(key)[1:9]) ss := readSliceBuf(value) // slice list for _, s := range ss if s.id > 0 slices[inode] = append(slices[inode], Slice{Id: s.id, Size: s.size}) } if m.getFormat().TrashDays == 0 return 0 return errno(m.scanTrashSlices(ctx, func(ss []Slice, _ int64) (bool, error) { slices[1] = append(slices[1], ss...) if showProgress != nil for range ss showProgress() return false, nil })) } 从对象存储 list 所有 objects 信息 // Scan all objects to find leaked ones blob = object.WithPrefix(blob, "chunks/") objs := osync.ListAll(blob, "", "", "", true) // List {vol_name}/chunks/ 下面所有对象 遍历所有 objects,跟元数据引擎中的 slice 信息比对 for obj := range objs { // key 格式:{slice_id/1000/1000}/{slice_id/1000}/{slice_id}_{index}_{size} parts := strings.Split(obj.Key(), "/") // len(parts) == 3 parts = strings.Split(parts[2], "_") // len(parts) == 3 sliceID, _ := strconv.Atoi(parts[0]) // slice id, JuiceFS globally unique blockID, _ := strconv.Atoi(parts[1]) // blockID in this slice blockSize, _ := strconv.Atoi(parts[2]) // block size, <= 4MB sliceSizeFromMetaEngine := sliceSizesFromMetaEngine[uint64(sliceID)] // tikv 中记录的 slice size var isEmptySize bool if sliceSizeFromMetaEngine == 0 { sliceSizeFromMetaEngine = sliceSizesFromTrash[uint64(sliceID)] isEmptySize = true } if sliceSizeFromMetaEngine == 0 { foundLeaked(obj) continue } if blockSize == chunkConf.BlockSize { // exactly 4MB if (blockID+1)*blockSize > sliceSizeFromMetaEngine foundLeaked(obj) } else { // < 4MB if blockID*chunkConf.BlockSize+blockSize != sliceSizeFromMetaEngine foundLeaked(obj) }- slice size 为 0,说明这个 slice 在元数据引擎中被 compact 过了;
- slice size 非零,
- block size == 4MB,可能是也可能不是最后一个 block;
- block size != 4MB,说明这个 block 是最后一个 block;
大致效果:
$ ./juicefs gc tikv://192.168.1.1:2379,192.168.1.2:2379,192.168.1.3:2379/foo-dev <INFO>: TiKV gc interval is set to 3h0m0s [tkv_tikv.go:138] <INFO>: Data use minio://localhost:9000/juicefs-bucket/foo-dev/ [gc.go:101] Pending deleted files: 0 0.0/s Pending deleted data: 0.0 b (0 Bytes) 0.0 b/s Cleaned pending files: 0 0.0/s Cleaned pending data: 0.0 b (0 Bytes) 0.0 b/s Listed slices: 6 327.3/s Trash slices: 0 0.0/s Trash data: 0.0 b (0 Bytes) 0.0 b/s Cleaned trash slices: 0 0.0/s Cleaned trash data: 0.0 b (0 Bytes) 0.0 b/s Scanned objects: 37/37 [=================================] 8775.9/s used: 4.268971ms Valid objects: 37 11416.0/s Valid data: 134.0 MiB (140509216 Bytes) 41.0 GiB/s Compacted objects: 0 0.0/s Compacted data: 0.0 b (0 Bytes) 0.0 b/s Leaked objects: 0 0.0/s Leaked data: 0.0 b (0 Bytes) 0.0 b/s Skipped objects: 0 0.0/s Skipped data: 0.0 b (0 Bytes) 0.0 b/s <INFO>: scanned 37 objects, 37 valid, 0 compacted (0 bytes), 0 leaked (0 bytes), 0 delslices (0 bytes), 0 delfiles (0 bytes), 0 skipped (0 bytes) [gc.go:379] 3 问题讨论 3.1 chunk id 和 slice id 的分配- 每个文件都是从 chunk0 开始的;
- 实际上没有 chunk id 的概念,只是在查找文件的过程中动态使用,并没有存储到数据和元数据中;
代码里就是直接根据 64MB 计算下一个 chunk id,接下来的读写都是 slice 维度的, slice id 是全局唯一的,会存储到数据(object key)和元数据(tikv keys/values)中。
下一个可用的 sliceID 和 inodeID 记录在 global unique 变量中,初始化:
Register("tikv", newKVMeta) // pkg/meta/tkv_tikv.go |-newBaseMeta(addr, conf) // pkg/meta/tkv.go |-newBaseMeta(addr, conf) // pkg/meta/base.go |-.freeInodes // initialized as default value of type `freeID` |-.freeSlices // initialized as default value of type `freeID`然后,以写文件为例,调用栈:
Write(off uint64, data) |-if f.totalSlices() >= 1000 { | wait a while | } |-chunkID := uint32(off / meta.ChunkSize) // chunk index, or chunk id |-pos := uint32(off % meta.ChunkSize) // position inside the chunk for writing |-for len(data) > 0 { | |-writeChunk | |-c := f.findChunk(chunkID) | |-s := c.findWritableSlice(off, uint32(len(data))) | |-if no wriatable slice { | | s = &sliceWriter{chunk: c, off: off, } | | go s.prepareID(meta.Background, false) // pkg/vfs/writer.go | | |-NewSlice | | |-*id = m.freeSlices.next // globally unique ID | | | | c.slices = append(c.slices, s) | | if len(c.slices) == 1 { | | f.refs++ | | go c.commitThread() | | } | |-} | |-return s.write(ctx, off-s.off, data) | NewSlice // pkg/meta/base.go |-} 3.2 JuiceFS pending delete slices 和 background job 3.2.1 设计初衷引入 pending delete slices 主要是大批量删除场景的性能优化:
- 每个 JuiceFS 客户端只允许并发 100 的删除操作;
- 超过 100 时,自动放入后台队列,由 background job 异步删除;
这个 maxDeleting 初始为一个 100 的 buffered channel,每次删除文件时,会尝试往里面放一个元素,
// pkg/meta/base.go func newBaseMeta(addr string, conf *Config) *baseMeta { return &baseMeta{ sid: conf.Sid, removedFiles: make(map[Ino]bool), compacting: make(map[uint64]bool), maxDeleting: make(chan struct{}, 100), // 代码里写死了 100 ... 3.2.3 潜在的问题后台删除是 JuiceFS client 中的 background job 做的,这个 background job 的开关是可配置的,
$ ./juicefs mount --no-bgjob ... # 关闭 background job这个开关的控制有点 tricky:
- 打开:如果一个 volume 的客户端太多,大家都会去做后台清理,都获取文件锁,对元数据引擎的压力非常大;
- 关闭:没有客户端去做后台清理,导致这些文件一直存在于对象存在中,也可以称为文件泄露,使用成本上升。
一种折中的做法:
- 客户端不太多的 volumes:默认启用 bgjob;
- 客户端太多的 volumes,默认关闭 bgjob,然后指定特定的 client 开启 bgjob,代表这个 volume 的所有客户端执行清理操作。
从以上定义可以看到,理论上 JuiceFS 支持的单个文件大小是 maxSliceID (int64) * maxChunkSize, 以默认的 maxChunkSize=64MB(2^26 Byte)为例,
- 理论上限:2^63 * 2^26 = 2^(63+26) Byte。
- 实际上限:2^31 * 2^26 = 2^(31+26) Byte = 128PiB,这个数字来自官方文档。
实际上限是 128PiB 的原因也很简单,在代码里写死了,
// pkg/vfs/vfs.go const ( maxFileSize = meta.ChunkSize << 31 ) 3.4 为什么 JuiceFS 写入对象存储的文件,不能通过对象存储直接读取?这里说的“不能读取”,是指不能直接读出原文件给到用户,而不是说不能读取 objects。
看过本文应该很清楚了,JuiceFS 写入对象存储的文件是按照 Chunk、Slice、Block 进行切分的, 只有数据内容,且保护重复数据,还没有文件信息元信息(文件名等)。
所以,以对象的存储的方式只能读这些 objects,是无法恢复出原文件给到用户的。
3.5 JuiceFS 不会对文件进行合并Highlight:JuiceFS 不会文件进行合并写入对象存储, 这是为了避免读放大。
4 总结至此,我们对 JuiceFS 数据和元数据设计的探索学习就告一段落了。希望有了这些知识, 用户和工程师在日常的使用和维护 JuiceFS 过程中,看问题和解决问题能更加得心应手。
参考资料- 官方文档:JuiceFS 如何存储文件, juicefs.com
- 官方文档:文件数据格式, juicefs.com