1.SPDK编译
从github 获取 spdk软件源,进行编译。
编译命令如下:
[user@]./scripts/pkgdep.sh --all
[user@] cd spdk && git submodule update --init
[user@]./configure && make /* 如果要编译RDMA, configure时加上RDMA 选项; ./configure --with-rdma */.
通常编译过程中都会遇到一些依赖缺失得问题,根据错误一个一个得解决即可。
2. SPDK NVMf 实例运行
· TGT的启动以及配置创建
[root@localhost spdk]# scripts/rpc.py nvmf_create_transport -t TCP -u 16384 -m 8 -c 8192
[root@localhost spdk]# scripts/rpc.py bdev_malloc_create -b Malloc0 512 512
Malloc0
[root@localhost spdk]# scripts/rpc.py nvmf_create_subsystem nqn.2021-09.io.spdk:cnode1 -a -s SPDK00000000000001 -d SPDK_Controller1
[root@localhost spdk]# scripts/rpc.py nvmf_subsystem_add_ns nqn.2021-09.io.spdk:cnode1 Malloc0
[root@localhost spdk]# scripts/rpc.py nvmf_subsystem_add_listener nqn.2021-09.iospdk:cnode1 -t tcp -a 201.1.1.11 -s 4420
此处以RPC命令配置TGT
· initrator 端连接TGT (映射远端盘到本地为nvme0n1)
注意: NVMe Over TCP 在测试客户端需要nvme-tcp 内核模块的加载,该模块在kernel 5.0之后的内核中才具有的功能。
· fio测试随机写4K的IO结果
3.SPDK概述
SPDK (Storage performance development kit) 是由Intel发起、用于加速使用NVMe SSD作为后端存储的应用软件加速库。该软件库的核心是用户态、异步、轮询方式的NVMe驱动。较之内核(诸如Linux Kernel) 的NVMe驱动,它可以大幅度降低NVMe command的延迟 (Latency) ,同时提高单CPU核的IOPS,从而形成一套高性价比的解决方案,例如使用SPDK的vhost解决方案可以应用于HCI (Hyper Converged Infrastructure,超融合基础架构) 加速虚拟机中的NVMe I/O。
为了实现上述目标,仅仅提供用户态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的的处理模型(run to completion)以及数据路径(data path)的无锁化机制。
4.NVMf TGT 启动初始化流程分析
SPDK NVMe-oF target的主程序位于 (spdk/app/nvmf_tgt) 目录中,大家可以看到有个文件命名为nvmf_main.c。 仔细一看相关的main函数,似乎也没做什么,只是调用了spdk_app_opts_init, 初始化了一下相应的参数; 然后调用了一下spdk_app_parse_args,用于解析程序执行时的命令行参数,比如“-m”用以指定SPDK占用的CPU核数。接着调用了一下spdk_app_start, 如果有错误,最终会执行spdk_app_fini 退出。
SPDK 的关键接口spdk_app_start的主要是:环境初始化app_setup_env、reactor初始化spdk_reactor_init、subsystem初始化接口bootstrap_fn消息发送(rpc命令会初始化rpc的服务器端用于接收rpc命令)、reactor启动spdk_reactors_start。
- app_setup_env: 主要是构建DPDK参数并调用rte_eal_init启动DPDK运行环境;
- spdk_reactor_init:基于CPU数量为每个CPU开辟一个reactor的数据结构,并创建基于DPDK通信的event mempool以及msg mempool(由接口spdk_thread_lib_init_ext创建msgpool), 前者用于reactor间event通信消息内存开辟池,后者用于线程间的消息内存开辟池;
- bootstrap_fn:由spdk 线程间msg消息传递启动的poller执行subsystem相关初始化流程(20.07之前的版本会有常规config文件,之后的版本都是通过json配置文件或rpc的命令进行初始化)。
- spdk_reactors_start:主要是启动SPDK reactor的运行,即为DPDK每个核上绑定的线程启动真正的任务,SPDK里面每个reactor对应一个DPDK的thread,这里reactor的真正执行函数为reactors_run,该函数主要执行reactor间通信任务event->fn, 线程间通信任务msg->fn,以及poller->fn;poller分为两类一类定时poller,一类非定时poller。
值得特别注意的是SPDK里面的subsystem有两个概念:
- 第一个subsystem的概念指模块的subsystem,主要位于代码目录spdk/module/event/subsystems中,比如现在SPDK之中有以下9个模块subsystem, 分别是accel bdev iscsi nbd net nvmf scsi vhost vmd。 这些模块的subsystem有些有依赖关系,我们在对这些模块初始化时会先根据依赖关系,排序,然后进行初始化。
- 第二个subsystem的概念指NVMe-oF中的NVM subsystem,这种subsystem又分为两类:其一: SPDK_NVMF_SUBTYPE_DISCOVERY的subsystem一般设置为给所有的host可见。其主要用于实现相应的log discovery命令,告诉host端有多少NVM subsystem在线。其二: SPDK_NVMF_SUBTYPE_NVME(NVM 类型的subsystem主要是后端bdev相关)。
4.1 NVMf subsystem模块启动流程
代码中nvmf subsystem 依赖于bdev、sock两个子系统,而bdev子系统依赖于accel子系统,故初始化nvmf子系统要启动共计需要先启动这3个子系统后,才会真正触发nvmf子系统的初始化。
SPDK的模块subsystem初始化主要是基于SPDK_SUBSYSTEM_REGISTER宏定义的构造函数,在main函数运行之前将各个模块注册进全局数据表项g_subsystems中。当APP(NVMf TGT)启动时由bootstrap_fn遍历每个subsystem并调用其init函数进行初始化启动。
subsystem的依赖关系由宏SPDK_SUBSYSTEM_DEPEND指定:
SPDK_SUBSYSTEM_DEPEND(nvmf, bdev) //nvmf依赖于bdev
SPDK_SUBSYSTEM_DEPEND(nvmf, sock) //nvmf依赖于sock
比如nvmf subsystem模块的代码位于spdk/module/event/subsystems/nvmf/nvmf_tgt.c,其初始化入口在于nvmf_subsystem_init,它调用接口nvmf_tgt_advance_state启动状态机初始化NVMf TGT资源。该状态机从NVMF_TGT_INIT_NONE状态启动,其详细状态跳变见函数nvmf_tgt_advance_state的具体实现,状态机的跳变策略基本上按照nvmf_tgt_advance_state的case顺序进行。
4.2 NVMF_TGT_INIT_CREATE_TARGET状态
该接口可以当做是subsystem的状态机的第一个有效状态NVMF_TGT_INIT_CREATE_TARGET的执行接口。其主要负责spdk_nvmf_tgt数据接口的创建、subsystem指针数组的存储空间开辟、accepter_poller注册(定时poller)、io_device的注册以及discovery_subsystem的创建与添加。
discovery_subsystem主要用于实现相应的log discovery命令,告诉host端有多少NVM subsystem在线,该类型的subsystem下不能有任何namespace。NVM subsystem未指定namespace数量时默认最多支持32个namespace。
执行完上述操作,修改状态机进入下一个状态NVMF_TGT_INIT_CREATE_POLL_GROUPS,该状态由nvmf_tgt_create_poll_groups接口完成。
值得注意的是: 该状态会注册transport层的accept,用于接收建链请求,spdk_nvmf_tgt_create 会注册accept_poller执行nvmf_tgt_accept接收建链。
4.3 NVMF_TGT_INIT_CREATE_POLL_GROUPS状态
状态NVMF_TGT_INIT_CREATE_POLL_GROUPS的执行接口为nvmf_tgt_create_poll_groups,该接口遍历所有CPU,为每个CPU上运行线程发送一个异步创建poll_group的message,message的执行函数是nvmf_tgt.c中实现的nvmf_tgt_create_poll_group。
nvmf_tgt.c中实现的nvmf_tgt_create_poll_group接口主要功能是通过spdk_get_io_channel为g_spdk_nvmf_tgt首次创建io channel,io channel在创建时,都会在channel尾部额外开辟一块数据结构为struct spdk_nvmf_poll_group的空间,这个数据结构后续会运行在每个SPDK thread上。
状态NVMF_TGT_INIT_CREATE_POLL_GROUPS的执行过程中最为关键的就是io device的回调接口dev->create_cb=nvmf_tgt_create_poll_group,该函数在/lib/nvmf/nvmf.c中实现。主要功能如下:
- 遍历所有的transport,对每一个transport创建一个polling group->tgroup, 然后加入到这个poll group的中的tgroups数据结构中,当前状态机中tgt→transport链表应该为空,该循环应该不会执行,后续create transport的时候会主动把transport加入到group中(rpc_nvmf_create_transport-->spdk_nvmf_tgt_add_transport)。
2. 把g_spdk_nvmf_tgt 中的所有NVM subsystem 加入到这个polling group中,存储 在sgroups (位于struct spdk_nvmf_poll_group) 中,第一次只是加载了discover subsystem子系统的相关指针于sgroups中。
3. 创建一个poller, 这个poller会调用nvmf_poll_group_poll, 这个函数用于在每个transport上polling,是后续业务处理的主要poller。poll group创建完成后该状态会通过异步线程消息执行nvmf_tgt_create_poll_group_done,该接口会将状态机的状态置为NVMF_TGT_INIT_START_SUBSYSTEMS进入下一个状态。
4.4 NVMF_TGT_INIT_START_SUBSYSTEMS状态
NVMF_TGT_INIT_START_SUBSYSTEMS状态的执行函数为spdk_nvmf_subsystem_start,主要是在每个poll group上把所有的NVMe-oF的subsystem 状态设置为ACTIVE。 然后进入状态NVMF_TGT_RUNNING,可以初始化下一个subsystem。
该状态启动的第一个subsystem是discover 的subsystem子系统。
4.5 SPDK NVMf的关键数据逻辑结构
NVMf的关键数据结构我们可以从spdk_nvmf_tgt开始梳理,它包含了四个比较关键的数据结构: spdk_poller、spdk_nvmf_subsystem、spdk_nvmf_transport、spdk_nvmf_poll_group。spdk_nvmf_tgt数据结构由spdk_nvmf_tgt_create接口来创建并存放于全局对象g_spdk_nvmf_tgt中。 这里特别要注意的是:该接口会调用spdk_io_device_register函数对g_spdk_nvmf_tgt 注册相应I/O device的创建、销毁回调接口。当用户执行spdk_nvmf_poll_group_create时,调用spdk_get_io_channel(&g_spdk_nvmf_tgt)第一次创建I/O channel时 ,io_device创建时被传入的I/O channel的create_cb 函数就会被触发()。
此外,这里需要说明一下I/O channel本质上是thread 与io_device的mapping,也就是说对于一组(thread,io_device)会产生唯一的I/O channel,直到这个io_device 最终被调用spdk_io_device_unregister时销毁。不同的thread 操作同一个device应该拥有不同的I/O channel,每个I/O channel在I/O路径上使用自己独立的资源就可以避免资源竞争,从而去除锁的机制。
5. NVMf transport数据结构动态创建流程
在SPDK21.07版本中,以json配置文件或者RPC命令初始化TGT资源时,最终响应接口都是在各个模块级的subsystem中对应的XXX_rpc.c文件实现。 此处以nvmf_tgt的transports创建为里,相关代码位于lib\nvmf_rpc.c中实现。rpc相关的实现方法由SPDK_RPC_REGISTER宏注册,不论是rpc命令还是json配置文件(TGT进程内部建链sock连接发送rpc request),其最终函数接口都是从SPDK_RPC_REGISTER的注册点开始。 其调用栈如下: