基于 NVMe SSD 的分布式文件存储 UFS 性能提高技术解析

UFS (UCloud File System) 是一款 UCloud 自主研发的分布式文件存储产品,此前已推出容量型 UFS 版本。UFS 以其弹性在线扩容、稳定可靠的特色,为众多公有云、物理云、托管云用户提供共享存储方案,单文件系统存储容量可达百 PB 级。前端

为了应对 IO 性能要求很高的数据分析、AI 训练、高性能站点等场景,UFS 团队又推出了一款基于 NVMe SSD 介质的性能型 UFS,以知足高 IO 场景下业务对共享存储的需求。性能型 UFS 的 4K 随机写的延迟能保持在 10ms 如下,4K 随机读延迟在 5ms 如下。node

性能的提高不只仅是由于存储介质的升级,更有架构层面的改进,本文将从协议、索引、存储设计等几方面来详细介绍性能型 UFS 升级改造的技术细节。数据库

协议改进编程

此前容量型 UFS 设计时支持的协议为 NFSv3,其设计理念是接口无状态,故障恢复的逻辑简单。此外 NFSv3 在 Linux 和 Windows 上被普遍支持,更易于跨平台使用。可是 NFSv3 的设计缺点致使的高延迟在高 IO 场景下是不可接受的,因此在性能型 UFS 中,咱们选择仅支持性能更好、设计更先进的 NFSv4 协议。缓存

NFSv4 与 NFSv3 相比,更先进的特性包括:支持有状态的 lock 语义、多协议间的 compound 机制等。特别是 compound 机制,可让屡次 NFS 协议交互在一个 RTT 中完成,很好地解决了 NFSv3 性能低效的问题。一次典型的 open for write 操做,在 NFSv3 和 NFSv4 上分别是这样的:安全

能够看到,在关键的 IO 部分,NFSv4 比 NFSv3 节省一半的交互次数,能够显著下降 IO 延迟。除了协议之外,性能型 UFS 的核心由业务索引和底层存储两部分组成,因为底层 IO 性能的提高,这两部分都须要进行深度改造以适应这种结构性的改变。下面咱们将分别介绍这两部分的改造细节。微信

业务索引网络

索引服务是分布式文件系统的核心功能之一。相比对象存储等其它存储服务,文件存储的索引须要提供更为复杂的语义,因此会对性能产生更大影响。多线程

索引服务的功能模块设计是基于单机文件系统设计思路的一种『仿生』,分为两大部分:架构

• 目录索引: 实现树状层级目录,记录各个目录下的文件和子目录项

• 文件索引: 记录文件元数据,包含数据块存储信息和访问权限等

索引服务各模块的功能是明确的,主要解决两个问题:

• 业务特性: 除了实现符合文件系统语义的各种操做外,还要保证索引数据的外部一致性,在各种并发场景下不对索引数据产生静态修改从而产生数据丢失或损坏

• 分布式系统特性: 包括系统拓展性、可靠性等问题,使系统可以应对各种节点和数据故障,保证系统对外的高可用性和系统弹性等

虽然功能有区别,目录索引和文件索引在架构上是相似的,因此咱们下面只介绍文件索引 (FileIdx) 架构。在以上的目标指导下,最终 FileIdx 采用无状态设计,依靠各索引节点和 master 之间的租约(Lease)机制来作节点管理,实现其容灾和弹性架构。

租约机制和悲观锁

master 模块负责维护一张路由表,路由表能够理解成一个由虚节点组成的一致性哈希环,每一个 FileIdx 实例负责其中的部分虚节点,master 经过心跳和各个实例节点进行存活性探测,并用租约机制告知 FileIdx 实例和各个 NFSServer 具体的虚节点由谁负责处理。若是某个 FileIdx 实例发生故障,master 只须要在当前租约失效后将该节点负责的虚节点分配给其余实例处理便可。

当 NFSServer 须要向文件服务请求具体操做 (好比请求分配 IO 块) 时,会对请求涉及的文件句柄作哈希操做确认负责该文件的虚节点由哪一个 FileIdx 处理,将请求发至该节点。每一个节点上为每一个文件句柄维持一个处理队列,队列按照 FIFO 方式进行执行。本质上这构成了一个悲观锁,当一个文件的操做遇到较多并发时,咱们保证在特定节点和特定队列上的排队,使得并发修改致使的冲突降到最低。

更新保护

尽管租约机制必定程度上保证了文件索引操做的并发安全性,可是在极端状况下租约也不能保持并发操做的绝对互斥及有序。因此咱们在索引数据库上基于 CAS 和 MVCC 技术对索引进行更新保护,确保索引数据不会由于并发更新而丧失外部一致性。

