网卡自定义RSS

重要

本文中含有需要您注意的重要提示信息,忽略该信息可能对您的业务造成影响,请务必仔细阅读。

网卡RSS(接收端扩展)通过哈希算法将网络流量智能分发至多CPU核心,实现负载均衡与性能提升,避免单核过载,尤其适用于云计算、视频流分发、高频交易系统等高并发场景。在处理加密通信或虚拟网络等特殊协议时,需结合业务特征自定义RSS规则,解决流量不均匀问题以最大化多核资源利用率。

什么是RSS

RSS(Receive Side Scaling,接收端扩展)是一种基于哈希算法的智能流量调度机制,其核心目标是通过网卡硬件与驱动的配合,基于数据包的特征(如源/目的IP、端口等)计算哈希值,将不同网络流量精准映射到多个RX队列,再由绑定的CPU核心并行处理,从而在多核CPU架构下实现负载均衡,提升吞吐量并降低单核处理延迟。

支持多队列的弹性网卡通过为每个队列独立设置RX(接收)和TX(发送)通道实现并行处理,其中RX队列负责接收并暂存来自网络的数据包。当数据包达到网卡时,网卡硬件会根据一定的RSS策略(如轮询、基于流的分配等策略),将不同数据流动态分配到多个RX队列,形成流量分流。默认ECS实例的RSS策略为VPC固定配置,实例内部无法查看和修改。

部分实例规格族支持网卡自定义RSS能力,您需要在支持的实例规格族上开启网卡自定义RSS功能,网卡即通过默认的自定义哈希规则进行流量分发,您还可以进一步根据实际流量特征调整RSS配置

网卡开启自定义RSS后,您也可以DPDK应用场景中配置RSS,以最大化多核性能。

原理与机制

ECS实例网卡自定义RSS的核心实现如下:

  • 哈希值计算:网卡根据收到的数据包的五元组(源IP、目标IP、源端口、目标端口、协议)、哈希密钥,基于一定的哈希算法计算哈希值。

    • 哈希密钥:目前仅支持固定的40字节,ECS实例网卡驱动在初始化加载网卡时,生成一个默认的哈希密钥。您可以根据实际业务需求调整哈希密钥,进而影响流量分布。

    • 哈希算法:目前仅支持默认的toeplitz算法,不支持修改。

    • 哈希规则:流量类型不同,计算哈希值的规则不同。ECS网卡RSS默认哈希计算规则如下(不支持修改):

      • IPv4/IPv6TCPUDP流量:按照四元组(源IP、目的IP、源端口、目的端口)+哈希密钥计算哈希值。

      • IPv4/IPv6的非TCP/UDP流量(如ICMP):按照二元组(源IP、目的IP)+哈希密钥计算哈希值。

      • IP报文(如ARP):发送到默认队列(0号队列),不进行哈希计算。

  • 间接表映射:RSS间接表为一个预定义的数组,用于将哈希值映射到目标接收队列。每个元素(哈希桶)存储目标队列编号(如 0、1、2),表示数据包应该分发到哪个队列处理。ECS实例网卡驱动加载时,会基于均匀分发模式自动生成一个均匀分配流量的间接表。

    • 间接表长度:即哈希桶总数,目前仅支持固定为128字节。

    • 分发模式

      RSS间接表分发模式是RSS中用于将哈希值映射到具体接收队列的核心机制,通过预定义间接表,将哈希计算结果转换为队列索引,从而实现流量在多核中灵活分发。

      不同分发模式(包括均匀分发模式、权重分发模式等)直接影响流量在多核CPU上的分布特性,进而影响系统吞吐量、延迟、资源利用率及业务性能。

      您可以根据实际应用场景选择合适的分发模式,详细信息,请参见间接表分发模式

    • 间接表映射流程

      1. 哈希桶索引计算索引=哈希值%间接表长度(如88 % 128 → 索引88)。您可以参考使用RSS计算脚本进行索引值的计算。

      2. 队列号填充:根据分发模式,将队列号写入间接表的每个哈希桶。

        间接表生成后,根据计算的索引号即可以查询到对应的队列号,即数据包进入该队列处理。

  • CPU核心绑定:当对应的队列收到数据包后,触发对应的中断,由绑定的CPU核心处理。

    Red Hat Enterprise Linux以外的镜像已默认支持网络中断亲和性,即每个队列已关联独立的中断,并且设置的中断亲和性将中断处理分散到不同的CPU核心。详细信息,请参见多队列的核心机制

网卡开启/关闭自定义RSS

重要

网卡自定义RSS功能目前在邀测中,如需使用,请提交工单申请。

在支持的实例规格上开启网卡自定义RSS功能后,网卡接收到的流量会根据默认的哈希规则被分发到多个接收队列,由不同CPU核心并行处理,从而提升吞吐量并降低单核负载。

限制条件

  • 网卡自定义RSS功能目前在按地域逐步开放中,当前支持的地域如下:

    地域名称

    地域ID

    中国香港

    cn-hongkong

  • 目前仅部分支持网卡多队列的实例规格族支持启用自定义RSS,包括:通用型实例规格族g9i内存型实例规格族r9i

    您可以通过DescribeInstanceTypes接口查询规格支持情况,返回值RssSupporttrue表示支持,false表示不支持。

  • 推荐使用Alibaba Clound Linux 3.2104 LTS 64位镜像。

    • 如果使用其他公共镜像,内核版本应大于等于6.12。

    • 如果在DPDK应用中使用自定义RSS功能,DPDK版本应大于等于21.11。

如何开启/关闭

