Skip to content

📒 高性能网络

RDMA

RDMA 基本概念

注册内存区域

RDMA 通信前,需要先注册内存区域 MR(memory region),供 RDMA 设备访问:

  • 内存页面必须被 Pin 住不可换出。
  • 注册时获得 L_Key(local key)R_Key(remote key)。前者用于本地访问,后者用于远程访问。

交换信息

在进行 RDMA 通信前,通信双方需要交换 R_Key 和 QP 等信息。可以先通过以太网建立 TCP 连接,或者使用 rdma_cm 管理 RDMA 连接。

异步通信

RDMA 基于三个队列进行异步通信:

  • Send、Receive 队列用于调度工作(work)。这两个队列也合称为 Queue Pair(QP)
  • Completion 队列用于通知工作完成。

RDMA 通信流程如下:

  • 应用程序将 WR(work request,也称为 work queue element)放入(post)到 Send 或 Receive 队列。
  • WR 中含有 SGE(Scatter/Gather Elements),指向 RDMA 设备可以访问的一块 MR 区域。在 Send 队列中指向发送数据,Receive 队列中指向接收数据。
  • WR 完成后,RDMA 设备创建 WC(work completion,也称为 completion queue element)放入 Completion 队列。应用向适配器轮询(poll)Completion 队列,获取 WC。

对于一个应用,QP 和 CQ 可以是多对一的关系。QP、CQ 和 MR 都定义在一个 Protection Domain(PD) 中。

rdma_queue_pair
RDMA 通信队列
InfiniBand Technology Overview - SNIA

访存模式

RDMA 支持两种访存模式:

  • 单边(one-sided):read、write、atomic 操作。
    • 被动方注册一块内存区域,然后将控制权交给主动方;主动方使用 RDMA Read/Write 操作这块内存区域。
    • 被动方不会使用 CPU 资源,不会知道 read、write 操作的发生。
    • WR 必须包含远端的虚拟内存地址R_key,主动方必须提前知道这些信息。
  • 双边(two-sided):send、receive 操作。
    • 源和目的应用都需要主动参与通信。双方都需要创建 QP 和 CQ。
    • 一方发送 receive,则对端需要发送 send,来消耗(consume)这个 receive。
    • 接收方需要先发送自己接收的数据结构,然后发送端按照这个数据结构发送数据。这意味着接收方的缓冲区和数据结构对发送方不可见。

在单个连接中,可以混用并匹配(mix and match)这两种模式。

内核旁路

RDMA 提供内核旁路(kernel bypass)功能:

  • 原先由 CPU 负责的分片、可靠性、重传等功能,现在由适配器负责。
  • RDMA 硬件和驱动具有特殊的设计,可以安全地将硬件映射到用户空间,让应用程序直接访问硬件资源。
  • 数据通路直接从用户空间到硬件,但控制通路仍然通过内核,包括资源管理、状态监控和清理等。保证系统安全稳定。

Question

事实上,通信双方的 QP 被直接映射到了用户空间,因此相当于直接访问对方的内存。

如果你对操作系统和硬件驱动有一些了解,不妨想一想下面的问题:

  • 如何才能让应用程序直接访问硬件资源,同时实现操作系统提供的应用隔离和保护呢?
  • 如果在两个独立的虚拟内存空间(可能在不同物理机、不同架构上)之间建立联系?

RDMA 编程

具体地说,学习的是 libibverbs 库。

Quote

接下来通过 NVIDIA Docs 提供的例子来学习 RDMA 编程。

IB Verbs

Quote

Note

相关头文件为 infiniband/verbs.h,Debian 软件包为 libibverbs-dev

代码见 RDMA_RC_example.c

准备阶段:

  • resource_create():创建资源,包括 PD、MR、QP、CQ 等。
  • connect_qp():通信双方交换信息,包括 LID、QP_NUM、RKEY 等,将 QP 状态更改为 INIT、RTR、RTS。
    • sock_sync_data():通过 TCP 通信交换信息。
    • modify_qp_to_init()
    • post_receive():预置接收队列,也可以放在通信阶段。
    • modify_qp_to_rtr()
    • modify_qp_to_rts()
    • 同步点
flowchart TD
 subgraph s1["resource_create()"]
  n27@{ shape: "rounded", label: "ibv_query_port()" }
  n26@{ shape: "hex", label: "ibv_port_attr" }
  n25@{ shape: "hex", label: "ibv_mr" }
  n17@{ shape: "hex", label: "char *" }
  n16@{ shape: "rounded", label: "ibv_get_device_name()" }
  n15@{ shape: "rounded", label: "ibv_get_device_list()" }
  n14@{ shape: "hex", label: "ibv_device" }
 n1@{ shape: "hex", label: "ibv_pd" }
 n2@{ shape: "rounded", label: "ibv_alloc_pd()" }
 n3@{ shape: "hex", label: "ibv_context" }
 n4@{ shape: "hex", label: "buf" }
 n3 --- n2
 n2 --- n1
 n5@{ shape: "rounded", label: "ibv_open_device()" }
 n5 --- n3
 n6@{ shape: "rounded", label: "ibv_create_cq()" }
 n3 --- n6
 n7@{ shape: "hex", label: "mr_flags" }
 n8@{ shape: "rounded", label: "ibv_reg_mr" }
 n4 --- n8
 n7@{ shape: "fr-rect", label: "mr_flags<br/>=IBV_ACCESS_REMOTE_READ|..." } --- n8
 n1 --- n8@{ shape: "rounded", label: "ibv_reg_mr()" }
 n9@{ shape: "hex", label: "ibv_qp_init_attr" }
 n10@{ shape: "hex", label: "ibv_cq" }
 n6 --- n10
 n9@{ shape: "hex", label: "ibv_qp_init_attr" }
 n10 ---|"send_cq, recv_cq"| n9
 n11@{ shape: "fr-rect", label: "qp_type<br/>=IBV_QPT_RC" }
 n11 --- n9
 n12@{ shape: "rounded", label: "ibv_create_qp()" }
 n9 --- n12
 n13@{ shape: "hex", label: "ibv_qp" }
 n12 --- n13
 end
 n15 --- n14@{ shape: "hex", label: "ibv_device **" }
 n14 --- n16
 n16 --- n17
 n14 --- n5
 subgraph s2["connect_qp()"]
  n36@{ shape: "rounded", label: "sock_sync_data()" }
  subgraph s3["cm_con_data_t"]
   n24@{ shape: "hex", label: "buf" }
   n23@{ shape: "hex", label: "lid" }
   n22@{ shape: "hex", label: "qp_num" }
   n20@{ shape: "hex", label: "rkey" }
   n21@{ shape: "hex", label: "gid" }
  end
  n18@{ shape: "hex", label: "ibv_gid" }
  n19@{ shape: "rounded", label: "ibv_query_gid()" }
 end
 n3 --- n19
 n19 --- n18
 n18 --- n21
 n8 --- n25
 n25 --- n20
 n4 --- n24
 n13 --- n22
 n27 --- n26
 n3 --- n27
 n26 --- n23
 n29 --- n30
 n13 --- n30
 n28 --- n29
 subgraph s5["post_receive()"]
  n33@{ shape: "rounded", label: "ibv_post_recv" }
  n32@{ shape: "hex", label: "ibv_recv_wr" }
  n31@{ shape: "hex", label: "ibv_sge" }
 end
 n25 ---|"lkey"| n31
 n4 --- n31
 subgraph s4["modify_qp_to_init, rts()"]
  n28@{ shape: "fr-rect", label: "qp_state<br/>=IBV_QPS_INIT" }
  n30@{ shape: "rounded", label: "ibv_modify_qp()" }
  n29@{ shape: "hex", label: "ibv_qp_attr" }
 end
 n31 --- n32
 n32 --- n33
 subgraph s6["modify_qp_to_rtr()"]
  n34@{ shape: "rounded", label: "Rounded Rectangle" }
  n35@{ shape: "hex", label: "ibv_qp_attr" }
 end
 n22 --- n35
 n23 --- n35
 n35 --- n34@{ shape: "rounded", label: "ibv_modify_qp()" }
 s3 --- n36

