ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

InfiniBand规范精读:QP状态机、BTH报文与RoCEv2实践

InfiniBand规范精读:QP状态机、BTH报文与RoCEv2实践

简介:《InfiniBand架构规范1.4版》由IBTA于2020年发布,面向数据中心网络架构师、高性能计算研发人员与RDMA学习者,用于系统理解InfiniBand高带宽、低延迟互连的设计目标、协议分层与工程实现。压缩包共1个文件,为12.64MB的PDF全文,完整收录Volume 1通用规范,涵盖概念概述、传输协议、服务质量、错误处理、连接管理与路由等章节,并附虚拟化及RoCE-v1、RoCE-v2附录。文档保留1.0至1.4的修订脉络,便于核对XRC整合、第9章勘误等版本变化,也可作为RoCEv2部署及QP、CQ等核心机制查阅时的手册。已有2859人学习,适合需要对照官方标准查漏补缺、夯实InfiniBand与RoCE基础的读者。

1. 从一次 3 微秒的往返说起:IB 规范为什么把「信任」写进硬件

大多数人第一次翻 InfiniBand 架构规范,都会卡在同一件事上:一份讲报文格式和位域的文档为什么能写到上千页。直觉上,互联技术把带宽和延迟讲清楚就够了;真读进去才发现,占篇幅最多的是「谁来保证消息不被发错、不被乱序、不被越权访问」。TCP/IP 把这类问题交给协议栈软件,IB 把它们压进了网卡和交换机的状态机。IB 标准 1.4 版 2020 年 4 月定稿,往前追到 1.0 隔了二十年,报文格式没推倒重来,只是一层层加附录。搞懂这层设计,等于搞懂「硬件替软件履行协议栈职责时,规范必须把哪些边界写死」——这也是 RoCEv2 能把同一套语义搬到以太网上的前提。

2. 队列对:为什么 IB 的发消息看起来像在操作内存

IB 的通信接口不是send()/recv(),而是「把一段描述符写进队列,再敲一下门铃」。描述符叫 WQE(Work Queue Element),队列叫工作队列,成对的发送队列和接收队列构成 QP(Queue Pair)。这个模型要解决的核心问题是零拷贝:应用把用户态缓冲区的地址、长度、访问密钥直接写进 WQE,网卡自己发起 DMA,CPU 全程不碰数据。代价是内存必须提前注册、队列上下文必须常驻网卡,于是「谁在什么时候可以动哪块内存」变成了一件必须在硬件里被强制的事。规范的第三章和第九章把 ABI 固定下来,各厂商驱动实现出一致行为。

2.1 QP 状态机:从 RESET 到 RTS 的四步在等什么

QP 不是建好就能发。规范给了一条严格的状态迁移链:RESET → INIT → RTR → RTS,中间还夹着一个 SQD(Send Queue Drained)用于优雅停机。每一步都只放行一部分能力,这个限制不是官僚主义,而是硬件状态分配的顺序问题。

INIT 之后接收队列才被激活。原因很直接:对端一旦进入 RTS 就会立刻发包,如果本地接收缓冲还没挂上去,硬件只有两个选择——丢包或者回 NAK。IB 选了第三条路:用状态机保证「能收」先于「能发」。

RTR(Ready To Receive)是真正配参数的地方。地址向量 AV(目标 LID/GID、服务级别 SL、源路径位)、路径 MTU、P_Key、重传超时都被锁在 QP 上下文里。注意 MTU 在这一步定死之后,要改只能退回 INIT 重来——因为序列号窗口和重组缓冲是按 MTU 预分配的,中途改会让在途包的 PSN 语义直接崩掉。这是「用硬件省状态」的典型代价:灵活性换延迟。

RTS(Ready To Send)才允许发送。RD(可靠数据报)比 RC 多一道握手:一端先到 RTR,另一端到 RTR 后回一个通信,两边都到 RTS 才通。XRC 的发送端 QP 只有 RTS 之前的半个状态机,因为它没有自己的接收队列。

# 简化版 QP 状态检查:只在合法状态下放行对应操作 ALLOWED = { "RESET": set(), # 什么都不许做 "INIT": {"post_recv"}, # 只能挂接收缓冲 "RTR": {"post_recv", "recv"}, # 能收,不能发 "RTS": {"post_recv", "recv", "post_send", "send"}, } def check(qp_state, op): if op not in ALLOWED[qp_state]: # 真实实现里这会变成 ibv_modify_qp 返回 EINVAL, # 或者 QP 进入 ERROR 态并产生一个 syndrome 非零的 CQE raise RuntimeError(f"{op} not allowed in {qp_state}")

上面这段伪代码对应规范里状态与允许操作的对应表。post_recv在 INIT 就合法,post_send只在 RTS 合法,recv(硬件侧接收完成)从 RTR 开始合法。真实网卡不会在软件里做这个判断,而是把状态编码进 QP 上下文的几个 bit,门铃触发时由硬件状态机比对,不匹配就直接把 QP 打进 ERROR 态并生成完成事件。所以排查「为什么 post_send 报错」时,先看 QP 到底停在哪个状态,比看代码管用。

2.2 一次 Send 在硬件里变成什么

用户调用一次发送,落到硬件上是三段数据:WQE、门铃、CQE。WQE 是控制段加若干数据段描述符(SGE),控制段里有操作码、目标 QP、WRID(用户请求 ID)、以及 signaling 标志。门铃是一个 MMIO 写,值是新 WQE 的序号;网卡读门铃记录(doorbell record,位于主机内存)来判断这是第几次写入,避免每次都要读自己的寄存器。

批量提交是这里的关键优化。一次门铃可以配多个 WQE,网卡按顺序处理,处理完再更新消费者索引。selective signaling 则让应用只对每第 N 个 WQE 请求完成事件,其余静默——这是高吞吐场景下把 CQ 从瓶颈里摘出去的标准手段。

/* WQE 控制段的典型字段(不同厂商布局不同,语义由规范约束) */ struct wqe_ctrl { uint32_t opcode : 8; /* SEND / RDMA_WRITE / ATOMIC ... */ uint32_t op_mod : 8; uint32_t wrid_hi : 16; /* 用户请求 ID,完成时原样带回 */ uint32_t wrid_lo : 32; uint32_t signaling: 1; /* 置位才产生 CQE */ uint32_t n_sge : 7; /* 后续 SGE 个数 */ uint32_t flags : 24; /* fence / inline / solicited 等 */ uint32_t imm; /* ImmDt 操作码下有效 */ };

字段含义需要对着规范一起看:opcode决定这个 WQE 在线上会展开成哪种传输头组合;wrid是软件与硬件的唯一对账凭据,CQE 回来时原样带回,应用据此找回自己的上下文;signaling为 0 时硬件不写 CQE,只推进消费者索引。fence位值得单独留意——它要求该 WQE 之前的所有 RDMA 读操作在本次写之前对其它节点可见,这是实现「先读后写」顺序保证的唯一手段,乱用会让网卡流水线停顿,不用又会出现数据竞争。

2.3 保护域与内存注册:为什么密钥必须先交给内核

注册内存(MR)是 IB 里最容易被低估的一步。应用把一段虚拟地址交给驱动,驱动做三件事:pin 住物理页防止换出,建立虚拟地址到物理地址的映射表,返回两个键——lkey 给本地网卡用,rkey 给远端用。此后所有 WQE 里的地址都是「lkey + 偏移」,远端 RDMA 请求里带的是「rkey + 虚拟地址」,网卡收到后查表验证,通过才发起 DMA。

保护域(PD)是这层验证的分组单位。一个 MR 只属于一个 PD,一个 QP 也只属于一个 PD,跨 PD 的引用一律拒绝。规范把这条写成硬约束,原因是硬件没有进程概念:如果没有 PD,任何拿到 rkey 的节点都能读写整台机器的内存。有了 PD,即使 rkey 泄漏,能碰到的也只是同一保护域内的缓冲区。

越权访问的失败形态很统一:QP 进 ERROR 态,CQE 带一个非零 syndrome,数据一个字节都不会被写。这比软件栈里「检查了但可能漏」强得多——硬件在 DMA 发起前比对,中间没有可被绕过的路径。内存窗口(Memory Window)则是把 rkey 的授予做成运行时可撤销的操作,适合「临时让对端读一段」的场景;快速注册(Fast Registration)允许在已有 MR 上重新绑定地址,省掉重复 pin 的开销。这两者在规范里都有独立的操作码,实现时要注意它们的失效语义比普通 MR 更激进。

3. 数据包格式:40 字节 GRH 与 8 字节 LRH 的分工是怎么定的

报文的字节布局决定了硬件的流水线形状。IB 把「交换机需要看的信息」和「端点需要看的信息」切得很干净:链路层头 LRH 8 字节,网络层头 GRH 40 字节,传输层头 BTH 12 字节,再往后按操作码挂可选的扩展头。交换机的转发逻辑只解析 LRH,这是它能做到极低转发延迟的物理原因;路由器才看 GRH;端点的传输层只看 BTH 及其后。

3.1 LRH 与 GRH:二层与三层的分界线

LRH 定长 8 字节,装的是 VL(虚拟通道,4 位)、SL(服务级别,4 位)、DLID(目的本地标识,16 位)、SLID(源本地标识,16 位)。子网内的转发全靠 DLID,一张 16 位的表就能覆盖 65536 个端点,交换机查表、改 VL、转发,动作固定。SL 存在的意义是 QoS 分层:它跟着包走,VL 则是每一跳的本地决策,SL→VL 的映射表在交换机每个端口上,这个设计让同一股流量在不同跳上可以走不同的物理队列。

GRH 长 40 字节,格式直接借用 IPv6 头:128 位源 GID、128 位目的 GID、流量类别、流标签、跳数限制、下一头部。跨子网时由路由器读它做三层转发。

关键取舍在这里:GRH 是可选的。同一子网内通信,报文可以只带 LRH,省掉 40 字节和一次解析;只有跨子网或者需要全局寻址时才加 GRH。BTH 里没有标志位显式说明「后面有没有 GRH」,接收端靠 LRH 里一个保留位加操作码约定来判断,这一点在写解析器时是常见的翻车点。

3.2 BTH 的 12 个字节里到底装了什么

BTH 是整份规范里信息密度最高的 12 个字节,字段一个都不能少:

字段位宽作用
Opcode8决定后续扩展头的组合方式
SE / M / PadCnt1 / 1 / 2solicited 事件、迁移状态、填充字节数
TVer4传输头版本,当前为 0
P_Key16分区密钥,接收端比对分区表
DestQP24目的队列对编号
AckReq1要求对端回 ACK
PSN24包序列号,可靠服务的核心

DestQP 是 24 位,意味着单个节点上最多约 1600 万个 QP,这个数量级是按「大规模并发进程 + 每进程多队列」估出来的。PSN 也是 24 位,可靠服务靠它做滑动窗口内的确认与重传,回绕处理是硬件必须内建的能力。AckReq 单独一位而不是由 opcode 隐含,是为了让应用能按需关闭确认——批量传输时每包都 ACK 会把对端 CQ 打爆。

P_Key 出现在 BTH 而不是在扩展头里,说明分区校验是每一跳都做的事:交换机端口上有 P_Key 表,不匹配的包直接丢。这是 IB 里少见的「中间设备也做安全校验」的设计,跟以太网上 VLAN 的位置类似,但语义更强——P_Key 是成员资格,不是标签。

3.3 扩展头按 opcode 拼装:定长带来的确定性

扩展头不是 TLV 结构,没有长度字段也没有类型循环。接收端先读 BTH 的 opcode,再按一张固定的组合表往下切:RDMA Write 后面必跟 RETH(16 字节),Send 带立即数后面必跟 ImmDt(4 字节),UD 包后面必跟 DETH(8 字节),可靠数据报在 BTH 和 DETH 之间插 RDETH(4 字节),原子操作跟 AtomicETH(28 字节),ACK 包跟 AETH(4 字节)。XRC 的包在 BTH 后插 XRCETH。