网卡自定义RSS功能默认关闭,您可以在创建网卡时或者网卡创建后,开启、关闭网卡自定义RSS功能:

  • 您可以在调用CreateNetworkInterface接口时,设置EnhancedNetwork参数中的EnableRsstrue、false,实现创建开启、关闭自定义RSS功能的弹性网卡。

    网卡自定义RSS功能在网卡绑定到实例上后生效。

  • 您也可以通过调用ModifyNetworkInterfaceAttribute,设置EnhancedNetwork参数中的EnableRsstrue、false修改网卡的自定义RSS功能,开启/关闭后,网卡自定义RSS功能在重新绑定到实例的时候生效:

  • 您可以通过DescribeNetworkInterfaceAttribute,指定AttributeenhancedNetwork查询网卡是否启用自定义RSS,返回的EnableRSStrue表示启用,false表示未启用。

    当网卡没有修改过RSS功能时,此接口不返回EnableRSS参数,即未启用。

查看网卡RSS

启用网卡自定义RSS后,网卡驱动在重新加载网卡的时候,会根据默认机制与规则生成默认的RSS配置。

您可以远程登录Linux实例后,执行ethtool -x eth0,查看主网卡的RSS配置。如果是辅助网卡,修改网卡标识即可,如eth1、eth2。

image

  • 哈希表(RX flow hash indirection table):定义了哈希值到接收队列(64个)的映射规则:

    • 哈希表中每行表示哈希值的范围,即起始索引,如0标识哈希值0-7,8标识8-15,依次类推。

    • 表中的数字(0、1、2)表示对应的接收队列的编号,本示例中64个队列(0-63)。

    • 当前配置:间接表按 0,1,2,...,63,0,1,2...63 循环填充,哈希值会被均匀映射到64个队列,即流量均匀分配。

  • RSS哈希密钥(RSS hash key):计算数据包哈希值的密钥,影响流量分发的均匀性。示例中的十六进制字符,表示40字节的密钥,用于哈希计算。

  • RSS哈希算法(RSS hash function):定义计算哈希值的算法。

    • toeplitz:当前启用的默认算法,支持基于五元组的对称哈希,适合通用流量。

    • xor/crc32:其他可选算法,通常用于特定场景,目前ECS仅支持toeplitz,其他均不支持,未启用。

  • 哈希规则:您可以进一步执行以下命令,查看主网卡接收流量的哈希字段配置。

    ethtool  -n eth0 rx-flow-hash tcp4
    • 您可以将tcp4替换成您希望查询的协议类型,如udp4tcp6udp6  。

    • ECS网卡RSS默认哈希计算规则如下(不支持修改):

      • IPv4/IPv6TCPUDP流量:按照四元组(源IP、目的IP、源端口、目的端口)+哈希密钥计算哈希值。

      • IPv4/IPv6的非TCP/UDP流量(如ICMP):按照二元组(源IP、目的IP)+哈希密钥计算哈希值。

      • IP报文(如ARP):发送到默认队列(0号队列),不进行哈希计算。

    • tcp4为例,对于IPv4TCP流量,默认会按照四元组(源IP、目的IP、源端口、目的端口)+哈希密钥计算哈希值:

      image

      预期流量特征如下:

      • 同一TCP连接:所有数据包的哈希值相同,会被分发到同一队列,确保数据包的顺序性。

      • 不同TCP连接,如果四元组不同,哈希值不同,流量会被分发到不同队列,实现多核CPU并行处理不同连接,提升吞吐量。

如果返回如下所示信息,表示网卡未开启自定义RSS或者网卡绑定的规格不支持自定义RSS,您需要确认实例规格支持后启用网卡自定义RSS功能

image

配置网卡自定义RSS

ECS实例的网卡开启自定义RSS以后,默认的RSS配置通常能够满足大部分场景,但在以下情况下您可能需要通过调整哈希密钥、配置间接表等方式进行优化与调整:

  • 流量不均衡:默认哈希值可能导致特定流量集中到少数队列。

  • 性能调优:根据业务流量特征(如UDP占比高)调整哈希规则,优化队列利用率。

  • 安全需求:通过自定义哈希密钥防止潜在的黑客预测流量分发路径。

重要
  • 本示例中实例镜像以Alibaba Cloud Linux 3.2104 LTS 64为例进行配置。

  • 本示例中以主网卡eth0为例说明手动设置RSS的步骤,您可以根据实际情况,替换网卡标识,如eth1、eth2,从而修改对应辅助网卡的RSS配置。

  • 当网卡RSS的配置不匹配业务流量特征时,可能导致流量分发不均、跨核状态竞争等问题,影响网络性能和应用稳定性,您需要根据实际业务流量特征和应用需求进行调整,调整后可通过监控工具实时观察各队列接收数据包情况,及时调整配置。

配置哈希密钥

如果您发现流量分布不均匀,如发现某些队列的 rx_packets 显著高于其他队列,或者为防止潜在攻击者通过逆向哈希值推断流量模式,您可能需要重新生成RSS哈希密钥。

重要

修改密钥会导致现有连接的哈希值变化,可能引起短暂乱序或重传,建议在业务低峰期操作。

  1. 执行以下命令,查看当前网卡的哈希密钥。

    ECS实例网卡驱动在初始化加载网卡时,会生成一个40字节的默认哈希密钥。

    ethtool -x eth0

    image

  2. 执行以下命令,使用OpenSSL生成新的随机密钥。

    openssl rand -hex 40 | fold -w2 | paste -sd: -
  3. 执行以下命令,应用新密钥到网卡。

    重要

    此方式为临时设置方式,在实例重启、网卡重新插拔后会失效,网卡驱动会自动初始化一个随机的默认配置。

    ethtool -X eth0 hkey <hash key>

    替换<hash key>为上一步生成的新的密钥。

  4. 执行以下命令,验证新密钥是否生效。

    ethtool -x eth0

    可以看到新的密钥已经生效:

    image

配置间接表

RSS间接表分发模式是RSS中用于将哈希值映射到具体接收队列的核心机制,通过预定义间接表,将哈希计算结果转换为队列索引,从而实现流量在多核中灵活分发。

不同分发模式(包括均匀分发模式、权重分发模式等)直接影响流量在多核CPU上的分布特性,进而影响系统吞吐量、延迟、资源利用率及业务性能。

不同分发模式适用于不同的业务场景,您可以根据实际需求进行配置调整。

重要

以下示例配置为临时设置,在实例重启、网卡重新插拔后会失效,网卡驱动会自动初始化一个随机的默认配置。

  • 均匀分发模式:将RSS间接表配置为均匀分发模式,使用前N个队列循环填充哈希桶,适用于通用的高并发业务场景。

    ethtool -X eth0 equal <队列数N>

    若设置为64,间接表会按 0,1,2,...,63,0,1,2...63,循环填充。

    image

  • 按权重分发:按权重比例分配哈希桶,支持非对称负载。适用于业务优先级分级(如队列0处理实时流量、队列1处理后台任务)、混合CPU性能等场景。

    ethtool -X eth0 weight <队列0权重> <队列1权重> ...

    如下示例,队列060%,队列160%,队列240%:

    说明

    如果一共4个队列,您只设置了两个队列权重,那么间接表中只会生成和队列0,1的映射。

    ethtool -X eth0 weight 6 4

    image

  • 部分队列分发:从指定队列开始,使用连续队列循环填充哈希桶,适用于定向流量到特定 CPU 范围(如 NUMA 节点)的场景。

    ethtool -X eth0 start <起始队列> equal <队列数>

    如下示例,使用队列2开始共40个队列(队列2-41)循环填充哈希桶:

    ethtool -X eth0 start 2 equal 40

    image

通过watch观察流量分布

实例网卡启用并配置自定义RSS后,您可以通过hping3生成不同特征的流量,并结合watch监控网卡队列的中断分布,可以验证当前RSS配置下,是否按预期分发流量到多核。

准备工作

购买两台ECS实例,配置如下:

  • 发送端ECS实例(10.0.0.252):安装hping3,用于生成不同特征的流量。

  • 接收端ECS实例:实例规格支持网卡多队列绑定辅助网卡(10.0.0.5),并且辅助网卡eth1启用自定义RSS

    说明
    • 本示例中以4个队列为例测试。

    • 为排除远程SSH登录接收端实例(主网卡)的流量影响,我们在辅助网卡eth1上开启自定义RSS测试。

  • 网络互通:两台ECS实例在同一安全组内网互通。

操作步骤

  1. 远程登录接收端ECS实例,查看辅助网卡的RSS配置,确认预期流量分布。RSS配置说明,请参见查看网卡RSS

    image

  2. 远程登录发送端ECS实例,执行以下命令,安装hping3

    yum install -y hping3
  3. 在接收端ECS实例执行以下命令,实时监控队列包计数。

    watch -n 1 "ethtool -S eth1 | grep rx[0,1,2,3]_packets"

    根据实际情况,替换接收端网卡的队列数配置。

  4. 远程登录发送端ECS实例,执行以下命令,模拟不同流量模式。

    • 场景一向接收端IP高速发送1万个SYN包,随机目标端口,确保流量哈希分散,验证哈希均衡性。

      sudo hping3 10.0.0.5 -S -a 10.0.0.252 --rand-dest -p 0 --baseport 10000 -c 10000 -i u100 -I eth0
      • rand-dest:随机目标端口

      • -p 0:与rand-dest配合使用

      • --baseport 10000:源端口起始值

      • -c 10000:发送1万个包

      • -i u100:发包间隔100微秒,高速发送

      • -I eth0:与rand-dest配合使用,指定发送端的网络接口

      观察接收端ECS实例的输出,预期四个队列的包计数接近均衡增长:

      image

    • 场景二固定源IP和端口发送流量,测试同一流量是否映射到同一队列,验证哈希一致性。

      以下命令表示向目标 10.0.0.5:80 发送1万个源IP固定(10.0.0.252)的 TCP SYN 包,源端口固定为 12345

      sudo hping3 10.0.0.5 -S -p 80 -c 10000 -s 12345 -a 10.0.0.252 --keep -i u100
      • 根据RSS脚本计算哈希索引值,预期流量进入队列0处理:

        image

      • 实际接收端ECS实例(10.0.0.5)上观察结果:

        image

  5. 如果查看业务流量分布结果不符合预期,如某个队列负载显著偏高,负载不均衡,您可以尝试通过调整哈希密钥配置间接表等方式优化。

DPDK中配置并使用RSS

DPDK(Data Plane Development Kit)是由英特尔发起、现由Linux基金会维护的开源用户态数据平面加速框架,旨在优化网络数据包处理性能。DPDK通过用户态驱动、零拷贝和轮询模式等技术,将网络数据包处理性能提升至接近线速水平,成为构建高性能网络应用的基石。适用于对吞吐和延迟敏感的领域,如电信云、金融科技及边缘计算。关于DPDK的更多信息,请参见Data Plane Development Kit (DPDK*)

RSS作为网卡的硬件特性,可以将流量分发到不同的队列,进而由不同的CPU核心处理,这对于DPDK的多核处理模型非常关键。网卡启用自定义RSS后,您可以通过DPDK中的testpmdl3fwd进行RSS功能的测试及验证。

ECS实例上安装并配置DPDK

说明
  • 如果在DPDK应用中使用自定义RSS功能,DPDK版本应大于等于21.11。

  • 本示例中以在规格为ecs.r9i.16xlarge(64个队列),镜像为Alibaba Cloud Linux 3.2104 LTS 64的实例上安装22.11.3版本的DPDK为例说明。

  • 本示例中以主网卡eth0为例说明手动设置RSS的步骤,您可以根据实际情况,替换网卡标识,如eth1、eth2,从而修改对应辅助网卡的RSS配置。

步骤一:安装DPDK

点击查看安装DPDK示例步骤

  1. 更新系统并安装基础工具。

    sudo yum update -y
    sudo yum install -y git wget gcc make kernel-devel-$(uname -r) numactl-devel python3 pciutils
  2. 安装DPDK依赖。

    sudo yum install -y libpcap-devel meson ninja-build
  3. 配置大页内存。

    • DPDK 绕过内核协议栈直接操作网卡,需高效管理内存以减少 TLB(Translation Lookaside Buffer)未命中率,大页内存(Huge Pages)通过使用比传统4 KB页面更大的内存页面(通常是2MB),减少地址转换过程中TLB(Translation Lookaside Buffer)的缺失次数,从而提高内存访问速度。

    • 大页内存如果分配过多,会减少留给操作系统动态分配的普通内存空间,可能导致其他非大页的应用程序或系统服务因内存不足而运行不畅或失败。当实例的大页内存设置过大,可能会导致实例无法连接等问题。

    • 您可以基于应用的实际内存需求和系统总内存,计算所需的大页数量。

      所需大页数量 = (应用所需内存大小 / 大页的实际大小)。Linux系统中,默认的大页大小通常是2 MB。例如,如果一个应用需要16 GB的大页内存,且大页大小为2MB,则所需的大页数量为16 GB / 2 MB = 8192。

    echo "vm.nr_hugepages = 8192" | sudo tee -a /etc/sysctl.conf
    sudo sysctl -p
  4. 创建并挂载大页目录。

    sudo mkdir -p /dev/hugepages
    sudo mount -t hugetlbfs hugetlbfs /dev/hugepages
  5. 下载DPDK源码(需开通公网)。

    cd ~
    wget https://fast.dpdk.org/rel/dpdk-22.11.3.tar.xz
    tar xf dpdk-22.11.3.tar.xz
    cd dpdk-stable-22.11.3
  6. 编译DPDK。

    # 初始化构建目录并配置项目选项,指定构建l3fw三层转发示例
    meson setup -Dexamples=l3fwd build
    cd build
    #编译
    ninja
    #将编译好的文件安装到系统目录
    sudo ninja install
    #更新系统的共享库缓存
    sudo ldconfig

    编译时候遇到如下图所示missing python module: elftools异常,

    image

    您需要指定当前使用的python版本,安装pyelftools库后再次重新编译即可:

    sudo /usr/bin/python3.8 -m pip install pyelftools

步骤二:加载内核模块

DPDK需要内核模块如UIOVFIO来支持用户态设备访问,通常优先选择VFIO以提高安全性(依赖IOMMU),而UIO适用于快速测试。本文以VFIO驱动为例说明配置。

  1. 启用IOMMU。

    VFIO依赖IOMMU实现安全的用户态设备绑定和DMA映射,需要启用IOMMU。

    1. 打开配置文件。

      sudo vim /etc/default/grub
    2. i切换到编辑模式,在GRUB_CMDLINE_LINUX中添加intel_iommu=on,然后保存配置文件。

      修改完成后的示例如下图所示。grub-config

    3. 应用修改后的配置。

      sudo grub2-mkconfig -o /boot/grub2/grub.cfg

      image.png

    4. 执行以下命令,重启实例并再次远程连接实例。

      reboot
      警告

      重启实例会造成您的实例停止工作,可能导致业务中断,建议您在非业务高峰期时执行该操作。

  2. 执行以下命令,安装VFIOVFIO-PCI驱动。

    sudo modprobe vfio && \
    sudo modprobe vfio-pci
  3. 配置noiommu_mode。

    sudo bash -c 'echo 1 > /sys/module/vfio/parameters/enable_unsafe_noiommu_mode'

步骤三:绑定网卡到DPDK驱动

  1. 开启网卡自定义RSS

    请确认待DPDK驱动接管的网卡已开启自定义RSS且已重新挂载到实例上生效,网卡驱动在重新加载时候会生成默认的RSS配置。

  2. 通过VNC连接实例

    • 本示例以主网卡为例,后续有卸载网卡的操作,如果通过SSH会话连接,会导致连接中断。

    • 您如果在辅助网卡上进行操作,则依然可以通过Workbench连接实例(主网卡)后,进行配置。

  3. 执行以下命令,查看网卡的PCI设备驱动绑定状态,默认情况下网卡被系统内核占用中。

    dpdk-devbind.py --status

    image

    • 设备 0000:00:05.0 (PCI设备标识符)当前由内核的 virtio-pci 驱动管理,对应的网络接口是 eth0,且该接口处于 活动状态(已配置 IP 或正在传输数据)。

    • 该设备可切换至 vfio-pci 驱动(用于 DPDK 用户态接管),但需先停用接口并解绑内核驱动。

  4. 执行以下命令,停用eth0。

    sudo ip link set dev eth0 down

    如果不停用,绑定到VFIO时候会提示异常信息:

    image

  5. 执行以下命令,解绑内核驱动并绑定到VFIO。

    dpdk-devbind.py -b vfio-pci 0000:00:05.0

    您需要根据实际情况,替换查询到的网卡的PCI设备标识符。

    说明

    您可以通过sudo pkill dpdk-app停止DPDK应用后,再dpdk-devbind.py -b virtio-pci 0000:00:05.0绑回内核驱动。

  6. 再次执行以下命令,查看网卡的PCI设备驱动绑定状态,预期网卡已经被DPDK接管。

    重要

    当网卡被DPDK用户态驱动接管后,内核不再控制该设备,因此无法通过 ip a 等命令查看其信息。

    dpdk-devbind.py --status

    image

    可以看到,设备 0000:00:05.0当前已绑定到用户态驱动 vfio-pci

通过testpmd配置RSS

testpmd(Test Packet Mode Driver)是DPDK中的一个核心测试工具,主要用于快速验证DPDK的功能、测试网卡驱动的性能,以及调试数据平面应用。关于更多使用testpmd的信息,请参见Testpmd Runtime Functions

重要

对网卡的配置修改(如队列数、RSS规则、RETA表等),仅在启动或重启数据包转发后生效。

  1. 通过VNC连接实例

  2. 执行以下命令,启动DPDKtestpmd数据包转发测试工具。

    dpdk-testpmd -a 0000:00:05.0 --socket-mem 1024 -- -i --portmask=0x1 --rxq=64 --txq=64  --forward-mode=rxonly
    • -a 0000:00:05.0:绑定PCI地址为0000:00:05.0的网卡到DPDK,您可以通过dpdk-devbind.py --status查看PCI地址。

    • --socket-mem 1024:为每个NUMA节点预分配1024MB大页内存。

    • -i:启动后进入交互式命令行模式,允许动态调整配置(如修改转发模式、查看统计信息)。输入quit退出。

    • --portmask=0x1:启用端口掩码 0x1(二进制 0001),表示仅使用第一个网卡(即PCI地址0000:00:05.0 对应的网卡)。

      • DPDK中,portmask的每个二进制位对应一个端口号(例如 0x1 表示启用端口0,0x3 表示启用端口01)。

      • 本示例中,只有1个设备(0000:00:05.0)被绑定到DPDK,所以所在DPDK中的端口号是0。

    • --rxq=64 / --txq=64:设置每个端口的接收队列(RXQ)和发送队列(TXQ)数量为 64,支持多队列并行处理。您可以替换为实际查询到的弹性网卡的队列数

    • --forward-mode=rxonly:指定转发模式为仅接收(接收数据包后丢弃,不发送),用于测试网卡接收性能或抓包。

  3. 进入交互模式,查询网卡当前RSS配置。

    • 查询哈希配置信息show port info <port_id>

      port_id:指定端口,本示例中为port 0。

      点击查看示例输出

      image

    • 查询哈希密钥show port <port_id> rss-hash key

      image

    • 查询间接表配置show port <port_id> rss reta <size> <mask0, mask1...>

      • size:查询的间接表条目数量,当前固定为128字节。

      • mask0,mask1:掩码,筛选要显示的哈希索引范围(十六进制格式),如掩码0=0xff 表示显示哈希索引 0-7 的条目。

        当前间接表大小为固定的128,需要2个掩码,每个掩码覆盖 64 个索引块,则输入show port 0 rss reta 128 (0xffffffffffffffff,0xffffffffffffffff)返回所有128个索引块和队列的映射信息:

        点击查看示例输出

        image

  4. 根据不同的需求,进行RSS配置。

    • 执行以下命令,配置新生成的哈希密钥。

      您可以使用OpenSSL生成新的随机密钥

      port config <port_id> rss-hash-key (ipv4|ipv4-frag|\
                        ipv4-tcp|ipv4-udp|ipv4-sctp|ipv4-other|\
                        ipv6|ipv6-frag|ipv6-tcp|ipv6-udp|ipv6-sctp|\
                        ipv6-other|l2-payload|ipv6-ex|ipv6-tcp-ex|\
                        ipv6-udp-ex <string of hex digits \
                        (variable length, NIC dependent)>)

      TCP over IPv4为,可执行port config 0 rss-hash-key ipv4 6D5A56DA255B0EC24167253D43A38FB0D0CA2BCBAE7B30B477CB2DA38030F20C6A42B73BBEAC01FC 配置哈希密钥并确定生效:

      image

    • 执行以下命令,配置RSS间接表,将特定的哈希值映射到指定的队列。

      port config all rss reta <hash,queue>,<hash,queue>..

      您需要根据实际队列数进行配置:

      • hash:哈希值的索引,范围取决于间接表大小(例如 0-63 对应 64 条目表)。

      • queue:目标接收队列号,如队列0,队列1。

l3fwd中应用RSS

如果您需要在DPDK应用程序中启用RSS,需要在相关代码中实现RSS哈希密钥、间接表等配置,以下以L3FWD为例说明。

L3FWD(Layer 3 Forwarding)是 DPDK(Data Plane Development Kit)中的一个三层网络转发示例应用,用于演示如何基于 IP 地址 实现高性能的数据包路由和转发。它通过 DPDK 的零拷贝、轮询模式驱动(PMD)等技术,实现接近线速的三层转发性能。

  1. 修改DPDKL3FWD源码(examples/l3fwd/main.c)。

    • 修改L3FWD示例代码中端口初始化部分static struct rte_eth_conf port_conf

      点击查看修改代码

      #define RSS_HASH_KEY_LENGTH 	40
      #define RSS_RETA_SIZE 			128
      
      static uint8_t hash_key[RSS_HASH_KEY_LENGTH] = {
          0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A,
          0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A,
          0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A,
          0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A,
      };
      
      static struct rte_eth_conf port_conf = {
      	.rxmode = {
      		.mq_mode = RTE_ETH_MQ_RX_RSS,
      		.offloads = RTE_ETH_RX_OFFLOAD_UDP_CKSUM | RTE_ETH_RX_OFFLOAD_TCP_CKSUM,
      	},
      	.rx_adv_conf = {
      		.rss_conf = {
      			.rss_key = hash_key,
      			.rss_hf = RTE_ETH_RSS_IP,
      			.rss_key_len = RSS_HASH_KEY_LENGTH,
      		},
      	},
      	.txmode = {
      		.mq_mode = RTE_ETH_MQ_TX_NONE,
      	},
      };
    • 新增配置哈希间接表函数和配置哈希密钥函数。

      点击查看新增代码

      /**
       * @brief Configure the RSS RETA (Redirection Table Array) for a given port.
       * 
       * @param port_id The ID of the port to configure.
       */
      
      void configure_rss_reta(uint16_t port_id) {
          struct rte_eth_rss_reta_entry64 reta_conf[RSS_RETA_SIZE / RTE_ETH_RETA_GROUP_SIZE];
          unsigned int i;
          uint16_t reta_size;
      	uint16_t nb_queues;
      	struct rte_eth_dev_info dev_info;
      
      	if (port_id >= RTE_MAX_ETHPORTS) {
              printf("port_id %d exceed max eth ports\n", port_id);
              return;
          }
      
          if (rte_eth_dev_info_get(port_id, &dev_info) != 0) {
      		printf("Failed to get device info for port %d\n", port_id);
      		return;
      	}
      
          reta_size = dev_info.reta_size;
          if (reta_size == 0) {
              printf("Device does not support RSS RETA configuration.\n");
              return;
          }
      
      	nb_queues = dev_info.nb_rx_queues;
      	if (nb_queues == 0) {
      		printf("port %d RX queues = 0\n", port_id);
      		return;
      	}
      
          // Initialize RETA table
          memset(reta_conf, 0, sizeof(reta_conf));
          for (i = 0; i < reta_size; i++) {
              reta_conf[i / RTE_ETH_RETA_GROUP_SIZE].reta[i % RTE_ETH_RETA_GROUP_SIZE] = (uint16_t)(i % nb_queues);
          }
      
          // Configure RETA table mask
          for (i = 0; i < reta_size; i += RTE_ETH_RETA_GROUP_SIZE) {
              reta_conf[i / RTE_ETH_RETA_GROUP_SIZE].mask = UINT64_MAX;
          }
      
      	// Update RSS RETA table to device
      	if (rte_eth_dev_rss_reta_update(port_id, reta_conf, reta_size) != 0) {
      		printf("Failed to update RSS RETA table for port %u\n", port_id);
      		return;
      	}
      }
      
      /**
       * @brief Configure the RSS hash key for a given port.
       * 
       * @param port_id The ID of the port to configure.
       */
      void configure_rss_hash_key(uint16_t port_id) {
          struct rte_eth_rss_conf rss_conf;
      
      	if (port_id >= RTE_MAX_ETHPORTS) {
              printf("port_id %d exceed max eth ports\n", port_id);
              return;
          }
      
          memset(&rss_conf, 0, sizeof(rss_conf));
          rss_conf.rss_key = hash_key;
          rss_conf.rss_key_len = sizeof(hash_key);
          
          // Update RSS hash key to device
          if (rte_eth_dev_rss_hash_update(port_id, &rss_conf) != 0) {
              printf("Failed to update RSS hash key for port %u\n", port_id);
      		return;
          }
      }
    • rte_eth_dev_start之后调用新增的配置函数。

      点击查看main中调用部分代码

      @@ -1472,12 +1576,15 @@ main(int argc, char **argv)
                      /* Start device */
      		ret = rte_eth_dev_start(portid);
      		if (ret < 0)
      			rte_exit(EXIT_FAILURE,
      				"rte_eth_dev_start: err=%d, port=%d\n",
      				ret, portid);
      				
      		configure_rss_reta(portid);
      		configure_rss_hash_key(portid);
  2. 修改源码后,重新编译L3FWD。

    cd ~/dpdk-stable-22.11.3/
    rm -rf build
    # 初始化构建目录并配置项目选项,指定构建l3fw三层转发示例
    meson setup -Dexamples=l3fwd build
    cd build
    #编译
    ninja
    #将编译好的文件安装到系统目录
    sudo ninja install
    #更新系统的共享库缓存
    sudo ldconfig
  3. 执行以下命令,指定端口、队列与核心的绑定关系,启动L3FWD。

    cd ~/dpdk-stable-22.11.3/build/examples
    ./dpdk-l3fwd --legacy-mem -a 0000:00:05.0 --socket-mem 1024 -- -p 0x1 --config="(PORT_ID, QUEUE_ID, LCORE_ID), (PORT_ID, QUEUE_ID, LCORE_ID), ..." --parse-ptype
    • --config:每个三元组表示:

      • PORT_ID:绑定的端口号(从 0 开始)。

      • QUEUE_ID:该端口的接收队列号(从 0 开始)。

      • LCORE_ID:处理该队列的逻辑核心(lcore)号(从 0 开始)。

    • 2个核心、2个队列为例测试:

      ./dpdk-l3fwd --legacy-mem -a 0000:00:05.0 --socket-mem 1024 -- -p 0x1 --config="(0,0,0),(0,1,1)"  --parse-ptype
      • 第一个三元组 (0,0,0):端口0 的队列0 由逻辑核心0(lcore0)处理。

      • 第二个三元组 (0,1,1):端口0 的队列1 由逻辑核心1(lcore1)处理。

      image

