搭建远端存储,深度解读SPDK NVMe-oF target

作者简介

杨子夜,Intel存储软件开发工程师,主要从事SPDK软件开发工作。

原文地址:DPDK与SPDK开源社区

导读

本文将介绍SPDK NVMe-oF target 代码的一些实现细节,针对SPDK高于7d9ed0cf4239493ee4ed2374fc11c985a38ddd60的版本号。

目前SPDK NVMe-oF target被各大厂商评估,所以很有必要在这篇文章中,从代码级别帮助大家理解SPDK NVMe-oF target的一些设计和实现细节。如果有必要,大家可以先再次阅读一下《SPDK应用程序编程框架》这篇文章,有助于大家理解SPDK NVMe-oF target 一些实现的基础。

此外这篇文章有点冗长,或者说过于注重实现的细节,主要对搞清楚SPDK NVMe-oF target的实现细节或者做二次开发比较有帮助。

1、 SPDK NVMe-oF target 启动和销毁的过程

SPDK NVMe-oF target的主程序位于 (spdk/app/nvmf_tgt) 目录中,大家可以看到有个文件命名为nvme_main.c. 仔细一看相关的main函数,似乎也没做什么,只是调用了spdk_app_opts_init,  初始化了一下相应的参数; 然后调用了一下spdk_app_parse_args,用于解析命令行的相应参数。 接着调用了一下spdk_app_start, 如果有错误,最终会执行spdk_app_fini 退出。

代码看上去非常简单,没有任何细节, 那么原因在哪里呢?

主要原因在于spdk_app_start 函数中会调用spdk_subsystem_init 进行所有subsystem模块的初始化。目前来讲, SPDK里面的subsystem有两个概念:

  • 第一个subsystem的概念指模块的subsystem,主要位于代码目录spdk/lib/event/subsystems中,比如现在SPDK之中有以下9个模块subsystem, 分别是 bdev  copy  iscsi  nbd  net  nvmf  rpc  scsi  vhost。 这些模块subsystem有些有依赖关系,我们在对这些模块初始化的时候会先根据依赖关系,排序,然后进行初始化。
  • 第二个subsystem的概念指NVMe-oF中的NVM subsystem。

在这篇文章中,大家请不要混淆这两个subsystem。另外在这里我们忽略模块subsystem 初始的流程,不是本文的重点。主要关注NVMe-oF这个模块的 subsystem的处理流程,主要.c文件的代码位于(spdk/lib/event/subsystems/nvmf) 中。

其中有四个文件:

  • conf.c,
  • nvmf_rpc.c,
  • nvmf_rpc_deprecated.c,
  • nvmf_tgt.c。

他们的功能如下:

1.1 conf.c

Conf.c: 主要解析配置文件。其调用入口是spdk_nvmf_parse_conf 函数,我们可以通过表1所提供的NVMe-oF target 配置文件的用例,来具体分析一下这个函数的处理流程。这个函数主要调用了两个函数:

spdk_nvmf_parse_nvmf_tgt

1、

解析配置文件中的“[NVMf]” section, 比如会调用函数  spdk_nvmf_read_config_file_tgt_conf,来解析配置文件中的AcceptorPollrate, 这个主要是用来控制g_acceptor_poller (即在nvmf_tgt.c 中 调用acceptor_poll的频率)。

2、

调用spdk_nvmf_tgt_create 来创建全局的NVMe-oF target 对象g_spdk_nvmf_tgt (数据类型是struct spdk_nvmf_tgt, 如表1 所示)。 这里特别要注意的是我们调用spdk_io_device_register函数对g_spdk_nvmf_tgt 注册了相应的I/O  device。

那么当用户调用spdk_get_io_channel(&g_spdk_nvmf_tgt) 的时候,在第一次I/O channel被创建的时候,spdk_nvmf_tgt_create_poll_group 这个当初被传入的I/O channel的call back 函数就会被触发。 

(附注:这里稍微提一下I/O channel本质上是thread 到一个io_device 的mapping,也就是说对于一组(thread, io_device)会产生唯一的一个I/O channel,直到这个I/O device 最终被调用spdk_io_device_unregister销毁掉。)

3、

调用spdk_add_nvmf_discovery_subsystem, 用于创建discovery NVM subsystem, 这个subsystem一般设置为给所有的host可见。其主要用于实现相应的log discovery命令,告诉host端有多少NVM subsystem在线。当然这个实际会存储在g_spdk_nvmf_tgt中的变量discovery_log_page中 (如表1所示),并且会做相应的更新。

