【华为云技术分享】避坑指南:关于SPDK问题分析过程

【前言】

这是一次充满曲折与反转的问题分析,资料很少,代码很多,经验很少,概念很多,当内核态,用户态,DIFLBA,大页内存,SGLRDMANVMESSD一起迎面而来的时候,问题是单点的意外,还是群体的无奈?

为了加深记忆,也为了分享出来给人以启示,特记录这次问题分析过程。

【现象】

同事L在项目中需要使用NVMF写盘,发现写盘失败,疯狂打印错误码:

图片中虽然截取的比较少,但实际是疯狂的一直打印。

故障现象简要描述一下就是:

通过NVMF写盘失败,疯狂打印错误码15

作为对照,通过本地写盘,一切正常。

注:这里的盘,都是指SSD盘。目前实验室使用的型号是公司V3版本(HWE3xxx)。

 

【分析】

在这里把涉及到的一些基本缩略语都记录一下:

缩写

英文名称

中文名称

说明

SSD

Solid-State Disk

固态硬盘

固态电子储存芯片阵列制成的硬盘,含控制器和存储单元;按Block写入,按Page清除,修改前面写入的数据,实际是新写入Block,将原来的Block标记为垃圾,等后面按Page来清除;

NVME

Non-Volatile Memory Express

非易失性内存规范

非易失性内存这种类型的设备管理与读写的规范;SSD是常见的形式,但不是唯一形式;

RDMA

Remote Direct Memory Access

远程直接内存访问

为了解决网络传输中服务器端数据处理的延迟而产生的;

NVMF

NVMe Over Fabrics

 

当前主要使用的是NVMe Over

RDMA,NVMe协议通过RDMA来传输。因此存在一个请求端,存在一个服务端,后面统一使用请求端和服务端来描述吧;在SPDK的描述中,请求端有个标准术语initiator,服务端是target;

 

 

用户态

当前应用基本运行在用户态,使用的三方组件DPDK,SPDK基本运行在用户态;

 

 

内核态

在上一行描述中,使用了基本一词,也就是说,有少量的一部分还是在内核态,只有很薄的一层用于注册设备的;

DIF

Data Integrity Field

数据完整性字段

保护数据的,项目中会将SSD格式化为512+8的带DIF格式的;

LBA

Logical Block Address

逻辑块地址

标识写盘偏移位置;

 

 

2M大页

为了提升CPU的Cache命中率,DPDK初始化时标配内存大页,当前主要使用2M大页;

SGL

Scatter/Gather List

分散聚合列表

数据列表,其中的单项叫SGE,NVMe在提交IO数据时的buffer,一般包含1个或多个SGE。本地写盘(NVMe Over PCIe)时,IO命令支持SGL和PRP(Physical Region page 物理内存区域页)两种格式,SGL中的SGE长度较为灵活,而PRP是按页(page)来取内容;

RDMA传输数据也是使用SGL;

SGE

Scatter/Gather Element

分散聚合单元

SGL里的一个单元,SGL Segment,描述一段数据空间,实现上是一个结构体,含有地址,长度,key;地址一般是指向一段数据空间,但是,存在一种形式是指向另一个SGL;

 

习惯了缩略语作为名词后,总是容易忽略其背后更多的含义,问题的分析,需要对这些有更深的理解,最初对这些理解不深,对数据处理流程不清晰,起步很艰难。

 

()    

开始分析。。。

在下发IO时,通过变换IO的大小,队列深度,发现数据量较小时,则几乎没有问题,直接下发1M大小IO时,则必现。

因此,可以明显的推测出IO的大小与问题的出现紧密相关。

直接运行业务来验证问题,过于笨重了,而且非常麻烦,将问题直接简化为,一个服务端和一个请求端,发现均能稳定复现,他们分别是:

1 运行SPDK自带的appnvmf_tgt程序,这个就是NVMF的服务端了;

a)       进入spdk目录后,配置好2M大页;

