APP下载

纯用户态的网络文件系统
——RUFS

2020-09-29董豪宇

计算机应用 2020年9期
关键词:键值服务器端客户端

董豪宇,陈 康

(清华大学计算机科学与技术系,北京 100084)

0 引言

传统的文件系统,例如Ext4[1],都实现在内核态;但内核态的编程需要具备内核相关的知识,有很高的开发门槛。内核态程序的错误常常会影响到整个操作系统稳定运行。如果程序使用到了内核的内部接口,整个程序会变得难以维护和移植。由于这些原因,将文件系统实现在用户态成为了新的趋势。分布式的文件系统,例如GlusterFS(Gluster File System)[2]、GFS(Google File System)[3],由于涉及到复杂的容错策略和网络通信,本身的逻辑很复杂,几乎都实现在用户态。本地文件系统添加某些功能(例如加密[4-5],检查点(checkpoint)设置优化[6]等),也会优先考虑使用FUSE(File system in USErspace)[7]搭建堆栈文件系统,将额外的功能实现在用户态,而不是直接在内核的文件系统上作改动。许多出于研究目的搭建的文件系统[8-10],也都通过FUSE 实现在了用户态。

许多用户态的文件系统,存储过程基于本地的文件系统,在存储的过程当中,会发生用户态与内核态的切换。这种切换会引发系统调用、上下文切换、用户态与内核之间内存的拷贝,这些过程为系统带来了额外的软件开销。

新一代的存储硬件——NVMe(Non-Volatile Memory express)固态硬盘(Solid State Drive,SSD),能够提供10 μs 以下的延迟和高达3 GB/s 的带宽。硬件速度的提高,使得存储系统的软件开销成为了不可忽视的一部分[11]。如果将整个存储过程迁移到用户态,消除系统因为进入内核而产生的开销,整个系统的性能就可能得到改善。

出于这样的目的,Intel 开发了一套高性能的存储性能开发 套 件(Storage Performance Development Kit,SPDK)[12]。SPDK 诞生于2015年,目前学术界已经有了一些基于SPDK 的研究[13-15]。由于绕过了内核,并采用了轮询的事件处理方式,相较于内核驱动,SPDK 的NVMe 驱动能够提供更低且更稳定的延迟。在用户态驱动之上,SPDK还提供了具有不同语义的存储服务,开发者可以在此基础上进行存储系统的开发,而无需关注驱动的实现细节。

在另外一个方面,随着InfiniBand 等新硬件的成本下降[16],以及RoCE(RDMA over Converged Ethernet)[17]技术的成熟,RDMA(Remote Direct Memory Access)技术逐渐在数据中心中普及,学术界也诞生了许多基于RDMA 构建的系统[18-21]和相关的研究[22-24]。RDMA 技术允许机器在目标机器CPU 不参与的情况下,远程地读写目标机器中的内存。相较于传统的TCP/IP 网络栈,RDMA 技术不仅能提供更低的延迟和更高的带宽[25],还减少了CPU 的开销。由于RDMA 协议工作在用户态,使用RDMA进行数据传输,还能避免内核-用户态切换、内存拷贝等过程带来的开销。

当前的用户态文件系统,都依赖于本地文件系统进行实际存储。由于内核-用户态切换的开销,无法充分地利用NVMe SSD 的性能。另外一方面,多数文件系统为了实现较高的性能,默认不保证数据实时保存到磁盘介质上。本文希望设计一个纯用户态的网络文件系统,减少存储过程中的软件开销,充分发挥NVMe SSD 的硬件性能,并提供同步语义,保证数据实时持久化。同时,利用RDMA进行网络通信,对外提供一个高吞吐和低延迟的文件系统服务。

本文设计并实现了一个基于高速网络与SSD的网络文件系统——RUFS(Remote Userspace File System)。RUFS 遵循客户端/服务器端架构,采用RDMA 协议进行通信。用户可以利用客户端提供的API,使用由服务器端提供的文件系统服务。服务器端是一个单机的文件系统,元数据管理基于键值存 储RocksDB(Rocks DataBase),数据管理基于SPDK Blobstore,所有存储过程都通过SPDK 提供的NVMe 驱动运行在用户态。通常遵循POSIX(Portable Operating System Interface X)语义的文件系统,只能保证元数据操作的原子性。而RUFS 具备同步语义,能够在遵循POSIX 语义基础之上,保证已经完成的数据和元数据操作,在服务器掉电之后不丢失,而无需使用fsync进行持久化。

仅使用一块SSD 作为数据盘,RUFS:在4KB 随机访问上,读、写操作就能获得超过400 MB/s的性能,较默认配置下NFS+ext4 的性能提升了202.2%和738.9%;对于4MB 顺序访问,RUFS相较于NFS+ext4也有至少40%的性能提升。在元数据性能上,RUFS的文件夹创建性能,相较于NFS+ext4,有5 693.8%的性能提升,其他大部分元数据操作也有显著的性能优势。

