使用完整文件系统代替传统压缩格式(展示版)

将Squashfs用作日常归档压缩格式

Posted by wszqkzqk on May 25, 2024
本文字数:7044

使用完整文件系统代替传统压缩格式

前言

Squashfs是一种只读文件系统,广泛应用于嵌入式、发行版镜像等场景,也可用于一般的备份或归档使用。相较于常见的zip7zrartar.gz等压缩格式,Squashfs的主要优势是它是一个完整的文件系统,在Linux下可以直接挂载使用,而不需要解压缩。这也意味着,Squashfs也可以作为独立的镜像直接传递给虚拟机等使用,而不需要更多的操作。

特点 传统压缩文件 Squashfs
类型 归档压缩文件 完整文件系统
读取方式 解压缩后读取 直接挂载读取
随机访问支持 支持较差 支持相对较好
是否可作为块设备

同时,Squashfs在使用合适的压缩算法及分块大小时,能够兼有高压缩率与高访问速度;Squashfs还支持文件级别重复数据删除,进一步节省空间。

Squashfs的支持也非常广泛,几乎所有的Linux发行版都支持Squashfs,而Windows和macOS也都有Squashfs的解压工具;对于zstd等相对较新的算法,在其他平台下打开采用对应压缩方式的Squashfs则可能需要安装额外的软件,例如7-Zip-zstd

基础用法

Squashfs支持多种压缩算法,包括:

  • gzip-comp gzip
  • lzo-comp lzo
  • lz4-comp lz4
  • xz-comp xz
  • zstd-comp zstd

在不追求极致压缩率的场景下,笔者推荐使用zstd:3的压缩算法,这样可以节省空间,而且压缩速度非常快、CPU占用很小。根据zstd算法的特性,使用较高压缩等级的zstd算法可以在牺牲一定创建压缩的速度的情况下获得更高的压缩率,但是并不会明显降低透明的解压缩的速度,因此,如果需要更高的压缩率,可以考虑使用更高等级的zstd压缩算法,例如将压缩等级设置为6或者15

笔者个人不太推荐使用解压性能相对较差的gzipxz等算法,因为这些算法的压缩率提升相较于zstd有限,而且解压速度较慢,不适合创建后的快速访问。

如果需要比zstd更快的透明访问/解压速度,可以考虑使用lz4算法,lz4的解压速度非常快,但是在实际场景中,zstd的速度其实也已经足够快了,而且在压缩率上一般还有一定的优势。(PS. zstdlz4的作者其实是同一个人)

需要注意的是,mksquashfs的基本命令格式为:

mksquashfs source1 source2 ...  FILESYSTEM [OPTIONS]

[OPTIONS]必须放到FILESYSTEM之后,否则会报错。

文件/目录级归档

如果需要归档已经挂载的文件系统上的文件,可以直接使用mksquashfs,例如:

# 归档 /home 目录
sudo mksquashfs /home /XXX/example/backup.sfs -comp zstd -Xcompression-level 3
# 归档 / 目录
sudo mksquashfs / /XXX/example/backup.sfs -comp zstd -Xcompression-level 3 -e /dev -e /proc -e /sys -e /tmp -e /run -e /mnt -e /media -e /lost+found -e /boot/efi -e /efi
# 归档 / 子卷
sudo mksquashfs / /XXX/example/backup.sfs -comp zstd -Xcompression-level 3 --one-file-system

这里只是用于演示较彻底的归档备份,实际使用中如果只是一般归档,往往并不需要sudo权限,例如:

# 归档 abc 目录
mksquashfs abc /XXX/example/backup.sfs -comp zstd -Xcompression-level 3

像这样创建的squashfs是一个直接包含归档文件的文件系统,可以直接挂载使用。

块设备级备份