b)       配置好nvmf.conf 配置文件,假设文件放在/opt/yy目录下;

                     i.            配置文件参考附录;

c)        运行./app/nvmf_tgt/nvmf_tgt -c /opt/yy/nvmf.conf

 

2 可以使用两种模式的请求端,

a)       一种是SPDK自带的perf程序,路径是./examples/nvme/perf/perf,会配置必要的参数;

                                i.            注意:系统也自带一个perf,不是系统自带的那一个;

                               ii.            Perf是一个测试工具,会随机产生数据大量写入,可以验证问题修复性,但不利于问题最初的分析;

b)       一种是自已改造nvme目录下的helloworld程序(初始版本,由同事C提供,后来经过了一些改良,后续称为DEMO程序);

                     i.            代码见附录;

 

因为都是运行在用户态,所以开启调试还是很方便的。两端同时开启调试模式,进行单步跟踪,发现错误码是在异步模式下轮循得到,如图

函数名称已经告知,是处理完成的结果;

调用是来自于这里,383行:

303行下断点,根据栈信息(没有有效信息,略)看,错误码可能来自于SPDK的某个异步调用,也可能来自于设备,查遍SPDK代码,发现根本没有15这个错误码的设置,基本推导为是由SSD返回的。

 

根据最初的信息可知,IO的数据量大小会影响问题出现,IO数据量较小时不会出现,那么分界点在哪里呢?

采用二分法在DEMO程序上尝试,发现LBA的个数为15时,是分界点

那么,怎么用起来呢?

单步跟踪,有一个参数进入视野,命名空间(NVME的协议规范吧,一块SSD下有一个控制,有若干个命名空间)的sectors_per_max_io参数。

修改这个参数,可以控制最后写盘时的大小,在DEMO程序上试验,问题消失。

但是当IO大小与深度较大,要么出现内存不足错误码,要么错误依然出现,另外多盘场景下非常容易再现。

 

给出有条件解决办法1

1     修改如上位置;

2     业务下发时要求对IO的大小和下发的盘数进行限定;

实际使用时,因为必需多盘,要改造成单盘,非常困难,不是理想的解决方案。

 

另外还发现不同版本的盘,最小适配值不一样,最安全值是7,但是后来主要选取一块15为安全线的盘来分析问题使用。

 

()    

为了快速解决问题,开始尝试广泛求助,这么明显的问题,别人有没有遇到?

在遍访hi3ms和搜遍google,以及请教相关可以找到的同事,嘿,还真没有第二例!

而且更为奇怪的是,在Intel的基线报告中明明就有较大的IO数据量的NVMF测试,还有正常的结果。

 

怎么在这里就有问题呢?

不同点:

Intel肯定使用Intel的盘;

这儿用的是公司的盘;

难道是因为这个?

硬件上,理论上没有这么大差异吧。

经过一番探索发现,当把硬盘格式化为不带DIF时,NVMF也是正常的,如果格式化为带DIF的,即512+8格式时,问题就会出现;

SOIntel为啥没有问题,基本已经确定,他们用的是不带DIF格式,同时发现不带DIF,时延会快一点点,这很好理解。

 

有一个疑惑,始终没有答案,为什么本地写没有出现,而NVMF写会出现呢?

这是需要回答的最重要的问题。

作为基础,需要先简单了解一下NVME的写盘。

这个过程是异步的;

写盘前,程序将数据按照队列(比如SGL)准备好,然后通知SSD,程序就完事了;

然后是SSD会到机器中把数据取出写入盘中,处理完成后,然后通知程序,程序检查结果队列。

可以看出,当前说的写盘,主要是指将数据按照队列准备好就完成了,后面一段是由SSD设备来处理的。

有了这个基础,可以较快理解本地写盘了,调用SPDK API后,由SPDK准备队列,然后提交,真正把数据存起来的事情是SSD里控制器做的。。。

 

但是NVMF写盘呢?毕竟中间有段网络,是怎么处理的。。。