如果新的subsystem 被加入,或者有旧的subsystem 被删除。Log page的更新请参照nvmf_update_discovery_log函数。

spdk_nvmf_parse_transports

其作用主要是初始化各个transport。 正常流程是:发现配置文件中有相应的 “[Transport]” section, 会调用以下函数:

1、spdk_nvmf_parse_transport 函数。

在spdk_nvmf_parse_transport中,每个transport 会解析以下的信息: 诸如Type, 这个类型是必须要的,主要指明需要用哪个transport, 表2中给出的是TCP,目前也可以指RDMA。 另外也可以传入以下各种参数配置, 诸如:MaxQueuesPerSession,MaxQueueDepth,InCapsuleDataSize,MaxIOSize,IOUnitSize,MaxAQDepth。但是这些不是必需的,如果不指定,就使用默认值。

接着就是调用spdk_nvmf_transport_create, 用于创建相应的transport,当然每个类型的transport 只会被创建一次,创建成功后,会调用spdk_nvmf_tgt_add_transport_done(这个是最新的流程,只有在初始化所有transport以后,才会处理subsystem)。 在这个函数中,如果有transport,就会初始化下一个;如果没有,则会调用spdk_nvmf_parse_subsystems

2、spdk_nvmf_parse_subsystems:  用于解析配置文件中的NVM subsystem。

其中会调用spdk_nvmf_parse_subsystem,解析如下的一些信息,诸如NQN,SN(Serial Number),  Namespace,Listen(监听的transport 信息),Allowed any host等。 对于Listen这个选项,parse结束以后,会调用spdk_nvmf_tgt_listen,对相应的transport的端口进行监听。并且调用spdk_nvmf_subsystem_add_listener加入到这个subsystem的listener list中。

表2 NVMe-oF target 配置文件用例

[Global]

[Bdev]

[Malloc]

NumberOfLuns 1

LunSizeInMB 64

[Nvmf]

AcceptorPollRate 10000

[Transport]

Type TCP

# Set the maximum number of submission and completion queues per session.

# Setting this to ‘8’, for example, allows for 8 submission and 8 completion queues

# per session.

#  MaxQueuesPerSession 4

# Set the maximum number of outstanding I/O per queue.

MaxQueueDepth 128

# Set the maximum in-capsule data size. Must be a multiple of 16.

# 0 is a valid choice.

InCapsuleDataSize 4096

# Set the maximum I/O size. Must be a multiple of 4096.

#MaxIOSize 131072

# IOUnitSize 131072

[Subsystem1]

NQN nqn.2016-06.io.spdk:cnode1

Listen TCP 10.67.110.197:4420

AllowAnyHost Yes

SN SPDK00000000000001

Namespace Malloc0

1.2 nvmf_rpc.c

Nvmf_rpc.c: 主要提供一些RPC的调用函数。用于在NVMe-oF tgt启动以后,动态地进行配置。

目前这个文件中主要包括以下的RPC 函数, 其中 括号内(A, B)中A代表./scripts/rpc.py 中相应的函数,B 代表SPDK 代码库中相应的C函数。 他们都是通过SPDK_RPC_REGISTER进行相应的注册。

(“get_nvmf_subsystems”, spdk_rpc_get_nvmf_subsystems):  
得到所有的NVM subsystem。


(“nvmf_subsystem_create”, spdk_rpc_nvmf_subsystem_create):
创建一个NVM subsystem。


(“delete_nvmf_subsystem”, spdk_rpc_delete_nvmf_subsystem):  
删除一个NVM subsystem。


(“nvmf_subsystem_add_listener”, nvmf_rpc_subsystem_add_listener): 
给NVM subsystem增加一个listener。

(“nvmf_subsystem_remove_listener”, nvmf_rpc_subsystem_remove_listener):
给NVM subsystem 删除一个listener。

(“nvmf_subsystem_add_ns”, nvmf_rpc_subsystem_add_ns): 
给NVM subsystem 增加一个namespace。

(“nvmf_subsystem_remove_ns”, nvmf_rpc_subsystem_remove_ns):
给NVM subsystem删除一个name space。

(“nvmf_subsystem_add_host”, nvmf_rpc_subsystem_add_host):
给NVM subsystem 增加一个可以访问的host,主要用于访问控制。

(“nvmf_subsystem_remove_host”, nvmf_rpc_subsystem_remove_host):
给NVM subsystem 删除一个可访问的host, 主要用于访问控制。

(“nvmf_subsystem_allow_any_host”, nvmf_rpc_subsystem_allow_any_host): 
设置NVM subsystem是否可以给任何host 访问。

(“set_nvmf_target_options”, nvmf_rpc_subsystem_set_tgt_opts): 这个已经是一个未来不再支持的函数调用,原来主要设置MaxQueuesPerSession,MaxQueueDepth,InCapsuleDataSize,MaxIOSize,IOUnitSize,MaxAQDepth等信息,目前这些信息已经从tgt中删除而被放入每个transport中。
    (“set_nvmf_target_max_subsystems”, nvmf_rpc_subsystem_set_tgt_max_subsystems):
 对于新版本应该不会支持。

(“set_nvmf_target_config”, nvmf_rpc_subsystem_set_tgt_conf):
 给出NVMe-oF配置文件的位置。

(“nvmf_create_transport”, nvmf_rpc_create_transport): 
创建相应的transport。

(“get_nvmf_transports”, nvmf_rpc_get_nvmf_transports): 
得到所有的transport。

1.3 nvmf_rpc_deprecated.c

Nvmf_rpc_deprecated.c: 主要支持一些legacy的函数调用,在未来可能被remove掉。比如:(”construct_nvmf_subsystem”, spdk_rpc_construct_nvmf_subsystem),创建一个NVMe-oF subsystem。 这个已经被“nvmf_subsystem_create”等有关函数替代。

1.4 nvmf_tgt.c

Nvmf_tgt.c: 提供NVMe-oF subsystem的函数入口和出口,以及相应的状态机跳转函数 (nvmf_tgt_advance_state) 。 

表 3   NVMe-oF TGT 状态机

enum nvmf_tgt_state {
NVMF_TGT_INIT_NONE = 0,
NVMF_TGT_INIT_PARSE_CONFIG,
NVMF_TGT_INIT_CREATE_POLL_GROUPS,
NVMF_TGT_INIT_START_SUBSYSTEMS,
NVMF_TGT_INIT_START_ACCEPTOR,
NVMF_TGT_RUNNING,
NVMF_TGT_FINI_STOP_SUBSYSTEMS,
NVMF_TGT_FINI_DESTROY_POLL_GROUPS,
NVMF_TGT_FINI_STOP_ACCEPTOR,
NVMF_TGT_FINI_FREE_RESOURCES,
NVMF_TGT_STOPPED,
NVMF_TGT_ERROR,
};

NVMF_TGT_INIT_NONE: 

根据 NVMe-oF tgt启动的时候设置g_num_poll_groups的值,并且创建g_poll_groups(类型struct nvmf_tgt_poll_group) 数组。如果正常,则进入NVMF_TGT_INIT_PARSE_CONFIG, 否则进入NVMF_TGT_ERROR 状态。

NVMF_TGT_INIT_PARSE_CONFIG:

发送一个thread 信息, 使得对方(可以是当前这个thread)调用nvmf_tgt_parse_conf_start。 这个函数调用spdk_nvmf_parse_conf(就是前文介绍)的来解析配置文件,配置transport以及解析subsystem。最后如果正常,则进入NVMF_TGT_INIT_CREATE_POLL_GROUPS 状态。

NVMF_TGT_INIT_CREATE_POLL_GROUPS: 

在每个SPDK thread上创建polling group 。 主要是调用nvmf_tgt_create_poll_group-> spdk_nvmf_poll_group_create。 

在spdk_nvmf_poll_group_create中,传入的channel的io_device的地址实际是:g_spdk_nvmf_tgt。 那么在每个SPDK thread上运行的polling group (数据结构是:struct spdk_nvmf_poll_group)就会被创建。实际上触发了spdk_nvmf_tgt_create_poll_group的调用,主要做了以下工作:

循环所有的transport, 对每一个transport创建一个polling group-> tgroup, 然后加入到这个poll group的中的tgroups数据结构中。

把g_spdk_nvmf_tgt 中的所有NVM subsystem 加入到这个polling group中,存储 在sgroups (位于struct spdk_nvmf_poll_group) 中。每个subsystem都拥有一个struct spdk_nvmf_subsystem_poll_group的数据结构,在这个数据结构中,定义了每个命名空间的channel(channels, num_channels), 以及这个subsystem在这个polling group的状态,并且包含了用于pending nvmf request的list(名字是queued)