有时候我们可能需要备份某些在Linux下不方便直接挂载并备份的分区,比如受BitLocker加密的NTFS分区等,这时,为了完成保留整个硬盘设备的信息,我们可以直接备份整个硬盘设备。如果需要备份整个硬盘设备,可以使用ddmksquashfs结合,即,用mksquashfs调用dd的输入流,例如:

sudo mksquashfs - /XXX/example/backup.sfs -p "backup.img f 0644 root root dd if=/dev/nvme0n1 bs=1M" -p "/ d 0755 0 0" -comp zstd -Xcompression-level 3

此处:

  • -表示标准输入流
  • -p表示添加文件/目录
    • -p后面的内容是mksquashfs的文件描述符
      • f表示类型为文件
      • 后面紧跟的是权限信息
      • dd if=/dev/nvme0n1 bs=1M表示调用dd的输入流
        • if=/dev/nvme0n1表示输入文件
        • bs=1M表示块大小
    • /表示根目录
    • d表示类型为目录
      • 后面同样跟有权限信息。

整体上,这个命令的意思是:

  • 需要在squashfs中创建根目录与backup.img文件
  • /dev/nvme0n1的输入流作为backup.img文件的内容
  • backup.img文件放在创建的squashfs文件系统的根目录

这样既保留了整个硬盘的完整存储信息,又启用压缩,节省了空间。不过,从这样的备份中读取内容时,需要先挂载squashfs文件系统,然后加载其中的backup.img文件为loop设备,再挂载所需要挂载的分区。(套娃)

利用pipe进行的其他备份

如前一节所示,mksquashfs支持从标准输入流读取数据,而且由于从一般的流输入通常需要另外指定权限信息与文件名等内容,从流中读取数据的用法和一般的程序形式[command A] | [command B]不同,可以归纳为:

mksquashfs - [FILESYSTEM] -p "filename f [mod] owner owner [command A]" -p "/ d [mod] owner owner" [OPTIONS]

除了dd外,我们还可以使用其他命令,例如,我们可以将btrfs send [subvolume name]的输出流作为mksquashfs的输入流,这样可以将btrfs的子卷快照备份为Squashfs:

mksquashfs - /xxx/example/backup.sfs -p "xyz.btrfs f 0644 0 0 sudo btrfs send /path/to/your/ro/subvolume" -p "/ d 0755 0 0" -comp zstd -Xcompression-level 10

将其他归档压缩格式转换为Squashfs

有时候,我们现有的归档压缩文件可能并不是Squashfs,而是其他格式,例如7ztar.gztar.xzzip等,这时,我们为了得到既节约空间,又不用解压缩就可以直接挂载使用的归档文件,往往需要将这些归档文件转换为Squashfs。

最容易想到的方法是:

  • 解压缩归档文件
  • 随后使用mksquashfs创建Squashfs

然而,这样做会解压产生很多中间文件,占用大量的空间,而且需要花费大量的IO时间

笔者在这里推荐充分利用管道,直接将归档文件的解压缩输出作为mksquashfs的输入流,在不产生中间文件的情况下,将归档文件转换为Squashfs。

转化tar.xxx到Squashfs

mksquashfs工具支持从标准输入流读取数据:

  • -代表标准输入流stdin
  • -tar参数指定从标准输入流读取tar格式的数据
  • 使用相应压缩软件gzzstd等的输出作为mksquashfs输入流

例如,小米发布的线刷包是一个tar.gz格式的归档文件,在使用时,一般需要先解压,然而,这势必会浪费大量的时间。而如果直接在本地存储解压后的文件,又会浪费大量的空间。因此,我们可以使用以下命令,将tar.gz格式的线刷包转换为Squashfs,既保证了压缩率,又不需要额外的解压缩过程:

gzip -cd zircon_images_V14.0.8.0.TNOCNXM_20231129.0000.00_13.0_cn_f63a143fa2.tgz | mksquashfs - zircon_images_V14.0.8.0.TNOCNXM_20231129.0000.00_13.0_cn_f63a143fa2.sfs -tar -p "/ d 0755 0 0" -comp zstd -Xcompression-level 6
  • gzip -cd表示解压缩(-c参数)tar.gz文件并输出到标准输出流stdout-d参数)
  • |表示管道
  • mksquashfs -表示将标准输入流作为mksquashfs的输入流
  • -tar表示从标准输入流读取未压缩的tar数据
  • -p表示定义Squashfs中的文件组织
  • / d 0755 0 0表示指定Squashfs文件系统的根目录
  • d表示类型为目录,后面跟有权限信息
  • -comp zstd -Xcompression-level 6表示使用zstd压缩,压缩等级为6

另外需要注意的是,解压命令需要与原压缩包的压缩格式相匹配,例如,如果原压缩包是tar.xz格式,那么解压命令应该是xz -cd,如果原压缩包是tar.zst格式,那么解压命令应该是zstd -cd

当然,对于本身即未压缩的tar数据,我们也可以直接使用mksquashfs-tar参数,直接用cat传入即可,例如:

cat example.tar |mksquashfs - example.sfs -tar -comp zstd -Xcompression-level 6

此外,如果本地还没有下载tar.xxx格式的归档文件,我们也可以直接使用wget的输出流进行转化,例如:

wget -O - https://example.com/example.tar.gz | gzip -d | mksquashfs - example.sfs -tar -comp zstd -Xcompression-level 6

这样,我们就在不产生中间文件的情况下,将远程的tar.xxx格式的归档文件转换为了Squashfs,可以直接挂载使用。

转化zip到Squashfs

由于mksquashfs工具可以高效地从标准输入流stdin中读取未压缩的tar数据,我们可以用类似的方法处理zip格式的归档文件,但细节上有所不同。

一般的zip归档文件通常与tar.xxx不同,它的归档与压缩均由zip完成,内部并不含有未压缩的tar数据。因此,我们在这里不能直接使用unzip的输出流进行管道操作。

幸运的是,bsdtar这一强大的工具可以直接读取zip格式的归档文件(只需要在文件路径前面加上@),并将其转化为tar流,通过管道正常传输给mksquashfs。因此,我们可以使用以下命令,将zip格式的归档文件转换为Squashfs:

bsdtar -c @test.zip | mksquashfs - test.sfs -tar -p "/ d 0755 0 0" -comp zstd -Xcompression-level 6
  • bsdtar -c @test.zip表示将test.zip文件转换为tar流并输出到标准输出流stdout
  • |表示管道
  • mksquashfs -表示将标准输入流作为mksquashfs的输入流
  • -tar表示从标准输入流读取未压缩的tar数据
  • -p表示定义Squashfs中的文件组织
  • / d 0755 0 0表示添加根目录
  • d表示类型为目录,后面跟有权限信息
  • -comp zstd -Xcompression-level 6表示使用zstd压缩,压缩等级为6

bsdtar直接读取归档的特性不仅仅适用于zip格式的归档文件,还适用于tar, pax, cpio, jar, ar, xar, rpm, 7z, ISO 9660 CDROM,以上格式均可利用类似方法转换为Squashfs。

Squashfs到Squashfs的转化

有时候,我们可能需要更换Squashfs的压缩算法、块大小、压缩等级等参数,这时,我们必须要另外创建新的Squashfs。

最简单的思路是:

  • 挂载现有Squashfs
  • 对挂载目录使用mksquashfs创建新的Squashfs

但是,这样的操作其实是在随机访问已挂载的Squashfs,IO性能可能较差。如果需要一次性访问Squashfs中的所有文件,先挂载再访问的方式一般会比使用unsquashfs

然而,如果直接使用unsquashfs解压缩Squashfs,又会产生大量的中间文件,占用大量的空间,而且需要花费大量的IO时间。