本文主要有三个方面的贡献:第一,为如何在用户态完成文件系统所有存储过程给出了详细的方案;第二,在此基础上,实现了一个网络文件系统原型RUFS,并报告了数据和元数据性能;第三,改进了BlobFS 的缓存策略,使得工作在BlobFS 之上的键值存储的读性能有了非常明显的提升,也间接提升了RUFS的元数据性能。

1 相关研究

1.1 基于键值存储的文件系统元数据管理

键值存储是一种NoSQL 存储,一般基于LSM Tree(Log Structured Merged Tree)[26]构建,提供有序键到任意长度值的持久化存储和查询。通过将随机写入转化为顺序写入,这种键值存储通常能够取得更好的性能。

2013 年,Ren 等[8]提出了TableFS(Table File System)。TableFS 利用LevelDB(Level DataBase)[27]构建了一个文件系统元数据模块,以键值对的键描述父节点到子节点的关系,并将文件的元数据作为键值对的值存在LevelDB 中。在此基础上,利用Ext4 作为对象存储,为TableFS 提供数据的存储服务。为了减少对下层Ext4 的访问,TableFS 还将小于4 KB 的文件也放在了LevelDB当中。

在此基础上,2014年,Ren等[28]提出了TableFS的分布式版本——IndexFS(Index File System),并在此基础上做了一些相关的工作[29-30]。2017年,Li等[31]提出了LocoFS(Loco File System)。LocoFS改进了用键值存储模拟目录树的方式,减少了元数据操作需要的网络请求数量,提高了元数据操作的性能。

1.2 SPDK技术

存储服务的性能由软件与硬件共同决定。对于传统存储硬件(如机械硬盘),由于硬件性能较差,软件上的开销只占整个存储服务开销的一小部分。但随着NVMe SSD 的出现,相当一部分存储硬件,例如Z-SSD、Optane SSD等,已经能够提供小于10 μs 的延迟和高达3 GB/s 的带宽[11]。硬件性能的大幅提高,使得软件栈的开销成为了整个存储服务开销中不可忽视的一部分。

为了充分利用NVMe SSD的性能,减少存储过程中的软件开销,Intel开发了SPDK。SPDK提供了一个纯用户态的NVMe驱动,消除了内核与系统中断带来的开销。SPDK提供的NVMe驱动采用了无锁的实现,支持多线程同时提交I/O请求。

根据SPDK 团队的论文[12],对于NVMe SSD(实验所用的SSD 型号为Intel P3700,容量为800 GB)的4 KB 随机访问。SPDK 的用户态NVMe 驱动,能用1 块SSD 提供450 kIOPS(Input/output Operations Per Second)的访问性能,略高于Linux内核中的NVMe 驱动。得益于无锁的实现方式,SPDK 提供的性能,能够随着SSD 的增多而线性增加,用8 块SSD 提供约3 600 kIOPS 的访问性能。而增加SSD 数量,对内核驱动提供的性能没有提高。在4 KB 随机读的延迟上,SPDK 能够将内核驱动造成的软件开销,降低约90%。

SPDK 为用户提供了一套事件驱动的编程框架。在这套框架中,每个线程相互独立,通过消息传递的方式进行线程间同步,线程间不共享任何资源。这种设计消除了资源共享带来的数据竞争,使框架具有良好的可扩展性。这个编程框架定义了3个重要的概念,分别是reactor、event和poller。reactor是一个常驻的线程,持有一个无锁的消息队列;event 代表一个任务,可以通过reactor 的消息队列在线程间传递;poller 与event 类似,也是一种任务,但需要注册在一个reactor 上,reactor 会周期性地调用已注册的poller。用户可以使用poller在用户态模拟系统中断。

Blobstore和BlobFS(Blob File System)是SPDK提供的两个存储服务,前者提供对象(blob)存储的语义,后者提供一个简易的文件系统,用户可以在此基础上搭建存储应用。Blobstore中最基础的存储单元被称为page,每个page 4 KB大小。Blobstore可以保证每个page写入的原子性。根据用户配置,Blobstore会将连续的多个page组织在一起(通常大小为1 MB),这一段连续的空间被称为一个cluster,而blob则是一个cluster的链表。用户可以在blob上进行随机、并发、无缓存的读写,还可以将键值对以元数据的形式存储在blob当中,但元数据需要用户自己手动调用sync md(同步元数据)操作才能持久化。

BlobFS是在Blobstore的基础上构建的一个简易的文件系统。每个文件都对应着Blobstore中的一个blob,文件的名字和长度,都以键值对的形式存储在blob的元数据当中。BlobFS只能支持创建根目录下的文件,不支持文件夹功能,不支持随机位置的写入,只支持增量写。当前BlobFS 已经能够作为键值存储系统的存储引擎,但由于不支持随机位置的写入,仍然不适合用于管理文件数据。BlobFS当中还实现了一个简单的缓存模块,可以为文件的顺序读和增量写带来一定的性能提升。

