ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RDMA传输层深度解析:QP状态机、PSN与流控机制

RDMA传输层深度解析:QP状态机、PSN与流控机制

1. 为什么单独把传输层拿出来讲

RDMA和InfiniBand这套体系,很多人一开始都是被“高带宽、低时延”这几个字吸引进来的,真到上手调试的时候才发现,绕不开的反而是传输层这一大堆看似枯燥的机制。前面几篇我们把协议栈的整体框架、链路层怎么把数据搬上链路、网络层怎么寻址转发都过了一遍,这篇专门深挖传输层。之所以单独拎出来写,是因为传输层是所有可靠性与语义保证的集中地——QP状态机、PSN、重传、ACK、流控、MTU分片这些概念全都堆在这里,应用层看到的“可靠连接”“消息语义”“内存直接访问”,每一层体验最终都落到传输层的具体实现上。

这篇适合谁?已经对InfiniBand有基础了解、动手写过一些RDMA程序、但遇到性能波动或者偶发连接异常时不知道怎么定位问题的开发者。读完之后,你对QP为什么这么设计、PSN怎么会乱、重传风暴是怎么产生的、credit流控卡住时HCA会有什么表现,都会有一个更具体的认知。

传输层的核心任务,说直白点就是三件事:第一,把发送端的工作请求可靠地送到对端,并让对端能正确识别和重组;第二,在多个QP共享物理链路的时候,不让任何一个QP把接收端buffer冲垮;第三,把错误、拥塞、重传这些情况用统一的机制处理后,尽量不影响上层语义。这三件事做得怎么样,直接决定RDMA实际跑起来是稳定还是鸡飞狗跳。

2. 传输层的功能全景与QP设计逻辑

2.1 传输层到底管了哪些事

InfiniBand的传输层位于协议栈的第三层,往上承接上层协议(比如消息层和 verbs 接口),往下使用网络层的转发服务和链路层的物理传输能力。传输层自己维护的是一批“连接”的抽象——从软件视角看,连接就是一对QP(Queue Pair);从硬件视角看,连接就是一段有状态、有编号、有流控关系的报文序列。

具体管的事情包括这几块:

  • QP状态机的创建、迁移和销毁,以及围绕状态机展开的错误处理。
  • 报文序列号PSN的分配、校验、确认和重传。
  • 报文的分片与重组,也就是MTU相关的逻辑。
  • ACK/NAK的生成与处理,超时重传参数的动态调整。
  • 基于credit的接收端流控,防止发送端速度超过接收端处理能力。
  • 可靠服务下对重复报文、乱序报文的检测与丢弃/重排。
  • 原子操作(FetchAdd、CompareSwap)在连接上的语义保证。

这些功能不是孤立的,它们围绕一个中心目标运转:让用户可以像使用本地内存一样使用远端内存,同时不让网络的不确定性破坏上层语义。所以你会发现,传输层几乎所有设计都在做同一件事——把“不可靠”的网络变成看起来“完全可靠”的通道。

2.2 QP类型的选择决定了传输层行为

QP是最核心的对象,传输层的所有状态都挂在QP上。IB定义了多种服务类型,实践中接触最多的是RC(可靠连接)、UC(不可靠连接)、UD(不可靠数据报)这三种。XRC和RDM在特定场景下会出现,但大部分RDMA使用者很少直接碰。

RC把可靠性做到极致,有ACK、NAK、重传、PSN校验、重复报文检测,支持RDMA Read/Write和原子操作。代价是消耗更多硬件资源,每个QP都要维护发送端和接收端的完整状态。UC不确认、不重传,但保留了连接的概念,仍然支持RDMA Write和Read(只是不做可靠性保证)。UD则像UDP,一个QP可以和多个远端QP通信,报文不保证到达,也没有包序号连续性,使用场景集中在控制面、键值类消息传递。

选哪种QP不是拍脑袋决定的。这里有个误区,很多人一上来就全用RC,觉得自己需要“可靠”。实际上RC的可靠性是有成本的,重传、ACK处理、重复报文检测这些都要占用HCA内部的流水线资源。如果业务本身可以容忍少量丢弃(比如某些分布式通信中的心跳探测),用UD能省下大量QP资源,扩展性也好很多。

2.3 QP状态机的运行逻辑

QP的每个状态变化背后都有明确意图。

Reset是所有QP的起点,硬件刚创建QP时没有任何上下文。写入Reset状态后,QP会丢弃所有尚未完成的WQE,接收端的包也一概不认。从Reset进入Init,QP获得了基本的属性,比如本地LID、端口号、QP号、服务类型,但还不能收发数据。Init状态下硬件已经可以做地址校验了,只是没有到“可以发”的程度。

真正让QP活过来的是RTR(Ready to Receive)状态。这里有个容易被忽略的细节:RC和UC类型下,远端QP必须提前知道本端的QP号、LID、端口、MTU等信息,才能让报文正确到达。也就是说,连接握手是软件负责的(通常通过UD交换连接信息),硬件只负责在RTR之后接收匹配的报文。

RTS(Ready to Send)是最后一个关键状态,进入RTS后发端才能post发送请求。很多初学RDMA的人遇到“发不出去”的问题,十有八九是QP卡在了RTR之前,或者对端QP没有进入RTS。硬件对状态检查是非常严格的,一个状态不匹配就会直接产生错误事件,并且错误原因会记录在异步事件里。

调试时建议养成一个习惯:每次QP建立失败,先去查两个QP分别停在哪个状态。我曾经遇到过一个场景,本端已经RTS了,对端一直停在Init,结果就是对端收不到任何数据,本端还以为链路出了问题。这种问题如果一开始就检查状态机,五分钟就能定位。

3. 可靠传输与PSN机制深挖

3.1 为什么PSN是可靠性的地基

RC服务可靠性的核心是PSN(Packet Sequence Number)机制。PSN是24位无符号数,每个QP独立维护,发送端每发一个数据报文,PSN加1;接收端根据PSN检查报文顺序和连续性。24位听起来不小,但足够大,实际使用中通常不会溢出——因为WR是连续提交的,硬件会自然处理模数循环。

PSN机制带来的关键能力是重复检测和按序递交。接收端收到一个报文后,会先检查PSN是否落在期望窗口内。落在窗口内且大于期望值,说明前面有报文丢了,这时候接收端不直接丢弃当前包,而是把它缓存起来,同时发送NAK(如果服务类型要求的话),请求发送端重传缺失的PSN区间。如果PSN小于期望值,说明收到重复报文,直接丢弃,不再向上层递交。

这个逻辑看起来简单,但硬件实现时极其敏感。我遇到过一种情况:QP因为远端异常导致PSN跳变很大,发送端还在按原来的PSN发送,接收端NAK不断,发送端不断重传,但重传的PSN仍然不在接收端期望窗口内,形成重传风暴。这种问题本质上是“状态失步”——双方对连接状态的认知已经不一致了,单纯调整重传参数解决不了,必须重置QP或者重新建连。

3.2 重传与ACK/NAK的互动细节

发送端发送报文后,并不会一直等待ACK才发下一个。RC是流水线式传输,发送端可以连续发送多个报文,接收端收到后延迟生成ACK(延迟ACK可以减少反向流量),发送端依赖超时机制兜底。如果发送端在Local ACK Timeout规定的时限内没有收到某些PSN的确认,就会触发重传。

这里有一个工程上很重要的参数:Local ACK Timeout。默认值通常由驱动设置,但在长距离、高延迟的网络下,默认值可能不够。比如数据中心内部典型时延是亚微秒级,但跨地域的RDMA链路时延可能达到几十甚至上百微秒。默认超时时间如果小于真实的RTT,会出现大量“假重传”——报文明明到达了对端,ACK还在路上,发送端就急着重传。假重传不光浪费带宽,还会导致接收端收到重复报文,进一步加重接收端处理负担。

调这个参数的标准思路是:先测出真实RTT,把Local ACK Timeout配置成明显大于RTT的值。但也不是越大越好,超时太大意味着真正丢包时恢复时间太长,业务卡顿会显现。实际测试中,我会把超时时间设为RTT的3到5倍,再根据误码率调整重传次数上限。

NAK的出现是一个强信号。NAK类型包括PSN序列错误、远端访问错误、远端操作错误等。收到NAK时要区分两类场景:PSN序列错误通常意味着连接本身还在,但乱序或丢失达到了接收端无法缓冲的程度,可以通过重传恢复;而远端访问错误(比如RDMA Read时地址未注册或者内存窗口无效)不是重传能解决的,是应用层bug,必须去查内存注册和WQE参数。

3.3 重传风暴的现场排查记录

这是我在模拟环境里复现过一次的场景:两台服务器用RC QP跑RDMA Write,持续大流量压测,链路层偶发误码,导致个别报文在物理层被丢弃。第一次出现丢包时,传输层正常触发重传,性能下降了一小截。但过了一会,性能断崖式下跌,大量重传报文出现在链路上。

排查过程:先查HCA事件,看到大量Retry Exceeded错误;再查QP状态,已经进入ERR。链路本身是通的,端口协商也正常,但传输层彻底放弃治疗了。根因是最初几次误码引发重传后,发送端重传的报文又遇到了拥塞,ACK迟迟未回,重传次数超过上限,QP被硬件强制置为ERR。

