存储性能软件加速库&SPDK

SPDK是由英特尔发起的,用于加速NVMe SSD作为后端存储使用的应用软件加速库。这个软件库的核心是用户态、异步、轮询方式的NVMe驱动。相比内核的NVMe驱动,SPDK可以大幅降低NVMe command的延迟,提高单CPU核的IOps,形成一套高性价比的解决方案,如SPDK的vhost解决方案可以被应用到HCI中加速虚拟机的NVMe I/O。

把内核驱动放到用户态,导致需要在用户态实施一套基于用户态软件驱动的完整I/O栈。

SPDK NVMe驱动

SPDK的核心组件之一就是用户态NVMe驱动,固态硬盘产品FlashSSD已经实现了用NVMe取代AHCI(对传统硬盘效果好),NVMe SSD是一个通用的PCI设备。

用户态驱动

用户态驱动节省了用户态和内核态的上下文切换开销,和系统调用的开销。而对固态硬盘设备访问使用UIO(Userspace IO)或VFIO(Virtual Funciton I/O)

UIO

UIO框架是Linux提供,实现用户态设备驱动,主要需要解决以下两个问题

  1. 访问设备的内存,通过映射物理设备内存到用户态来实现,UIO通过限制不相关的物理设备的映射改善了这个问题。由此基于UIO开发的用户态驱动不需要关心与内存映射相关的安全性和可靠性的问题。
  2. 处理设备中断,中断本身需要内核处理,UIO是提供一个小的内核模块通过最基本的中断服务程序。用户态驱动和UIO内核模块通过/dev/uioX设备来实现基本交互,同时通过sysfs来得到相关的设备、内存映射、内核驱动等信息。

VFIO

VFIO除UIO所能提供访问设备内存和处理设备中断的服务,还把设备I/O、中断、DMA暴露到用户空间,从而可以在用户空间完成设备驱动的框架。这里的一个难点是如何将DMA以安全可控的方式暴露到用户空间防止设备通过写内存的任意页来发动DMA攻击。

IOMMU(I/O Memory Management Unit) 对设备进行限制,设备I/O地址需要IOMMU重新映射为内存物理地址,那么恶意的或存在错误的设备就不能读/写没有被明确映射过的内存。操作系统以互斥的方式管理MMU和IOMMU,这样物理设备将不能绕过或污染可配置的内存管理表项。

用户态DMA

主要是CPU平台技术演进

大页Hugepage

虚拟地址映射到物理地址的工作主要是TLB(Translation Lookaside Buffers)与MMU一起来完成的。以4KB的页大小为例,虚拟地址寻址时,首先在TLB中查找,如果没有找到,则需要通过MMU加载的页表基地址进行多次寻表来找到对应的物理地址。如果找不到,则产生缺页,这时会有相应的handler进行处理,来填充页表和更新TLB。

总的来说,通过页表查询而导致缺页带来的CPU开销是非常大的,TLB的出现能很好地解决性能问题。但是经常性的缺页是不可避免的,为此我们可以采取大页的方式。通过使用Hugepage分配大页可以提高性能。因为页大小的增加,可以减少缺页异常。例如,2MB大小的内容(假设是2MB对齐),如果是4KB大小的页粒度,则会产生512次缺页异常,但是使用2MB大小的页,只会产生一次缺页异常。页粒度的改变,使得TLB同样的空间可以保存更多虚存空间到物理空间的映射。尽可能地利用TLB,少用MMU,以减少寻址和缺页处理带来的开销,从而提高应用程序的整体性能。
大页还有一个优势是这些预先分配的内存基本上不会被换出,当进行DMA的时候,所对应的虚拟地址永远有相对应的物理页。结合VFIO,可以显著并安全地提升用户态对设备的读/写操作效率。当然,大页也有缺点,比如它需要额外配置,需要应用程序事先预估使用多少内存,大页需要在进程启动之前实现启用和分配好内存。目前,在大部分场景下,系统配置的主内存越来越多,这个限制不会成为太大的障碍。

总的来说Linux 内核NVMe驱动和SPDK NVMe驱动实现方式区别如下

内核NVMe驱动 SPDK NVMe驱动
工作模式 中断 异步轮询
I/O路径是否需要同步 CPU之间需要同步 CPU之间无锁方式运行
是否需要系统调用 需要 不需要,直接绕过
I/O内存年页管理 DMA内存页映射 大页,通过hugepage
块设备支持 通过内核通用块驱动 专门为Flash优化

无锁化:读/写处理要在一个CPU核上完成,避免核间的缓存同步,DPDK为SPDK提供了基础的内存管理,单核上的资源依赖于DPDK的内存管理,不仅提供了核上的专门资源,还提供了高效访问全局资源的数据结构,如mempool、无锁队列、环等

SPDK应用框架

除了用户态NVMe驱动的操作函数,SPDK提供完整的编程框架。使用框架使我们更高效完成工作。

应用框架主要分为:

  1. 对CPU core和线程的管理
  2. 线程间的高效通信
  3. I/O的处理模型
  4. 数据路径的无锁化机制

对CPU core和线程的管理

SPDK在初始化程序的时候限定使用绑定CPU的哪些核。可以在配置文件或命名行中配置,如在命令行中使用“-c 0x5”,这是指使用core 0和core 2来启动程序。

SPDK,每个核上运行一个thread, 被称为Reactor,Reactor中是一个while(1)的循环。而用户的任务通过Poller封装。thread检查这些Poller的状态,并进行相关的调用。由于一个CPU核上,只有一个Reactor thread,所以不需要锁机制来保护资源,如果不同CPU核上的thread需要通行,那么SPDK封装了线程间异步传递消息。

线程间的高效通信

SPDK提供了事件调用(Event)的机制。这个机制的本质是每个Reactor对应的数据结构维护了一个Event事件的环,这个环是多生产者和单消费者(Multiple Producer Single Consumer,MPSC)的模型,意思是每个Reactor thread可以接收来自任何其他Reactor thread(包括当前的Reactor thread)的事件消息进行处理。SPDK的Event环依赖于DPDK的实现

I/O的处理模型及数据路径的无锁化机制

SPDK主要的I/O处理模型是运行直到完成。如前所述,使用SPDK应用框架,一个CPU core只拥有一个thread,这个thread可以执行很多Poller(包括定时器和非定时器)。运行直到完成的原则是让一个线程最好执行完所有的任务。