创建一个poller, 这个poller会调用spdk_nvmf_poll_group_poll, 这个函数用于在每个transport上polling。

如果所有的polling group被创建完毕,那么进入状态NVMF_TGT_INIT_START_SUBSYSTEMS

NVMF_TGT_INIT_START_SUBSYSTEMS:

在每个poll group上把所有的NVMe-oF的subsystem 状态设置为ACTIVE。 然后进入状态NVMF_TGT_INIT_START_ACCEPTOR。

NVMF_TGT_INIT_START_ACCEPTOR: 

这个状态,实际是根据所定义的acceptor_poll_rate, 设置一个定时器poller, 这个poller会调用函数acceptor_poll, 用于在每个transport上处理监听到事件。然后进入NVMF_TGT_RUNNING状态。

NVMF_TGT_RUNNING:

这个状态表明NVMe-oF 这个模块的subsystem已经初始化好了,可以初始化下一个subsystem。

NVMF_TGT_FINI_STOP_SUBSYSTEMS:

这个状态只有在整个app退出的时候,实际上主要是主动退出,或者大部分状态是收到kill (比如ctrlr+c)命令的时候,才会触发NVMf 模块的subsystem(注意这里指的是NVMf subsystem 这个模块)的退出,即被调用spdk_nvmf_subsystem_fini函数,然后被调用到_spdk_nvmf_shutdown_cb函数。进入这个状态后,我们会按照顺序关闭每一个NVMe-oF 这个 subsystem。接着进入:NVMF_TGT_FINI_DESTROY_POLL_GROUPS状态。

NVMF_TGT_FINI_DESTROY_POLL_GROUPS:

在每个SPDK thread上,调用nvmf_tgt_destroy_poll_group, 来销毁polling group, 在这个函数里面会调用spdk_nvmf_poll_group_destroy销毁这个polling group上的所有qpair。当nvmf_tgt_destroy_poll_group_done被调用到的时候,我们进入NVMF_TGT_FINI_STOP_ACCEPTOR。

NVMF_TGT_FINI_STOP_ACCEPTOR: 

销毁处理所有transport 监听事件的poller。然后进入NVMF_TGT_FINI_FREE_RESOURCES状态。

NVMF_TGT_FINI_FREE_RESOURCES: 

销毁g_spdk_nvmf_tgt 所拥有的资源。最终调用函数nvmf_tgt_destroy_done, 然后进入NVMF_TGT_STOPPED状态。

NVMF_TGT_STOPPED: 

NVMe-oF 这个模块的subsystem已经被销毁,可以处理下一个模块的subsystem。

NVMF_TGT_ERROR: 

主要是有错误的时候进入这个状态,然后进行下一个模块subsystem的初始化。

2、SPDK NVMe-oF qpair的处理流程

前一部分,主要介绍了SPDK 中这个NVMe-oF 模块的子系统的启动和销毁过程,下面,我们会介绍SPDK NVMe-oF qpair处理的过程,包括qpair上request的一些处理。

