EROFS: A Compression-friendly Readonly File System for Resource-scarce Devices

#作者

Gao Xiang @ 华为,MIngkai Dong @ IPADS @ SJTU

USENIX ATC 2019

#背景

安卓系统本身对设备存储的占用越来越大。

这些系统数据大部分都是Read-only的。Read-only为压缩存储提供了巨大的优化空间。

#Ext4

Ext4允许只读挂载文件系统,但是可以通过重新挂载绕过这种限制,不够安全。

#Btrfs

支持压缩,文件会分成128KB的块

是general-purposed的文件系统,不能只为了压缩只读做很多优化

#SquashFs

Squashfs 广泛使用,支持多种压缩算法,可选的块大小4KB~1MB。

元数据可以被压缩,inode和目录紧密存储,文件内容逐个块压缩,在inode中用一个list保存每个块压缩后的大小。

#SquashFs 的问题

在低端设备上使用SquashFs会导致数秒的巨大延迟(使用FIO测试,文件enwik9)。

#IO放大和计算浪费:

SquashFs 按输入固定大小分块压缩,压缩后的数据是变长的。然后变长的数据被compact存储。

Squashfs固定要对原始数据先按fixed-size分块,分别压缩,再拼接到一起,即使只改了一个字节也要重复整个过程;

读数据需要先把整个block读进来,整个解压缩后才能定位到需要读的数据;

工程实现问题:压缩过的块和磁盘不强制对齐,最坏情况需要多读和解压两个块;

可以把块大小减小到4KB,然而会降低压缩率

#内存放大和数据拷贝

SquashFs 在解压数据的时候需要大量的临时内存。

读写数据的时候需要多次分配内存用于page cache/保存解压缩后的数据等;

在小内存机器上会额外加剧swapping开销;

额外参考:Android 为何不使用 SquashFS

#EROFS:增强的压缩文件系统

#Fixed-sized output compression

为了缓解读放大的问题,EROFS对压缩后的数据(而非压缩前)分块,这样的好处有:

  1. 压缩比更大,因为一次压缩输入的数据可能更多;
  2. 解压的时候只需要取包含了需要数据的块,没有额外的块读取和解压开销;
  3. 允许进行就地解压,内存开销能够稍微小一些;

EROFS采用了滑动窗口来压缩数据,在 输入大小到达 1MB 或者 输出大小到达 4k 时输出一个新的block。

#缓存层优化

对于压缩型文件系统,为了保存解压后的数据,需要额外分配内存,这部分开销很大。

EROFS对于是否要读完整的块,采用了两种不同策略,以优化读性能:

  • Cached I/O:正常使用内核的Page Cache;
  • In-place I/O:绕过Page Cache机制;

对于只需要读一部分数据(部分解压)的块,使用 Cached I/O。EROFS使用了一个特别的inode的page cache保存 物理块号->压缩块 之间的映射关系,在读取时可以直接定位到需要的块。

对于需要读全部数据(完全解压)的块,使用 In-place I/O。每次读文件会主动从Page Cache层分配页去保存文件数据,然后就地解压数据;空闲下来的页会被复用,以减小频繁内存分配。

这里的想法是一个已经解压过的块不太可能被再次使用(因为已经有了解压后的原始数据),占用的page cache就可以回收利用;如果仍然cache的话就没有必要,而且会增加内存抖动。

#解压缩优化

EROFS对解压做了很多细致的优化。

#vmap解压

在EROFS中,每次解压一个块可能会得到多块数据,如果只需要读其中一部分,那么解压出来的其他数据块就没必要保存。为此,EROFS利用了虚拟内存映射机制,将不需要的块映射到临时页。

实现上,每个压缩的硬盘块中会保存原始数据的元数据(如长度),在解压时会先准备好足够的 Buffer,然后使用 vmap 手动映射成连续的虚拟地址空间, 以调用解压缩算法。

在解压时,对于需要的部分,会vmap映射一段page cache空间,对于不需要的部分,只会映射到临时页。

#Per-CPU解压

上述的方式还有两个问题:

  1. 还是需要动态分配内存页,不好;
  2. 每次解压都要调用vmapvunmap,不好;

为此,EROFS静态分配了若干个页专门用于解压,具体来说,每个CPU会持有kk个页。当解压出来的数据不超过kk页的时候,就用这些页装解压后的数据,然后再memcpy到目标page cache位置。

论文中采用的kk为4。

#滚动解压

由于LZ4算法需要当前位置之前最多64KB的数据才能解压。EROFS会给每个CPU预先分配若干个物理页,作为vmap解压时存储数据的基本buffer。这些物理页会循环使用。

#实现

EROFS已经合并进内核。使用内核的4KB块大小,LZ4 (v1.8.3) 压缩算法,文件元数据没有被压缩。

atc19-erofs 是合并进5.3-rc1的分支。

atc19-mkfs是论文版本的utils,最新版本在erofs-utils

kmod-erofs (WIP)是out-of-tree版本的erofs模块,还没完成。

#EROFS镜像布局

和其他文件系统一样,EROFS镜像的开头也是Super block;然后是混合存储的元数据和数据区。

EROFS 允许元数据(inode,目录)和数据混合存储,即inline data,目的是提高局部性,减少I/O操作。

TODO

#解压策略

商业版:少于4页时采用per-CPU解压,否则采用vmap解压;

最新版:增加了in-place解压的优化。

#优化

#索引内存优化

TODO

#调度优化

EROFS没有单独的解压线程,而是由读数据的线程解压数据。

一旦硬盘块(硬件)准备好就会立即开始解压。

#协同解压

对同一个数据的多个请求会去重,避免重复解压相同的数据。

#镜像补丁

在镜像末尾放置更新数据,解压之后用新数据覆盖。

#评估

#相关工作