本篇文章介绍DPDK-RSS
相关的功能,RSS
是网卡提供的分流机制,简单讲就是一个HASH
值,如果使用DPDK
收包,开启RSS
后,会根据配置项将数据包分流到不同的收包队列,用来是实现负载均衡。
通过DPDK-L3FWD
样例,添加打印参数进行详细说明,大致分为以下流程:
1、DPDK
如何开启RSS
(开启和关闭位置,以及开启关闭影响参数);
2、如何查看网卡所支持的RSS
选项;
3、DPDK
如何配置不同的RSS
选项(包括RSS
对称算法,根据IP/UDP/TCP
进行分流等)获取不同的RSS
值;
4、如何通过DPDK
携带的API
接口获取RSS
值(通过将IP
五元组信息传入到API
中获取到RSS
值);
操作系统版本:CentOS 8.4
DPDK版本:dpdk-20.11.3
1、开启RSS
1.1、RSS开关
借助DPDK-L3FWD
样例,在收包API
调用之后,打印每个struct rte_mbuf
,因为DPDK
如果启用RSS
时,struct rte_mbuf *m
结构中有一个参数会附上数值,那就是rss
,如下图所示:
通过打印m->hash.rss
即可获取对应数据包的RSS
。
DPDK
初始化网卡端口是会传入port_conf
结构体,包含的有RSS
相关内容,DPDK-L3FWD
样例中,初始化网卡端口API
如下:
查看port_conf
结构:
结构体中各个参数解释如下:
static struct rte_eth_conf port_conf = {
.rxmode = { // rx收包相关
.mq_mode = ETH_MQ_RX_RSS, //启用RSS, 注释掉该项则关闭RSS
.max_rx_pkt_len = RTE_ETHER_MAX_LEN,
.split_hdr_size = 0,
.offloads = DEV_RX_OFFLOAD_CHECKSUM,
},
.rx_adv_conf = {
.rss_conf = { // RSS配置相关
.rss_key = NULL, // 如果不为空,则配置对称算法KEY
.rss_hf = ETH_RSS_IP, //根据IP进行hash
},
},
.txmode = { // tx发包相关
.mq_mode = ETH_MQ_TX_NONE,
},
};
DPDK-L3FWD
默认是启用RSS
的。
1.2、RSS打印
添加当接收到数据包时,打印该数据包的RSS
值代码如下:
编译生成可执行文件,启动DPDK-L3FWD
程序,对端重放数据包,数据包内如如下:
通过打印双向的数据包,查看双向获取到的RSS
值是否一致:
这里可以发现,获取到的RSS
值不同。后面会介绍为什么,以及怎么配置可以使获取到的RSS
值相同
1.3、RSS关闭
DPDK-L3FWD
关闭RSS
,代码改动如下:
重现编译运行样例程序,对端重发同样的ICMP
数据包,结果如下:
可以看到,获取到的RSS
和预期的一样全为0
。至此,DPDK
中RSS
开关位置以及简单的配置已经介绍完毕,后面章节会详细一点介绍RSS
相关的参数以及如何灵活配置达到自己的需求。
2、配置RSS
2.1、RSS结构体
struct rte_eth_conf
结构体用于存放网卡RSS
相关参数,其它详细信息可参考源代码中注释,写的很清楚,这里只介绍RSS
相关:
mq_mode = ETH_MQ_RX_RSS
用来标识开启RSS
,开启RSS
后,可以根据rss_conf
结构参数进行HASH
计算,包含3个参数,信息如下:
/**
* A structure used to configure the Receive Side Scaling (RSS) feature
* of an Ethernet port.
* If not NULL, the *rss_key* pointer of the *rss_conf* structure points
* to an array holding the RSS key to use for hashing specific header
* fields of received packets. The length of this array should be indicated
* by *rss_key_len* below. Otherwise, a default random hash key is used by
* the device driver.
*
* The *rss_key_len* field of the *rss_conf* structure indicates the length
* in bytes of the array pointed by *rss_key*. To be compatible, this length
* will be checked in i40e only. Others assume 40 bytes to be used as before.
*
* The *rss_hf* field of the *rss_conf* structure indicates the different
* types of IPv4/IPv6 packets to which the RSS hashing must be applied.
* Supplying an *rss_hf* equal to zero disables the RSS feature.
*/
struct rte_eth_rss_conf {
uint8_t *rss_key; /**< If not NULL, 40-byte hash key. */
uint8_t rss_key_len; /**< hash key length in bytes. */
uint64_t rss_hf; /**< Hash functions to apply - see below. */
};
字段 | 描述 |
---|---|
rss_key | 哈希key ,如果为空,则使用网卡rss_key ,如果不为空,则为40字节的数组用来作为has key |
rss_key_len | 哈希key 长度,rss_key 数组字节数,为空则填写0,不为空则填写40 |
rss_hf | 哈希函数,用来标识根据IP 进行哈希,还是根据IP+PORT 进行哈希,还是根据IP+PORT+PROTOCOL 进行哈希 |
2.2、RSS参数解释
常见的rss
配置如下:
static struct rte_eth_conf port_conf = {
.rxmode = {
.mq_mode = ETH_MQ_RX_RSS, // 启用RSS
/* ... */
},
.rx_adv_conf = {
.rss_conf = {
.rss_key = NULL, // 使用网卡默认rss_key, 一般都不能做到对称
.rss_key_len = 0, // rss_key数组长度
.rss_hf = ETH_RSS_IP, // 哈希函数,根据三层IP进行哈希
| ETH_RSS_UDP, // 根据四层UDP协议进行哈希
| ETH_RSS_TCP, // 根据四层TCP协议进行哈希
},
},
/* ... */
};
其中rss_hf
配置常用的三项参数,具体如下:
#define ETH_RSS_IP ( \
ETH_RSS_IPV4 | \
ETH_RSS_FRAG_IPV4 | \
ETH_RSS_NONFRAG_IPV4_OTHER | \
ETH_RSS_IPV6 | \
ETH_RSS_FRAG_IPV6 | \
ETH_RSS_NONFRAG_IPV6_OTHER | \
ETH_RSS_IPV6_EX)
#define ETH_RSS_UDP ( \
ETH_RSS_NONFRAG_IPV4_UDP | \
ETH_RSS_NONFRAG_IPV6_UDP | \
ETH_RSS_IPV6_UDP_EX)
#define ETH_RSS_TCP ( \
ETH_RSS_NONFRAG_IPV4_TCP | \
ETH_RSS_NONFRAG_IPV6_TCP | \
ETH_RSS_IPV6_TCP_EX)
解释如下:按照上面配置,实现机制为,如果数据包四层传输层不是UDP
或者TCP
则按照三层网络层IP
地址进行hash
,对于源地址和目的地址相同的数据包获取到的RSS
值相同,如果四层传输层是UDP
或者TCP
,怎按照IP+PROTOCOL
进行哈希。不仅需要源地址和目的地址相同,还需要四层传输层相同。
2.3、RSS配置
可以看出,将RSS
中rss_key
字段设置为NULL
,会出现上下行数据包获取到的rss
值不一样的问题,如何解决这个问题,就需要将rss_key
这个字段设置一下,其中Intel 82599ES
网卡使用ixgbe
驱动,可以添加如下配置代码完成:
static uint8_t rss_intel_key[40] = {
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 = ETH_MQ_RX_RSS,
.max_rx_pkt_len = RTE_ETHER_MAX_LEN,
.split_hdr_size = 0,
.offloads = DEV_RX_OFFLOAD_CHECKSUM,
},
.rx_adv_conf = {
.rss_conf = {
.rss_key = rss_intel_key,
.rss_key_len = 40,
.rss_hf = ETH_RSS_IP | ETH_RSS_UDP | ETH_RSS_TCP,
},
},
.txmode = {
.mq_mode = ETH_MQ_TX_NONE,
},
};
重新编译代码,运行可执行程序,对端打包之后得到如下结果:
可以看到,这时得到的上下行数据包的rss
值相同了。
3、生成RSS
由于rss
这个值在网卡硬件接收阶段已经生成了,一次如果我们想要计算这个值,可以通过调用DPDK
代码中API
接口来实现,这样就可以通过软件方法得到和硬件一致的rss
值了。
具体实现代码如下:
/* SPDX-License-Identifier: BSD-3-Clause
* Copyright(c) 2010-2016 Intel Corporation
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <inttypes.h>
#include <sys/types.h>
#include <sys/queue.h>
#include <netinet/in.h>
#include <setjmp.h>
#include <stdarg.h>
#include <ctype.h>
#include <errno.h>
#include <getopt.h>
#include <signal.h>
#include <arpa/inet.h>
#include <stdbool.h>
#include <rte_common.h>
#include <rte_log.h>
#include <rte_malloc.h>
#include <rte_memory.h>
#include <rte_memcpy.h>
#include <rte_eal.h>
#include <rte_launch.h>
#include <rte_atomic.h>
#include <rte_cycles.h>
#include <rte_prefetch.h>
#include <rte_lcore.h>
#include <rte_per_lcore.h>
#include <rte_branch_prediction.h>
#include <rte_interrupts.h>
#include <rte_random.h>
#include <rte_debug.h>
#include <rte_ether.h>
#include <rte_ethdev.h>
#include <rte_mempool.h>
#include <rte_mbuf.h>
#include <rte_string_fns.h>
#include <rte_ip.h>
#include <rte_thash.h>
#include "rss.h"
static volatile bool force_quit;
static void
signal_handler(int signum)
{
if (signum == SIGINT || signum == SIGTERM) {
printf("\n\nSignal %d received, preparing to exit...\n",
signum);
force_quit = true;
}
}
static void get_tuple(void **tuple, char strsrcIpv4[], char strdstIpv4[], char strsrcIPV6[], char strdstIPV6[],
int *input_len)
{
//uint32_t input_len;
//void *tuple;
int ip_type = 0;
struct rte_ipv4_tuple ipv4_tuple;
struct rte_ipv6_tuple ipv6_tuple;
struct rte_ipv4_hdr ipv4_hdr;
struct rte_ipv6_hdr ipv6_hdr;
memset(&ipv4_hdr, 0, sizeof(struct rte_ipv4_hdr));
memset(&ipv6_hdr, 0, sizeof(struct rte_ipv6_hdr));
memset(&ipv4_tuple, 0, sizeof(struct rte_ipv4_tuple));
memset(&ipv6_tuple, 0, sizeof(struct rte_ipv6_tuple));
inet_pton(AF_INET, strsrcIpv4, &ipv4_hdr.src_addr);
inet_pton(AF_INET, strdstIpv4, &ipv4_hdr.dst_addr);
inet_pton(AF_INET6, strsrcIPV6, &ipv6_hdr.src_addr);
inet_pton(AF_INET6, strdstIPV6, &ipv6_hdr.dst_addr);
//printf("src: %u, dst: %u\n", ipv4_hdr.src_addr, ipv4_hdr.dst_addr);
ip_type = 4;
if(ip_type == 4) {
ipv4_tuple.src_addr = rte_be_to_cpu_32(ipv4_hdr.src_addr);
ipv4_tuple.dst_addr = rte_be_to_cpu_32(ipv4_hdr.dst_addr);
//*tuple = &ipv4_tuple;
memcpy(*tuple, &ipv4_tuple, sizeof(ipv4_tuple));
*input_len = RTE_THASH_V4_L3_LEN;
} else if(ip_type == 6) {
rte_thash_load_v6_addrs(&ipv6_hdr, (union rte_thash_tuple *)&ipv6_tuple);
//*tuple = &ipv6_tuple;
memcpy(*tuple, &ipv6_tuple, sizeof(ipv6_tuple));
*input_len = RTE_THASH_V6_L3_LEN;
} else {
printf("IP type Error!\n");
}
//printf("input_len = %u\n", *input_len);
return;
}
int
main(int argc, char **argv)
{
(void)argc;
(void)argv;
uint32_t hash1, hash2;
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
signal(SIGPIPE, signal_handler);
char strsrcIpv4[16] = {"10.20.90.237"};
char strdstIpv4[16] = {"192.168.10.70"};
char strsrcIPV6[64] = {"2409:891e:80:230b::86"};
char strdstIPV6[64] = {"2409:891e:80:230b:ecd7:fca4:2de9:a288"};
// 1.rss_key初始化
rss_init();
// 2.从参数中获取五元组信息(ip类型和源/目的IP)
void *tuple1;
void *tuple2;
int input_len1, input_len2;
get_tuple(&tuple1, strsrcIpv4, strdstIpv4, strsrcIPV6, strdstIPV6, &input_len1);
get_tuple(&tuple2, strdstIpv4, strsrcIpv4, strdstIPV6, strsrcIPV6, &input_len2);
// 3.获取RSS值
hash1 = rss_hash_data((uint32_t *)tuple1, input_len1);
hash2 = rss_hash_data((uint32_t *)tuple2, input_len2);
printf("hash1 = %u\n", hash1);
printf("hash2 = %u\n", hash2);
return 0;
}
编译完成之后,直接执行,结果如下:
可以看到,软件计算出来的rss
数值和硬件获取到的一样。
3.1、代码逻辑
简单介绍一下软件生成rss
代码逻辑:
主要涉及到三个步骤:
1、rss_key
初始化
这一步需要和DPDK
中设置的rss_key
保持一致,初始化接口如下:
2、从参数中获取五元组信息(ip
类型和源/目的IP
)
这一步主要为了得到五元组相关的信息,赋值完成之后调用rss_hash_data
获取HASH
值,即rss
值,这里需关注几个参数:
参数1:IP
地址,包括源/目的IP
;
参数2:IP
类型,IPV4
或者IPV6
;
参数3:PORT
值,包括源/目的PORT
;(该测试样例中没有给PORT
赋值)
参数4:input_len
,这个值尤其重要,因为它决定了通过哪一层进行哈希,功能和rss_hf
类似,具体赋值解释如下:
/**
* length in dwords of input tuple to
* calculate hash of ipv4 header only
*/
#define RTE_THASH_V4_L3_LEN ((sizeof(struct rte_ipv4_tuple) - \
sizeof(((struct rte_ipv4_tuple *)0)->sctp_tag)) / 4)
/**
* length in dwords of input tuple to
* calculate hash of ipv4 header +
* transport header
*/
#define RTE_THASH_V4_L4_LEN ((sizeof(struct rte_ipv4_tuple)) / 4)
/**
* length in dwords of input tuple to
* calculate hash of ipv6 header only
*/
#define RTE_THASH_V6_L3_LEN ((sizeof(struct rte_ipv6_tuple) - \
sizeof(((struct rte_ipv6_tuple *)0)->sctp_tag)) / 4)
/**
* length in dwords of input tuple to
* calculate hash of ipv6 header +
* transport header
*/
#define RTE_THASH_V6_L4_LEN ((sizeof(struct rte_ipv6_tuple)) / 4)
3、获取RSS
值
将第2步获取到的参数,传入到rss_hash_data
接口,即可获取到对应的rss
值
4、总结
DPDK-L3FWD
代码可参考文章,【DPDK】dpdk样例源码解析之三:dpdk-l3fwd_001
DPDK-RSS
源代码下载链接
有问题欢迎在评论区探讨~