1.3 基于RDMA的RPC技术

RDMA 是指一种允许处理器直接读写远程计算机内存的技术。相较于传统网络,RDMA 能够提供极低的延时和很高的带宽。最新商用的RDMA 网卡可以提供低至600 ns的延时和每端口高达200 Gb/s 的带宽[25]。RDMA 编程一般使用verbs API,需要开发者自己控制网络连接、任务轮询等细节,编程复杂度较高。

RPC(Remote Procedure Call)技术[32],是一种允许本地机器透明地调用远端函数或者过程的技术。RPC技术向用户隐藏了数据的发送、接收、序列化、反序列化等细节,大大降低了编程复杂度。Mercury 是面向超算领域的RPC 框架,于2013 年由Soumagne等[33]提出。Mercury包含了一个网络抽象层,可以通过不同的通信插件,在不同网络硬件下进行数据传输。当前,Mercury 采用了OFI(Open Fabric Interface)[34]作为支持RDMA传输的通信插件。Mercury在常规的RPC接口之外,还提供了一组块(bulk)传输接口。Bulk接口能够充分利用RDMA单边通信的性能,消除不必要的内存拷贝。用户可以把一块内存注册为一个bulk,并将bulk句柄发送给其他机器,其他机器就能通过bulk句柄远程地读写被注册的内存。在Mercury的基础上,Intel正在开发一套支持组通信的RPC 框架,CaRT(Collective and RPC Transport),作为其在新的存储系统DAOS(Distributed Asynchronous Object Storage)[35-36]中的传输层。CaRT不仅支持传统的点对点RPC通信,还能支持RPC的组播。

2 系统架构与设计

本章主要介绍了RUFS 的设计与实现,包括元数据管理的策略、数据管理的策略、保证元数据与数据的一致性策略。

2.1 系统架构

RUFS 是一个纯用户态的文件系统,采用客户端/服务器端架构,服务器端是一个单机系统,可以同时支持多个客户端。服务器端与客户端通过CaRT进行通信。

RUFS客户端为用户提供了一套类POSIX语义的、并发安全的文件系统操作API(RUFS-API),支持的操作包括:access、mkdir、rmdir、stat、rename、opendir、readdir、closedir、open、creat、close、ftruncate、unlink、pread、pwrite、read、write,支持文件和文件夹操作。当用户调用客户端API 时,客户端会将请求通过RPC的形式发送到RUFS服务器端,并等待请求返回。

RUFS 服务器端实现了一个纯用户态文件系统(RUFSserver)。系统需要至少两块SSD才能工作,其中一块用来建立BlobFS 实例,用来支持RocksDB[37]的数据存储。RUFS 将利用RocksDB 对元数据进行存储和管理。剩下的每块SSD 都会创建一个单独的Blobstore 实例,用来存储数据,其中的每个blob都包含着一个文件的数据。RUFS能利用多个SSD来提高服务器端的文件读写的吞吐能力。在RUFS服务启动时,系统会为每一块用于存储数据的SSD 建立一个Blobstore 实例,同时,启动一定数量的reactor线程,负责处理读写请求。reactor线程的数量可以自行配置,但不能超过Blobstore 的实例数量,每个Blobstore实例受一个固定的reactor线程的管理。

图1 RUFS架构Fig.1 RUFS architecture

2.2 元数据管理

2.2.1 基于键值存储与Blobstore的元数据协同管理

文件系统的元数据通常组织为目录树。目录树的节点存储了文件的元数据,每一个节点都有一个唯一的编号(inode number),目录树的边代表目录对下一级节点的包含关系。RUFS利用键值存储模拟目录树,同样模拟了目录树的节点和边,并为每个节点赋予了一个唯一的UUID(Universally Unique IDentifier)。目录树的节点和边用不同的键值对模拟,前者称为N型(node)键值对,后者称为E型(edge)键值对。两者在键值存储中,有不同的前缀,N 型键值对模拟节点,键由前缀、节点UUID 拼接而成,值包含了该节点的一部分元数据(记为Meta-N),E 型键值对模拟父节点到子节点的边,键由前缀、父节点UUID、子节点文件名拼接而成,值中包含子节点UUID 和子节点的一部分元数据(记为Meta-E)。基于键值存储的键的有序性,拥有相同父节点的E 型键值对会聚合到一起,这方便了readdir 的实现。RUFS 可以将readdir 对子节点的遍历,转化为RocksDB对键值对的遍历。

图2 目录树与键值对的对应关系Fig.2 Relationship between directory tree and key-value pairs

