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

【前言】

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

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

【现象】

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

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

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

经过NVMF写盘失败,疯狂打印错误码15;数据结构

做为对照,经过本地写盘,一切正常。app

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

【分析】

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

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

分析步骤(一)

在下发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程序); 代码见附录;

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

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

调用是来自于这里,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格式时,问题就会出现;

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,能够在两种模式中切换;

这样的目的是,能够造成彻底的对比,对齐全部能对齐的条件,分析在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;

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

如图:

这是一个重要的发现,基本能够解释为何解决办法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了,如图:

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

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

分析步骤(三)

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

立刻构造相同的形态,

写本地,发现重现了!

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

几乎能够顺利成章的推论出,是否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结尾的,因此能够解释错误刷屏没有那么密集了。

看起来,仍是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中长度问题,如图:

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

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

修改!

验证!

各用例测试经过!

问题消失!

提供第2个解决办法,按如上代码,能够完全解决问题。

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

可是总感受这种改法太土了!能够消除问题,可是隐隐感受不爽!

分析步骤(五)

有没有其它方法?

带着疑问继续挖。

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

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

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

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

红框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

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

相关文章
相关标签/搜索