这个案例给我们的教训是:传输层的可靠性不是无限度的,它依赖链路层的整体稳定。如果物理层误码率偏高,重传机制反而会放大问题。生产环境里,与其拼命调大重传次数,不如先解决链路质量。重传次数调大只是把Q(延迟)拉长,对误码率居高不下的链路毫无意义。

4. 基于credit的流控机制

4.1 credit流控的基本原理

InfiniBand的流控不是TCP那种基于窗口的端到端调整,而是一种逐跳的、基于接收端可用buffer空间的信用机制。每条链路(VL)上,接收端会告诉发送端自己还有多少空间可以接收数据。发送端每次发送报文,就消耗对应的credit;credit用完,发送端必须停下来等待新的credit补充。

这个机制类似现实中的“先存款后消费”。接收端在初始化时给发送端一个初始credit额度,发送端知道自己最多能发多少数据而不至于淹没接收端。每发一个报文(按字节算),本地缓存里的credit余额就减少。接收端处理完报文,释放了buffer,再把credit通过链路层流控帧返回给发送端。

这跟我见过的很多人对RDMA的理解有出入——很多开发者以为RDMA不需要流控,只要HCA性能够快就能一直发。实际上credit流控是InfiniBand能又好又稳跑高吞吐的前提。如果没有credit机制,一个快生产者QP可能瞬间把接收端的整个内存池都灌满,导致接收端必须丢包,破坏传输层的可靠性保证。

4.2 接收端buffer规划与credit调整

接收端的buffer大小和credit额度直接相关。分配过小,发送端很快就会用完credit而停等,吞吐上不去;分配过大,又浪费内存。实践中,接收端WQ(Work Queue)深处数设置得越大,credit就越高,发送端越能放手跑。

这里有个常见的调优矛盾:为了降低时延,一些人喜欢把接收端的RQ depth设得特别小,比如只有两个WR,这样CQE能快速返回,时延很低。但credit随之变小,对于需要持续大流量发送的场景,远端会频繁停等,整体吞吐反而差。

我测试下来比较稳的做法是:把接收端RQ depth设置成至少能满足一个RTT内发送端能发出的报文数量,公式可以粗算一下——带宽乘以RTT,再除以平均报文大小。举例,100Gbps链路,RTT为5微秒,带宽时延积约为62.5KB,如果平均报文大小2KB,那接收端至少需要约32个WR的depth。这只是一个基线,实际还要考虑突发和CPU处理抖动。

4.3 流控停等现象的识别

当发送端credit耗尽时,HCA会暂停对应QP的发送调度,表现是:链路上没有明显错误,但发送端的SQ(发送队列)里积压了很多未完成的WQE,而完成事件迟迟不来。应用层看到的现象是“发了个大消息,等了很久才完成”。

这跟拥塞的表现很容易混淆。两者的区别是,credit停等是接收端能力限制导致的,而拥塞是网络中间设备或路径上出现瓶颈。排查方式可以这样区分:如果只有特定QP受影响,其他QP都正常,优先怀疑接收端RQ depth配置;如果所有QP都慢,再看链路层拥塞。

一次压测中我发现RC QP写带宽远低于预期,HCA端口速率正常,没有错误计数。最后用性能计数器一看,本端发送的报文数和远端实际收到的报文数完全一致,但中间出现过大量发送暂停周期。罪魁祸首就是远端RQ depth太小,我把它从16调到128后,吞吐立刻上去了。这个问题的隐蔽性在于链路层一切正常,应用也不知道原因,只能靠计数器逐层排查。

5. MTU、分片重组与多连接调度

5.1 MTU选择对传输层的影响

InfiniBand的MTU选项包括256、512、1024、2048、4096字节。传输层在发送端负责把上层消息(WQE中的大数据块)切割成不超过MTU的报文,每个报文拥有独立PSN;接收端则根据PSN顺序重组这些分片,恢复成完整的消息再递交给应用。

MTU不是越大越好。大MTU能减少报文数量,降低头开销,但对链路误码的容忍度更差——一个物理错误会损坏更大的数据块,导致更大范围的重传。实际部署中,短距离数据中心内部,2048或4096是常见选择;跨设备、链路质量不是特别好时,1024或2048更稳。

值得注意的另一个点是:MTU必须和路径上所有设备保持一致。比如端到端路径经过两个交换机,一个支持4096,另一个只支持2048,那么实际最大可用MTU是2048。如果发送端配置了4096,报文可能会被交换机丢弃或分片(IB网络层不支持IP那样的分片),结果就是大量重传和诡异的不稳定问题。所以MTU规划的真正依据不是单点能力,而是整条路径的最大公共值。

5.2 分片顺序与接收端重组窗口

