避坑指南:关于SPDK问题分析过程

【前言】

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

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

【现象】

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

避坑指南:关于SPDK问题分析过程_第1张图片

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

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

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

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

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

【分析】

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

避坑指南:关于SPDK问题分析过程_第2张图片

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

分析步骤(一)

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

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

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

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

  • 进入spdk目录后,配置好2M大页;
  • 配置好nvmf.conf 配置文件,假设文件放在/opt/yy目录下;配置文件参考附录;
  • 运行./app/nvmf_tgt/nvmf_tgt -c /opt/yy/nvmf.conf;

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

  • 一种是SPDK自带的perf程序,路径是./examples/nvme/perf/perf,会配置必要的参数; 注意:系统也自带一个perf,不是系统自带的那一个; Perf是一个测试工具,会随机产生数据大量写入,可以验证问题修复性,但不利于问题最初的分析;
  • 一种是自已改造nvme目录下的helloworld程序(初始版本,由同事C提供,后来经过了一些改良,后续称为DEMO程序); 代码见附录;

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

避坑指南:关于SPDK问题分析过程_第3张图片

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

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

避坑指南:关于SPDK问题分析过程_第4张图片

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

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

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

那么,怎么用起来呢?

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

避坑指南:关于SPDK问题分析过程_第5张图片

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

避坑指南:关于SPDK问题分析过程_第6张图片

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

给出有条件解决办法1:

(1) 修改如上位置;

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

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

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

分析步骤(二)

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

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

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

怎么在这里就有问题呢?

不同点:

  • Intel肯定使用Intel的盘;
  • 这儿用的是公司的盘;

难道是因为这个?

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

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

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

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

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

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

这个过程是异步的;

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

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

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

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

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

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

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

那为啥数据块指定17呢?

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

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

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

避坑指南:关于SPDK问题分析过程_第7张图片

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

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

本地写:

  1. 在请求端,申请了一块连续的内存1M大小,块大小以4K大小对齐;
  2. 将其中的17个块(也就是1M大小只用了17*520字节)通过调用SPDK的API进行写盘;
  3. SPDK的API会调用以PCIE模式接口(系统初始化时,注册的回调函数,在初始化入口时,上面图中红框的参数决定了会走向PCIE对应接口);
  4. 准备数据队列,提交SSD写盘请求,返回;
  5. 轮循处理完成的接口,获取到写盘成功通知;

NVMF写:

请求端侧:

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

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

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

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

服务端侧:

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

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

(7)数据一路调用bdev,spdk,nvme的api,地址被转换为物理地址,最后调用pcie的数据接口提交;

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

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

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

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

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

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

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

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

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

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

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

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

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

如图:

避坑指南:关于SPDK问题分析过程_第8张图片

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

捋一下,就清楚多了:

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

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

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

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

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

避坑指南:关于SPDK问题分析过程_第9张图片

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

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

分析步骤(三)

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

马上构造相同的形态,

避坑指南:关于SPDK问题分析过程_第10张图片

写本地,发现重现了!

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

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

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

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

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

。。。

。。。

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

8320是什么,8192是什么?

8192是512 * 16;

8320是520 * 16;

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

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

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

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

DEMO的本地正常了,

DEMO的NVMF也正常了,

似乎真相大白了。。。

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

分析步骤(四)

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

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

避坑指南:关于SPDK问题分析过程_第11张图片

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

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

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

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

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

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

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

修改!

验证!

屏住呼吸。。。

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

不气馁,再战!

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

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

避坑指南:关于SPDK问题分析过程_第12张图片

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

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

避坑指南:关于SPDK问题分析过程_第13张图片

修改!

验证!

各用例测试通过!

问题消失!

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

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

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

分析步骤(五)

有没有其它方法?

带着疑问继续挖。

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

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

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

避坑指南:关于SPDK问题分析过程_第14张图片

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

避坑指南:关于SPDK问题分析过程_第15张图片

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

修改!

验证!

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

问题消失!

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

【小结】

(1)问题最后的解决办法就是: NVMF的配置文件中需要显性设置IOUnitSize的大小,与所用的Block大小成整数倍对齐,当前使用520的Block,建议设置为8320;修改创建内存池参数;最后图中的一个参数即可。

(2) 过程非常曲折,但是只要不放弃,跟着代码,再翻阅资料,大胆假设,小心求证,不断迭代,终能找到问题所在;如果对相关概念与处理过程熟悉,会大幅度节约时间;

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

附录:

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

 

点击关注,第一时间了解华为云新鲜技术~

你可能感兴趣的:(避坑指南:关于SPDK问题分析过程)