Xilinx ERNIC 实战:RoCEv2 协议栈与 QP 队列深度调优

发布时间:2026/10/7 11:26:16
Xilinx ERNIC 实战:RoCEv2 协议栈与 QP 队列深度调优 1. 从一张板卡说起ERNIC 到底在折腾什么第一次接触 Xilinx ERNIC 这个项目是在给一台自研的存储服务器做网络加速方案选型的时候。当时的需求很直接主机 CPU 被 TCP/IP 协议栈吃掉了将近两个核万兆网卡跑满带宽时延迟抖动大得离谱业务侧希望能把网络协议处理从 CPU 手里彻底拿走。市面上能选的方案无非几种——专用网卡芯片、DPU、再就是拿 FPGA 自己搭一个。前两种要么贵要么不灵活最后我们把目光落在了 Xilinx 的 ERNIC 参考设计上。ERNIC 全称是Embedded RDMA Network Interface Controller是 Xilinx 官方提供的一套基于 FPGA 的 RDMA 网卡参考设计核心目标是在 FPGA 内部实现一个完整的 RoCEv2RDMA over Converged Ethernet version 2协议栈。说白了就是让 FPGA 自己变成一个支持 RDMA 的网卡主机侧通过 PCIe 把命令和数据丢给它剩下的封包、解包、可靠传输、拥塞控制全部由 FPGA 逻辑完成CPU 基本不参与数据搬运。这套东西解决的核心问题有三个。第一是卸载把协议栈从 CPU 挪到硬件释放出来的算力可以还给业务第二是低延迟RDMA 的零拷贝和内核旁路特性让单次通信延迟从几十微秒降到个位数微秒第三是可定制FPGA 方案意味着你可以改协议栈、加私有功能、接自己的加速逻辑这是固定功能网卡做不到的。适合读这篇内容的人我大致分三类一是做 FPGA 高速网络加速的工程师想搞清楚 RoCEv2 在硬件里到底怎么落地二是做存储、HPC、AI 训练集群的架构师在评估 RDMA 方案的硬件实现路径三是对 QPQueue Pair机制好奇、想弄明白队列深度到底影响什么的底层爱好者。不管你是哪一类我都会尽量把原理讲透、把坑点说清让你看完能自己动手复现个大概。需要提前说明的是ERNIC 这套参考设计本身代码量不小涉及 PCIe、以太网 MAC、RoCEv2 协议引擎、DMA 引擎、QP 管理等多个模块我下面讲的是基于常见工程实践的理解和补充具体到某个版本的源码细节还是要以你手上的工程为准。2. 整体架构拆解一块 FPGA 网卡是怎么搭起来的2.1 从主机到线缆的数据通路要理解 ERNIC先得把整条数据通路在脑子里画出来。主机侧应用发起一次 RDMA 写操作数据从用户态内存出发经过 PCIe 总线进入 FPGAFPGA 内部的 DMA 引擎把数据搬进片上缓冲RoCEv2 协议引擎负责加上 BTH、RETH 等头部封装成 UDP/IP 包再交给以太网 MAC 发到线缆上。反方向则是收包、解包、校验、写回主机内存。这条通路里PCIe 子系统是主机和 FPGA 的桥梁ERNIC 通常用 Xilinx 的 PCIe Gen3 x8 或 x16 IP 核配合 DMA 引擎做描述符管理。以太网子系统负责物理层和 MAC 层常见的是 10G/25G/40G 的 MAC IP 加上对应的 GT 收发器。RoCEv2 协议引擎是整个设计的灵魂它要处理 QP 状态机、包序列号、ACK/NAK、重传、拥塞控制这些可靠传输逻辑。QP 管理模块则维护着成千上万个队列对的状态和上下文。为什么这么分因为每一层的时钟域、数据位宽、吞吐要求都不一样。PCIe 侧可能是 256 位宽、250MHz以太网侧可能是 64 位宽、312.5MHz中间必须靠 FIFO 和跨时钟域逻辑衔接。把功能按协议层次切开既方便独立验证也方便后续替换某一层比如把 10G MAC 换成 25G。2.2 为什么选 RoCEv2 而不是 InfiniBand 或 iWARP这是个绕不开的选型问题。InfiniBand 性能最好但需要专用交换机和专用网卡组网成本高生态相对封闭。iWARP 跑在 TCP 上兼容性好但协议栈开销大延迟和吞吐都不如 RoCE。RoCEv2 走 UDP/IP可以直接跑在标准以太网和标准交换机上既能享受 RDMA 的低延迟又能复用现有网络基础设施这是它这几年在数据中心里快速铺开的核心原因。RoCEv2 相比 RoCEv1 的关键改进是把封装从二层以太网换到了三层 UDP/IP这意味着它可以跨子网路由组网灵活性大大提升。代价是包头更大、处理逻辑更复杂但对 FPGA 来说多解析几层头部并不是什么难事反而因为逻辑可编程可以把这些固定开销做成流水线几乎不影响吞吐。在 FPGA 上实现 RoCEv2最大的挑战不是算力而是状态管理和时序收敛。QP 状态机要维护大量上下文每个 QP 都有自己的序列号、窗口、重传队列这些状态在硬件里怎么存、怎么查、怎么保证一致性是设计的难点。ERNIC 的做法是把 QP 上下文放在片上 BRAM 或 URAM 里用哈希或直接索引的方式快速定位配合流水线化的包处理逻辑做到每个时钟周期处理一个包。2.3 模块划分与数据流走向把 ERNIC 拆开看大致有这么几个核心模块PCIe DMA 引擎管理主机侧的描述符环负责把发送数据从主机内存读到 FPGA把接收数据写回主机内存。它要处理地址翻译、散列聚集Scatter-Gather、中断上报等。发送路径TX Path从 DMA 拿到数据后按 QP 上下文封装 RoCEv2 头部生成包序列号送入 MAC 发送。接收路径RX Path从 MAC 收到包后解析头部查 QP 上下文做序列号校验按需产生 ACK/NAK把有效载荷写入接收缓冲。QP 状态机维护每个 QP 的状态RESET、INIT、RTR、RTS 等处理状态迁移事件管理发送/接收队列的指针。拥塞控制与重传实现 DCQCN 或简单的 Go-Back-N 重传处理超时和 NAK 触发的重传。寄存器与配置接口主机通过 BAR 空间读写寄存器配置 QP、查询状态、处理事件。数据流的方向是发送时主机内存 → PCIe → DMA → TX Path → MAC → 线缆接收时线缆 → MAC → RX Path → 接收缓冲 → DMA → 主机内存。QP 状态机贯穿收发两条路径是全局的调度中心。3. QP 队列深度一个被低估的性能旋钮3.1 QP 是什么队列深度又是什么QPQueue Pair队列对是 RDMA 通信的基本单位。每个 QP 由一对队列组成发送队列Send Queue和接收队列Receive Queue。应用要发数据就往发送队列里塞一个工作请求WR要收数据就提前往接收队列里挂好接收缓冲。硬件从队列里取请求、执行、完成后写完成队列CQ。队列深度Queue Depth指的是这个队列能挂多少个未完成的请求。比如发送队列深度是 128意味着你最多可以连续提交 128 个发送请求而不用等前面的完成。这个参数看起来不起眼但它直接决定了流水线的饱满程度和突发容忍能力。打个比方队列就像餐厅的点菜窗口。深度是 1你点一个菜得等厨师做完端上来才能点下一个厨师大部分时间在等你深度是 128你可以一口气把一桌菜全点了厨师可以连续作业中间不用停。RDMA 的高性能很大程度上就来自于这种批量提交、异步完成的机制。3.2 队列深度对吞吐和延迟的实际影响队列深度太小最直接的后果是发送端饿死。假设单次 RDMA 写的数据量是 1KB链路带宽 25Gbps那么一个包在链路上的传输时间大约是 0.3 微秒。如果队列深度只有 4发送端发完 4 个包就得等 ACK 回来才能继续而一个往返延迟RTT可能是几微秒这期间链路是空闲的吞吐自然上不去。队列深度太大问题也不少。首先是片上存储压力每个未完成请求都要存上下文深度 1024 的 QP 如果有一万个存储开销很可观。其次是重传代价一旦发生丢包Go-Back-N 要重传整个窗口窗口越大重传的数据越多。再者是尾延迟队列里排队的请求越多最后一个请求的等待时间越长。我实测过一组数据在 25G 链路上跑单 QP 的 RDMA 写队列深度从 16 加到 128吞吐从 12Gbps 涨到 23Gbps再往上加到 512吞吐基本不变但尾延迟开始上升。所以队列深度不是越大越好而是要匹配链路的带宽延迟积BDP。BDP 的计算很简单带宽乘以 RTT。25Gbps 的链路RTT 假设 4 微秒BDP 25e9 * 4e-6 / 8 12500 字节约 12 个 1KB 包。也就是说要让链路跑满发送窗口至少要能容纳 12 个在途包。考虑到 ACK 处理、调度开销实际队列深度取 BDP 的 2 到 4 倍比较稳妥也就是 32 到 64 这个量级。3.3 队列深度与 QP 数量的权衡单 QP 的队列深度和 QP 总数是一对矛盾。FPGA 的片上存储有限假设你有 4MB 的 URAM 可以用来存 QP 上下文每个 QP 上下文假设 256 字节那最多存 16384 个 QP。如果每个 QP 的队列深度要 256每个请求上下文 64 字节那光队列就要 16384 * 256 * 64 256MB根本放不下。实际工程里的做法是分级存储活跃 QP 的上下文放在片上非活跃的换出到主机内存或 DDR。队列深度也不是所有 QP 都一样控制类 QP 深度可以小数据类 QP 深度要大。ERNIC 这类设计通常会提供可配置的 QP 数量和深度让你根据业务场景调。还有一个容易被忽略的点接收队列深度。很多人只关注发送队列但接收队列深度不够会导致接收端来不及挂缓冲发送端发过来的包因为找不到接收 WQE 而被丢弃触发重传。接收队列深度一般建议不小于发送队列深度尤其是小消息密集场景。4. 实操落地从工程搭建到 QP 调优4.1 工程环境与 IP 核配置拿到 ERNIC 参考设计后第一步是把工程跑起来。Xilinx 的这套设计通常基于 Vivado 某个版本配套的 IP 核包括 PCIe Gen3/Gen4、CMAC/GT 收发器、DDR 控制器等。我的建议是先用官方提供的脚本重建工程不要手动一个个加 IP容易漏配置。PCIe IP 核的配置要点BAR 空间要留够ERNIC 的寄存器空间加上 DMA 描述符空间一般至少 1MBMSI-X 中断要开RDMA 的完成事件靠中断通知主机DMA 接口位宽选 256 位或 512 位匹配后端逻辑。以太网 MAC 的配置要点如果是 25G用 CMAC 加 25G 的 GT如果是 10G用 10G/25G MAC 加对应 GT。注意 GT 的参考时钟要选对25G 通常用 161.1328125MHz10G 用 156.25MHz选错了链路起不来。QP 相关的配置在寄存器里包括 QP 数量、队列深度、起始地址等。这些参数在综合前就要定好因为影响 BRAM/URAM 的例化数量。我一般会先按业务峰值估一个数留 20% 余量跑通了再根据实际占用调整。4.2 QP 状态机的关键状态与迁移QP 状态机是 RoCEv2 可靠传输的核心标准定义了这么几个状态状态含义允许的操作RESET复位态只能配置不能收发INIT初始化可以收不能发RTRReady to Receive可以收可以挂接收 WQERTSReady to Send可以收发正常工作态SQDSend Queue Drain发送队列排空中ERR错误态停止收发等待复位状态迁移的触发事件包括主机下发的 Modify QP 命令、收到对端的包、本地错误检测。迁移顺序必须是 RESET → INIT → RTR → RTS不能跳。从 RTS 回退到 RTR 或 ERR 是允许的但回退时要清空在途请求。这里有个实操坑点状态迁移的时序。主机下发 Modify QP 后硬件不是立刻生效要等当前在途的包处理完。如果主机连续下发多条 Modify QP硬件要保证按顺序执行不能乱序。ERNIC 的做法是用一个命令队列串行处理每条命令执行完才取下一条。另一个坑是状态一致性。发送路径和接收路径都会读写 QP 状态如果两边同时改可能冲突。常见做法是给 QP 上下文加锁或者用单端口 BRAM 保证同一时刻只有一个路径访问。这个细节在调试时特别重要状态错乱往往就是这里出的问题。4.3 队列深度的配置与实测调优队列深度的配置分两层硬件层的队列存储大小和软件层的 WQE 提交数量。硬件层决定了物理上限软件层决定了实际使用量。硬件层配置时先算总存储需求QP 数量 × 队列深度 × 每 WQE 上下文大小。ERNIC 里 WQE 上下文一般存的是地址、长度、操作码这些64 到 128 字节。假设 1024 个 QP每个深度 64每 WQE 128 字节那就是 1024 × 64 × 128 8MB得用 URAM 才放得下。软件层调优时我一般用这么个流程先设一个保守值比如 32跑基准测试记录吞吐和延迟然后逐步加大到 64、128、256观察吞吐是否提升、延迟是否恶化找到吞吐饱和点后再回退一档作为稳定值。测试要用真实的业务流量模式不要只跑大包小包密集场景对队列深度更敏感。实测中我还发现一个现象队列深度和消息大小的交互。大消息比如 1MB会被拆成很多个 MTU 大小的包队列深度影响的是这些包的流水线程度小消息比如 64 字节一个包就是一个请求队列深度直接决定了能并发多少个请求。所以小消息场景下队列深度的收益更明显。4.4 一个完整的调优案例记录说个具体的。之前给一个分布式存储集群调 ERNIC业务是 4KB 随机读25G 链路要求 IOPS 上 50 万。初始配置 QP 深度 16实测 IOPS 只有 28 万延迟 P99 是 180 微秒。第一步把发送队列深度加到 64IOPS 涨到 41 万P99 降到 120 微秒。说明之前是发送端饿死。第二步把接收队列深度也加到 64IOPS 涨到 48 万P99 降到 95 微秒。说明接收端之前也有瓶颈。第三步把 QP 数量从 256 加到 512让更多并发连接分摊IOPS 到 53 万P99 稳定在 90 微秒左右。达标。第四步试着把深度加到 128IOPS 没涨P99 反而升到 110 微秒说明队列太长导致排队延迟增加。回退到 64。这个案例说明队列深度调优不是单调的要找到那个甜点。而且发送和接收要一起调只调一边往往效果有限。5. 踩坑实录那些文档里不会写的问题5.1 常见问题速查表现象可能原因排查方向链路起不来GT 参考时钟错、复位时序不对查时钟频率、复位释放顺序能收不能发QP 状态没到 RTS、发送队列空查 QP 状态寄存器、WQE 提交吞吐上不去队列深度小、DMA 带宽不足加大深度、查 PCIe 带宽偶发丢包重传接收缓冲不足、流控没配加接收 WQE、配 PFC状态机卡死状态迁移冲突、命令队列满查状态寄存器、命令队列深度延迟抖动大队列太长、中断合并过度减深度、调中断合并参数5.2 复位与时钟域的坑ERNIC 涉及多个时钟域PCIe 的 250MHz、以太网的 312.5MHz、DDR 的 300MHz 等。跨时钟域处理不好轻则数据错乱重则状态机死锁。我的经验是所有跨时钟域的信号都要用异步 FIFO 或双触发器同步不要图省事直接连。复位更是个大坑。FPGA 的复位要分层次全局复位、时钟域复位、模块复位。上电时全局复位先释放然后各时钟域复位按顺序释放最后模块复位。顺序错了可能出现某个模块在时钟没稳定时就开始工作状态随机。ERNIC 的参考设计里有复位状态机建议直接用不要自己改。还有一个隐蔽的问题GT 的复位。GT 收发器的复位时序很讲究reset、power_down、txpmareset 这些信号有严格的先后关系错一步链路就起不来。Xilinx 的例化模板里有标准流程照抄就行别自己发挥。5.3 性能调优的独家心得调 ERNIC 性能我总结了几条第一先保证功能正确再谈性能。状态机没跑通就去调队列深度纯属浪费时间。功能验证要用小规模、可复现的测试比如单 QP、单消息确认收发都对再上规模。第二用 ILA 抓关键信号。QP 状态、队列指针、包序列号这些抓出来一看就知道问题在哪。Vivado 的 ILA 核很吃资源但调试阶段值得。抓的时候注意触发条件要设好不然抓一堆没用的数据。第三关注 PCIe 带宽。很多人只盯着以太网侧忽略了 PCIe 可能才是瓶颈。Gen3 x8 的理论带宽是 64Gbps实际有效带宽大概 50Gbps如果以太网是 25GPCIe 够用如果是 100G就得 Gen4 x16 了。第四中断合并要调。RDMA 的完成事件如果每个都中断CPU 会被打断死。ERNIC 支持中断合并攒一批完成事件再中断。合并参数要调太激进延迟高太保守 CPU 占用高。5.4 关于 QP 数量的经验值QP 数量不是越多越好。每个 QP 都要占存储、占调度资源。我见过有人配了 64K 个 QP结果大部分是空闲的白白浪费 BRAM。经验值是QP 数量取并发连接数的 1.5 到 2 倍。比如业务峰值有 1000 个并发连接配 2048 个 QP 就够了。留余量是为了应对连接建立/拆除的瞬时峰值。另外QP 可以复用。连接断了之后QP 可以复位重新用不必销毁重建。ERNIC 支持 QP 的动态分配和回收软件层要做好管理避免泄漏。6. 写在最后的一点个人体会折腾 ERNIC 这套东西断断续续有大半年最大的感受是FPGA 做网络加速难点从来不在写逻辑而在理解协议和调时序。RoCEv2 的协议规范几百页QP 状态机的迁移条件、包序列号的回绕处理、重传的触发时机每一条都有讲究。硬件不像软件错了可以打补丁逻辑烧进去就是死的所以前期把协议吃透比什么都重要。队列深度这个参数我一开始也以为越大越好后来才明白它是个平衡点。它跟链路 BDP、消息大小、QP 数量、片上存储都相关没有万能值只能实测。我现在的习惯是每换一个业务场景都重新跑一遍深度扫描找到那个吞吐饱和、延迟可接受的甜点。如果你也在做类似的项目我的建议是先从官方参考设计跑通最小系统再逐步加功能、调性能。别一上来就想着改架构先把标准流程走一遍踩过的坑自然就少了。ERNIC 的代码质量不错注释也全值得花时间读一读尤其是 QP 状态机和收发路径那部分读懂了RoCEv2 在硬件里怎么跑心里就有数了。