摘要:宋体是一款自主研发的分布式文件存储产品,此前已推出容量型版本。宋体性能的提升不仅仅是因为存储介质的升级,更有架构层面的改进,本文将从协议索引存储设计等几方面来详细介绍性能型升级改造的技术细节。
UFS (UCloud File System) 是一款 UCloud 自主研发的分布式文件存储产品,此前已推出容量型 UFS 版本。UFS 以其弹性在线扩容、稳定可靠的特点,为众多公有云、物理云、托管云用户提供共享存储方案,单文件系统存储容量可达百 PB 级。
为了应对 IO 性能要求很高的数据分析、AI 训练、高性能站点等场景,UFS 团队又推出了一款基于 NVMe SSD 介质的性能型 UFS,以满足高 IO 场景下业务对共享存储的需求。性能型 UFS 的 4K 随机写的延迟能保持在 10ms 以下,4K 随机读延迟在 5ms 以下。
性能的提升不仅仅是因为存储介质的升级,更有架构层面的改进,本文将从协议、索引、存储设计等几方面来详细介绍性能型 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,这就导致两个问题: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 也基于这一理念进行了模型设计。
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 内的偏移,然后读取数据
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/117611.html