使用RSS脚本计算哈希索引

RSS规则配置完成后,您可以通过我们提供的python脚本,通过输入四元组、hash key等信息计算接收队列。使用该脚本可计算特定四元组报文在配置了指定hash key时 ,ECS后端计算得到的间接表的索引值,可用于指导配置间接表在指定队列上接收特定报文流。

点击查看ali_ecs_rss_calc.py脚本内容

#!/usr/bin/python

import sys
import argparse
import re
import ipaddress

prog_name = sys.argv[0]
USAGE_EXAMPLE = """
Usage example:
    Calculate the Toeplitz hash of a packet sent from 1.2.3.4 to 1.2.3.5 with source/destination
    port of 7000:

    - Argument hash key is required since virtio-net driver creates a random hash key for all your NICs:

    $ {prog_name} -t 1.2.3.4 -T 7000 -r 1.2.3.5 -R 7000 -k 77:d1:c9:34:a4:c9:bd:87:6e:35:dd:17:b2:e3:23:9e:39:6d:8a:93:2a:95:b4:72:3a:b3:7f:56:8e:de:b6:01:97:af:3b:2f:3a:70:e7:04
    
    - If you want to calculate the RSS value of a packet whose protocol is not TCP or UDP, don't specify
      the source port and destination port, the script will calculate the hash value based on only the IP addresses:
      
    $ {prog_name} -t 1.2.3.4 -r 1.2.3.5 -k 77:d1:c9:34:a4:c9:bd:87:6e:35:dd:17:b2:e3:23:9e:39:6d:8a:93:2a:95:b4:72:3a:b3:7f:56:8e:de:b6:01:97:af:3b:2f:3a:70:e7:04
    
    - Use "--ipv6" to calculate RSS value for IPv6 packets:
    
    $ {prog_name} -t 2001:250:250:250:250:250:250:1 -T 7000 -r 2001:250:250:250:250:250:250:2 -R 7000 -k 77:d1:c9:34:a4:c9:bd:87:6e:35:dd:17:b2:e3:23:9e:39:6d:8a:93:2a:95:b4:72:3a:b3:7f:56:8e:de:b6:01:97:af:3b:2f:3a:70:e7:04 --ipv6

    Please note that Linux kernel 5.9 or newer is required for hash function/key configuration support
    
    Also the Linux kernel older than ANCK 5.10-018 had a bug in which the default hash key showed by ethtool is not 
    equal to the one used by the device before changing any RSS configuration mannually. The script will print the
    right hash value in this case if you don't specify the hash key while calculating.
""".format(prog_name = prog_name)

# The default key on instances for old alinux kernel(< ANCK 5.10-018) should be as below, not the one you get from "ethtool -x <nic_name>".
RSS_DEFAULT_KEY = [
	0x6D, 0x5A, 0x56, 0xDA, 0x25, 0x5B, 0x0E, 0xC2,
	0x41, 0x67, 0x25, 0x3D, 0x43, 0xA3, 0x8F, 0xB0,
	0xD0, 0xCA, 0x2B, 0xCB, 0xAE, 0x7B, 0x30, 0xB4,
	0x77, 0xCB, 0x2D, 0xA3, 0x80, 0x30, 0xF2, 0x0C,
	0x6A, 0x42, 0xB7, 0x3B, 0xBE, 0xAC, 0x01, 0xFA,
]
# The default key on instances for new alinux kernel(>= ANCK 5.10-018) is randomly generated.

TOEPLITZ_KEY_SIZE = 128
BITS_IN_BYTE = 8

def circular_shift_key_one_left(key):
    """The function does a cyclic shift left of the whole key.
    To be able to shift the whole 40 bytes left in a cyclic manner, the function
    shifts the bits between two adjacent bytes each time"""

    l = len(key)
    return [ ((key[i] << 1) & 0xff) | ((key[(i + 1) % l] & 0x80) >> 7) for i in range(0, l) ]

def or_32msb_bits_of_key(key):
    return (key[0] << 24) | (key[1] << 16) | (key[2] << 8) | key[3]

def calculate_hash(rx_ip, rx_port, tx_ip, tx_port, initial_value, key):
    """Calculate the Toeplitz hash based on the given parameters. Note that this
    implementation is relevant for ENA, and doesn't claim to be compatible with
    the standard implementation"""

    hash_result = initial_value
    input_bytes = list()
    input_bytes += tx_ip + rx_ip + tx_port + rx_port

    for input_byte in input_bytes:
        for i in range(BITS_IN_BYTE):
            # is the (8 - i -1) bit set
            if (input_byte & (1 << (BITS_IN_BYTE - i - 1))):
                hash_result ^= or_32msb_bits_of_key(key)

            key = circular_shift_key_one_left(key)

    return hash_result

def ipv4_addr_type(str):
    """type function to argparse which transforms an
    ipv4 string into its hexadecimal number"""
    if not re.match(r"^([0-9]{1,3}\.){3}[0-9]{1,3}$", str):
        raise argparse.ArgumentTypeError("IP address needs to have the format 1.2.3.4")

    return [int(octet) for octet in str.split('.')]

def ipv6_addr_type(str):
    """type function to argparse which transforms an
    ipv6 string into its hexadecimal number"""
    
    try:
        ip_str = ipaddress.IPv6Address(unicode(str))
    except ValueError as e:
        raise argparse.ArgumentTypeError("Invalid IPv6 address format, %s" % e)
    
    parts = ip_str.exploded.split(':')
    try:
        bytes_list = [int(part, 16) for part in parts]
        bytes_list = [(byte >> 8, byte & 0xff) for byte in bytes_list]
    except ValueError as e:
        raise argparse.ArgumentTypeError("Invalid IPv6 address format, %s" % e)
    
    return [item for sublist in bytes_list for item in sublist]

def toeplitz_key_type(str):
    """type function to argparse which transforms a
    Toeplits key string into an array of hexadecimal values"""
    if not re.match(r"^([0-9a-zA-Z]{1,2}:){39}[0-9a-zA-Z]{1,2}$", str):
        raise argparse.ArgumentTypeError("Toeplitz key hash format is invalid (should be 40 hex values delimeted with columns)")

    return [int(key_elem, 16) for key_elem in str.split(':')]

def main():

    parser = argparse.ArgumentParser(description='virtio-net Toeplitz hash calculator',
                                     formatter_class=argparse.RawDescriptionHelpFormatter,
                                     epilog=USAGE_EXAMPLE)

    parser.add_argument('-r', '--rx-ip', help='Receiving side ipv4', dest='rx_ip', nargs='?',
                        required=True, type=str)
    parser.add_argument('-R', '--rx-port', help='Receiving side port', dest='rx_port', nargs='?', type=int)
    parser.add_argument('-t', '--tx-ip', help='Transmitting side ipv4', dest='tx_ip', nargs='?', 
                        required=True, type=str)
    parser.add_argument('-T', '--tx-port', help='Transmitting side port', dest='tx_port', nargs='?', type=int)
    parser.add_argument('-k', '--toeplitz-key',
                        help='The Toeplitz key (only in instances that support changing it)',
                        dest='toeplitz_key', nargs='?', required=True, type=toeplitz_key_type)
    parser.add_argument('-i', '--ipv6',  action='store_true', help='Use IPv6 address type for --rx-ip')

    args = parser.parse_args()

    if args.ipv6:
        rx_ip   = ipv6_addr_type(args.rx_ip)
        tx_ip   = ipv6_addr_type(args.tx_ip)
    else:
        rx_ip   = ipv4_addr_type(args.rx_ip)
        tx_ip   = ipv4_addr_type(args.tx_ip)
    
    if args.rx_port and args.tx_port:
        # "break" port number into two byte representation
        rx_port = [(args.rx_port & 0xff00) >> 8, args.rx_port & 0x00ff]
        tx_port = [(args.tx_port & 0xff00) >> 8, args.tx_port & 0x00ff]
    else:
        rx_port = tx_port = []
    
    key = args.toeplitz_key

    # calculate the hash with inital value of 0x0xffffffff
    hash = calculate_hash(rx_ip, rx_port, tx_ip, tx_port, 0, key)
    rss_table_entry_128 = hash % 128
    rss_table_entry_256 = hash % 256

    if args.ipv6:
        print("Sending traffic from [{}]:{} to [{}]:{}".format(args.tx_ip, args.tx_port, args.rx_ip, args.rx_port))
    else:
        print("Sending traffic from {}:{} to {}:{}".format(args.tx_ip, args.tx_port, args.rx_ip, args.rx_port))
    print("""the hash is calculated over the following fields:
    Source IP address
    Destination IP address""")
    if args.rx_port and args.tx_port:
        print("""    Source port
    Destination port""")
    print("Should result in the hash for all drivers:".ljust(50) + "{}".format(hex(hash)))
    print("RSS table entry (total length 128):".ljust(50) + "{}".format(rss_table_entry_128))
    print("RSS table entry (total length 256):".ljust(50) + "{}".format(rss_table_entry_256))
    return

if __name__ == '__main__':
    main()

点击查看脚本使用方式

您可以通过python ali_ecs_rss_calc.py -h查看脚本使用方式

image

以如下64个队列的网卡RSS配置为例说明:

image

基于以下五元组信息(您可以根据实际情况修改),通过RSS计算脚本计算RSS哈希值:

  • 目标IP地址(-r):10.0.0.1,即配置了RSS的接收端实例网卡的IP

  • IP地址(-t):10.0.0.251,即发送端网卡的IP

  • 目标端口(-R):26000

  • 源端口(-T):18042

  • 哈希密钥:RSS间接表配置获取

python ali_ecs_rss_calc.py -r 10.0.0.1 -t 10.0.0.251 -R 26000 -T 18042 -k 69:e8:7c:56:bf:03:9f:63:d7:c5:e5:96:b3:00:36:93:02:8c:d2:8f:cc:a9:00:65:fd:c8:94:71:5f:fd:c8:de:7a:30:a9:73:b3:33:0c:c6

脚本根据传入的以上参数,基于toeplitz算法进行哈希计算,如下图返回结果所示,当前仅支持长度为128的间接表,那么此次计算结果哈希索引值为117,即此次连接的数据包由RSS间接表中的117号索引对应的队列处理。

image

再次查看RSS间接表配置,可以看到117号索引对应的队列为53,即数据包由队列53处理。

image