使用 DPDK

本页介绍了如何在 U4C Compute Engine 实例上使用数据平面开发套件 (DPDK)。

关于通过 AF_XDP 使用 DPDK

数据平面开发套件 (DPDK) 是一种框架,适用于需要快速数据包处理、低延迟和一致性能的性能密集型应用。DPDK 会绕过 Linux 内核网络堆栈,直接在用户空间中运行。您可以使用 AF_XDP 架构在 U4C 实例上运行 DPDK。

DPDK 提供了一个 AF_XDP 轮询模式驱动程序 (PMD),这是一个虚拟设备 (vdev),可让 DPDK 应用在复制模式或零复制模式下在 AF_XDP 之上运行。如需了解详情,请参阅 DPDK 文档中的 AF_XDP 轮询模式驱动程序。与 Compute Engine 上的典型部署不同,U4C 实例上基于 AF_XDP 的 DPDK 不需要配置 VFIO、UIO 或 dpdk-devbind.py

将 DPDK 与 ULL 解决方案搭配使用时,支持流转向。 您可以将特定流量流直接导向到指定的接收队列 (RX),从而绕过默认的接收端扩展 (RSS) 哈希处理。ULL 解决方案支持针对 ULL 单播和多播流量的 3 元组流量导向(协议、目标 IP 地址和目标端口)。

准备工作

在 U4C Compute Engine 实例上使用 DPDK 之前,您必须满足以下要求。

创建 U4C 实例

创建 U4C Bare Metal 实例(如果尚未创建)。请参阅创建 ULL Compute Engine 实例

使用 SSH 连接到您的实例

如果您尚未连接到实例,请先使用 SSH 连接。

切换到根用户

以下过程中的命令和脚本会修改系统级设置、内核参数和网络接口。如需成功运行这些命令,您必须以根用户身份执行它们。您可以运行 sudo su 切换到根 shell,也可以根据需要添加 sudo,然后再运行命令。

在 U4C 实例上安装 DPDK

如需在 U4C 实例上安装 DPDK,请按照以下步骤操作:

  1. 配置 DPDK 安装的依赖项:

    apt-get update && apt-get upgrade -yq
    apt-get install -yq build-essential ninja-build python3-pip \
        linux-headers-$(uname -r) pkg-config libnuma-dev
    pip install pyelftools meson
    
  2. 安装 DPDK。

    wget https://fast.dpdk.org/rel/dpdk-VERSION.tar.xz
    tar xvf dpdk-VERSION.tar.xz
    cd dpdk-VERSION

    VERSION 替换为您要安装的 DPDK 版本,例如 26.07。如有需要,请参阅 DPDK 下载页面。

  3. 如需使用示例构建 DPDK,请运行以下命令:

    meson setup -Dexamples=all build
    ninja -C build install; ldconfig
    

为 AF_XDP 配置网络接口

如需在 Google 虚拟 NIC (gVNIC) 上使用 AF_XDP,您必须调整默认驱动程序功能,以准备网络接口。

您可以手动执行这些步骤,也可以使用自动配置脚本。选择以下某个标签页:

手动

针对要配置的每个网络接口,执行以下步骤。

  1. 减少 RX 和 TX 队列数:gVNIC 默认使用支持的最大 RX 和 TX 队列数,但您必须将此数量减少一半,以确保有足够的 TX 队列来处理正常的内核流量。

    ethtool -L NIC_NAME rx NUM_SOCKETS \
    tx NUM_SOCKETS

    替换以下内容:

    • NIC_NAME:网络接口的操作系统名称,例如 eth1
    • NUM_SOCKETS:要配置的 AF_XDP 套接字数量。 将此值设置为不大于接口最大队列数的一半。 对于 U4C 实例,此值通常为 8(默认 16 个队列的一半)。 您可以运行 ethtool -l NIC_NAME 来验证最大队列数。
  2. 停用硬件 GRO 和 LRO:由于 gVNIC 不支持多缓冲区 XDP,因此您必须停用大型接收分流 (LRO) 和硬件通用接收分流 (GRO):

    ethtool -K NIC_NAME rx-gro-hw off
    ethtool -K NIC_NAME lro off
  3. 缩短 RX 缓冲区长度:默认情况下,较新的驱动程序会将 4 KB(4,096 字节)的缓冲区发布到网络接口以进行 RX,但 XDP 需要的缓冲区长度为 2048

    ethtool -G NIC_NAME rx-buf-len 2048

脚本

或者,您也可以针对要配置的每个网络接口运行以下 Bash 脚本。该脚本通过减少队列数量、停用分流和调整 RX 缓冲区长度,自动为 XDP 准备给定的网络接口:

#!/bin/bash
# Usage example: NUM_SOCKETS=8 prep_xdp.sh eth0

DEV=$1
NUM_SOCKETS=${NUM_SOCKETS=1}