IO 块分配优化

在性能型 UFS 中,底层存储的 IO 延迟大幅下降带来了更高的 IOPS 和吞吐,也对索引模块特别是 IO 块的分配性能提出了挑战。频繁地申请 IO 块致使索引在整个 IO 链路上贡献的延迟比例更高,对性能带来了损害。一方面咱们对索引进行了读写分离改造,引入缓存和批量更新机制,提高单次 IO 块分配的性能。

同时,咱们增大了 IO 块的大小,更大的 IO 数据块下降了分配和获取数据块的频率,将分配开销进行均摊。后续咱们还将对索引关键操做进行异步化改造,让 IO 块的分配从 IO 关键路径上移除,最大程度下降索引操做对 IO 性能的影响。

底层存储

设计理念

存储功能是一个存储系统的重中之重,它的设计实现关系到系统最终的性能、稳定性等。经过对 UFS 在数据存储、数据操做等方面的需求分析,咱们认为底层存储 (命名为 nebula) 应该知足以下的要求:・简单:简单可理解的系统有利于后期维护・可靠:必须保证高可用性、高可靠性等分布式要求・拓展方便:包括处理集群扩容、数据均衡等操做・支持随机 IO・充分利用高性能存储介质

Nebula: append-only 和中心化索引

基于以上目标,咱们将底层存储系统 nebula 设计为基于 append-only 的存储系统 (immutable storage)。面向追加写的方式使得存储逻辑会更简单,在多副本数据的同步上能够有效下降数据一致性的容错复杂度。更关键的是,因为追加写本质上是一个 log-based 的记录方式,整个 IO 的历史记录都被保存,在此之上实现数据快照和数据回滚会很方便,在出现数据故障时,更容易作数据恢复操做。

在现有的存储系统设计中,按照数据寻址的方式能够分为去中心化和中心化索引两种,这二者的典型表明系统是 Ceph 和 Google File System。去中心化的设计消除了系统在索引侧的故障风险点,而且下降了数据寻址的开销。可是增长了数据迁移、数据分布管理等功能的复杂度。出于系统简单可靠的设计目标,咱们最终选择了中心化索引的设计方式,中心化索引使集群扩容等拓展性操做变得更容易。

数据块管理:extent-based 理念

中心化索引面临的性能瓶颈主要在数据块的分配上,咱们能够类比一下单机文件系统在这方面的设计思路。早期文件系统的 inode 对数据块的管理是 block-based,每次 IO 都会申请 block 进行写入,典型的 block 大小为 4KB,这就致使两个问题:一、4KB 的数据块比较小,对于大片的写入须要频繁进行数据块申请操做,不利于发挥顺序 IO 的优点。二、inode 在基于 block 的方式下表示大文件时须要更大的元数据空间,能表示的文件大小也受到限制。

在 Ext4/XFS 等更先进的文件系统设计中,inode 被设计成使用 extent-based 的方式来实现,每一个 extent 再也不被固定的 block 大小限制,相反它能够用来表示一段不定长的磁盘空间,以下图所示:

![](user-gold-cdn.xitu.io/2019/9/4/16… jpeg&s=34026) 显然地,在这种方式下,IO 可以获得更大更连续的磁盘空间,有助于发挥磁盘的顺序写能力,而且有效下降了分配 block 的开销,IO 的性能也获得了提高,更关键的是,它能够和追加写存储系统很是好地结合起来。咱们看到,不只仅在单机文件系统中,在 Google File System、Windows Azure Storage 等分布式系统中也能够看到 extent-based 的设计思想。咱们的 nebula 也基于这一理念进行了模型设计。

存储架构 Stream 数据流

在 nebula 系统中存储的数据按照 stream 为单位进行组织,每一个 stream 称为一个数据流,它由一个或多个 extent 组成,每次针对该 stream 的写入操做以 block 为单位在最后一个 extent 上进行追加写,而且只有最后一个 extent 容许写入,每一个 block 的长度不定,可由上层业务结合场景决定。而每一个 extent 在逻辑上构成一个副本组,副本组在物理上按照冗余策略在各存储节点维持多副本,stream 的 IO 模型以下:

streamsvr 和 extentsvr

基于这个模型,存储系统被分为两大主要模块:・streamsvr:负责维护各个 stream 和 extent 之间的映射关系以及 extent 的副本位置等元数据,而且对数据调度、均衡等作管控・extentsvr:每块磁盘对应一个 extentsvr 服务进程,负责存储实际的 extent 数据存储,处理前端过来的 IO 请求,执行 extent 数据的多副本操做和修复等

