分布式技术的发展深刻地改变叻我们编程的模式和思考软件的模式。值 2019 岁末PingCAP 联合 InfoQ 共同策划出品“分布式系统前沿技术 ”专题, 邀请众多技术团队共同参与一起探索這个古老领域的新生机。本文出自 UCloud 后台研发工程师邓瑾
为了应对 IO 性能要求很高的数据分析、AI 训练、高性能站点等场景,UFS 团队又推出了一款基于 NVMe SSD 介质的性能型 UFS以满足高 IO 场景下业务对共享存储的需求。性能型 UFS 的 4K 随机写的延迟能保持在 10ms 以下4K 随机读延迟在 5ms 以下。
性能的提升不僅仅是因为存储介质的升级更有架构层面的改进,本文将从协议、索引、存储设计等几方面来详细介绍性能型 UFS 升级改造的技术细节
此湔容量型 UFS 设计时支持的协议为 NFSv3,其设计理念是接口无状态故障恢复的逻辑简单。此外 NFSv3 在 Linux 和 Windows 上被广泛支持更易于跨平台使用。但是 NFSv3 的设計缺点导致的高延迟在高 IO 场景下是不可接受的所以在性能型 UFS 中,我们选择仅支持性能更好、设计更先进的 NFSv4 协议
可以看到,在关键的 IO 部汾NFSv4 比 NFSv3 节省一半的交互次数,可以显著降低 IO 延迟
除了协议以外,性能型 UFS 的核心由业务索引和底层存储两部分组成由于底层 IO 性能的提升,这两部分都需要进行深度改造以适应这种结构性的改变下面我们将分别介绍这两部分的改造细节。
索引服务是分布式文件系统的核心功能之一相比对象存储等其它存储服务,文件存储的索引需要提供更为复杂的语义所以会对性能产生更大影响。
索引服务的功能模块設计是基于单机文件系统设计思路的一种『仿生』分为两大部分:
索引服务各模块的功能是明确的,主要解决两个问题:
虽然功能有区别目录索引和文件索引在架构上是类似的,所以我们下面只介绍文件索引 (FileIdx) 架构在以上的目标指导下,最终 FileIdx 采用无状態设计依靠各索引节点和 master 之间的租约(Lease)机制来做节点管理,实现其容灾和弹性架构
master 模块负责维护一张路由表,路由表可以理解成一個由虚节点组成的一致性哈希环每个 FileIdx 实例负责其中的部分虚节点,master 通过心跳和各个实例节点进行存活性探测并用租约机制告知 FileIdx 实例和各个 NFSServer 具体的虚节点由谁负责处理。如果某个 FileIdx 实例发生故障master
只需要在当前租约失效后将该节点负责的虚节点分配给其他实例处理即可。
当 NFSServer 需要向文件服务请求具体操作 (比如请求分配 IO 块) 时会对请求涉及的文件句柄做哈希操作确认负责该文件的虚节点由哪个 FileIdx 处理,将请求发至該节点每个节点上为每个文件句柄维持一个处理队列,队列按照 FIFO 方式进行执行本质上这构成了一个悲观锁,当一个文件的操作遇到较哆并发时我们保证在特定节点和特定队列上的排队,使得并发修改导致的冲突降到最低
尽管租约机制一定程度上保证了文件索引操作嘚并发安全性,但是在极端情况下租约也不能保持并发操作的绝对互斥及有序所以我们在索引数据库上基于 CAS 和 MVCC 技术对索引进行更新保护,确保索引数据不会因为并发更新而丧失外部一致性
在性能型 UFS 中,底层存储的 IO 延迟大幅降低带来了更高的 IOPS 和吞吐也对索引模块特别是 IO 塊的分配性能提出了挑战。频繁地申请 IO 块导致索引在整个 IO 链路上贡献的延迟比例更高对性能带来了损害。一方面我们对索引进行了读写汾离改造引入缓存和批量更新机制,提升单次 IO 块分配的性能
同时,我们增大了 IO 块的大小更大的 IO 数据块降低了分配和获取数据块的频率,将分配开销进行均摊后续我们还将对索引关键操作进行异步化改造,让 IO 块的分配从 IO 关键路径上移除最大程度降低索引操作对 IO 性能嘚影响。
存储功能是一个存储系统的重中之重它的设计实现关系到系统最终的性能、稳定性等。通过对 UFS 在数据存储、数据操作等方面的需求分析我们认为底层存储 (命名为 nebula) 应该满足如下的要求:
简单:简单可理解的系统有利于后期维护。
可靠:必须保证高可用性、高可靠性等分布式要求
拓展方便:包括处理集群扩容、数据均衡等操作。
充分利用高性能存储介质
基于以上目标,我们将底层存储系统 Nebula 设计为基于 append-only 的存储系(immutable storage)面向追加写的方式使得存储逻辑会更简单,在多副本数据的同步上可以有效降低数据一致性的容错复杂度更关键的昰,由于追加写本质上是一个 log-based 的记录方式整个 IO
的历史记录都被保存,在此之上实现数据快照和数据回滚会很方便在出现数据故障时,哽容易做数据恢复操作
在现有的存储系统设计中,按照数据寻址的方式可以分为去中心化和中心化索引两种这两者的典型代表系统是 Ceph 囷 Google File System。去中心化的设计消除了系统在索引侧的故障风险点并且降低了数据寻址的开销。但是增加了数据迁移、数据分布管理等功能的复杂喥出于系统简单可靠的设计目标,我们最终选择了中心化索引的设计方式中心化索引使集群扩容等拓展性操作变得更容易。
中心化索引面临的性能瓶颈主要在数据块的分配上我们可以类比一下单机文件系统在这方面的设计思路。早期文件系统的 inode 对数据块的管理是 block-based每佽 IO 都会申请 block 进行写入,典型的 block 大小为 4KB这就导致两个问题:
1. 4KB 的数据块比较小,对于大片的写入需要频繁进行数据块申请操作不利于发挥順序 IO 的优势。
2. inode 在基于 block 的方式下表示大文件时需要更大的元数据空间能表示的文件大小也受到限制。
在 Ext4/XFS 等更先进的文件系统设计中inode 被设計成使用 extent-based 的方式来实现,每个 extent 不再被固定的 block 大小限制相反它可以用来表示一段不定长的磁盘空间,如下图所示:
显然地在这种方式下,IO 能够得到更大更连续的磁盘空间有助于发挥磁盘的顺序写能力,并且有效降低了分配 block 的开销IO 的性能也得到了提升,更关键的是它鈳以和追加写存储系统非常好地结合起来。我们看到不仅仅在单机文件系统中,在 Google File System、Windows Azure Storage 等分布式系统中也可以看到 extent-based
的设计思想我们的 nebula 也基于这一理念进行了模型设计。
在 Nebula 系统中存储的数据按照 Stream 为单位进行组织每个 Stream 称为一个数据流,它由一个或多个 extent 组成每次针对该 Stream 的写叺操作以 block 为单位在最后一个 extent 上进行追加写,并且只有最后一个 extent 允许写入每个 block 的长度不定,可由上层业务结合场景决定而每个 extent
在逻辑上構成一个副本组,副本组在物理上按照冗余策略在各存储节点维持多副本Stream 的 IO 模型如下:
基于这个模型,存储系统被分为两大主要模块:
在存储集群中所有磁盘通过 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 分配一个唯一的 blockid,通过两级索引转换进行寻址:
这种实现方式面临两个问题,(1)第一级的转换需求导致 streamsvr 需要记录的索引量很大而且查询茭互会导致 IO 延迟升高降低性能。(2)第二级转换以 Facebook Haystack 系统为典型代表每个 extent 在文件系统上用一个独立文件表示,extentsvr 记录每个 block 在 extent
文件中的偏移並在启动时将全部索引信息加载在内存里,以提升查询开销查询这个索引在多线程框架下必然因为互斥机制导致查询延迟,因此在高性能场景下也是不可取的而且基于文件系统的操作让整个存储栈的 IO 路径过长,性能调优不可控也不利于 SPDK 技术的引入。
为避免上述不利因素我们的存储引擎是基于裸盘设计的,一块物理磁盘将被分为几个核心部分:
基于这个设计,我们可以将 block 的寻址优化为无须查询的纯计算方式当写完┅个 block 之后,将返回该 block 在整个 stream 中的偏移客户端请求该 block 时只需要将此偏移传递给 extentsvr,由于 segment 是定长的extentsvr 很容易就计算出该偏移在磁盘上的位置,從而定位到数据进行读取这样就消除了数据寻址时的查询开销。
我们之前出于简单可靠的理念将存储系统设计为 append-only但是又由于文件存储嘚业务特性,需要支持覆盖写这类随机 IO 场景
因此我们引入了一个中间层 FileLayer 来支持随机 IO,在一个追加写的引擎上实现随机写该思路借鉴于 Log-Structured File System 嘚实现。LevelDB 使用的 LSM-Tree 和 SSD 控制器里的 FTL 都有类似的实现被覆盖的数据只在索引层面进行间接修改,而不是直接对数据做覆盖写或者是
COW(copy-on-write)这样既可鉯用较小的代价实现覆盖写,又可以保留底层追加写的简单性
操作可以更简单可靠地进行甚至回滚,而由于每次 compaction 涉及的数据域是确定的也便于我们检验 compaction 操作的 invariant:回收前后数据域内的有效数据必须是一样的。每个 segment 则由一个索引流和一个数据流组成它们都存储在底层存储系统 nebula 上,每次写入 IO 需要做一次数据流的同步写而为了提升 IO
性能,索引流的写入是异步的并且维护一份纯内存索引提升查询操作性能。為了做到这一点每次写入到数据流中的数据是自包含的,这意味着如果索引流缺失部分数据甚至损坏我们可以从数据流中完整构建整個索引。客户端以文件为粒度写入到 dataunit 中dataunit 会给每个文件分配一个全局唯一的 fid,fid 作为数据句柄存储到业务索引中 (FileIdx 的 block
至此存储系统已经按照設计要求满足了我们文件存储的需求,下面我们来看一看各个模块是如何一起协作来完成一次文件 IO 的
从整体来说,一次文件写 IO 的大致流程是这样的:
1. 用户在主机上发起 IO 操作会在内核层被 nfs-client 在 VFS 层截获 (仅以 Linux 系统下为例)通过被隔离的 VPC 网络发往 UFS 服务的接入层。
2. 接入层通过对 NFS 协议的解析和转义将这个操作***为索引和数据操作。
3. 经过索引模块将这个操作在文件内涉及的 IO 范围转化为由多个 file system block(固定大小默认 4MB)表示的 IO 范围。
请求会被 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 等技术;使用户能够享受到更先進的技术带来的性能和价格红利
作者介绍:邓瑾,UCloud 后台研发工程师
本文是「分布式系统前沿技术」专题文章,目前该专题在持续更新Φ欢迎大家保持关注?