# Reduce RX/TX queue counts to the number of AF_XDP sockets
ethtool -L $DEV rx $NUM_SOCKETS tx $NUM_SOCKETS

# Disable LRO/HW-gro
OFFLOAD=$(ethtool -k $DEV | \
grep "rx-gro-hw\|large-receive-offload" | \
grep -v fixed | cut -d ":" -f 1)
ethtool -K $DEV ${OFFLOAD} off

# Reduce RX buffer length to 2048
ethtool -G $DEV rx-buf-len 2048

运行 DPDK 应用

如需使用 AF_XDP PMD,请在 DPDK 应用的环境抽象层 (EAL) 实参中添加 --vdev 标志。

以下示例命令包含多个关键参数。如需详细了解设置和参数,请参阅 DPDK 文档中的 AF_XDP 轮询模式驱动程序

DPDK_APPLICATION -a PCIE_BDF \
  --vdev=net_af_xdp,iface=NIC_NAME,queue_count=NUM_SOCKETS,start_queue=START_QUEUE,xdp_prog=XDP_PROG \
  -- APPLICATION_ARGS

替换以下内容:

  • DPDK_APPLICATION:要运行的 DPDK 应用二进制文件。
  • PCIE_BDF:网络接口的 PCI 地址。您可以通过运行 ethtool -i NIC_NAME 并检查 bus-info 值(例如 0000:00:04.0)来找到此值。
  • NIC_NAME:网络接口的操作系统名称,例如 eth1
  • NUM_SOCKETS:要打开的 AF_XDP 套接字数量。每个套接字都连接到单个队列对。此值必须与您在接口上配置的队列数量一致。
  • START_QUEUE:AF_XDP 套接字的起始队列索引。
  • XDP_PROG:要在接收到的数据包上运行的自定义 XDP 程序。如果省略,DPDK 会使用 libxdp 提供的默认 XDP 程序。
  • APPLICATION_ARGS:特定于您的 DPDK 应用的实参。

使用驱动程序功能

本部分提供了有关流量转向和 RX 时间戳的使用信息。

流程导向

您可以将流导向与 XDP 结合使用,将应用数据包导向到特定的队列子集,并将剩余的队列用于内核流量。以下各部分介绍了可用于流规则编程的两种方法。

预节目流程导向(推荐)

由于 AF_XDP PMD 不支持使用 ioctl 调用进行即时流编程,并且需要修改 DPDK 应用,因此我们建议您在启动应用之前使用 ethtool 预先对流规则进行编程。

例如,如需对将 IPv4 UDP 流量引导至队列 0 的流规则进行编程,请执行以下操作:

ethtool -N NIC_NAME flow-type udp4 \
  dst-ip DST_IP dst-port DST_PORT action 0 loc 0

替换以下内容:

  • NIC_NAME:网络接口的操作系统名称,例如 eth1
  • DST_IP:要引导的流量的目标 IP 地址。
  • DST_PORT:要引导的流量的目标端口。

即时制定节目流规则

AF_XDP PMD 不支持即时流编程。如需使用此方法,您必须修改 DPDK 应用以手动发送 ethtool ioctl 调用。

我们建议您避免使用此方法,除非您的应用需要处理大量临时连接。gVNIC 最多支持 20,000 条 3 元组流转向规则;如果您需要的规则较少,并且提前知道目标 IP 地址和端口,请改为预先编程您的规则。

如果您必须动态地对规则进行编程,请参阅以下 C 代码示例:

展开可查看 C 代码示例

struct flow_rule_info {
  uint32_t src_ip;
  uint32_t dst_ip;
  uint16_t src_port;
  uint16_t dst_port;
  uint32_t target_queue;
  uint32_t rule_id;
}

int add_flow_rule(const char *ifname,
                  struct flow_rule_info *rule_info,
                  bool is_5tuple) {
  struct ethtool_rxnfc cmd;
  struct ifreq ifr;
  int fd;

  fd = socket(AF_INET, SOCK_DGRAM, 0);
  if (fd < 0) {
    fprintf(stderr, "Failed to open socket: %s", strerror(fd));
    return -1;
  }

  memset(&cmd, 0, sizeof(cmd));
  memset(&ifr, 0, sizeof(ifr));

  cmd.cmd = ETHTOOL_SRXCLSRLINS;
  cmd.fs.flow_type = UDP_V4_FLOW;

  cmd.fs.h_u.udp_ip4_spec.ip4dst = rule_info->dst_ip;
  cmd.fs.h_u.udp_ip4_spec.pdst = htons(rule_info->dst_port);
  cmd.fs.m_u.udp_ip4_spec.ip4dst = 0xFFFFFFFF;
  cmd.fs.m_u.udp_ip4_spec.pdst = 0xFFFF;

  if (is_5tuple) {
    cmd.fs.h_u.udp_ip4_spec.ip4src = rule_info->src_ip;
    cmd.fs.h_u.udp_ip4_spec.psrc = htons(rule_info->src_port);
    cmd.fs.m_u.udp_ip4_spec.ip4src = 0xFFFFFFFF;
    cmd.fs.m_u.udp_ip4_spec.psrc = 0xFFFF;
  }

  cmd.fs.ring_cookie = rule_info->target_queue;
  cmd.fs.location = rule_info->rule_id;

  strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1);
  ifr.ifr_data = (void *)&cmd;

  int ret = ioctl(fd, SIOCETHTOOL, &ifr);
  if (ret)
    fprintf(stderr, "Failed to send ioctl: %s\n", strerror(errno));
  close(fd);

  return ret;
}