在存储集群中,全部磁盘经过 extentsvr 表现为一个大的存储池,当一个 extent 被请求建立时,streamsvr 根据它对集群管理的全局视角,从负载和数据均衡等多个角度选取其多副本所在的 extentsvr,以后 IO 请求由客户端直接和 extentsvr 节点进行交互完成。在某个存储节点发生故障时,客户端只须要 seal 掉当前在写入的 extent,建立一个新的 extent 进行写入便可,节点容灾在一次 streamsvr 的 rpc 调用的延迟级别便可完成,这也是基于追加写方式实现带来的系统简洁性的体现。

由此,存储层各模块的架构图以下:

至此,数据已经能够经过各模块的协做写入到 extentsvr 节点,至于数据在具体磁盘上的存储布局,这是单盘存储引擎的工做。

单盘存储引擎

前面的存储架构讲述了整个 IO 在存储层的功能分工,为了保证性能型 UFS 的高性能,咱们在单盘存储引擎上作了一些优化。

线程模型优化

存储介质性能的大幅提高对存储引擎的设计带来了全新的需求。在容量型 UFS 的 SATA 介质上,磁盘的吞吐较低延迟较高,一台存储机器的总体吞吐受限于磁盘的吞吐,一个单线程 / 单进程的服务就可让磁盘吞吐打满。随着存储介质处理能力的提高,IO 的系统瓶颈逐渐从磁盘往处理器和网络带宽方面转移。

在 NVMe SSD 介质上因为其多队列的并行设计,单线程模型已经没法发挥磁盘性能优点,系统中断、网卡中断将成为 CPU 新的瓶颈点,咱们须要将服务模型转换到多线程方式,以此充分发挥底层介质多队列的并行处理能力。为此咱们重写了编程框架,新框架采用 one loop per thread 的线程模型,并经过 Lock-free 等设计来最大化挖掘磁盘性能。

block 寻址

让咱们思考一个问题,当客户端写入了一片数据 block 以后,读取时如何找到 block 数据位置?一种方式是这样的,给每一个 block 分配一个惟一的 blockid,经过两级索引转换进行寻址:

・第一级:查询 streamsvr 定位到 blockid 和 extent 的关系

・第二级:找到 extent 所在的副本,查询 blockid 在 extent 内的偏移,而后读取数据

这种实现方式面临两个问题,(1)第一级的转换需求致使 streamsvr 须要记录的索引量很大,并且查询交互会致使 IO 延迟升高下降性能。(2)第二级转换以 Facebook Haystack 系统为典型表明,每一个 extent 在文件系统上用一个独立文件表示,extentsvr 记录每一个 block 在 extent 文件中的偏移,并在启动时将所有索引信息加载在内存里,以提高查询开销,查询这个索引在多线程框架下必然由于互斥机制致使查询延迟,所以在高性能场景下也是不可取的。并且基于文件系统的操做让整个存储栈的 IO 路径过长,性能调优不可控,也不利于 SPDK 技术的引入。

为避免上述不利因素,咱们的存储引擎是基于裸盘设计的,一块物理磁盘将被分为几个核心部分:

**•superblock: ** 超级块,记录了 segment 大小,segment 起始位置以及其余索引块位置等

**•segment: ** 数据分配单位,整个磁盘除了超级块之外,其余区域所有都是 segment 区域,每一个 segment 是定长的 (默认为 128MB),每一个 extent 都由一个或多个 segment 组成

**•extent index / segment meta region: extent/segment ** 索引区域,记录了每一个 extent 对应的 segment 列表,以及 segment 的状态 (是否可用) 等信息

基于这个设计,咱们能够将 block 的寻址优化为无须查询的纯计算方式。当写完一个 block 以后,将返回该 block 在整个 stream 中的偏移。客户端请求该 block 时只须要将此偏移传递给 extentsvr,因为 segment 是定长的,extentsvr 很容易就计算出该偏移在磁盘上的位置,从而定位到数据进行读取,这样就消除了数据寻址时的查询开销。

随机 IO 支持:FileLayer 中间层

咱们以前出于简单可靠的理念将存储系统设计为 append-only,可是又因为文件存储的业务特性,须要支持覆盖写这类随机 IO 场景。

所以咱们引入了一个中间层 FileLayer 来支持随机 IO,在一个追加写的引擎上实现随机写,该思路借鉴于 Log-Structured File System 的实现。LevelDB 使用的 LSM-Tree 和 SSD 控制器里的 FTL 都有相似的实现,被覆盖的数据只在索引层面进行间接修改,而不是直接对数据作覆盖写或者是 COW (copy-on-write),这样既能够用较小的代价实现覆盖写,又能够保留底层追加写的简单性。

FileLayer 中发生 IO 操做的单元称为 dataunit,每次读写操做涉及的 block 都在某个 dataunit 上进行处理,dataunit 的逻辑组成由以下几个部分:

dataunit 由多个 segment 组成 (注意这和底层存储的 segment 不是一个概念),由于基于 LSM-Tree 的设计最终须要作 compaction, 多 segment 的划分相似于 LevelDB 中的多层 sst 概念,最下层的 segment 是只读的,只有最上层的 segment 容许写入,这使得 compaction 操做能够更简单可靠地进行甚至回滚,而因为每次 compaction 涉及的数据域是肯定的,也便于咱们检验 compaction 操做的 invariant:回收先后数据域内的有效数据必须是同样的。

每一个 segment 则由一个索引流和一个数据流组成,它们都存储在底层存储系统 nebula 上,每次写入 IO 须要作一次数据流的同步写,而为了提高 IO 性能,索引流的写入是异步的,而且维护一份纯内存索引提高查询操做性能。为了作到这一点,每次写入到数据流中的数据是自包含的,这意味着若是索引流缺失部分数据甚至损坏,咱们能够从数据流中完整构建整个索引。

客户端以文件为粒度写入到 dataunit 中,dataunit 会给每一个文件分配一个全局惟一的 fid,fid 做为数据句柄存储到业务索引中 (FileIdx 的 block 句柄)。

dataunit 自己则由 fileserver 服务进程负责,每一个 fileserver 能够有多个 dataunit,coordinator 根据各节点的负载在实例间进行 dataunit 的调度和容灾。整个 FileLayer 的架构以下:

至此,存储系统已经按照设计要求知足了咱们文件存储的需求,下面咱们来看一看各个模块是如何一块儿协做来完成一次文件 IO 的。

The Big Picture:一次文件写 IO 的全流程

从总体来讲,一次文件写 IO 的大体流程是这样的:

①用户在主机上发起 IO 操做会在内核层被 nfs-client 在 VFS 层截获 (仅以 Linux 系统下为例),经过被隔离的 VPC 网络发往 UFS 服务的接入层。

②接入层经过对 NFS 协议的解析和转义,将这个操做分解为索引和数据操做。

③通过索引模块将这个操做在文件内涉及的 IO 范围转化为由多个 file system block (固定大小,默认 4MB) 表示的 IO 范围。

④NFSServer 拿到须要操做的 block 的句柄 (bid) 后去请求 FileLayer 进行 IO 操做 (每一个 bid 在 FileLayer 中表明一个文件)。

请求会被 NFSServer 发往负责处理该 bid 对应的文件的 fileserver 上,fileserver 获取该文件所在的 dataunit 编号 (此编号被编码在 bid 中) 后,直接往该 dataunit 当前的数据流 (stream) 中进行追加写,完成后更新索引,将新写入的数据的位置记录下来,本次 IO 即告完成,能够向 NFSServer 返回回应了。相似地,当 fileserver 产生的追加写 IO 抵达其所属的 extentsvr 的时候,extentsvr 肯定出该 stream 对应的最后一个 extent 在磁盘上的位置,并执行一次追加写落地数据,在完成多副本同步后返回。

至此,一次文件写 IO 就完成了。

性能数据

通过前述的设计和优化,性能型 UFS 的实际性能数据以下:

总结

本文从 UFS 性能型产品的需求出发,详细介绍了基于高性能存储介质构建分布式文件系统时,在协议、业务架构、存储引擎等多方面的设计考虑和优化,并最终将这些优化落实到产品中去。性能型 UFS 的上线丰富了产品种类,各种对 IO 延迟要求更高的大数据分析、AI 训练等业务场景将获得更好的助力。

后续咱们将在多方面继续提高 UFS 的使用体验,产品上会支持 SMB 协议,提高 Windows 主机使用文件存储的性能;底层存储会引入 SPDK、RDMA 等技术,并结合其它更高性能的存储介质;在冷存数据场景下引入 Erasure Coding 等技术;使用户可以享受到更先进的技术带来的性能和价格红利。

产品最新优惠:性能型 UFS 原价 1.0 元 /GB/ 月,如今福建可用区优惠价 0.6 元 /GB/ 月,国内其余可用区 0.8 元 /GB/ 月,欢迎联系客户经理申请体验!

如您有本篇文章相关问题,欢迎添加做者微信咨询。WeChat ID:cheneydeng

相关文章
相关标签/搜索