首先声明一点,本文探讨的不是如何在生产环境中高效存储百万量级的小文件,而是在日常生活中该如何处理那些包含上万个小文件的情景。

前言

在 HDD 上处理大量小文件是一件非常头痛的事情,它存在一下问题:

1.速度很慢

这个慢体现在诸多方面——创建慢、删除慢、复制慢。

以我维护的某直接使用了文件作为缓存后端的辣鸡 laravel 项目为例。它的缓存文件夹只有数百兆大小,然而有近十万个文件,五万个文件夹!

在服务器上时,因为 SSD 性能强劲的原因,这玩意儿工作起来还是没啥问题的。但是我本地 HDD 调试的时候就非常痛苦了——就这么区区几百兆的缓存,不仅每次创建都要花好几分钟,就连删掉它们也要很久。

2. 占用大量 inode

嘛……虽然说 inode 在个人电脑上一般不会用完,但是既然能省还是要省一点好。

3.占用大量空间

创建文件系统时如果没有调整的话,默认是 4K 一个块,这意味着存储大量小于 4K 的文件时会产生极大浪费。还是以那个辣鸡项目为例,du -sh 显示缓存文件夹占用了 629M,但是 du -sh --apparent-size 的结果却只有 237M——浪费了近 62% 的空间!

4.影响索引服务的速度

不要花时间索引这种无用的东西啊喂!

解决方案

打包存储

场景:持久保存、很少写入、频繁读取

优点:节省空间、方便传输、方便访问

缺点:不方便写入

如果你很确信这些文件不需要再修改了的话,那最简单的方案就是使用 SquashFS,这也是大部分 livecd 所使用的文件系统。

如果你还需要一点点可修改性的话,也可以选择用 zip 打包压缩,然后使用万能的 fuse 来挂载:

  • 如果只在 dolphin 里访问,安装 kio-extras 软件包并在 dolphin 里勾选“将归档文件作为文件夹打开”即可像访问普通文件夹一样访问压缩包。Gnome 的 GVFS 应该也有同样的功能,但是我不用 Gnome(

  • 如果需要其他程序访问,则需要使用 fuse-zip(支持读取、写入)或 avfs(仅读取、但支持的格式多)来手动挂载。

使用 tmpfs

场景:非持久保存、体积小、读取写入都很频繁

优点:一个字,快!!!

缺点:占内存……

tmpfs 中的内容直接储存在内存之中,因此速度上没话说。还是以我那个辣鸡项目为例,原先要花数分钟才能搞定的事情,用了 tmpfs 数秒钟就能完成了。

使用 tmpfs 最简单的方案就是直接 ln -s /tmp ./cache 建一个符号链接;Docker 的话麻烦一点,得参考 https://docs.docker.com/storage/tmpfs/ 手动挂载一下。

使用 loop device

场景:持久保存、读取写入都很频繁

优点:(搭配特定文件系统)节省空间、方便传输、方便访问

缺点:体积不够灵活

loop device 是一个非常棒的功能。通俗点来讲,就是把一个文件映射成一个块设备(block device)。我们可以像操作正常硬盘的块设备一样操作这个 loop deivce,比如格式化、挂载。对于需要持久存储并且也需要频繁读取写入的情景,这是一个很好的解决方案。

步骤举例:

  1. 创建一个 1G 的稀疏文件 disk.img

    dd if=/dev/zero of=./disk.img bs=1 count=0 seek=1G

  2. 格式化

    mkfs.ext4 -L Disk -T small disk.img

    这个地方可以根据自己的需要调整参数或是选择其他文件系统,这里为简单起见直接使用 ext4 + 预设的 small 配置。

  3. 挂载

    1
    2
    
    mkdir ./a_dir
    sudo mount disk.img ./a_dir
    

    接下来就可以将其当成一个普通目录使用了!

这样一来,访问、移动、复制、删除这个文件夹时都会非常方便。而且因为我们调整了块大小,文件存储得会更加紧凑,节省空间。

甚至,如果有压缩需求的话,我们还可以在这个地方使用 zfs/btrfs 以进一步节省空间。

以 btrfs 为例,首先格式化:

1
2
# 官方建议对小于 1GB 的文件系统启用 `--mixed` 开关,可以允许元数据和文件存储在同一个块上,有效节省储存空间
mkfs.btrfs -L Disk -M disk.img

接着挂载时启用压缩与自动碎片整理功能:sudo mount -o compress-force=zstd,autodefrag disk.img ./a_dir

我使用这种方法储存了一个包含约 5 万个文档、大小为 1GB 的文件夹,btrfs 的透明压缩在这里为我省下了约一半的空间(尽管我的镜像大小本身就有 1GB……)

这个方案唯一的缺点就是不够灵活——不管你存了多少文件,设备的大小是固定的。这一方面导致如果空间没存满的话就浪费了不少空间(当然,稀疏文件可以缓解这一问题,但是磁盘碎片还是会不可避免地浪费空间),另一方面空间不够的话扩容也是个麻烦事:linux - Can I expand the size of a file based disk image? - Super User