在RUFS 中,一个文件/文件夹的元数据包括:mode(其中包含了节点类型、权限信息)、gid、uid、atime、ctime、mtime。对于文件,还包括文件的长度、文件对应的blob的相关信息。许多元数据操作的接口,都是基于路径名的(例如creat、rmdir等),系统需要从根节点开始,通过文件名和E型键值对,顺着目录树的树边逐层往下查找节点,直到找到路径名对应的节点,再做相应的操作。在查找目标节点的过程中,根据POSIX语义的要求,系统同时要判断操作对节点是否有访问权限。这需要读取节点元数据中的mode、gid 和uid。为了消除在权限判断过程中,对N 型键值对的额外访问,RUFS 将mode、gid和uid 划分到了E 型键值对中。图3 展示了节点元数据是如何存储在不同的键值对中的。

图3 在键值对中存储元数据的方式Fig.3 Method of storing metadata in key-value pairs

根据POSIX语义的要求,文件在被进行读写时,文件的时间戳需要被相应地改变,文件的长度也可能发生变化。如果要将这些改动同步到RocksDB 当中,当系统需要同时处理大量的读写请求时,RocksDB 的写入性能就会成为整个系统的瓶颈。因此,在RUFS 中,文件的长度和时间戳还会存储在对应的blob 的元数据中。当文件被读写时,文件长度和时间戳的变化只会存储到blob的元数据中,当文件被关闭时,长度和时间戳才会被同步回RocksDB中。

2.2.2 元数据操作的原子性和并发安全性

某些元数据操作(例如rename)需要对目录树进行多次改动,为了保证操作的原子性,RUFS 中所有可能涉及目录树变化,或是在操作过程中默认目录树不发生结构变化的操作,都利用了RocksDB事务来保证元数据操作的原子性。

RUFS服务器端作为一个多线程的系统,能够并发地更改目录树的结构,如果不进行并发控制,就会产生错误。而单纯使用RocksDB事务,无法避免这样的错误。图4展示了一种出错的情况。在目录树为初始状态时,系统同时收到了creat 操作和rmdir操作,由于RocksDB 事务只处理写-写操作的冲突,因此两个元数据操作事务得以并发地执行,并进行了事务提交,结果造成了creat 操作创建了一个空悬的节点。为了解决这一问题,RUFS 利用了RocksDB 事务中的get_for_update 操作。这一操作会促使RocksDB为目标键值对加上一个读写锁,通过为目录树上的节点和边上读写锁,就能避免元数据的并发操作破坏目录树结构。在加锁的顺序上,RUFS总是遵循这样的规则:对于两个待加锁的节点A 和B,若两者在目录树中深度不同,那么RUFS会从较浅的节点到较深的节点加锁。若两者在目录树中的深度相同,RUFS会从UUID较小者到UUID较大者加锁。RUFS通过有顺序的加锁,来避免死锁问题。

图4 并发的元数据操作导致的错误Fig.4 Error caused by concurrent metadata operations

2.3 数据管理

SPDK 提供3 个存储服务,分别是BDev(Block Device)、Blobstore 和BlobFS。其中:BDev 只提供块设备的语义,过于简单,不适合用作管理文件数据;BlobFS不支持对文件的随机写入;而Blobstore则能提供对象存储的语义,提供针对blob的创建、删除、随机读写、长度变更等操作。RUFS容易将针对文件内容的操作,映射到Blobstore 中针对blob 的操作。因此,RUFS选择将数据存储在Blobstore中。

每创建一个文件,RUFS就在Blobstore中创建一个相应的blob。目录树中的文件节点与Blobstore 中的blob 一一对应。blob 的位置信息(blob 所属的Blobstore 和blob ID),会成为文件元数据的一部分,存储在RocksDB 当中。每一个Blobstore实例都由一个固定的reactor管理,对Blobstore的任何操作,包括blob 的创建、删除、读写,都需要提交给对应的reactor,由reactor完成。

2.3.1 元数据与数据的一致性策略

当用户创建或删除一个文件时,RUFS不仅需要改变目录树的结构,还需要在Blobstore 上创建或者删除相应的blob,维持文件节点与blob的一一对应。宕机会导致文件的创建或删除执行不完整,破坏文件节点与blob一一对应的关系。如果产生了游离的blob(没有对应文件节点的blob),则造成存储空间的泄漏,如果文件节点没有对应的blob,则意味着数据丢失。

RUFS利用了一种基于blob标记的手段来解决这个问题。基本的思路是,将没有和元数据建立联系的blob 标记为已解耦(detached),并将位置信息记录到RocksDB 中,这样即使服务器宕机,重启RUFS之后,系统也能够回收这些blob。