在这里我们会用NVMe-oF TCP 这个transport 作为辅助的例子进行分析(不过我们不会把重点放在具体的transport中, transport的定义位于spdk/lib/nvmf/transport.h中,主要给出了transport数据结构的定义(spdk_nvmf_transport)以及相应的操作集合(spdk_nvmf_transport_ops)。

2.1 qpair的创建

前面我们提到有一个定时器poller,会调用spdk_nvmf_tgt_accept,在每个transport上监听相应的地址和端口。

比如对于tcp这个transport,则会被以下函数调用: spdk_nvmf_transport_accept-> spdk_nvmf_tcp_accept。 然后对于tcp 这个transport上所有要监听的<地址,端口>列表进行监听, 如果有fd 过来,则调用_spdk_nvmf_tcp_handle_connect函数。

在这个函数中,我们对这个qpair进行初始化,结束后,调用相应的call back函数,即位于lib/event/subsystems/nvmf/nvmf_tgt.c这个文件中的new_qpair 函数。

这个new_qpair函数在正常情况下(这里忽略一些一些异常处理的case), 会选择一个CPU core,算法目前采用的是round-robin,然后通过thread之间传播时间的函数spdk_event_allocate, 使得这个core执行nvmf_tgt_poll_group_add。 

在这个函数中,会根据qpair属于的transport,然后调用spdk_nvmf_transport_poll_group_add加入到这个transport所在的thread的polling group中。

2.2 qpair上cmd的处理

前面我们提到每个thread上都会创建一个定时器来运行spdk_nvmf_poll_group_poll。 在这个函数中,我们会调用这个transport对应的polling 函数。

诸如TCP transport的polling函数是spdk_nvmf_tcp_poll_group_poll, 这个函数会处理每个qpair过来的请求。比如对于TCP transport来说,我们利用epoll创建了一个socket的polling group。

当有数据包请求的时候,我们会执行相应的call back函数spdk_nvmf_tcp_sock_cb, 然后会执行spdk_nvmf_tcp_sock_process。

经过处理,会执行spdk_nvmf_request_exec 这个函数,然后进行了以下工作: 进行一些简单的异常检查后,判断来request中cmd的opcode:

如果是fabric command (0x7f), 则调用spdk_nvmf_ctrlr_process_fabrics_cmd。

在这个函数中,也有一些处理:

如果qpair的ctrlr为空,则仅仅处理连接相关的操作,调用spdk_nvmf_ctrlr_connect (这个函数可以处理admin qpair和I/O qpair的连接)。

如果qpair的ctrlr不为空,则判断是不是admin是不是admin qpair。如果是,可以允许NVMe-oF 的property set或者get相关的命令。

判断该qpair是不是admin qpair,如果是,则执行spdk_nvmf_ctrlr_process_admin_cmd。

如果request的cmd opcode 不是fabric command,或者不是admin qpair,则执行spdk_nvmf_ctrlr_process_io_cmd。

无论是哪种cmd,无论命令是不是同步,最终总要执行spdk_nvmf_request_complete, 这个函数中会调用spdk_nvmf_transport_req_complete 函数。

比如如果这个transport是TCP, 则会调用spdk_nvmf_tcp_req_complete。然后TCP transport 会进行后续的处理。

2.3 qpair的销毁

在通用NVMe-oF层,主要是调用spdk_nvmf_qpair_disconnect进行对qpair的断链。这个函数的触发有多种情况。

由transport 层汇报上来的事件。比如在TCP transport中,侦测到协议处理的问题,会调用这个函数。

在通用NVMe-oF层主动断开连接,比如(1) qpair ctrlr所对应的NVM subsystem pause了,或者要被删除; (2) qpair连接的时候,出现错误。

目前SPDK 中有关qpair的异常处理,需要进一步加强。因为在很多场景下涉及到NVM subsystem的热插拔相关的事件(比如用RPC 命令增加一个新的NVM subsystem或者删除一个NVM subsystem),会导致qpair的处理变得相对复杂。

3、总结

本文对SPDK NVMe-oF target的代码做了一些简单的介绍,希望对大家学习SPDK NVMe-oF target有所帮助。最后也欢迎大家对SPDK NVMe-oF target 多测试,多报一些问题(希望有具体可复现的步骤,帮助定位问题),甚至提供一些相关的patch,最终帮助SPDK NVMe-oF target 这个开源程序能够变得更好。

4、Q&A

SPDK NVMe-oF target近期的Roadmap会是什么?

  • 1. 增加和完善新的Transport 支持。目前SPDK NVMe-oF的transport 支持RDMA 和TCP。 以后还会支持基于FC, 会由Broadcom和Netapp提交相应的代码。 另外对于已有的Transport会继续进行功能的完善,包括错误处理流程方面的加强;
  • 2. 遵循NVMe express给出的spec,继续完善通用NVMe-oF 层代码的功能处理;
  • 3. 最重要的一点是不断的改进性能,以及扩展性等等。

近期 SPDK NVMe-oF TCP transport的开发预期是什么?

  • 1.继续功能性方面的完善;
  • 2. 测试这个transport相关的性能;
  • 3. 开展和VPP的整合和测试工作

近期对 SPDK NVMe-oF RDMA transport有何具体计划?

我们会不断地完善以及优化RDMA transport,尤其是针对一些exceptional case。另外我们虽然我们使用ibverbs的标准API, 但是我们也会在不同的RDMA协议(RoCEV2 & iWarp)上, 即在支持不同RDMA协议的网卡上进行相关的测试。

发表评论

电子邮件地址不会被公开。 必填项已用*标注