这个选择牺牲了扩展性,换来了硬件解析的确定性:不需要循环、不需要边界检查、每个头的位置在流水线里是编译期常量。代价是 opcode 空间成了稀缺资源,新功能要么复用已有组合,要么占用新 opcode。XRC 在 1.3 版被整合进正文时,靠的就是新增 XRCETH 加几个 opcode,而不是改 BTH 布局——二十年不破坏兼容性的秘密就在这里。

def parse_headers(buf: bytes) -> dict: """按 opcode 决定扩展头组合,不做通用 TLV 扫描""" lrh = { "vl": buf[0] >> 4, "sl": buf[5] & 0x0F, "dlid": int.from_bytes(buf[2:4], "big"), "slid": int.from_bytes(buf[6:8], "big"), } off = 8 if buf[off] >> 4 == 0x0: # 简化判断:无 GRH 时 BTH 紧接 LRH pass bth = { "opcode": buf[off], "pkey": int.from_bytes(buf[off+4:off+6], "big"), "dest_qp": int.from_bytes(buf[off+6:off+9], "big"), "ackreq": (buf[off+9] >> 7) & 1, "psn": int.from_bytes(buf[off+9:off+12], "big") & 0xFFFFFF, } return {"lrh": lrh, "bth": bth}

这段代码只解析了固定部分,opcode到扩展头的映射被刻意省略——真实实现里那是一张 256 项的表,每项写明「后续头类型序列」。写解析器时最容易错的是字节序:规范第 1.5.1 节明确所有多字节字段按大端解释,但网卡在主机内存里写的队列元素是小端,两套字节序在同一个系统里并存,调试时值看起来「像反了」基本都是这个原因。填充字节(PadCnt)也要留意,它只用于让包长对齐到 4 字节,不参与任何语义,但算长度时漏掉会让后续头的偏移全错。

4. 传输服务:RC、UD、XRC 分别在替谁还债

IB 定义了五种传输服务:RC(可靠连接)、UC(不可靠连接)、RD(可靠数据报)、UD(不可靠数据报)、XRC(扩展可靠连接)。它们不是「功能多少」的梯度,而是对同一个问题给出五种答案——可靠性和连接状态该由谁维护、维护到什么粒度。选错服务的代价通常不在功能上,而在扩展性和故障域上。

4.1 RC 的序号与 ACK 合并:AETH 里的 4 个字节

RC 是最常用的服务:一个 QP 对端固定,PSN 连续递增,接收端按序收、按需回 ACK,发送端超时重传。它的可靠性完全由硬件承担,应用看到的是「要么成功,要么 QP 报错」。

ACK 包里只有 4 字节的 AETH,装的是 Syndrome(8 位)和 MSN(Message Sequence Number,24 位)。Syndrome 表示这次确认是正常 ACK 还是 NAK(以及 NAK 的原因:序列号错、重传超限、远程访问错)。MSN 是这套机制里容易被忽略的一半:PSN 确认的是「包收到了」,MSN 确认的是「接收队列消费到哪了」。两个计数分开,是因为接收端可能已经收下包但应用还没取走 CQE,缓冲区没释放,发送端不能无限发。

ACK 合并是 RC 能在高带宽下不崩的原因。接收端不会每包都回 ACK,而是攒够一定数量或等到超时再回一个,AETH 里的 PSN 一次性确认之前所有包。发送端的重传策略是 go-back-N:从第一个未确认的 PSN 开始重发。选 go-back-N 而不是选择性重传,是因为 IB 的乱序窗口很小、链路误码率低,维护一张重传位图的硬件成本换不来多少收益——这是典型的「按场景下注」而不是「按理论最优下注」。

4.2 UD:无连接换来的自由与代价

UD 没有连接状态,一个 QP 可以发给任意节点,接收端靠 DETH(8 字节)里的 Q_Key 和源 QP 号识别来源。它不支持 RDMA 读写,只能 Send/Recv,且一个消息必须装进一个包——不能分段,所以消息大小受路径 MTU 硬限制。

这些限制换来的是可扩展性:UD 的 QP 数量与通信对端数无关,管理框架里的 MAD(管理数据报)全部跑在 UD 上,因为子网管理器需要跟成千上万个节点通信而不想维护那么多连接。代价也很直接:不保证顺序、不保证送达、没有重传。接收端没挂缓冲就直接丢,发送端不会知道。

用 UD 踩坑最常见的地方是把「不可靠」理解成「偶尔丢」,实际是「可能大量丢」——尤其在接收队列深度不够时,丢包率会随突发流量线性上升。判断 UD 是否合适,看两件事:消息能否单包发送,以及应用能否自己处理丢失。

4.3 XRC 与 SRQ:省的是网卡里的 QP 上下文

RC 的问题在中大规模并行场景会暴露:每个进程跟每个对端通信都要一个 QP,QP 上下文存在网卡缓存里,几千个 QP 就把缓存挤爆,性能断崖式下降。XRC 把发送侧做成共享:一个 XRC 发送 QP 可以对接多个远端进程的接收 QP,每个远端有自己的 XRCETH 里携带的编号,发送队列只有一份。

服务连接状态可靠性主要开销典型用途
RC每对端一个 QP硬件重传QP 上下文随对端数增长点对点大数据传输
UC每对端一个 QP无重传同 RC可容忍丢包的流
UD无无单包、需自处理丢失管理报文、广播
RD每对端一个 EEC硬件重传EEC 上下文多对端可靠小消息
XRC发送共享硬件重传接收侧仍需 QP大规模并行

SRQ(共享接收队列)是同一思路在接收侧的版本:多个 QP 共用一个接收队列,CQE 里带 QP 号指明来源。省的是接收缓冲的冗余——不为每个潜在对端预留缓冲,而是按实际并发量准备。这两项加起来,是 1.3 版把 XRC 从附录整合进正文的主要动机:它解决的不是功能缺失,而是「规模上去之后 QP 上下文成为稀缺资源」这个真实瓶颈。规范里对 XRC 的编号管理写得很细,因为共享发送队列意味着错误必须能精确归因到某一个远端接收者,否则一个对端的故障会污染整条共享队列。

5. 排查:那些看起来像网络故障的 IB 问题

IB 的故障表现有个共同特点:链路状态全是绿的,业务不通。这是因为大量约束是在 QP 建立和包解析阶段生效的,链路层根本感知不到。下面五条是按「现象 → 原因 → 解决」整理的高频踩坑记录,覆盖了从建链到多路径的典型路径。

5.1 现象:建链到 RTR 就失败,日志只说不支持

原因八成是 P_Key 不匹配。默认分区键是 0xFFFF,管理类流量走它,但业务流量通常被分配了别的 P_Key。QP 在 RTR 阶段要指定 P_Key,网卡会拿它跟端口上的 P_Key 表比对,不在表里就拒绝。另一个可能是分区成员资格由子网管理器下发,SM 没跑或者没配好,端口上根本没有业务 P_Key。

解决顺序是:先看端口 P_Key 表里有没有目标值,再看 SM 是否已完成子网初始化,最后确认 QP 的 P_Key 和接收端一致。P_Key 的低 15 位是分区号,最高位表示是否允许访问默认分区,这个位搞反了会出现「单向通」的玄学现象。

5.2 现象:小消息正常,超过某个尺寸就超时

这是 MTU 不匹配的典型特征。QP 在 RTR 阶段锁定路径 MTU,发送端按自己的 MTU 分段,中途某条链路的最小 MTU 更小就会丢包——IB 没有分片机制,不匹配就是不匹配。规范里 MTU 一旦设定,只能退回 INIT 重配,不允许热改。

解决方法是沿路径查每一跳的 MTU 配置,取最小值,然后销毁 QP 重建。注意 UD 服务受这个限制最狠:消息必须单包装下,MTU 小一档,能发的消息就小一档。

5.3 现象:多进程共用一块网卡,偶发本地保护错误

CQE 上 syndrome 指向本地访问违规,但代码看起来没错。常见原因是 MR 被注销(dereg)时还有在途 WQE 引用它——注销只是把映射表项清掉,硬件手里的 WQE 不会自动失效,等它执行到那一步就查表失败。另一种是两个 PD 共用了一个 MR,或者 rkey 在错误的 QP 上被使用。

解决靠顺序:注销 MR 之前先把 QP 打到 SQD 或销毁,确保所有 WQE 都已完成;跨进程共享内存时用同一个 PD 派生 QP。这类问题在压测下才暴露,所以血泪经验是「所有 MR 生命周期必须晚于引用它的 QP」。

5.4 现象:加了第二条路径,吞吐反而下降

多路径不是插上线就生效。SL→VL 的映射表在交换机每个端口上独立配置,默认映射常把所有 SL 压到同一个 VL,两条物理路径的流量在同一个出队列里排队,头阻塞比单路径还严重。另一种情况是自适应路由打开了,但 VL 配置没跟上,包在两条路径间来回跳。

解决要落到配置:先确认 SL 到 VL 的映射在每一跳一致,再确认不同业务流用了不同 SL,最后才看路由策略。这部分没有通用答案,只能按拓扑逐跳核对。

5.5 现象:RoCEv2 环境里丢一个包,性能断崖

IB 的可靠传输假设链路本身几乎不丢包,重传是兜底而不是常态。RoCEv2 把传输层搬到了以太网上,但链路层的无损保证没了,一旦拥塞丢包,go-back-N 的重传会把有效带宽打对折。1.4 版把 RoCEv1/v2 收进附录时明确写了这个前提:依赖优先级流控和显式拥塞通知来近似无损。

解决是两件事一起做:链路侧配好流控,避免拥塞时直接丢;端侧确认注入速率控制参数与链路能力匹配。只做一边,症状会从「断崖」变成「抖动」,更难查。

6. 设计取舍:二十年只加附录,是因为规范本身就是一份 ABI

从 1.0 到 1.4,这份规范没有改过一次 BTH 布局,也没有推翻过 QP 状态机。这不是保守,而是它服务的对象决定的:规范一旦被固化进网卡芯片,任何破坏性修改都意味着存量设备变成孤儿。所以 1.4 版新增的内容全是「往边上加」——虚拟化附录、RoCEv1 与 RoCEv2 附录,都不动主干语义,只定义新的部署形态下怎么复用主干。

虚拟化附录解决的是一个具体的边界问题:一个物理网卡要被切成多个虚拟功能,每个虚拟机有自己的 QP 空间和 PD。规范把「隔离在哪一层生效」写死,是因为硬件没有进程上下文,隔离只能靠 PD 和 QP 归属关系在 DMA 发起前判断。这条一旦定错,上层再怎么补都补不回来。

RoCE 的两个附录更能看出取舍的锋利。RoCEv1 把 IB 的链路层换成以太网,BTH 及以上原样保留;RoCEv2 再进一步,把 GRH 换成真正的 IP 头加 UDP,BTH 仍然保留。保留 BTH 是为了让传输层语义——PSN、ACK 合并、QP 编号——一行都不用改,应用和驱动几乎无感。丢掉的则是 IB 链路层自带的东西:VL 提供的无头阻塞、基于信用的流控、链路级的重传。这些在以太网上要靠优先级流控和拥塞通知来近似,近似得越像,配置越复杂。

维度原生 IBRoCEv2
传输层BTH 及其后完全相同
网络层GRH(可选)IP + UDP
无损保证链路层内建依赖外部流控配置
多路径VL + SL 映射依赖 ECMP,粒度更粗

判断一份规范值不值得深读,我有个笨办法:看它有没有把「失败时的行为」写清楚。这份文档里大量篇幅在讲越权访问会返回什么 syndrome、MTU 不匹配会怎么失败、状态机非法迁移会进哪个态——这些细节不写,厂商实现就会各做各的,上层应用永远在猜。从那以后我每次评估一套 RDMA 方案,都会先去翻它的错误语义章节,而不是先看带宽数字,因为跑得快的方案很多,能让你查得清故障的很少。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

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