接收端为了重组消息,需要在内部暂存分片。不同服务类型对暂存的要求不同:RC必须做到按序重组,因为可靠连接承诺按序交付;UD则不做重组,因为单个UD报文本身就是完整的消息。

重组窗口大小有限,如果某个分片丢失,后续分片虽然到了接收端也不能立即递交,只能暂存等待重传。极端情况下,如果多个消息的大量分片同时in-flight,而某个分片丢失后重传迟迟不来,接收端缓冲区可能会被占满,触发credit减少,发送端被迫降速,形成一个连锁反应。

这块的工程启示是:不要把MTU调大与性能提升画等号。在某些低时延场景下,大MTU确实好,但在有损链路上,大MTU的重传代价更大。建议做完基准测试后,实际跑一下有损场景(模拟少量丢包),观察吞吐跌落的幅度,再决定最终MTU。

5.3 多QP并发与VL的调度联动

一台HCA上通常运行成百上千个QP,传输层需要决定把这些QP的报文分配到哪些VL(Virtual Lane)上。VL是链路层的概念,但传输层的SL(Service Level)到VL的映射决定了不同QP之间怎么共享物理带宽。

流控是基于per-VL的,也就是说,每个VL有独立的credit池。这带来一个好处:不同VL之间发生流控停等时互不干扰。比如一个高优先级QP所在VL因为远端RQ depth太小而停等,另一个VL上的普通流量不受影响。这种隔离特性在生产环境里非常重要,建议有条件的场景把关键业务放在独立SL/VL上,避免被其他业务的burst拖累。

6. 传输层错误排查与问题分类实录

6.1 把错误分成“能重传恢复”和“必须重置”两类

传输层错误处理上,我习惯先分类。第一类是暂时性错误,比如个别报文丢失导致的PSN重传、临时credit耗尽导致的发送暂停。这类错误可以通过硬件自身的重传/恢复机制消化掉,应用无感知。第二类是结构性错误,比如QP状态机失步、远端访问错误、非法WQE参数。这类错误一旦发生,几乎无法通过重传恢复,必须重置QP或直接重新建连。

判断标准很简单:看事件日志里有没有伴随“Retry Exceeded”“Remote Access Error”这类状态码。出现Remote Access Error时,先查内存注册的rkey是否有效、内存地址范围是否越界。出现Retry Exceeded时,则要排查链路质量、Local ACK Timeout和重传次数上限。

6.2 典型问题排查顺序

这里分享一套我常用的排查FAQ思路,按顺序来基本能覆盖大部分传输层问题:

先说PSN重传风暴的问题。计数器里IP包重传率显著偏高,QP没挂,但延迟飙升。排查顺序是:先看链路误码统计,再测真实RTT,最后检查Local ACK Timeout和重传次数配置。三步下来基本能定位。

再说QP建连失败。本端RTS了,对端一直无法到达这个状态。先确认对端QP号会不会填错,再确认对端QP是否已经初始化完成,最后确认两端MTU和LID配置是否匹配。我见过很多次原因是QP号写错了,因为软件握手层把QP号放到了别的地方,排查时要先抓包确认握手内容。

还有credit耗尽导致的吞吐异常,通常表现为“发大块数据时突然变慢”。直接检查接收端RQ depth和credit计数,如果确实耗尽,先调大depth,再决定是否调整WQE大小和接收端内存分配策略。

有个经验之谈是:当传输出问题时,不要急着改参数。先抓一段时间/计数器的snapshot,包括:发端发送报文数、收端接收报文数、重传报文数、credit耗尽暂停次数、错误事件历史。往往几组数据一对照,问题的指向性就非常明显了。

6.3 软件层几句忠告

最后补充一点软件侧的体会。RDMA编程模型里,传输层可靠性由硬件承担,但内存注册、WQE生命周期管理完全由软件负责。我踩过最多的坑都集中在内存注册和WQE回收上——比如rkey生成后内存被重新分配,但remote侧还在用旧的rkey访问;或者WQE完成后没有及时回收buffer,导致接收端buffer碎片化,最终credit越来越少。

这些问题不在传输层协议里,但会直接触发传输层错误事件。所以每次遇到奇怪的传输层问题,除了查协议参数,也回头审视一遍应用的内存管理逻辑。很多时候“传输层故障”其实是“应用层埋雷”。做RDMA开发越久,越明白一个道理:协议栈虽然复杂,但它是有章法的;真正不按常理出牌的,往往是应用层代码。

把这些机制搞清楚了,再去调整参数和排查问题,心里就有底了。好多网上流传的“神奇调优参数”,本质上都是针对某一类具体瓶颈的妥协方案,没有放之四海而皆准的值。所有配置都要回到传输层的工作机制上来理解,否则参数的改动只能是碰运气。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进