SPDK宣布在NVMe-oF Fabrics中支持TCP transport

作者:杨子夜,Intel存储软件开发工程师,主要从事SPDK软件开发工作。
原文地址:DPDK与SPDK开源社区

随着由NVM Express 发布新闻宣告发布了 NVMe/TCPtransport [1][2](NVMe-oF TP 8000 TCP Transport)的spec,SPDK 也基于master branch (https://github.com/SPDK) 发布了patch[3]进行对这个transport的支持。在这篇文章中我们会简要介绍一下为什么需要NVMe/TCP transport,并且对SPEC做简单介绍, 然后介绍SPDK library中对于NVMe/TCP transport的支持情况, 包括现状、发展蓝图以及一些简单的使用方法。

1. NVMe/TCP transport spec的诞生

近日NVM Express (NVMe 协议标准工作组) 发布了NVMe_TP_8000这个SPEC文档。这个SPEC从2017年7月19日开始起草,历时16个月的时间,终于发布了第一个版本。这个SPEC 弥补了NVMe over Fabrics的一些短板。由于NVMe的初衷是提供一个协议用于高速访问本地的PCIe SSD (NVMe over PCIe), 去除SCSI 层,使用简化的并发协议,以提高性能。在本地高速访问协议出现以后,很自然地有用户希望NVMe可以进行远程访问(Remote Access)。就像SCSI 协议在以太网协议上扩展成为iSCSI协议,NVMe 协议也在Fabrics, 包括InfiniBand, Fibre Channel, Ethernet 上扩展成为了NVMe over Fabrics (简称NVMe-oF)协议,如图 1所示。为了保证NVMe低延迟的属性在fabrics 上继续延续,通过远程Fabrics的访问必须继续高效。于是最开始的NVMe-oF 协议制定了基于RDMA (Remote Direct Memory access) 协议的一个transport。 RDMA协议非常高效,对于极端情况,在fabrics上的延迟的开销会非常小。比如通过NVMe 协议对于本地盘进行随机读,对于(4KB大小,qd=1)的随机读的latency在几十微妙左右,那么在RDMA的transport上只会增加同样数量级的时延,这样NVMe协议的高效访问对于远程依然有效,NVMe协议的初衷依然得以保存和延续。于是用户可以使用各种Fabrics(例如Inifiniband 或者Ethernet), 只要该transport能够支持NVMe-oF协议中规定的使用RDMA的交互协议, 当然基于FC Transport的有专门的FC-NVMe 管理,定义在INCITS 540 Fibre Channel中。


图 1 NVMe over Fabrics 架构

但是从去年开始,标准组织又开始制定基于TCP的transport。众所周知,和RDMA协议相比,使用TCP 协议会破坏NVMe 协议设计的初衷,主要原因是使用TCP 协议会带来远高于本地PCIe访问的网络延迟,使得NVMe协议低延迟的目标遭到破坏。不过笔者认为有以下4个原因(仅供参考)促使了NVMe/TCP Transport的诞生:

1. NVMe 虚拟化的出现。 在NVMe虚拟化实现的前提下,NVMe-oF target那端并不一定需要真实的NVMe 设备,可以是由分布式系统抽象虚拟出来的一个虚拟NVMe 设备,为此未必继承了物理NVMe设备的高性能的属性 。那么在这一前提下,使用低速的TCP协议也未尝不可。

2. 向后兼容性。NVMe-oF协议,在某种程度上希望替换掉iSCSI 协议(iSCSI最初的协议是RFC3720,有很多扩展)。iSCSI协议只可以在以太网上运行,对于网卡没有太多需求,并不需要网卡一定支持RDMA。 当然如果能支持RDMA, 则可以使用iSER协议,进行数据传输的CPU 资源卸载(workload offloading)。 但是NVMe-oF 协议一开始没有TCP的支持。于是当用户从iSCSI向NVMe-oF 转型的时候,很多已有的网络设备无法使用。这样会导致NVMe-oF协议的接受度下降。在用户不以性能为首要考量的前提下,显然已有NVMe-oF协议对硬件的要求,会给客户的转型造成障碍,使得用户数据中心的更新换代不能顺滑地进行。

3. TCP offloading。虽然TCP协议在很大程度上会降低性能,但是TCP也可以使用offloading,或者使用Smart NIC,或者FPGA。那么潜在的性能损失,可以得到一定的弥补。那么提供一个TCP协议的transport,是可行的。总的来说短期有性能损失,长期来讲协议对硬件的要求降低,性能可以改进。为此总的来讲,接受度会得到提升。

4. 和Software RoCE的比较。在没有TCP transport的时候,用户在不具备RDMA网卡设备的时候。如果要进行NVMe-oF的测试,需要通过Software RoCE,把网络设备模拟成一个具有RDMA功能的设备,然后进行相应的测试。其真实实现是通过内核的相应模块,实际UDP 包来封装模拟RDMA协议。有了TCP transport协议,则没有这么复杂,用户可以采用更可靠的TCP协议来进行NVMe-oF的一些相关测试。 从测试部署来讲更加简单,有效。

2. NVMe/TCCP transport 协议简要介绍

NVMe/TCP transport 的协议,在一定程度上借鉴了iSCSI的协议,例如iSCSI数据读写的传输协议 。这个不太意外,因为有些协议的指定参与者,也是iSCSI协议的指定参与者。另外iSCSI协议的某些部分确实写得很好。 但是NVMe/TCP 和iSCSI协议相比更加简单,可以说是取其精华。 接下来我们先看一些TCP transport协议的定义,然后再看一下基于TCP transport的交互过程。

NVMe/TCP PDU的定义

图 2 和图3给出了NVMe/TCP PDU (Protocol Data Unit)的定义,一共包括5个部分:HDR (= CH + PSH), HDGST, PAD, DATA, DDGST。HDR由CH + PSH 组成。这个 NVME PDU 被封装在一个TCP PDU 中, 也可以被分割在不同TCP PDU的Payload中。对于这个点,不需要使用NVMe/TCP transport的编程人员考虑。

1 . HDR: PDU header的头部定义。 由CH (Common header,通用头部) 和 PSH (PDU specific header)组成。

a. 其中CH固定长度8个bytes,并且定义了以下的数据字段,以byte为单位。
i. PDU-type (offset=0, len=1): PDU的类型。一共有9种不同类型的PDU header。
ii. Flags (offset=1, len=1: 不同类型的PDU 有不同类型的标志位。
iii. Header Length (hlen: offset=2, len=1): PDU type的长度包括 header digest的长度。
iv. PDU Data Offset (PDO: offset=3, len=1): 如果有数据字段,这个值代表数了数据 (Data, 如果存在的话) 在整个PDU中的offset的起始值,这个值最大不超过128 。
v. PDU Length (PLEN: offset=4, len=4): 整个PDU的长度,包括CH + PSH + HDGST(可选) + PAD(可选) + DATA(可选) + DDGST (可选)。

b. PSH:不同类型的PDU,请参考TP 8000SPEC( [2] 中ZIP包解压后名字叫做“NVMe-oF – TP 8000 TCP Transport 2018.11.13-Ratified”的文件)中描述的每个PDU的定义。

2 . HDGST: Header Digest. Header的摘要,其固定长度是4 bytes,header digest是可选的。 如果target 和host 协商需要使用Header Digest,相应的PDU 才会有。

3 . PAD:主要用于数据的Padding。根据Spec的定义,Data在整个PDU中的最大的offload起始值是128,所以data padding的长度是有限。填充长度是根据CPDA(Controller PDU Data Align, target决定,当然是双方已经沟通好的值) 或者HPDA (Host PDU Data Align, 由host那段决定,当然是双方已经沟通好的值) 的值,构建出一个不超过128长度大小的值A,再减去(HDR 的Len以及可能的HDGST的长度 , 计为B)。 如果这个值(A – B)是负,则填充长度为0。 一般情况下,PAD的填充的长度是0, 也就是说未必有padding 的数据。

4 .DATA. PDU中带有的数据,未必一定携带。Data的数据一般包含在C2HData或者H2CData中。当然在CapsuleCmd,H2CTermReq, ,2HTermReq 类型的PDU中也会包含。对于这些类型的PDU,Data size的长度可能会有最大长度的限制。

5 . DDGST:是对持有PDU中持有数据的哈希值,其长度为4。Data Digest 也是可选的,只有target 和host协商使用Data Digest,相应的PDU才会有。

在9种不同类型的PDU 中, Host端使用以下4种:

  • 1) Initialize Connection Request PDU (ICReq): 类型代号0x0
  • 2) Host to Controller Terminate Connection Request PDU (H2CTermReq):类型代号是0x2
  • 3) Command Capsule PDU (CapsuleCmd): 类型代号是 0x4
  • 4) Host To Controller Data Transfer PDU (H2CData):类型代码是0x6

Target 端使用的PDU类型:

  • 1) Initialize Connection Response PDU (ICResp):类型代号 0x1
  • 2) Controller to Host Terminate Connection Request PDU (C2HTermReq): 类型代码0x3
  • 3) Response Capsule PDU (CapsuleResp):类型代号0x5
  • 4) Controller To Host Data Transfer PDU (C2HData):类型代号0x7
  • 5) Ready To Transfer PDU (R2T): 类型代号0x9

这9种PDU的类型不同,他们有以下的异同:
hlen:由于PSH长度不一样,所以HDR的长度(hlen)也不一样,但是hlen最大不会超过128。
HDGST: ICReq, H2CTermReq, ICResp 和C2HTermReq这4种类型的PDU是必然没有header digest。
DDGST: ICReq, H2CTermReq, ICResp, C2HTermreq, CapsuleResp以及R2T 这六种类型的PDU 是必然没有data digest的。

基于NVMe /TCP transport的交互

使用TCP transport, target 和host 需要建立相应的TCP 连接。然后NVMe协议中的每一对qpair (由submission queue, 和completion queue组成, 可以是admin qpair 或者是I/O qpair) 会被映射到一个TCP 连接上。举个例子,如果host和target了建立了1个admin qpair,3个I/O qpair, 那么就会有4个TCP连接。总的来讲,通过TCP transport 建立一个连接的简要流程如下:

Target 端监听到一个TCP 端口上,等到host的连接。

Host 通过TCP 请求发起一个连接,连接建立后。发送ICReq PDU, 然后等待Target端的反应。

Target端接收数据,解析PDU ,判断是否是ICReq,如果请求内容正确,则返回ICResp PDU,另外把这个连接设置为active状态,进入这个状态中,意味着不能再接收ICResp PDU。如果在解析过程中出现错误,则返回C2HTermReq, 同时target进入等待时间 (一般为设置为30 秒)。 如果host 没有关闭连接(指收到相应的数据包),则会主动关闭连接。

Host 端接收数据,解析PDU,判断是否是ICResp PDU在过程中,出现错误则返回H2CTermReq,然后关闭连接。再收到正确的ICResp PDU后,标记连接的状态是ACTIVE。以后再收到ICResp,则判断为错误状态。如果收到ICResp内容正确,Host会封装上层发送的NVMe command,封装成为CapsuleCmd, 发送出去。

Target端接收数据,发现是CapsuleCmd PDU后,会提取其中NVMe command (命令为64bytes)进行执行。

a. 如果从host传送过来的PDU已经包含数据 (指InCapsuleData),或者不需要数据,则直接交给NVMe-oF/NVMe 协议层处理; 如果需要数据(没有InCapsuleData, 说明这个NVMe command是写操作),但是没有发送过来,Target端就发送R2T PDU。 Host接收到R2T PDU后, 如果包解析正确,则会发送一个或者多个H2C PDU,然后host等待新的PDU 包过来。 Target收到所有H2C PDU后,获得数据后,则交给NVMe-oF/NVMe 层执行。
b. 在target端的NVMe-oF/NVMe层执行,最终会返回。如果是操作有数据返回(说明从host发过来的NVMe command的是读操作),则会返回若干个C2H PDU给host 端,然后发送CapsuleResp(封装16bytes的NVMe response命令), 其实这个回复是可选的, 如果NVMe SQ 的控制流被禁止了。 如果返回不需要数据,则直接发送CapsuleResp。
c. Host 接收相应的数据,要么是若干个C2H PDU + 可选的CapsuleResp, 要么是单独的CapsuleResp.
总的来讲,a、b、c 步骤完成了一轮从host 端发送封装NVMe command -> CaspsuleCmd PDU,然后最终target 端返回相应数据,以及CapsuleResp(对于读NVMe command非必需)的操作。当然在实际实现过程中,有相应的优化手段,远远没有这么简单。

3. 基于SPDK的NVMe-oF TCP 的transport 的实现以及使用介绍

SPDK TCP transport的现状和细节

SPDK 库支持NVMe/TCP transport的代码主要分为以下几块:

  • 头文件的定义: 主要位于spdk/include/spdk/nvme_spec.h, spdk/incude/spdk_internel/nvme_tcp.h
  • Host端的实现:主要位于 spdk/lib/nvme 目录,主要实现在nvme_tcp.c文件中
  • Target端的实现: 主要位于spdk/lib/nvmf目录,主要实现在tcp.c文件中

对于 SPDK第一版本的TCP transport实现,主要目标是基本功能的支持, 包括和kernel 实现的交互性。当然由于kernel版本也刚发布,可能会存在一些问题,比如SPDK 实现的bug,或者kernel实现的bug,为此还处于继续测试和集成的阶段。
另外在TCP transport的实现中,SPDK的target和host 端的TCP transport的PDU接收采用了同样的状态机的逻辑:

状态1:<接收 PDU CH>。 一旦接受满8个byte,则判断是否是个合法PDU。如果是则跳到状态2, 否则跳到状态4。
状态2:<接收 PSH + HDGST(可选) + PAD(可选)>; 如果接收的数据没问题 ,那么有两个可能: (a)需要继续接受数据,则跳到状态3;(b)执行相关NVMe-oF/NVMe 层的逻辑,并且跳到状态1,可以接续接收包。如果接收的数据有问题,则跳到状态4。
状态3:<接收 DATA + DDGST(可选)>:接收相应的数据。数据正确,则执行相应的NVMe-oF/NVMe层的逻辑,PDU接收逻辑跳转到状态1, 否则进入状态4。
状态4:< 错误状态>。发送 termreq命令。 Target 发送C2HTermreq, host发送H2CTERMReq,同时停止接收处理后续的数据。

SPDK对于NVMe/TCP transport的一些Roadmap

1. 功能的继续完善:继续遵循NVM Express 制定的有关TCP transport的SPEC。不仅确保了SPDK target 和host的交互性正常,另外要继续保证和Kernel 实现的交互性正常。

2. 性能优化,可能有以下几个方面:
a. 优化已有的TCP transport中的代码。
b. 采用用户态的TCP 协议栈 + 用户态的网卡驱动,比如集成VPP来优化TCP Transport, 就像iSCSI target可以整合VPP一样。
c. 后续可能会使用hardware的offloading,诸如FPGA, Smart NIC。这个可能需要设计offloading的接口,来适配各种offloading的引擎。

测试基于TCP transport的SPDK NVMe-oF

总的来讲,在SPDK 中使用NVMe/TCP transport比较简单。和使用RDMA transport相比,只要在原有的命令中把RDMA 替换成为 TCP 即可,以下是一些参考。

测试 SPDK NVMe-oF target

SPDK Target 端使用如下的配置(作为参考)

1. 下载支持TCP的branch([3]),编译后,启动SPDK NVMe-oF target。
例如使用命令:./app/nvmf/nvmf_tgt.
2. 在另外一个Shell窗口执行以下RPC 命令:

Host 端的测试方法(作为参考)

1. 使用SPDK的自带程序测试基于SPDK的NVMe-oF target.

2. 使用kernel host测试SPDK的NVMe-oF target.

测试SPDK NVMe-oF Host

Kernel Target 端使用如下的配置(作为参考)

1. 可以采用以下的script, 进行简单配置。命名为add.sh。

然后在shell中执行:

2. 当然后续要清理的时候可以执行以下命名为delete.sh, 内容如下:

备注:add.sh和delete.sh 仅供参考。具体配置Linux kernel NVMe-oF Target请参考官方文档

Host 端的测试方法(作为参考)

a.使用 perf 程序:

b.使用identify 程序:

备注:如果在Target端安装的Linux kernel 中的NVMe/TCP不支持header摘要(HDGST)或者 Data摘要(DDGST), 那么使用SPDK host的时候,需要在一开始发送ICReq PDU中,把相关Digest的bit清零。目前我们还没提供较为简易的使用SPDK NVMe-oF host的配置(之后的Patch会提供),但是可以参考Reference中的Patch [5]。

参考文献

[1] “WelcomeNVMe™/TCP to the NVMe-oF™ Family of Transports,”:https://nvmexpress.org/welcome-nvme-tcp-to-the-nvme-of-family-of-transports/.
[2] “NVMe/TCPTransport Binding specification,”:https://nvmexpress.org/wp-content/uploads/NVM-Express-over-Fabrics-1.0-Ratified-TPs.zip.
[3] “TheSPDK NVMe/TCP support,”:https://review.gerrithub.io/#/c/spdk/spdk/+/425191/.
[4] “TheLinux Kernel NVMe/TCP support,”:http://git.infradead.org/nvme.git/shortlog/refs/heads/nvme-tcp.
[5] “SPDKtemporary Patch for using SPDK host testing Linux TCP tranposrt based NVMe-oFtarget,”:https://review.gerrithub.io/#/c/spdk/spdk/+/427189/.

发表评论

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