【SPDK入门】


前言

Storage Performance Development Kit 存储性能开发套件

一、SPDK是什么?

1. SPDK概念

SPDK (Storage performance development kit, http://spdk.io) 是由Intel发起、用于加速使用NVMe SSD作为后端存储的应用软件加速库。Intel对SPDK的定义是利用用户态、异步、轮询方式的NVMe驱动,用于加速NVMe SSD作为后端存储使用的应用软件的加速库

在这里插入图片描述

传统磁盘IO操作是经过内核处理的,采用中断的方式进行DMA将数据从内核态拷贝到用户态,再交给用户程序处理,而这些操作必然会消耗性能,内核主要完成两件事:

  1. 支持虚拟文件系统、各个存储子系统以及各个驱动的管理;
  2. 用户程序和文件系统之间的系统调用

2. SPDK核心思想

正常情况下,普通机械硬盘在通过内核进行IO操作;但是当前基于NVMe协议的SSD固态硬盘,拥有更快的读写速度,在IO请求频繁的场景,内核和用户态频繁切换,则会降低性能,而SPDK提供的就是绕过内核(kernel bypass)的解决方案,可以大幅度降低NVMe command的延迟 (Latency) ,同时提高单CPU核的IOPS(相对的,读写一定大小的数据量,bypass方案占用的CPU则是更低的),其核心思想主要为以下几点:

  • 用户态(user space):将所有必要的驱动程序移动到用户空间,从而避免 syscalls,并允许从应用程序中访问零拷贝。基于UIO或VFIO的支持直接将存储设备的的地址空间映射到应用空间的方式,并利用NVMe规范来初始化NVMe SSD设备,实现基本的I/O操作,从而构筑用户态驱动。
  • 异步(asynchronous):不需要等下发任务处理完即返回
  • 轮询(polled-mode):对磁盘进行CPU轮询(周期执行),一旦查询到IO操作,立马触发回调函数,而不是依赖中断(上下文切换,带来不稳定的性能,延时更高),这降低了总延迟和延迟方差;轮询速度快,中断消耗时间
  • 无锁(lockless):spdk设计的主要目标之一就随着使用硬件(e.g. SSD,NIC,CPU)的增多而获得性能的线性提升,为了达到这目的,spdk的设计者就必须消除使用更多的系统资源带来的overhead,如:更多的线程、进程间通信,访问更多的存储硬件、网卡带来的性能损耗。避免 I/O 路径中的所有锁(避免锁带来的性能损耗),而是依靠消息传递。依赖的dpdk的实现,其本质是使用cas(compare and swap)实现了多生产者多消费者FIFO队列

在这里插入图片描述

SPDK整体架构分为四层,自上而下,最上层的应用协议层指代SPDK对外支持的协议以及相关的存储应用,包含有网络存储NVMe-oF,iSCSI Target以及虚拟化vhost-blk/scsi Target等;第二层为存储服务层,他提供了对块或者文件的抽象,用来支持更多的存储业务,例如提供了Blobstore;第三层抽象了通用的块存储设备bdev,用来支持后端不同的存储方式,例如NVMe,NVMe-oF,Ceph RBD等,并支持自定义的存储设备;底层则是驱动层,在这一层上,SPDK实现了用户态驱动用来加速各类存储应用。右侧例举了一些SPDK可集成的服务以及应用场景。

spdk编程最基本的准则,就是避免在spdk核上出现进程上下文切换。其会打破spdk高性能框架,造成性能降低甚至不能工作。进程上下文切换会因为很多原因导致,大致列举如下:

  • cpu时间片耗尽
  • 进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行。
  • 进程主动调用sleep等函数让出cpu使用权。
  • 当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行。
  • 硬件中断会导致CPU上的进程被挂起,转而执行内核的中断服务程序。

3. 相关名词解释

PCIe:从PCI发展过来的,“e” 是 “express”的简称,是“快”的意思,PCI使用并口数据传输,PCIe使用串口传输,但是PCIe的速度更快。

NVMe:面向PCI-E固态硬盘的接口协议

用户空间文件系统:Filesystem in Userspace,简称FUCE,完全在用户态实现的系统,可以使用存储性能开发工具包spdk,所有操作在用户空间,无需内核操作,数据零拷贝,降低系统延时。性能比较好的用户空间文件系统有libpfs和libpmfs。spdk提供的用户态文件系统为blobfs

热插拔:带电插拔,在不断电的情况下,将硬件模块拔出系统,不影响正常工作,提高系统的稳定性,抗灾害能力。

DMA:直接内存访问(Direct Memory Access),在进行IO设备和内存的数据传输时,数据搬运工作全部交给DMA控制器,CPU不参与数据拷贝的事情(拷贝数据不经过CPU,但是整个过程还是会涉及CPU,决定拷贝什么数据,从哪里传输到哪里)

零拷贝:如何把地址同步给其他使用者,减少上下文切换和数据拷贝的次数,如mmap。mmap() 系统调用函数会直接把内核缓冲区里的数据「映射」到用户空间,这样,操作系统内核与用户空间就不需要再进行任何的数据拷贝操作。
在这里插入图片描述

PageCache:磁盘高速缓存,缓存最近被访问的数据,预读功能;对于大文件,可以提高文件读写的性能

UIO(Userspace I/O):是运行在用户空间的I/O技术,Linux系统中一般的驱动设备都是运行在内核空间,而在用户空间用应用程序调用即可,而UIO则是将驱动的很少一部分运行在内核空间,而在用户空间实现驱动的绝大多数功能!使用UIO可以避免设备的驱动程序需要随着内核的更新而更新的问题。
在这里插入图片描述

用户空间下的驱动程序比运行在内核空间的驱动要多得多,UIO框架下运行在内核空间的驱动程序所做的工作很简单,常做的只有两个:分配和记录设备需要的资源和注册uio设备和必须在内核空间实现的小部分中断应答函数。

RDMA:远程直接内存访问,远程直接内存访问两个或者多个计算机进行通讯的时候使用DMA, 从一个主机的内存直接访问另一个主机的内存。
在这里插入图片描述

RDMA是一种host-offload, host-bypass技术,允许应用程序(包括存储)在它们的内存空间之间直接做数据传输。具有RDMA引擎的以太网卡(RNIC)–而不是host–负责管理源和目标之间的可靠连接。使用RNIC的应用程序之间使用专注的QP和CQ进行通讯。

二、SPDK框架

1. 资源管理

SPDK利用CPU的亲和性,将线程和CPU核做绑定,设计了线程模型,应用程序从收到这个核的I/O操作到运行结束,都在该核上完成,这样可以更高效的利用缓存,同时也避免多核之间的内存同步问题。与此同时,在单核上的内存资源的管理,利用了大页存储来加速。需要说明的是,SPDK借助了DPDK对基础内存资源的管理,比如大页分配(大页配置说明: https://www.kernel.org/doc/Documentation/vm/hugetlbpage.txt),因此需要DPDK相关库作为依赖,另外DPDK也为SPDK提供了一些基础数据管理结构,例如内存池,无锁队列等等。

2.线程模型

在SPDK中的线程模型架构上,每一个CPU核中拥有一个内核线程,会初始化一个reactor。每一个reactor下可持有零到多个SPDK抽象出的轻量用户态线程spdk_thread。为了提高在reactor间通信与同步的效率,SPDK放弃了传统加锁的方式,而是通过向每个reactor的spdk_ring来发送消息。在抽象得到的spdk_thread下拥有poller,用来注册用户函数。基本的框架图如下:
在这里插入图片描述

SPDK一大宗旨是使用最少的CPU核和线程来完成最多的任务。为此,SPDK在初始化程序时(目前调用spdk_app_start函数)限定使用绑定CPU的哪些核,可以在配置文件或命名行中配置,例如在命令行中使用-c 0x5是指使用core0 和core2来启动程序。通过CPU核绑定函数的亲和性可以限制住CPU的使用,并且在每个核上运行一个thread,该thread在SPDK中被称为Reactor (如Figure 1所示)。目前SPDK的环境库 (ENV) 缺省仍旧使用了DPDK的EAL库来进行管理。总而言之,Reactor thread执行一个函数 (_spdk_reactor_run), 该函数的主体包含一个while (1) {} 功能的函数,直到Reactor的state被改变,例如受到 (spdk_app_stop 的调用)。为了高效,上述循环中也会有一些相应的机制让出CPU资源 (诸如sleep)。这样的机制大多时候会导致CPU使用100%的情况,这点和DPDK比较类似。
换言之,假设一个使用SPDK编程框架的应用运用了两个CPU core,那么每个core上就会启动一个Reactor thread。如此一来,用户怎么执行自己的函数呢?为了解决该问题,SPDK提供了一个Poller的机制,即用户定义函数的分装。SPDK提供的Poller分为两种:(1) 基于定时器的Poller;(2) 非定时器的Poller。SPDK的Reactor thread对应的数据结构(struct spdk_reactor) 有相应的列表来维护Poller的机制。例如,一个链表维护定时器的Poller,一个链表维护非定时器的Poller,并且提供Poller的注册和销毁函数。在Reactor的while循环中,它会不停的check这些Poller的状态,进行相应的调用,用户的函数也因此可以进行相应的调用。由于单个CPU上只有一个Reactor thread,所以同一个Reactor thread 中不需要一些锁的机制来保护资源。当然,位于不同CPU的core上的thread还是需要通信必要。为了解决该问题,SPDK封装了线程间异步传递消息 (Async Messaging Passing) 的方式。
在这里插入图片描述

3. 线程间通信

SPDK放弃使用传统的加锁方式来进行线程间的通信,因为这种方案比较低效。为了使同一个thread只执行自己所管理的资源,SPDK提供了Event (事件调用) 机制。该机制的本质是每个Reactor对应的数据结构 (struct spdk_reactor) 维护了一个Event事件的ring (环)。这个环是多生产者和单消费者 (MPSC: Multiple producer Single Consumer) 的模型,即每个Reactor thread可以接收来自任何其他Reactor thread (包括当前的Reactor Thread) 的事件消息进行处理。目前SPDK中Event ring的缺省实现依赖于DPDK的机制,应该有线性锁的机制,但是相较于线程间采用锁的机制进行同步要高效得多。毫无疑问,Event ring处理的同时也在进行Reactor的函数 (_spdk_reactor_run) 处理。每个Event事件的数据结构 (struct spdk_event) 其实包括了需要执行的函数、加上相应的参数以及要执行的core。简单而言,一个Reactor A 向另外一个Reactor B通信,其实就是需要Reactor B执行函数F(X) (X是相应的参数)。基于上述机制,SPDK就实现了一套比较高效的线程间通信机制。具体例子可以参照SPDK NVMe-oF target内部的一些实现,主要代码位于 (lib/nvmf) 目录。

4. IO处理无锁化

SPDK主要的I/O 处理模型是Run-to-completion,指运行直到全部完成。上述内容中提及,使用SPDK应用框架时,一个CPU core只拥有一个thread,该thread可以执行很多Poller (包括定时和非定时器)。Run-to-completion的宗旨是让一个线程最好执行完所有的任务。显而易见,SPDK的编程框架满足了该需要。如果不使用SPDK应用编程框架,则需要编程者自己注意这个事项。例如,使用SPDK用户态NVMe驱动访问相应的I/O QPair进行读写操作,SPDK 提供了异步读写的函数 (spdk_nvme_ns_cmd_read),同时检查是否完成的函数 (spdk_nvme_qpair_process_completions)。这些函数的调用应由一个线程完成,不应该跨线程处理。
SPDK 的I/O 路径也采用无锁化机制。当多个thread操作同意SPDK 用户态block device (bdev) 时,SPDK会提供一个I/O channel的概念 (即thread和device的一个mapping关系)。不同的thread 操作同一个device应该拥有不同的I/O channel,每个I/O channel在I/O路径上使用自己独立的资源就可以避免资源竞争,从而去除锁的机制

总结

参考文档
零拷贝技术
初识SPDK
PCIE学习
RDMA概述
项目地址: https://github.com/spdk/spdk
Development: https://spdk.io/development/

  • 0
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

自动驾驶小哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值