为了便于分析,所以选择改造DEMO,主要是perf比较复杂,随机的LBA和大数据量对分析有较大干扰。

DEMO程序中,指定在0LBA开始提交数据,而且每次提交17块数据(总长度17*520=8840)。

那为啥数据块指定17呢?

因为15及以下是不会出现问题的,根据前面的分析,这块SSD的正常分界线是15,而1624次方,在计算机中2N次方过于特殊,因此选择普通的17

其次,保证其它地方完全一样,仅在初始化时,形成两种模式,一种是本地写,一种是NVMF写;

如图,手动直接改变红框里的参数,由tr_rdmatr_pcie,可以在两种模式中切换;

这样的目的是,可以形成完全的对比,对齐所有能对齐的条件,分析在NVMF的哪个环节出现问题。

在初步单步跟踪了一下调用过程,可以梳理出本地写与NVMF写的基本处理流程:

本地写:

1)                在请求端,申请了一块连续的内存1M大小,块大小以4K大小对齐;

2)                将其中的17个块(也就是1M大小只用了17*520字节)通过调用SPDKAPI进行写盘;

3)                SPDKAPI会调用以PCIE模式接口(系统初始化时,注册的回调函数,在初始化入口时,上面图中红框的参数决定了会走向PCIE对应接口);

4)                准备数据队列,提交SSD写盘请求,返回;

5)                轮循处理完成的接口,获取到写盘成功通知;

 

NVMF写:

请求端侧:

1     在请求端,申请了一块连续的内存1M大小,块大小以4K大小对齐;

2     将其中的17个块(也就是1M大小只用了17*520字节)通过调用SPDKAPI进行写盘;

3     SPDKAPI会调用以RDMA模式接口(同上,初始化时,注册了RDMA的回调函数,上图中红框的参数决定了,这里的调用走向RDMA对应接口);

4     准备数据队列,通过RDMA网络传送到服务端,返回;

服务端侧:

5     服务端的RDMA在轮循(poll)中收到数据到来的通知;

6     组装数据结构,便于内部API调用;

7     数据一路调用bdevspdknvmeapi,地址被转换为物理地址,最后调用pcie的数据接口提交;

8     然后按规范按下提交门铃,返回;

两侧异步(提交请求后,只能异步等待结果打印)打印结果:

9     请求端轮循处理完成的接口,如果错误会出现打印;

通过debug可以看到错误码是15

10  服务端轮循处理完成的接口,如果错误,会出现打印:

反复对本地和NVMF下发数据(上面0开始,17块数据),逐个流程与参数对比(双屏提供了较大的便利),确实发现不少异同点:

1         本地写的过程与NVMF写的请求端过程,几乎一样,不同的是本地写的数据提交是到SSDNVMF请求端的写调用RDMA的接口;

2         NVMF服务端有很长的调用栈(有30层深),而本地写根本不存在这个过程;

3         NVMF服务端在经过系列调用后,最后走到了像本地写盘一样的函数调用,nvme_transport_qpair_submit_request

a)       似乎是个显然的结论,NVME OVER RDMA实际是,数据经过了RDMA传输后,还是NVME OVER PCIE

4         本地写时,只有1SGL,这个SGL里面只有1SGENVMF的请求端在调用RDMA前,也是只有1SGL,这个SGL里也只有1SGE

5         NVMF服务端的在写盘前,只有1SGL,但是这个SGL里有2SGE

整个过程,用图来描述如下:

如图:

这是一个重要的发现,基本可以解释为什么解决办法1部分场合是有效的(15的安全线内数据大小小于8k,保证1SGL里只有1SGE),但无法解释有一些场合失败。

捋一下,就清楚多了:

RDMANVMF的请求端拿到的数据是1SGL内含1SGE,经过RDMA后,从NVMF服务端拿到的数据是1SGL内含2SGE

至此,似乎基本“锁定”了肇事者了,就是RDMA了!

 

