1. SPDK应用程序架构概述
SPDK是由Intel发起、用于加速使用NVMe SSD作为后端存储的应用软件加速库。该软件库的核心是用户态、异步、轮询方式的NVMe驱动。较之内核(诸如Linux Kernel) 的NVMe驱动,它可以大幅度降低NVMe command的延迟 (Latency) ,同时提高单CPU核的IOPS,从而形成一套高性价比的解决方案。
为了实现上述目标,仅仅提供用户态NVMe驱动的一些操作函数或源语是不够的。如果在某些应用场景中使用不当,不仅不能发挥出用户态NVMe驱动的高性能,甚至会导致程序错误。虽然NVMe的底层函数有一些说明,但为了更好地发挥出底层NVMe的性能,SPDK提供了一套编程框架 (SPDK Application Framework),用于指导软件开发人员基于SPDK的用户态NVMe驱动以及用户态块设备层 (User space Bdev) 构造高效的存储应用。用户有两种选择:(1) 直接使用SPDK应用编程框架实现应用的逻辑;(2) 使用SPDK编程框架的思想,改造应用的编程逻辑,以更好的适配SPDK的用户态NVMe驱动。
总体而言,SPDK的应用框架可以分为以下几部分:(1) 对CPU core和线程的管理;(2) 线程间的高效通信;(3) I/O的的处理模型以及数据路径(data path)的无锁化机制。
2. SPDK 中CPU对线程的管理
SPDK的线程模型对SPDK来说至关重要,它的设计确保了SPDK的无锁化编程模型,其主要任务就是使用最少的CPU完成最多的任务,为此SPDK在APP启动的时候就会通过函数spdk_app_start调用DPDK的rte_eal_init进行CPU的绑核,具体使用那些CPU核可以由APP的命令行参数也可以通过配置文件进行限定。例如-m 0x3 指的就是SPDK APP绑定了core0、core1来启动程序。rte_eal_init函数执行成功后,会按照APP的coremask 在每个core上启动一个thread 并通过接口pthread_setaffinity_np进行CPU亲和性的设定。在SPDK层面这个thread 被称之为reactor, reactor thread 执行一个函数reactor_run,主核直接调用该函数,从核由rte_eal_remote_launch 分发该执行函数;该函数的主体是一个while(1) {}的功能函数,直到reactor的state被改变才会退出(主要接收spdk_app_stop的响应)。如此,reactor thread 就会100%的占用CPU资源, 那么SPDK的具体业务逻辑是怎么运行的呢? 为了解决这个问题,SPDK提供两种poller 机制,其一: 基于定时器的poller ,其二:非定时poller。
在SPDK的reactor thread的数据结构struct spdk_reactor 中, 每个reactor有自己独立的TAILQ_HEAD(, spdk_lw_thread) threads链表(与数据结构struct spdk_thread对应),每个spdk_thread下面有自己独立的非定时poller链表TAILQ_HEAD(active_pollers_head, spdk_poller)active_pollers以及定时poller链表RB_HEAD(timed_pollers_tree, spdk_poller) timed_pollers; SPDK的集体任务就是基于这两类基础poller运行起来的。如此就是一个CPU core 只拥有一个thread对应一个reactor,reactor 挂载spdk_thread链表,每个spdk_thread拥有不同的poller链表挂载很多poller。其部分代码如下:
3. 线程间的高效通信
SPDK 放弃了传统加锁式的通信方式,改用了EVENT机制,SPDK线程间通信分为两种场景。其一:reactor之间的event通信,其二是spdk_thread间的通信;其消息分发接口分别为:spdk_event_call 及 spdk_thread_send_msg。具体的消息中能够携带具体任务的入口地址,这样就能够把任务发送到其他指定的线程执行。当然event 消息的处理也是在reactor thread中执行。
这两种通信模式的内部实现机制主要包含两大块:其一:DPDK的无锁队列 rte_ring 构建消息队列,其二: 基于非阻塞模式的events_fd的消息通知机制;前者本身就是无锁且支持多生产者多消费者模式,后者是一种高效的事件通知机制。毫无疑问通过这两种技术的加持,SPDK的reactor thread 能够及时高效的处理相关交换任务。具体代码如下:
4. IO 处理的无锁化模型
SPDK 的IO是一种run-to-completion模式,即IO从接收进入SPDK开始,直到IO结束都是在同一个核上处理,这样SPDK的IO处理就不存在资源竞争,处于一种无锁化模式。此外,当多个spdk_thread操作同一个block device (bdev)时,SPDK提供了一个IO channel的概念与bdev之间建立映射关系;即不同的thread操作同一个bdev时,应该拥有不同的IO channel, 每个IO channel在IO路径上使用自己独立的资源不存在资源竞争从而免锁。