int try_add_xdp_flow_rule(int port, struct flow_rule_info *rule_info) {
  char dev_name[RTE_ETH_NAME_MAX_LEN];
  char *af_xdp_driver = "net_af_xdp";
  struct rte_eth_dev_info dev_info;
  int err;

  if (!rte_eth_dev_is_valid_port(port)) return -1;

  err = rte_eth_dev_info_get(port, &dev_info);
  if (err) {
    fprintf(stderr, "Error getting info for port %d: %s\n", port,
            strerror(err));
    return retval;
  }

  if (strncmp(dev_info.driver_name, af_xdp_driver,
              strlen(af_xdp_driver)) != 0) {
    fprintf(stderr, "Not an AF_XDP vdev!\n");
    return -EINVAL;
  }

  err = rte_eth_dev_get_name_by_port(port, dev_name);
  if (retval) {
    fprintf(stderr, "Failed to get dev_name: %s\n", strerror(err));
    return retval;
  }

  /* replace ens4 with correct ifname; must be passed in as application argument */
  err = add_flow_rule("ens4", rule_info));
  if (err)
    fprintf("Failed to add flow rule: %s\n", strerror(err));
  return err;
}

/* From somewhere in the application: */
int application_func(...) {
  ...

  struct flow_rule_info *rule_info {
    .src_ip = 0x0a000001,
    .dst_ip = 0x0a000002,
    .src_port = 0x1110,
    .uint16_t dst_port = 0x1011,
    .uint32_t target_queue = 0,
    .uint32_t rule_id = 1,
  };
  try_add_xdp_flow_rule(xdp_port_id, &rule_info, /*is_5tuple=*/false);
  ...
}

RSS 节目

使用流转向时,RSS 有助于提供更好的流量隔离。 与流量转向一样,您可以使用 ethtool 配置 RSS。建议您先配置 RSS,然后再运行应用。

以下示例展示了如何配置 RSS 以便与 AF_XDP 搭配使用:

# Example: Kernel queues 0-3, XDP queues 4-7
NUM_SOCKETS=4 bash prep_xdp.sh eth0

# Program flow rules
ethtool -N eth0 flow-type udp4 dst-ip DST_IP dst-port DST_PORT_0 action 4 loc 0
ethtool -N eth0 flow-type udp4 dst-ip DST_IP dst-port DST_PORT_1 action 5 loc 1
ethtool -N eth0 flow-type udp4 dst-ip DST_IP dst-port DST_PORT_2 action 6 loc 2
ethtool -N eth0 flow-type udp4 dst-ip DST_IP dst-port DST_PORT_3 action 7 loc 3
# If there are more ports that the application polls on, flow rules can be added in a round-robin fashion in a script.

# Program RSS
ethtool -X eth0 start 0 equal 4

# Run the application
./path/to/application -a 0000:00:03.0 --vdev net_af_xdp,iface=eth0,start_queue=4,queue_count=4 -- APPLICATION_ARGS

接收时间戳

虽然上游 DPDK 代码库中不支持 AF_XDP PMD 的 RX 时间戳,但您可以通过执行以下操作来使用 RX 时间戳:

  1. 必需的补丁应用到 DPDK 源代码树,然后重新编译 DPDK 源代码和您的应用。 这些补丁在接收到的 mbufs 中添加了对 rte_eth_read_clock 和 RX 时间戳的支持。

  2. 使用将时间戳加载到元数据中的 XDP 程序。

  3. 启动 DPDK 应用时,请提供以下额外的 AF_XDP vdev 参数:

    • xdp_meta_rx_ts_offset:从 XDP 元数据开头算起,64 位 RX 时间戳值所在的字节偏移量。
    • xdp_meta_valid_hint_offset:(可选)涵盖 1 字节标志字段的字节偏移量,用于指示时间戳是否有效。
    • xdp_meta_rx_ts_valid_mask:(可选)用于提取有效标志位的位掩码。

      如果 ctx 是元数据的开头,则 (ctx->data_meta + xdp_meta_valid_hint_offset) & xdp_meta_rx_ts_valid_mask 的值用于描述时间戳是否有效。

后续步骤

  • 如需将实例时钟与宿主服务器的物理 NIC 时钟同步,请参阅配置准确时间