但是,在翻阅RDMA的资料,SSD的资料后,发现1SGL里,1SGE2SGE根本是自由的,自由的。。。

虽然,RDMA在接收数据后,将1SGE分成2SGE,有引起问题的嫌疑,但是从资料介绍看,似乎不能直接构成问题。

为了验证1SGL里多个SGE是不是问题,又开始改造DEMO了,构造了写数据前,将数据分为多个SGE了,如图:

先试了试NVMF,发现可以复现,和前面的NVMF没有什么两样,

接下来试了试本地,发现没有问题,也就是说,疑问没有消除。

 

()    

山重水复疑无路,只好推倒,从头再来分析,一次偶然的NVMF下发中发现,2SGE的地址中,第2SGE的地址在前,第1SGE的地址在后,然后密切关注,即便在DEMO程序中,这个地址的先后也有一定的随机,多数时候是顺序的,少数时候是颠倒的,但是无论怎样,1SGE与另1SGE中是不连续,也就是SGE1SGE2之间有空洞。

马上构造相同的形态,

写本地,发现重现了!

这是一个重要发现!本地也能重现!

几乎可以顺利成章的推论出,是否NVMF不是关键!那么也就排除了RDMA的嫌疑了!

写盘时,如果多个SGE的数据区完全连续,则没有问题,如果多个SGE的数据区不连续,则会出现问题。

那么,很容易推导出问题所在点,当前用的这个SSD不支持不连续的SGE!难道是SSD?!

然后。。。(此处略去一段文字不表。。。)

。。。

。。。

是的,SSD没有问题,有问题的是那个8192的长度,正确的应该是8320

8320是什么,8192是什么?

8192512 * 16

8320520 * 16

看看,之前一直不理解那个刷屏的错误提示,什么叫“DATA SGL LENGTH INVALID”,这个含糊不清的提示,也有很多可能,既可能是SGL里的SGE个数不对,也可能是SGE里的长度不对,还可能是里面的长度字段读写不对,还可能是寄存器出错,还可能内存被踩。。。

但是,真相就是,SGE里的数据长度没有和BLOCK的基本大小520对齐!现在用的格式是带DIF区的,512+8=520

那个提示是告诉你,数据块没有对齐,SGE里的长度无效!

 

当各个点针对性的改好了这个基本参数时,

DEMO的本地正常了,

DEMONVMF也正常了,

似乎真相大白了。。。

然而,还没高兴几分钟,使用perf下发1MIO时,问题又复现了!

 

()    

细心的跟踪后发现,虽然问题复现了,但是没有以前刷屏那么多了,而且通过单步发现,只要SGE数据的地址是以FF000结尾的,就会出现问题。

回溯这个地址,可以看到,来源于RDMA在收到数据后就出现了,偶尔会出现FF000结尾的,所以可以解释错误刷屏没有那么密集了。

看起来,还是RDMA有问题啊~

继续分析可以发现,这些地址,实际也不是RDMA临时分配的,而是从缓冲队列里获取的。

基本可以认为,缓冲队列中有很多可供选择,偶尔会拿到FF000结尾的这种来做缓冲,只要这种地址就会出现问题。

 

那么,为什么这种地址就会出现问题呢?

还记得前面有一个步骤吗?设置2M大页内存,SPDK是基于DPDK的,DPDK内存队列是要求大页内存的,最常用的是2M大页。

这些缓冲就是从DPDK那些大页里获取的,而FF000就是靠近2M边界的,一般的缓冲使用也没有啥问题,但是SSD不接受跨大页的空间,因此在准备提交队列时,如果遇到要跨大页的,将这个SGE做切分,1分为2,以FF000结尾的地址上只能存4096字节,因此一个SGE4096,余下的放在下一个SGE里,而4096又不是520的对齐倍数,所以出问题了。

针对性的解决办法是,在获取地址前,加一个判断,如果是这种地址就跳过。

修改!

验证!

屏住呼吸。。。

但是,再一次出乎意料,用perf在大IO下测试依然有问题!

 

不气馁,再战!

打开日志(因为是异步,而且是大数据量测试,所以只好在关键地方增加日志,记录下这些地址分配细节,主要地点,一个是提交请求时,见上面的文件和代码行,就不贴代码了,一个是入RDMA收到数据最开始拿到的地方,还有一个是完成时的结果),继续分析。

一下就看到,还有一种地址分配异常,也会形成SGE中长度问题,如图:

 

再一次在获取地址的位置进行修改屏蔽之,将两种要跳过的直接合一。

如图(471~475,另外在nvmf_request_get_buffers函数中需要配置进行跳过处理):

修改!

验证!

各用例测试通过!

问题消失!

提供第2个解决办法,按如上代码,可以彻底解决问题。

 

虽然问题解决了,跳过一些特殊地址,有一些浪费,

但是总感觉这种改法太土了!可以消除问题,但是隐隐感觉不爽!

 

()    

有没有其它方法?

带着疑问继续挖。

既然RDMA只是使用缓冲的队列,那就有一个地方是分配这种缓冲队列的,分配出来却不用,明显有点浪费,那至少可以做到,分配的时候就不要分配这种数据吧。

一路回溯,终于找到申请的地方,但是甚是复杂,容后慢慢消化吧。

发现有段文字描述很长,和地址的分配很相关,

带着这些信息再来单步查看分配缓冲过程,大致推测修改过程中的一个参数,就可以影响到后面的处理流程了。

红框1为代码默认参数,修改为红框2的,红框2两个参数的含义为单生产者单消费者,DEMO程序中完全匹配这个模式。

修改!

验证!

RDMA在获取SGE地址时,是单向增长的。

问题消失!

一个参数消除掉问题,对比起来,舒适多了!

 

【小结】

1     问题最后的解决办法就是:

a)       NVMF的配置文件中需要显性设置IOUnitSize的大小,与所用的Block大小成整数倍对齐,当前使用520Block,建议设置为8320

b)       修改创建内存池参数;最后图中的一个参数即可。

 

2     过程非常曲折,但是只要不放弃,跟着代码,再翻阅资料,大胆假设,小心求证,不断迭代,终能找到问题所在;

a)       如果对相关概念与处理过程熟悉,会大幅度节约时间;

 

3     最后安利一下,VSC,配上Remote – SSH,可以直接在呈现Linux机器上的代码,进行可视化调试,在代码里任意穿梭,哪里疑惑点哪里,对本次分析问题有极大的帮助;

 

附录:

1.      ,如果打不开,直接邮件索取;

2.      Nvmf的配置文件如下

[Global]

[Nvmf]

[Transport]

  Type RDMA

  InCapsuleDataSize 16384

  IOUnitSize 8192

[Nvme]

  TransportID "trtype:PCIe traddr:0000:04:00.0" Nvme0

  TransportID "trtype:PCIe traddr:0000:05:00.0" Nvme1

  TransportID "trtype:PCIe traddr:0000:82:00.0" Nvme2

[Subsystem1]

  NQN nqn.2020-05.io.spdk:cnode1

  Listen RDMA 192.168.80.4:5678

  SN SPDK001

  MN SPDK_Controller1

  AllowAnyHost Yes

  Namespace Nvme0n1 1

[Subsystem2]

  NQN nqn.2020-05.io.spdk:cnode2

  Listen RDMA 192.168.80.4:5678

  SN SPDK002

  MN SPDK_Controller1

  AllowAnyHost Yes

  Namespace Nvme1n1 1

[Subsystem3]

  NQN nqn.2020-05.io.spdk:cnode3

  Listen RDMA 192.168.80.4:5678

  SN SPDK003

  MN SPDK_Controller1

  AllowAnyHost Yes

           Namespace Nvme2n1 1

 

点击这里,了解更多精彩内容

已标记关键词 清除标记
相关推荐