图5(a)展示了文件的creat 过程,新创建的blob 会被标记为detached,并记录在RocksDB 当中,只有在blob 元数据设置成功,并且将位置信息记录在目录树上后,RocksDB 中的detached 记录才会被删除。如果因为宕机导致操作没有完全执行,RUFS 在重新启动时,通过检查blob 的元数据和RocksDB中的记录,就能回收游离的blob。Detached记录的删除过程与目录树的操作处于同一个RocksDB 事务中,能保证creat 成功后,被创建出的blob 不会被错误地回收。图5(b)展示了文件的unlink 操作,标记blob 为detached 的过程会和删除文件节点的过程放在同一个RocksDB 事务中。这能保证只要元数据的删除操作成功,即使出现意外宕机,游离的blob也总能被回收。

图5 创建和删除文件的流程Fig.5 Processes of file creation and deletion

2.3.2 句柄与读写状态管理

根据POSIX 标准的要求,open、creat、opendir等操作,需要向调用者返回一个句柄。通过句柄,用户可以进一步地对文件或文件夹进行其他操作,而不用再进行从路径到文件节点的搜索和权限判断。在RUFS 中,通过句柄,用户可以读写文件(read、write、pread、pwrite),遍历文件夹下的子项目(readdir)。

RUFS 的句柄包含两个字段:一个字段是被打开节点的UUID,用来标示被打开的节点;另一个字段是一个唯一的64位无符号数(fd ID),用来标示打开同一个文件的不同句柄。通过句柄,RUFS-server能够查找当前句柄对应的读写状态。

RUFS-server采用了图6中的数据结构来管理句柄的读写状态,这个数据结构用了一个以UUID 为键的哈系表,来维持被打开文件/文件夹的内存节点。内存节点中包含了对其进行操作所需要的数据;文件内存节点中包含了文件对应的blob的位置信息,缓存了文件的长度和时间戳;文件夹内存节点,存储了以该文件夹为父节点的E 型键值对的键的公共前缀(前缀E+文件夹UUID),这个前缀用来在遍历E 型键值对时,判断被访问的键值对是否指向该文件夹的子节点。每个内存节点中包含了一条链表,链表上的每一个元素,都记录了某个句柄对应的读写状态,如果句柄属于一个文件,那么读写状态就是当前偏移(offset)的位置,如果句柄属于一个文件夹,那么读写状态就是readdir 操作所需的RocksDB 迭代器。利用图6 中的数据结构,RUFS 还能通过哈希表快速地判断某个节点是否被打开,阻止被打开的节点被删除。

图6 RUFS对句柄的管理Fig.6 Management of handles in RUFS

3 系统实现与优化

3.1 网络传输优化

RUFS 采用了CaRT 作为客户端与服务器端通信的手段。CaRT 为用户提供了RPC 接口和bulk接口,bulk接口能够充分利用RDMA 的单边通信性能,避免不必要的内存拷贝。元数据操作需要传输的数据量通常很少,因此RUFS 只使用CaRT提供的RPC 接口来发送元数据操作。但读写操作,可能需要传输较多的数据,为了充分利用RDMA的单边通信原语,提高传输性能,RUFS利用bulk接口来传输读写缓冲区中的内容。

但使用bulk接口,需要用户自己申请内存,并将内存注册为一个块(bulk)。注册bulk非常耗时,将一块仅1 B的内存注册为bulk,需要耗费大约60 μs,如果在客户端和服务器端都进行内存的注册,一次通信会产生额外的120 μs的开销,随着被注册内存的增大,耗时还会增大。图7 展示了发送不同大小的负载时复用bulk(记为reuse-bulk)和每次注册新的bulk(记为register-bulk)在传输延迟上的性能差距。

图7 不同负载下bulk传输的延迟Fig.7 Bulk transfer latency with different payload sizes

为了解决这一问题,RUFS 设计了一个内存池,Bulk-Mempool。Bulk-Mempool 会提前将一些大块的内存注册为一个bulk,并在这块内存上进行进一步的分配。在读写操作的过程中,服务器和客户端用到的读写缓冲区就从这个内存池中分配,这就消除了RUFS 在每次读写操作时,将读写缓冲区所在的内存注册为bulk而带来的开销。

Bulk-Mempool 并不是一个单一策略的内存池,而是由两个不同策略的内存池BM-small 和BM-mid 组成的。BM-small实现比较简单,分配开销较小,只分配4 KB 大小的内存;BM-mid内部实现了一个buddy system 内存池,开销相对较大。小文件读写通常触发小于等于4 KB 的内存分配请求,此时由开销较小的BM-small 进行内存分配,能够保证小文件读写的性能;大于4 KB、小于等于4 MB 的内存分配请求,则由BMmid 负责。BM-mid 能够分配不同大小的内存,提高内存的利用率。

图8 Bulk-Mempool的架构Fig.8 Architecture of Bulk-Mempool

Bulk-Mempool 没有对大于4 MB 的内存分配进行优化,是因为以4 MB 为单位的数据传输,已经能够充分地利用InfiniBand 的高带宽,使数据传输不会成为整个系统的瓶颈,本文的实验也说明了这一点(见4.3 节)。如果用户需要传输大文件,将每次读写请求分割为4 MB大小即可。

