11

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

 3 years ago
source link: https://segmentfault.com/a/1190000022889512
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

【前言】

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

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

【现象】

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

Rrauuib.png!web

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

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

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

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

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

【分析】

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

zIrUzuQ.jpg!web

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

分析步骤(一)

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

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

jIZBzmJ.png!web

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

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

BFrUzqU.png!web

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

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

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

那么,怎么用起来呢?

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

N7fyiaR.png!web

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

ZJn6niR.jpg!web

但是当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,可以在两种模式中切换;

jAFVrir.png!web

这样的目的是,可以形成完全的对比,对齐所有能对齐的条件,分析在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)请求端轮循处理完成的接口,如果错误会出现打印;

uUjyuaY.png!web

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

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

yymAFfb.png!web

反复对本地和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;

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

如图:

ueuyu2Z.png!web

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

m6Vbqq7.png!web

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

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

分析步骤(三)

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

马上构造相同的形态,

2EbyiiU.jpg!web

写本地,发现重现了!

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

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

UbeMrmZ.jpg!web

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

IRFJjym.jpg!web

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

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

iAfyuuu.png!web

修改!

验证!

各用例测试通过!

问题消失!

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

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

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

分析步骤(五)

有没有其它方法?

带着疑问继续挖。

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

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

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

uEjUJze.jpg!web

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

VVraim6.png!web

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

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

jmQzIfb.png!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK