避坑指南:关于SPDK问题分析过程
【前言】
这是一次充满曲折与反转的问题分析,资料很少,代码很多,经验很少,概念很多,当内核态,用户态,DIF,LBA,大页内存,SGL,RDMA,NVME和SSD一起迎面而来的时候,问题是单点的意外,还是群体的无奈?
为了加深记忆,也为了分享出来给人以启示,特记录这次问题分析过程。
【现象】
同事L在项目中需要使用NVMF写盘,发现写盘失败,疯狂打印错误码:
图片中虽然截取的比较少,但实际是疯狂的一直打印。
故障现象简要描述一下就是:
通过NVMF写盘失败,疯狂打印错误码15;
作为对照,通过本地写盘,一切正常。
注:这里的盘,都是指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写的基本处理流程:
本地写:
- 在请求端,申请了一块连续的内存1M大小,块大小以4K大小对齐;
- 将其中的17个块(也就是1M大小只用了17*520字节)通过调用SPDK的API进行写盘;
- SPDK的API会调用以PCIE模式接口(系统初始化时,注册的回调函数,在初始化入口时,上面图中红框的参数决定了会走向PCIE对应接口);
- 准备数据队列,提交SSD写盘请求,返回;
- 轮循处理完成的接口,获取到写盘成功通知;
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