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 对压缩后的数据(而非压缩前)分块,这样的好处有:
- 压缩比更大,因为一次压缩输入的数据可能更多;
- 解压的时候只需要取包含了需要数据的块,没有额外的块读取和解压开销;
- 允许进行就地解压,内存开销能够稍微小一些;
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 解压
上述的方式还有两个问题:
- 还是需要动态分配内存页,不好;
- 每次解压都要调用
vmap
和vunmap
,不好;
为此,EROFS 静态分配了若干个页专门用于解压,具体来说,每个 CPU 会持有个页。当解压出来的数据不超过页的时候,就用这些页装解压后的数据,然后再 memcpy 到目标 page cache 位置。
论文中采用的为 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 没有单独的解压线程,而是由读数据的线程解压数据。
一旦硬盘块(硬件)准备好就会立即开始解压。
#协同解压
对同一个数据的多个请求会去重,避免重复解压相同的数据。
#镜像补丁
在镜像末尾放置更新数据,解压之后用新数据覆盖。