通信阶段:

  • post_send():创建并发送 WR,WR 的类型取决于 opcode
  • poll_completion():轮询得到 WC。
flowchart TD
 subgraph s1["post_send()"]
  n12@{ shape: "rounded", label: "ibv_post_send()" }
  n7@{ shape: "fr-rect", label: ".opcode<br/>IBV_WR_SEND<br/>IBV_WR_RDMA_READ<br/>IBV_WR_RDMA_WRITE" }
  n1@{ shape: "hex", label: "ibv_sge" }
  n2@{ shape: "hex", label: "ibv_send_wr" }
 end
 n3@{ shape: "hex", label: "ibv_mr" }
 n3 ---|".lkey"| n1
 n4@{ shape: "hex", label: "buf" }
 n4 --- n1
 n1 --- n2
 subgraph s2["IBV_WR_SEND only"]
  n5@{ shape: "hex", label: ".rkey" }
  n6@{ shape: "hex", label: ".remote_addr" }
 end
 s2 ---|".wr.rdma"| n2
 n7 --- n2
 subgraph s3["poll_completion()"]
  n11@{ shape: "rounded", label: "assert()" }
  n10@{ shape: "rounded", label: "ibv_poll_cq()" }
  n9@{ shape: "hex", label: "ibv_wc" }
 end
 n8@{ shape: "hex", label: "ibv_cq" }
 n9 --- n10
 n8 --- n10
 n10 ---|".status == IBV_WC_SUCCESS"| n11
 n2 --- n12

接下来的小节对几个重要的数据结构和 API 进行解读:

Work Request
  • enum ibv_wr_opcodeIBV_WR_SENDIBV_WR_RDMA_READIBV_WR_RDMA_WRITE,决定 RDMA 操作的类型。

RDMA_CM

Note

相关头文件为 rdma/rdma_cma.h,Debian 软件包为 librdmacm-dev

代码见 mckey.c

RDMA_CM 用于管理 RDMA 连接,包装了使用 socket 编程交换 QP、R_Key 等信息的过程,减少代码量。它的接口与 Socket 类似:

rdma_listen()
rdma_connect()
rdma_accept()

与 Socket 编程的比较:

  • 操作异步进行,通过 rdma_event_channel 进行事件通知。
  • rdma_cm_id(identifier)与 fd 类似,用于标识连接。
  • 使用 rdma_bind_addr()rdma_cm_idsockaddr 绑定,类似 bind()

本例为多播通信,需要使用 rdma_join_multicast()rdma_leave_multicast() 进出多播组。

flowchart TD
 subgraph s1["run()"]
  subgraph s2["connect_evnets() [LOOP]"]
   subgraph s5["join_handler()"]
    n14@{ shape: "rounded", label: "ibv_create_ah()" }
    n15@{ shape: "rounded", label: "inet_ntop()" }
   end
   subgraph s4["addr_handler()"]
    n12@{ shape: "rounded", label: "rdma_join_multicast()" }
    n13@{ shape: "rounded", label: "ibv_post_recv()" }
   end
   subgraph s3["cma_handler()"]
   end
   n9@{ shape: "rounded", label: "rdma_ack_cm_evnet()" }
   n11@{ shape: "rounded", label: "rdma_get_cm_event()" }
   n10@{ shape: "hex", label: "rdma_cm_event" }
  end
  n8@{ shape: "rounded", label: "rdma_bind_addr()" }
  n7@{ shape: "hex", label: "char *" }
  n6@{ shape: "rounded", label: "getaddrinfo()" }
  n5@{ shape: "hex", label: "sockaddr" }
  n2@{ shape: "rounded", label: "rdma_create_id()" }
  n1@{ shape: "hex", label: "rdma_cm_id" }
 end
 n3@{ shape: "hex", label: "rdma_event_channel" }
 n4@{ shape: "rounded", label: "rdma_create_event_channel" }
 n4@{ shape: "rounded", label: "rdma_create_event_channel()" } --- n3
 n3 --- n2
 n2 --- n1
 n6 --- n5
 n7 --- n6
 n5 ---|"src_addr"| n8
 n1 --- n8
 n11 --- n10
 n10 --- n9
 s3 --- s4
 s3 --- s5
 n10 --- s3
 n3 --- n11
 n1 --- s3
 n16@{ shape: "rounded", label: "rdma_leave_multicast()" }
 n1 --- n16
 n5 ---|"dst_addr"| n12
 n12 --- n16

RDMA Verbs

Note

相关头文件为 rdma/rdma_verbs.h,Debian 软件包为 librdmacm-dev,相关 API 一般以 rdma_ 开头。

srq.c 是一个使用 SRQ(Shared Receive Queue)的例子。

DevX

Note

相关头文件为 infiniband/mlx5dv.h,相关的 API 一般以 mlx5dv_ 开头。

DevX API 支持从用户空间直接访问 mlx5 驱动的设备资源,越过 libibverbs 的数据通路。

Linux RDMA 运维

ib_read_bw
show_gids

DPDK


待整理

  • 交换芯片 BlueField:用于 DPU 产品线,简单来说就是网卡上有独立的 CPU,可以处理网络流量。例如,下面是 BlueField-2 DPU 的架构图:
rdma_bf2_arch
BlueField-2 DPU 架构
Nvidia Bluefield DPU Architecture - DELL

RoCE 原理

Quote

RoCE 协议存在 RoCEv1 和 RoCEv2 两个版本,取决于所使用的网络适配器。

  • RoCE v1:基于以太网链路层实现的 RDMA 协议 (交换机需要支持 PFC 等流控技术,在物理层保证可靠传输)。
  • RoCE v2:封装为 UDP(端口 4791) + IPv4/IPv6,从而实现 L3 路由功能。可以跨 VLAN、进行 IP 组播了。RoCEv2 可以工作在 Lossless 和 Lossy 模式下。
    • Lossless:适用于数据中心网络,要求交换机支持 DCB(Data Center Bridging)技术。

RoCE 包格式

rdma_packet_infiniband roce_packet_version
RoCE 包格式
RoCE 指南 - FS

DPDK

问题记录

编译相关

  • 编译 DPDK 时

    Generating drivers/rte_common_ionic.pmd.c with a custom command
    FAILED: drivers/rte_common_ionic.pmd.c
    ...
    Exception: elftools module not found
    

    解决方法:pip 安装 pyelftools 模块

其他相关知识

  • DMA 机制:包括 IOVA、VFIO 等。
  • Linux 内存管理机制:包括 Hugepage、NUMA 等。
  • Linux 资源分配机制:包括 cgroup 等。
  • 基础线程库 pthread。

  • Non-Uniform Memory Access (NUMA):非一致性内存访问。

    • 与 UMA 相比的优劣:内存带宽增大,需要编程者考虑局部性
    • 每块内存有
      • home:物理上持有该地址空间的处理器
      • owner:持有该块内存值(写了该块内存)的处理器
    • Linux NUMA:https://docs.kernel.org/mm/numa.html
      • Cache Coherent NUMA
      • 硬件资源划分为 node
        • 隐藏了一些细节,不能预期同个 node 上的访存效率相同
        • 每个 node 有自己的内存管理系统
        • Linux 会尽可能为任务分配 node-local 内存
        • 在足够不平衡的条件下,scheduler 会将任务迁移到其他 node,造成访存效率下降
      • Memory Policy
    • Linux 实践
      • numactl
      • lstopo
  • Hugepage