SplitFS(SOSP'19)
导读:
- 本质:这是一篇挺有意思的NVM文件系统论文。其本质是利用
mmap()
实现对已有的文件系统套壳,然后通过避免陷入内核来减少NVM文件系统的软件开销。这是一种新颖且具启发性的研究思路,让人看完不禁赞叹:原来NVM文件系统还能这么设计,原来顶会论文还能这么发……- 延伸:个人感觉其做的工作与PMDK库类似。考虑到PMDK依靠下层文件系统提供的基本
mmap()
接口,然后为上层提供更易用的NVM读写接口,例如:pmem_persist()
(该函数写入并调用fence
、flush
持久化数据);而SplitFS为上层提供的是POSIX接口。- 感想:
- 系统设计中很难有非常创新的idea,因此动机在存储系统设计中占有举足轻重的地位。
- 系统的设计思路不需要很复杂,足够为其动机服务即可。
1. 背景 (Background)
1.1 PM与PM文件系统
关于PM的特点,在之前的文章中已经讲过许多,这里对SpliFS的介绍进行简单概括。PM(NVM)拥有与DRAM相同数量级的性能参数:
Latency (PM/DRAM) | Bandwidth (PM/DRAM) | |
---|---|---|
写 (Store) | ≈ 1 × \approx 1\times ≈1× | 1 / 6 1/6 1/6 |
读 (Load) | 2 − 3.7 × 2-3.7\times 2−3.7× | 1 / 3 1/3 1/3 |
单个机器可以配备至多6TB的PM设备(每台机器有两个CPU,每个CPU可以插6块PM,目前每块PM最大512GB,因此6TB),因此将PM设备作为存储介质是很重要的一种应用场景。
传统的文件系统在写路径上有着巨大的开销,例如:块分配、日志、更新复杂的结构等;最新的PM文件系统针对PM特点进行设计,大大减少了软件开销。其中Aerie与Strata两个文件系统都通过尽可能避免内核态操作来减少软件开销:
- Aerie: Aerie构建了一个用户态文件系统库,并且利用一个微内核模块对PM空间进行粗粒度分配;
- Strata: Strata将文件系统部署在用户态,包括一个用户态文件系统库以及一个用户态元数据服务器(用于更新元数据);
然而,这些文件系统在数据写入方面还是存在着较大的开销。
1.2 DAX与Memory Mapping
- DAX文件系统可以绕过page cache,利用memory mapping直接访问PM
mmap()
将进程虚拟地址空间中的一个或多个页映射到 PM 上的数据块,这样应用对进程虚拟地址的访问就会被硬件翻译转换到对PM的访问- DAX和
mmap()
虽然提供了对PM的低延迟访问,但却没有提供原子操作、文件管理等,因此,PM文件系统仍然为应用和用户提供了非常有用的特性。
2. 动机(Motivation)
本文以顺序追加写入4K大小块为出发点,观察了几个不同文件系统的写入开销,如下表所示:
值得说明的是,在本文的实验环境下,写入4KB数据到PM上只消耗671ns,因此 O v e r h e a d = A p p e n d T i m e − 671 n s Overhead=Append\ Time - 671ns Overhead=Append Time−671ns。从表中的结果来看,可以发现对于文件追加写这一场景来说,现有的PM文件系统造成了 3.5 − 12.4 × 3.5-12.4\times 3.5−12.4×的额外开销。
上述观察说明了问题的严重性。但我们还应该思考的是:**问题在真实场景下真有这么严重吗?**频繁Append I/O是存在的吗?或者说存在的情况多吗?此是后话,旨在唤醒读者的个人思考。
3. SplitFS设计与实现 (Design & Implementation)
3.1 设计目标 (Design goals)
- 低软件开销: SplitFS主要目标是减少数据操作的开销,尤其是写入与追加写入;
- 透明性: 上层应用不感知;
- 最小化数据拷贝与写I/O:尽可能减少对PM的写,一方面提升性能,另一方面避免PM的磨损;
- 减少开发复杂性:基于EXT4-DAX进行开发,减少对SplitFS的代码实现与维护量;
- 灵活性:应用有多种一致性选项可以选择,例如POSIX、SYNC等;而现有的PM文件系统为所有应用提供了相同的一致性选项(因为大家都用同一文件系统)。
3.2 模式选项 (Mode)
作者为SplitFS提供了多种选项,猜测在其研究过程中,发现STRICT模式下性能欠佳,通过放松语义性能更高。
SplitFS为应用提供了三种不同的模式:POSIX、SYNC以及STRICT,并行的应用可以通过使用不同的模式来达到不同的目标(例如:有些应用不需要每次I/O都是同步的,而有些应用则需要)。下表总结了各模式之间的差异:
1. POSIX模式
在POSIX模式下,SpiltFS提供元数据一致性,这与EXT4 DAX相似。当系统崩溃后,文件系统根据元数据回滚到一致的状态。在该模式下,覆盖写入是同步的,并且是原地更新的(即,可能出现数据写一半掉电的情况)。注意Append写入不是同步的,需要显示调用fsync()
函数来进行持久化。然而,SplitFS能够保证原子地追加写。SplitFS的POSIX与传统POSIX语义有所不同的地方在于:当文件被访问或是修改时,文件的元数据不会立刻被更新。事实上,这与O_DSYNC
语义类似。参考这里。
2. SYNC模式
在SYNC模式下,SpiltFS在POSIX的基础上保证了操作的同步性:任何文件操作都不需要通过显式调用fsync()
即可保证其已被持久化。但是在该模式下,操作同样不是原子的,例如:数据写一半掉电。
3. STRICT模式
在该模式下,SplitFS在SYNC的基础上保证了操作的原子性。然而,该模式不能保证跨系统调用的原子性,例如,不能保证同时更新两个文件的原子性。
3.3 系统概述 (Overview)
下表总结了SpiltFS使用的技术和其达到的目的:
1. Split architecture:
SplitFS架构如下图所示:
包含两个主要部分:
- U-Split: 链接到不同应用的用户态文件系统库
- K-Split: 一个内核文件系统
其中,SplitFS在用户态执行数据操作(例如,read()
,write()
等),而将元数据操作(例如,fsync()
,open()
等)导向下层内核文件系统。这种设计方法类似Exokernel,其中所有控制操作都由内核处理,数据操作都在用户态处理。
2. Collection of mmaps
读和覆盖写通过mmap()
一个文件的2MB来实现,且读通过调用memcpy()
,写通过调用non-temporal store
。值得说明的是,一个逻辑文件的数据可能存储在多个物理文件内(被mmap()
的文件)。例如,SplitFS会将对某文件的Append数据先发送到staging file
内(后面讲到staging file
,其实就是类似一个缓冲区),这样一来,该文件的数据就至少存在于原文件与staging file
内了。
SplitFS通过Collection of mmaps来处理这种情况,即,为每个文件关联一系列对不同物理文件的mmap()
集合,此后,读和覆盖写便能够正确地被引导到不同的mmap()
区域内了。
3. Staging
SplitFS利用临时的staging file
来处理append操作和原子地数据操作。对某文件的Append操作会先被引导到一个staging file
,然后被批量relink
到该文件内(后面讲到什么是relink
)。类似的,在STRICT模式下文件的覆盖写(要保证原子性)也会先被发送到staging file
中,然后再被relink
到相应的文件内。
4. Relink
Relink
操作由fsync()
触发,如前文所述,在staging file
中的append操作和覆盖写操作数据会被relink
到相应的文件中。
为了实现这个目标,一种很简单的方案是为被relink
的目标文件分配新的数据块,然后将数据拷贝至这些数据块中。然而,这种方式带来额外的数据拷贝,造成了极大的写放大。Relink
优雅地解决了这个问题。如下图所示
对上图进行简单解释:
- 每个
staging file
会预分配块(这些块是被mmap()
出来的); - 对目标文件进行append操作后,数据会被写入到
staging file
中,后续对该数据块的读也会被映射到该区域中; - 调用
fsync()
后,relink
将staging file
中的数据块逻辑链接到目标文件中,不修改物理块与mmap()
5. Optimized logging
在STRCIT模式下,SplitFS利用Operation Log
和redo logging
的方式保证操作的原子性。每个U-Split都有一个独占的、预分配的(利用mmap()
)的Operation Log
,对Operation Log
的写入利用non-temporal store (nt-store)
进行。同时,SplitFS还利用fence
指令来保证Operation Log
中记录项的顺序性。
为了减少Logging带来的开销,对于每个常见的操作(例如:write()
,open()
等),SplitFS向Operation Log
中写入一个cache line (64B)
的数据,并且使用单个fence
指令。需要说明的是,Operation Log
不包含数据,只包含指向这些数据的指针。
此外,作者对Operation Log
做出如下优化:
-
减少fence。计算checksum保证日志项的有效与否,避免了二次
fence
。传统在PM上写日志项的流程是:先写日志项内容,然后fence、flush;然后标记日志项有效,然后fence、flush;使用checksum后,可以写checksum+日志项内容,然后fence、flush,此时,不完整的写入可以在后续进行checksum校验的时候被检查出来。
这里需要考虑的是checksum计算的速度。
-
高并发。日志的尾部在内存中被原子更新,支持高并发写日志。
-
清零与重放。初始化SplitFS时,将Log清零。在崩溃恢复过程中,通过检查非0的日志项的checksum来判断日志项是否有效。当Log满后,SplitFS为当前应用的所有已打开文件调用
relink
,即,清空staging file
。接下来便可以将Log清零,然后复用。
6. 原子操作
SplitFS的原子操作结合了staging file
、relink
和optimized log
。比如,append操作数据先写到staging file
,然后在optimized log
中记录该操作以及数据的位置。在fsync()
的过程中,staging file
中的数据将通过relink
操作被原子地追加到目标文件中。
3.4 读、覆盖写、追加写操作
1. 读
读操作会在Collection of mmaps中查询对应offset
的最新数据(因为数据可能在staging file
中,或是被覆盖写,或是被append)
如果对应的数据块没有被mmap()
,那么该offset
周围2MB的数据区域将会被映射上来,并且被添加到Collection of mmaps中,然后再被读取。
2. 覆盖写
与读操作类似,如果offset
已经被映射了,那么覆盖写就直接改映射后的区域;否则mmap()
上来再改。
在STRICT模式下,SplitFS会先将数据写入staging file
中,然后将覆盖写操作用Operation Log
记录下来,最后在fsync()
或是close()
的时候relink
。
3. 追加写
所有追加写操作都先被写入staging file
中,然后在fsync()
或是close()
的时候relink
。同样,在STRICT模式下,SplitFS会把append操作记录到Opeartion Log
中。
3.5 系统实现 (Implementation)
- 系统调用截取:利用
LD_PRELOAD
截获诸如open()
、write()
等系统调用即可 - Relink:通过
ioctl
实现relink
操作。relink
操作会释放目标文件(被link
的文件)的被替换的数据块。在实现过程中,relink
不会进行任何拷贝、持久化操作,只会对文件的源数据进行修改。同时,需要保证relink
后的mmap()
区域仍然存在,这样能够有效避免page fault
- 文件的
open()
与close()
:在open()
一个文件的时候,SplitFS将该文件的属性缓存在内存中,以便于后续调用。此后,当文件close()
后,SplitFS也不会释放该缓存信息。当文件被删除后,该信息才被清除。同样,如果文件被mmap()
了,当且仅当文件被删除时才会unmmap()
该文件。这些被缓存的信息被U-Split用于快速检查文件的状态。 - Fork:当
fork()
被调用后,SplitFS会同时被拷贝到新进程的地址空间中,这样一来子进程也可以访问SplitFS。 - Execve:
execve()
会覆盖当前进程的地址空间,但是已经被打开的文件描述符(open file descriptors)应该能被继续使用。为了处理这种情况,SplitFS在调用execve
之前将在内存中有关打开的文件描述符信息先拷贝到/dev/shm/pid
中,pid是当前进程的进程号。执行完execve()
后,SplitFS检查/dev/shm/pid
,并且把相应的信息拷贝到当前进程中。 - Dup:当文件描述符被复制后,文件的偏移量和文件状态标志将被共享。为了处理这种情况,SplitFS为每个打开的文件维护一个
offset
,并且使用指针指向该文件。如此一来,如果两个线程都dup()
了同一个文件描述符,那么offset
的修改对这两个线程都可见(通过指针)。 - Staging files:SplitFS会在初始化时预先分配
staging file
,它会创建10个160MB的文件。当一个staging file
被使用殆尽后,一个后台线程会被唤醒,并预先创建、分配新的staging file
,这避免了在关键路径上创建staging file
的开销。 - memory-mappings缓存:SplitFS将所有
mmap()
缓存起来,放到Collection of memory mappings中,一个mmap
区域当前仅当unlink
时才被unmmap
。这减少了在关键路径上的创建mmap()
开销。 - 多线程访问:SplitFS通过一个无锁队列来管理
staging file
。它还利用细粒度读写锁来保护内存中关于已打开文件、inodes和mmap
区域的信息。
3.6 可调节参数 (Tunable parameters)
mmap()
大小:目前的实现中,SplitFS支持mmap()
的大小为2MB到512MB。默认选项是2MB。staging file
在初始时的数量:默认情况下是10个。因为实验表明初始化10个staging file
在性能、初始化开销以及空间利用间提供了较好的平衡。Operation Log
的大小:默认128MB,支持2M个操作。意味着尾延迟可能较小。
3.7 安全保证 (Security)
相对于内核文件系统,SplitFS并没有带来额外的问题,因为:
- 所有元数据操作都在内核执行
- 只允许有权限的用户访问
- 各U-Split间隔离
4. 评估 (Evaluation)
本节通过评估SplitFS、PMFS、EXT4 DAX、NOVA,回答以下6个问题
- SplitFS的各系统调用性能和EXT4 DAX比何如(因为SplitFS是基于EXT4 DAX实现的)?
- SplitFS各项技术对最终性能的贡献?
- SplitFS相比其他文件系统在不同的访问模式下何如?
- SplitFS相较于其他PM文件系统来说有减少软件开销吗?
- SplitFS在真实场景下如何?
- SplitFS的计算和存储开销是怎样的?
4.1 实验环境
- Optane DCPMM:768GB
- DRAM:375GB
- LLC:32MB
- OS:Ubuntu 16.04
- Kernel:Linux 4.13
4.2 测试负载
下面简单介绍一下各负载情况:
- TPC-C on SQLite:TPC-C是一个在线事务处理的基准测试负载。它有5种包含不同比例的读写的事务种类;
- YCSB on LevelDB:YCSB是个键值存储基准测试负载;
- Redis:Redis会将更新记录到一个日志文件中,然后每秒对该文件调用
fsync()
; - Git、Tar、Rsync:
- Git:对Linux Kernel运行
git add
,git commit
十次 - Tar:压缩Linux Kernel 4.18
- Rsync:将7GB的数据集从PM的一个位置拷贝到另一个位置
- Git:对Linux Kernel运行
4.3 正确性与恢复测试
- 正确性测试:对比运行SplitFS后和运行EXT4 DAX后文件系统的状态,发现一致;
- 恢复时间:
- 在POSIX和SYNC模式下,SplitFS依赖EXT4 DAX便可恢复;
- 在STRYICT模式下,SplitFS的恢复时间依赖于日志中的日志项多少。测试结果表明,最坏情况下的恢复时间为6s;
4.4 SplitFS的系统调用开销
为了说明以开销较大的元数据操作换取快速数据操作是一个很好的trade-off,设计了一个类似Filebench中Varmail的负载进行测试:
得出如下三点结论:
- SplitFS的数据操作远快于EXT4 DAX
- SplitFS的元数据操作略慢于EXT4 DAX,因为SplitFS还要构建一些自己的结构
- 随着语义的增强,开销越来越大
总的来说,SplitFS达到了最初的设计目标。
4.5 SplitFS性能分解测试
这里设计了顺序4K覆盖写与追加写两个实验,fsync()
在每次操作后都会被调用,实验结果如下:
- 顺序覆盖写:性能是EXT4 DAX的两倍,因为SplitFS直接通过
nt-store
进行数据写入; - 顺序追加写入:仅使用
staging file
来buffer数据性能提高了2倍,这是因为在fsync()
时存在大量数据拷贝;加入relink
后,避免了这些数据拷贝,因此性能变为EXT4 DAX的5倍;
4.6 不同I/O模式下性能
测试顺序读、随机读、顺序写、随机写、追加写,单次I/O 4KB,共128MB。测试结果如下图所示
在所有模式下,SplitFS均有不同程度的提升,其中:
- 在POSIX和SYNC模式下append提升最显著,因为避免了陷入内核,所有写操作均在用户态完成。
- 在STRICT模式下,SplitFS的性能有了下降,这是因为日志机制。但其仍然快于NOVA。
4.7 减少软件开销
定义软件开销时间=一次系统调用的时间-访问PM设备的时间。下图展示了SplitFS在写繁重负载下的软件开销:
这里简要说明上图的负载:
- Load A与Run A:LevelDB上运行YCSB的Load A和Run A
- TPCC:SQLite上运行TPCC
不用多说,SplitFS怎么都是最少的。
4.8 数据繁重型负载上的性能
和NOVA、EXT4 DAX、PMFS相比,在YCSB、TPCC等工作负载上均有提升。
与Strata相比,SplitFS也均有不小的提升:
4.9 元数据繁重型负载上的性能
GIT、TAR、RSYNC是元数据繁重型负载,如图6所示,SplitFS在这些负载的性能上没有太大的提升,反而有所下降,最大性能下降为13%。这是因为SplitFS在元数据操作方面要构建、维护额外的结构。
4.10 资源开销
SplitFS要消耗内存资源来维护文件相关的元数据,例如追踪已打开的文件描述符、staging file
等,这里对SplitFS的内存消耗和CPU利用率进行了评估:
- 内存开销:最多只使用100MB,STRICT模式下多用40MB;
- CPU利用率:后台线程占用一个物理线程,有时会占用100%的CPU;
总结
SplitFS最大的贡献在于提出了一种新型文件系统架构:利用mmap()
构建从用户态到内核态的关联,并能够在用户态对数据进行操作,从而大大提升了文件系统数据操作的性能。
此外,SplitFS的多一致性模式设计(允许不同应用同时运行不同的一致性模式)也为我们带来了不小的启发。