Bulk-Mempool 提供get 和put接口。通过get 接口,调用者能够获得一个胖指针,其中包括了指向被分配内存的指针、被分配内存的大小、被分配内存在bulk 中的偏移,以及bulk 句柄。前两者使得调用者可以在本地读写分配到的内存,后两者使得远端机器能够正确在分配到的内存上进行读写。

3.2 读写吞吐能力优化

按照POSIX 语义的要求,文件的读写会导致文件的大小和时间戳发生变化,导致元数据的改动。这在Blobstore 中就表现为需要通过sync md(同步元数据)操作同步blob 的元数据,但sync md 操作非常耗时,甚至超过了一次4 KB 的读或写。如果每次文件读写都要更新时间戳或是文件长度,频繁触发sync md 操作,会让整个系统的吞吐能力受到极大的影响。为此,RUFS提供了两个优化策略,以减小sync md的触发频率。

第一个策略是,提供了ftruncate 操作,并鼓励用户尽可能在写入数据前,将文件扩展到合适的大小,这样能够避免写入操作“撑大”文件大小,进而触发sync md。另一个策略是,放宽对时间戳更新的要求,每次进行读写时,将系统当前的时间,与文件当前的时间戳进行比较,只有文件需要更新的时间戳,与当前的时间相差多于5 ms 时,才选择更新时间戳。考虑到系统本身就存在着时间上的误差,这样的放松策略是可以接受的。

3.3 可靠元数据性能优化

RocksDB 会将写入操作记录到日志里,但并不会立刻将日志写入到磁盘中。在内存中缓存一定数量的日志之后,RocksDB 才会一次性地将所有内存中的日志写到硬盘上。这个特性被称作“组提交(group commit)”,组提交特性显著地减少了向磁盘写入数据的次数,对RocksDB 的写入性能有很大的提升。但同时,由于写入的数据不能被及时持久化,服务器断电就可能导致元数据操作的丢失。考虑到Blobstore会保证数据持久化后再返回,为了使元数据与数据保持一致,提供同步的文件系统语义,RUFS 也需要保证元数据操作返回后,就已经持久化到了硬盘上。

为了提供这样的保证,RUFS 打开了RocksDB 的同步模式。同步模式下,每次写入操作后,RocksDB 都会调用fsync保证日志写入硬盘,使数据在宕机后不丢失。然而,本地文件系统的fsync 性能很差,这导致了RocksDB 同步模式下的写入性能也很差,降低了RUFS 整体的元数据性能。为了解决这个问题,RUFS采用了由SPDK团队修改并开源的RocksDB[38]。这个版本的RocksDB 将底层的存储环境更换为了BlobFS。BlobFS 有很好的同步写入性能,能够显著提升RocksDB 在同步模式下的写入性能。

RocksDB 的读操作触发的都是文件的随机读,而BlobFS当前仅针对顺序读进行缓存。缓存的缺失使RocksDB 的读性能变得很差。为了解决这个问题,在BlobFS 中添加了一个支持缓存的随机读方法,当RocksDB 调用这个方法进行读操作时,BlobFS 会预取所需数据所在的一个256 KB 的数据块,并缓存在内存当中。利用缓存,相比以文件系统作为存储环境,RocksDB 在BlobFS 上的写性能能得到显著提升,且保持读性能基本相同。

3.4 统一的SPDK环境管理模块

RUFS 利用一个或多个Blobstore 管理数据,利用由SPDK团队提供的RocksDB 管理元数据。这两者都需要工作在SPDK环境下。当前,由SPDK团队提供的RocksDB,会在内部自行启动一个SPDK 环境。由于一个进程只能启动一个SPDK 环境,因此RocksDB 启动的SPDK 环境,会和RUFS 启动的SPDK环境产生冲突,导致整个系统启动失败。

为了解决这一问题,RUFS 去掉了RocksDB 中启动SPDK环境的功能,并将这部分功能整合到了RUFS 中,再加上对Blobstore 依赖的SPDK 环境的管理功能,形成了统一的SPDK环境管理模块(SPDK-env-mod)。系统启动时,SPDK-env-mod会初始化SPDK 环境,同时创建BlobFS。在RocksDB 初始化时,SPDK-env-mod 会将BlobFS 暴露给RocksDB,使RocksDB顺利在BlobFS上初始化和运行。

除了解决SPDK 环境冲突的问题,SPDK-env-mod 还方便了系统管理员对Blobstore 的管理。SPDK-env-mod 提供了一个配置文件,系统管理员可以通过该配置文件,指定用来管理数据的SSD,以及用于管理数据的reactor 线程的数量。系统启动后,SPDK-env-mod 会根据配置文件,在每块用于管理数据的SSD 上,建立Blobstore 实例。同时,根据配置文件,启动一定数量的reactor 线程,并按照平均分配的原则,将Blobstore绑定到不同的reactor线程上。

除此之外,SPDK-env-mod 会给每一个Blobstore 赋予一个从0 开始的、单调递增的唯一编号,同时在内存中维持一个计数器,每次系统需要创建一个blob 时,就将计数器的值对Blobstore 的数量取模,以此选出一个Blobstore 实例,在这个实例上创建blob,并将计数器原子地加1。由于Blobstore实例与用来管理数据的SSD 一一对应,这样的分配方案,可以保证blob均匀地分布在各个SSD上。

4 测试与评估

本章将评估RUFS 在元数据、读写延迟和读写吞吐方面的性能。RUFS的总体性能会和NFS+ext4进行比较;而RUFSserver的性能会和ext4进行比较。本章还会讨论SPDK对元数据的加速效果和多SSD对吞吐性能的提升。

4.1 测试配置

所有的测试都在两台服务器上进行,其中一台作为RUFS的服务器,另一台作为RUFS 的客户端。RUFS 客户端装配了2 块6 核CPU,128 GB 内存;RUFS 服务器端装配了4 块12 核CPU、768 GB 内存、8 块容量为512 GB 的NVMe SSD。两台服务器通过56 Gb/s 带宽的InfiniBand 网卡相连。表1 是测试环境的具体参数。

在所有测试中,NFS 与ext4 均采用默认配置,ext4 建立在服务器端,使用1块SSD,利用NFS挂载到客户端。RUFS使用2 块SSD,分别用来管理数据和元数据,使用16 个RPC 处理线程,1 个reactor 线程。客户端利用RUFS 客户端提供的API 访问RUFS服务器。

表1 测试环境设置Tab.1 Testing environment configuration

4.2 元数据性能

本文采用mdtest[39]对元数据性能进行测试,用每秒的操作数量(Operations Per Second,OPS)衡量性能。该测试对比了NFS+ext4 与RUFS 整体的元数据性能。在元数据性能测试中,客户端使用8 个mdtest 进程,文件节点的最大深度为4,文件/文件夹总数大约为50 万。如果测试对象为RUFS,需要将mdtest中的文件系统函数换成RUFS-API。

4.2.1 需要关注的元数据操作

在本节的测试中,主要关注如下的元数据操作:D-creat、D-stat、D-remove、F-creat、F-stat、F-read 和F-remove,表2 展示了它们的意义和在过程中会触发的操作。

表2 元数据操作和它们的属性和含义Tab.2 Metadata operations and their attributions and meanings

4.2.2 测试结果

从图9 来看:RUFS 在F-creat 和F-remove 两个操作上,与NFS+ext4 的性能大致相同;在其他元数据操作上,RUFS 都具有显著的优势,取得了至少70%的提升;特别对于D-creat 操作,RUFS 相对于NFS+ext4 有大约5 693.8%的性能提升。横向对比RUFS各个元数据操作的性能,F-creat和F-remove由于需要在Blobstore 上进行多次操作,因此性能显著低于其他元数据操作。F-read 操作包含了一次open 操作和一次close 操作,且需要访问Blobstore,因此性能也同样较差。

图9 RUFS与NFS+ext4元数据性能的比较Fig.9 Metadata performance comparison of RUFS and NFS+ext4

4.2.3 SPDK为元数据带来的性能提升

为了达到同步语义,RUFS在使用RocksDB 时会打开同步模式,这会导致RocksDB的写入性能大幅下降。SPDK能够为存储应用带来更低的延迟、更高的吞吐性能。通过将RocksDB 的存储环境替换为优化后的BlobFS,RocksDB 的同步写性能有了很大的提升,并且读性能没有受到影响。本节将展示BlobFS对元数据性能的影响。

图10 展示了RUFS 元数据性能在RocksDB 在不同配置(同步模式或组提交模式,分别记为sync 和group-commit)、不同存储环境下(文件系统或BlobFS,分别记为fs和SPDK)的结果。从非同步模式切换为同步模式,无论存储环境是BlobFS还是文件系统,涉及到RocksDB写入的元数据操作,都会有明显的性能下降。在BlobFS 环境下,creat 操作性能损耗最大,大约为38.8%,但由于原本性能很好,因此性能依然可以接受。存储环境为本地文件系统时,元数据操作的性能损耗变得不可接受,性能损耗最多的元数据操作依然是creat,损耗比例高达98.7%,基本处于不可用的状态。其他元数据操作,除了D-stat 与F-stat 不发生RocksDB 写入,不受同步模式的影响,其他操作的OPS都小于2 500。

图10 SPDK对元数据操作的性能的影响Fig.10 Impact of SPDK on metadata operation performance

4.3 数据性能

本节将讨论RUFS、NFS-ext4、RUFS-server 和ext4 在4 KB随机读写延迟、4 KB 随机读写吞吐、4 MB 顺序读写吞吐几个场景上的性能。由于当前RUFS 还没有加入对缓存的支持,因此在对ext4 进行测量时,尽量消除了缓存对ext4 的影响。ext4 的写入包括两个项目:ext4-direct 和ext4-sync。前者在打开文件时,使用了O_DIRECT 选项,避免数据写入到缓存;后者在打开文件时使用了O_SYNC 选项,保证写入数据能够持久化到硬盘。需要说明的是,由于O_SYNC 选项不影响读操作,因此在测试读性能时,ext4-sync 与ext4-direct 会使用同一个数据。RUFS-server 仍然使用16 个RPC 处理线程,用1 块SSD管理元数据,1块SSD管理数据。

4.3.1 延迟

测量了RUFS-server、ext4-sync、ext4-direct、RUFS、NFS+ext4的4 KB 随机读写的延迟,图11展示了测试结果。从结果上来看,RUFS-server 的读延迟,大约只有ext4 的20%。在网络环境下,RUFS 总体的读延迟,只有NFS+ext4 的26%左右。而对于本地写性能,RUFS-server 仅略快于ext4-direct,但要注意,ext4-direct 并不保证操作返回时,能将数据持久化在硬盘上。提供这一保证的ext4-sync,写延迟则是RUFS-server 的近60 倍,在网络环境下,RUFS 总体的写延迟也远远小过NFS+ext4。

图11 RUFS与NFS+ext4关于4 KB随机访问的延迟Fig.11 4 KB random access latency of RUFS and NFS+ext4

4.3.2 吞吐

吞吐性能测试包括了4 KB 随机读写和4 MB 顺序读写两个项目。图12 展示了4 KB 随机读写吞吐性能的结果。这个结果和延迟测试类似,RUFS-server 在读写性能上,都远远地超过了ext4-sync,同时略强于不提供持久化保证的ext4-direct。总体性能上,RUFS 读性能是NFS+ext4 的3 倍以上,写性能是NFS+ext4的8倍以上。

图12 NFS+ext4与RUFS关于4 KB随机访问的吞吐性能Fig.12 4 KB random access bandwidth of NFS+ext4 and RUFS

在4 MB 的顺序读写上,ext4 与RUFS 的差距就相对小了一些。没有网络参与时,无论是读还是写,RUFS-server 均快于ext4,但性能提升不超过30%。但值得注意的是,在大文件的顺序读写中,RUFS 的总体性能与RUFS-server 的吞吐性能几乎持平,这意味着网络传输提供了足够高的带宽,没有成为整个系统的瓶颈。

图13 RUFS与NFS+ext4关于4 MB顺序访问的吞吐性能Fig.13 4 MB sequential access bandwidth of RUFS and NFS+ext4

4.3.3 多SSD带来的性能提升

RUFS-server默认只用1个SSD管理数据,因此也只使用1个reactor 线程管理读写请求。如果使用多个SSD 管理数据,RUFS-server 就能启动多个reactor 处理读写请求,这能够提升RUFS-server 的吞吐性能。图14 展示了RUFS-server 在多块SSD下吞吐性能的提升。当使用6块SSD管理数据时,通过将文件分散到各块SSD,并用6 个reactor 同时处理读写请求,RUFS-server的吞吐性能能获得246%到450%的提升。

图14 多SSD为RUFS-server带来的加速比Fig.14 Speedup ratio brought by multi-SSD on RUFS-server

5 结语

本文设计并实现了一个基于高速网络和NVMe SSD 的用户态网络文件系统,RUFS。RUFS 利用RocksDB 管理元数据,利用Blobstore 管理数据,使用RDMA 技术对外提供服务。RUFS 充分地利用了NVMe SSD 的性能,所有的存储过程都通过SPDK 提供的NVMe 驱动运行在用户态。RUFS 在随机读写、顺序读写和元数据性能上,相较于NFS+ext4 都有十分明显的优势。除此之外,RUFS 还具备同步语义,能够保证用户请求返回后,数据就已经被持久化到硬盘当中。

通过RUFS 的开发和测试,也充分证明了SPDK 在存储领域的潜力,尤其是保证数据可靠写入、并持久化在硬盘的性能,明显地好于本地文件系统。因此SPDK 也十分适合于开发对存储持久性要求较高的应用,例如关系型数据库的存储引擎。

猜你喜欢

键值服务器端客户端
你的手机安装了多少个客户端
“人民网+客户端”推出数据新闻
——稳就业、惠民生,“数”读十年成绩单
非请勿进 为注册表的重要键值上把“锁”
虚拟专用网络访问保护机制研究
一键直达 Windows 10注册表编辑高招
基于Qt的安全即时通讯软件服务器端设计
基于Qt的网络聊天软件服务器端设计
一种基于Java的IM即时通讯软件的设计与实现
基于C/S架构的嵌入式监控组态外设扩展机制研究与应用
新华社推出新版客户端 打造移动互联新闻旗舰