这里我们同样可以使用管道,将unsquashfs的输出作为mksquashfs的输入流,在不产生中间文件的情况下,将Squashfs转换为新的Squashfs。例如:

unsquashfs -pf - source.sfs | mksquashfs - output.sfs -pf - -comp zstd -Xcompression-level 6 -b 16K
  • unsquashfs -pf - source.sfs表示将source.sfs文件解压缩到标准输出流stdout
    • 同时也一并输出了文件组织的结构、权限等信息
  • |表示管道,mksquashfs -表示将标准输入流作为mksquashfs的输入流
  • -pf -表示从标准输入流读取文件组织的结构、权限等信息
  • -comp zstd -Xcompression-level 6表示使用zstd压缩,压缩等级为6
  • -b 16K表示块大小为16K

由此,我们在不产生中间文件的情况下,利用unsquashfs更优秀的IO性能,将原来的Squashfs快速转换为了新的Squashfs,并重新指定了压缩算法、块大小、压缩等级等参数。

展望

Squashfs是一个非常实用的只读文件系统,然而,这一文件系统创建于2009年,今天看来它的某些特性已经不是那么合适:Squashfs对随机访问的性能仍然有很大的提升空间。

EROFS是一个较新的只读文件系统,已广泛应用于Android的系统分区,它特别针对读取性能进行了优化,对随机访问有更好的支持。

  • Squashfs文件系统最多由九个部分组成
    • 超级块superblock
    • 压缩类型compression options
    • 数据块/片段datablocks & fragments
    • 索引节点表inode table
    • 目录表directory table
    • 片段表fragment table
    • 导出表export table
    • uid/gid查找表uid/gid lookup table
    • 扩展属性表xattr table
  • 这些部分按照字节对齐,结构紧凑
  • 数据块并没有对齐,不利于随机访问

访问未对齐的数据块往往会被迫读取多个数据块,造成额外的IO开销。而注重随机访问性能的一般文件系统,例如ext4、XFS、Btrfs、F2FS等,都会对数据块进行块对齐,以减少随机访问时的IO开销。EROFS也默认采用了块对齐的设计,对随机访问有更好的支持。

#~/img/storage-device/erofs-design-comparison.svg
Squashfs等未采用块对齐的文件系统与EROFS等采用块对齐的文件系统设计对比

块对齐的优势在于提高了I/O操作的效率,减少了不必要的内存占用,增强了数据缓存的灵活性和效率,特别是在内存有限的设备上。

此外,EROFS还支持了Squashfs所不支持的块级去重、ACL等功能:1

Feature (as of Linux 6.6) EROFS SquashFS
Minimal block size 512 B Unaligned
Inode size 32/64 B Varied
Limitation of total UIDs/GIDs No Yes (64k)
Pre-1970 / ns timestamps Yes No
Filesystem UUID Yes No
Filesystem label (Volume label) Yes No
Inline data Yes No
Data compression Yes Yes
Largest compression granularity 1 MiB 1 MiB
Default compression granularity 1 Block 128 KiB
Fragments Yes Yes
Metadata compression No Yes
Multiple compression algorithms Per-file No
Data deduplication Extent-based File-based
Extended attribute support Yes Yes
File-based distribution Yes No
External data (multi-devices) Yes No
POSIX.1e ACL support Yes No
Direct I/O support Yes No
FIEMAP support Yes No
FSDAX support Yes No
Large folio support Yes No

然而,由于EROFS社区开发者有限,且主要开发力量集中于内核空间,以及主要服务于Android的向前移植,目前EROFS的用户空间程序(EROFS文件系统制作程序)完善程度仍然不足:

  • 一切操作,包括IO、压缩等均在一个线程中完成,创建很慢
  • 块级去重仍为实验性功能
    • 去重的速度太慢

用户空间程序的不完善,使得EROFS在日常备份应用中的普及受到了一定的限制。不过,随着EROFS的不断发展,相信它会逐渐成为Squashfs的替代品。