Intel HDSLB 高性能四层负载均衡器 — 代码剖析和高级特性
2024-06-16 16:09 云物互联 阅读(32) 评论(0) 编辑 收藏 举报目录
@
前言
在前 2 篇文章中,我们从快速入门、应用场景、基本原理、部署配置这 4 个方面,整体地介绍了 Intel HDSLB 作为新一代高性能四层负载均衡器的研发背景、解决方案以及性能优势,并通过 step by step 的方式,希望帮助更多的读者能够便捷地在自己的开发机运行和使用起来。在本篇中,我们将继续向前,对 HDSLB-DPVS 开源版本的代码进行剖析,并介绍其中一些有趣的高级特性。
代码剖析
下载代码:
git clone https://github.com/intel/high-density-scalable-load-balancer.git
软件架构
上图是 HDSLB 的软件架构图,自上而下的可以分为下述 5 个层面。
-
Control Plane 控制面层:依旧沿用了 LVS 的控制面,实现了下列 3 个 CLI 工具,使用了 Local unix socket 通信机制:
- ipvsadm:用于管理 ipvs 的配置。
- dpip:用于设置 VIP、RIP 和相关的 Route 规则。
- keepalive:用于 HA 和 RS 健康检查。
-
Load Balancer 负载均衡层:实现了 scheduler 流量调度、proto 四层协议、conn 连接跟踪、FastPath 等功能模块。尤其是 FastPath 快慢路径分离是 HDSLB 对 DPVS 的核心优化之一。
-
Lite IP-Stack 层:实现了 ARP / IPv4v6 / ICMP 等 L2-3 层网络协议、以及 inetaddr 和 L3 routing 等功能模块。
-
Net Devices 层:实现了物理网卡纳管、bonding、VLAN、KNI、TC 流量控制,hw-addr-list 地址列表等功能模块。
-
Hardware Acceleration 层:提供了 Intel CPU 和 NIC 硬件级别的加速技术,包括:FDIR mark、FDIR to queue、RSS、Checksum offload、AVX512、DLB、SR-IOV 等等。
目录结构
$ high-density-scalable-load-balancer git:(main) tree -L 2
.
├── Makefile
├── conf # 配置示例目录
│ ├── hdslb.bond.conf.sample
│ ├── hdslb.conf.items
│ ├── hdslb.conf.sample
│ ├── hdslb.conf.single-bond.sample
│ └── hdslb.conf.single-nic.sample
├── include # 头文件库目录
│ ├── cfgfile.h
│ ├── common.h
│ ├── conf
│ ├── ctrl.h
│ ├── dpdk.h
│ ├── flow.h
│ ├── global_conf.h
│ ├── icmp.h
│ ├── icmp6.h
│ ├── inet.h
│ ├── inetaddr.h
│ ├── ip_tunnel.h
│ ├── ipset.h
│ ├── ipv4.h
│ ├── ipv4_frag.h
│ ├── ipv6.h
│ ├── ipvs
│ ├── kni.h
│ ├── laddr_multiply.h
│ ├── lb
│ ├── linux_ipv6.h
│ ├── list.h
│ ├── log.h
│ ├── match.h
│ ├── mbuf.h
│ ├── md5.h
│ ├── mempool.h
│ ├── ndisc.h
│ ├── neigh.h
│ ├── netif.h
│ ├── netif_addr.h
│ ├── parser
│ ├── pidfile.h
│ ├── route.h
│ ├── route6.h
│ ├── route6_hlist.h
│ ├── route6_lpm.h
│ ├── sa_pool.h
│ ├── sys_time.h
│ ├── tc
│ ├── timer.h
│ ├── uoa.h
│ └── vlan.h
├── patch # DPDK 补丁目录
│ ├── dpdk-16.07
│ ├── dpdk-20.08
│ ├── dpdk-stable-17.05.2
│ ├── dpdk-stable-17.11.2
│ └── dpdk-stable-19.11
├── scripts # 运维脚本目录
│ ├── ipvs-tunnel.rs.deploy.sh
│ ├── setup.fnat.two-arm.sample.sh
│ ├── setup.snat-gre.sample.sh
│ ├── setup.snat.sample.sh
│ └── setup.tc.sample.sh
├── src # 核心源码目录
│ ├── Makefile
│ ├── VERSION
│ ├── cfgfile.c
│ ├── common.c
│ ├── config.mk
│ ├── ctrl.c
│ ├── dpdk.mk
│ ├── global_conf.c
│ ├── icmp.c
│ ├── inet.c
│ ├── inetaddr.c
│ ├── ip_gre.c
│ ├── ip_tunnel.c
│ ├── ipip.c
│ ├── ipset.c
│ ├── ipv4.c
│ ├── ipv4_frag.c
│ ├── ipv6
│ ├── ipvs # ipvs 业务逻辑实现目录
│ ├── kni.c
│ ├── laddr_multiply.c
│ ├── lb # lb 转发逻辑实现目录
│ ├── log.c
│ ├── main.c
│ ├── mbuf.c
│ ├── mempool.c
│ ├── neigh.c
│ ├── netif.c
│ ├── netif_addr.c
│ ├── parser.c
│ ├── pidfile.c
│ ├── route.c
│ ├── sa_pool.c
│ ├── sys_time.c
│ ├── tc
│ ├── timer.c
│ └── vlan.c
└── tools # 工具库目录
├── Makefile
├── dpip
├── ipvsadm
├── keepalived
└── lbdebug
配置解析
- global_defs 全局配置模块:指定日志级别,日志路径等。
! global config
global_defs {
log_level DEBUG # 方便调试
! log_file /var/log/hdslb.log
! log_async_mode on
}
- netif_defs 网卡设备配置模块:
- pktpool 指定 DPDK memory/cache pool 的大小。
- device 指定 DPDK 网卡设备。
- tx/rx 指定 DPDK 网卡设备的硬件队列数。
- bonding 指定 DPDK 网卡绑定。
- kni 指定 DPDK 网卡设备对应的 kni 虚拟网络接口设备。DPDK 程序会将物理网卡设备纳管,流量 bypass 内核,如果其它程序,比如 ssh 想使用网络接口,则需要通过 kni 模块来提供虚拟网络接口,DPDK 程序会将不感兴趣的流量送到内核。
- RSS(Receive Side Scaling):指定网卡设备的接受多队列和多核处理器的映射关系,充分利用网卡多队列和多核处理器的技术优势,提高网络吞吐量和数据包处理效率。
- FDIR(Flow Director):指定网卡设备的流量识别和分类模式,提高对老鼠流量、大象流量等特定流量的处理效率。
! netif config
netif_defs {
<init> pktpool_size 1048575
<init> pktpool_cache 256
<init> device dpdk0 {
rx {
queue_number 3
descriptor_number 1024
! rss all
}
tx {
queue_number 3
descriptor_number 1024
}
fdir {
mode perfect
pballoc 64k
status matched
}
! promisc_mode
kni_name dpdk0.kni
}
! <init> bonding bond0 {
! mode 0
! slave dpdk0
! slave dpdk1
! primary dpdk0
! kni_name bond0.kni
!}
}
- worker_defs 工作核心配置:DPDK 将 CPU 抽象为 lcore,有 master、slave 两种类型。通常的,master 做 Control Plane 处理,而 slave 作为 Data Plane 处理。每个 lcore 可以负责多个网卡设备的多个队列。另外,DPDK 将网卡设备抽象为 Port,rx_queue_ids 和 tx_queue_ids 分别是接收和发送的队列编号。其中 isol_rx_cpu_ids 表示当前 lcore 专职负责接收数据,isol_rxq_ring_sz 专职接收数据的 ring buffer 大小。
! worker config (lcores)
worker_defs {
# control plane CPU
<init> worker cpu0 {
type master
cpu_id 0
}
# data plane CPU
# dpdk0、1 这 2 个 Port 的同一个收发队列共用同一个 CPU
<init> worker cpu1 {
type slave
cpu_id 1
port dpdk0 {
rx_queue_ids 0
tx_queue_ids 0
! isol_rx_cpu_ids 9
! isol_rxq_ring_sz 1048576
}
port dpdk1 {
rx_queue_ids 0
tx_queue_ids 0
! isol_rx_cpu_ids 9
! isol_rxq_ring_sz 1048576
}
}
}
- timer_defs 定时器配置:
! timer config
timer_defs {
# cpu job loops to schedule dpdk timer management
schedule_interval 500
}
- neight_defs 邻居子系统配置:Lite IP-Stack 包括 L3 Route 子系统和 L2 Neighbor 子系统。
! hdslb neighbor config
neigh_defs {
<init> unres_queue_length 128
<init> timeout 60
}
- ipv4/v6_defs 三层网络配置:
! hdslb ipv4 config
ipv4_defs {
forwarding off
<init> default_ttl 64
fragment {
<init> bucket_number 4096
<init> bucket_entries 16
<init> max_entries 4096
<init> ttl 1
}
}
! hdslb ipv6 config
ipv6_defs {
disable off
forwarding off
route6 {
<init> method hlist
recycle_time 10
}
}
- ctrl_defs 控制面配置:使用 Local unix socket 通信方式。
! control plane config
ctrl_defs {
lcore_msg {
<init> ring_size 4096
sync_msg_timeout_us 30000000
priority_level low
}
ipc_msg {
<init> unix_domain /var/run/hdslb_ctrl
}
}
- ipvs_defs 核心配置:
- conn 指定用于维护网络 conntrack 连接跟踪表资源的相关配置。
- udp/tcp 协议处理配置。
- synproxy 是与 TCP SYN flood 相关的配置。
! ipvs config
ipvs_defs {
conn {
<init> conn_pool_size 2097152
<init> conn_pool_cache 256
conn_init_timeout 30
! expire_quiescent_template
! fast_xmit_close
! <init> redirect off
}
udp {
! defence_udp_drop
uoa_mode opp
uoa_max_trail 3
timeout {
normal 300
last 3
}
}
tcp {
! defence_tcp_drop
timeout {
none 2
established 90
syn_sent 3
syn_recv 30
fin_wait 7
time_wait 7
close 3
close_wait 7
last_ack 7
listen 120
synack 30
last 2
}
synproxy {
synack_options {
mss 1452
ttl 63
sack
! wscale
! timestamp
}
! defer_rs_syn
rs_syn_max_retry 3
ack_storm_thresh 10
max_ack_saved 3
conn_reuse_state {
close
time_wait
! fin_wait
! close_wait
! last_ack
}
}
}
}
- FDIR sa_pool 配置:
! sa_pool config
sa_pool {
pool_hash_size 16
}
启动流程分析
- 程序入口:
high-density-scalable-load-balancer/src/main.c main(int argc, char *argv[])
- NUMA 节点数量检查:
if (get_numa_nodes() > DPVS_MAX_SOCKET) {
fprintf(stderr, "DPVS_MAX_SOCKET is smaller than system numa nodes!\n");
return -1;
}
- CPU 亲和性设定:NUMA 亲和和 CPU 绑定是 DPDK 程序的一大特性。
if (set_all_thread_affinity() != 0) {
fprintf(stderr, "set_all_thread_affinity failed\n");
exit(EXIT_FAILURE);
}
- 初始化 RTE 运行时环境:完成 DPDK 运行时环境的基础配置,详情请浏览《DPDK — EAL 环境抽象层》
err = rte_eal_init(argc, argv);
if (err < 0)
rte_exit(EXIT_FAILURE, "Invalid EAL parameters\n");
argc -= err, argv += err;
- 进入 HDSLB 核心业务流程:
RTE_LOG(INFO, DPVS, "HDSLB version: %s, build on %s\n", HDSLB_VERSION, HDSLB_BUILD_DATE);
- 初始化配置解析器:加载并解析 hdslb.conf 配置文件。
if ((err = cfgfile_init()) != EDPVS_OK)
rte_exit(EXIT_FAILURE, "Fail init configuration file: %s\n",
dpvs_strerror(err));
- bond 虚拟接口配置:如果配置文件中没有 bond 则不做处理。
if ((err = netif_virtual_devices_add()) != EDPVS_OK)
rte_exit(EXIT_FAILURE, "Fail add virtual devices:%s\n",
dpvs_strerror(err));
- 初始化 lcore 定时器:每个 lcore 都有自己的定时器,底层通过调用 timer_lcore_init 完成初始化,用于实现 conn 老化等业务逻辑。
if ((err = dpvs_timer_init()) != EDPVS_OK)
rte_exit(EXIT_FAILURE, "Fail init timer on %s\n", dpvs_strerror(err));
- 初始化 traffic control 流控模块:
if ((err = tc_init()) != EDPVS_OK)
rte_exit(EXIT_FAILURE, "Fail to init traffic control: %s\n",
dpvs_strerror(err));
- 初始化 DPDK 网卡设备:Data Plane 的核心 jobs 处理函数在这里被注册,netif_init->netif_lcore_init 函数中会注册 3 个 NETIF_LCORE_JOB_LOOP。
if ((err = netif_init(NULL)) != EDPVS_OK)
rte_exit(EXIT_FAILURE, "Fail to init netif: %s\n", dpvs_strerror(err));
/* Default lcore conf and port conf are used and may be changed here
* with "netif_port_conf_update" and "netif_lcore_conf_set" */
- 初始化 ctrl 和 tc_ctrl 控制面接口:ctrl_init->msg_init 会注册 1 个 NETIF_LCORE_JOB_LOOP。
if ((err = ctrl_init()) != EDPVS_OK)
rte_exit(EXIT_FAILURE, "Fail to init ctrl plane: %s\n",
dpvs_strerror(err));
if ((err = tc_ctrl_init()) != EDPVS_OK)
rte_exit(EXIT_FAILURE, "Fail to init tc control plane: %s\n",
dpvs_strerror(err));
- 初始化 L2 VLAN 网络:
if ((err = vlan_init()) != EDPVS_OK)
rte_exit(EXIT_FAILURE, "Fail to init vlan: %s\n", dpvs_strerror(err));
- 初始化 TCPv4 网络:inet_init 注册了一系列的 NETIF_LCORE_JOB_XXX,L4 LB 的核心。
if ((err = inet_init()) != EDPVS_OK)
rte_exit(EXIT_FAILURE, "Fail to init inet: %s\n", dpvs_strerror(err));
- 初始化 FDIR 的 sa_pool:
if ((err = sa_pool_init()) != EDPVS_OK)
rte_exit(EXIT_FAILURE, "Fail to init sa_pool: %s\n", dpvs_strerror(err));
- 初始化 Tunnel:如果配置文件中没有启动 IP tunnel 模式则不做处理。
if ((err = ip_tunnel_init()) != EDPVS_OK)
rte_exit(EXIT_FAILURE, "Fail to init tunnel: %s\n", dpvs_strerror(err));
- 初始化原始 lvs 的功能:包括注册处理 IPv4 包的钩子函数 dp_vs_in 和 dp_vs_pre_routing。
if ((err = dp_vs_init()) != EDPVS_OK)
rte_exit(EXIT_FAILURE, "Fail to init ipvs: %s\n", dpvs_strerror(err));
- 初始化 netif_ctrl 控制面接口:
if ((err = netif_ctrl_init()) != EDPVS_OK)
rte_exit(EXIT_FAILURE, "Fail to init netif_ctrl: %s\n",
dpvs_strerror(err));
- 启动 DPDK 网络设备:包括 Port、rx/tx queues、cpu binding 等等。
/* config and start all available dpdk ports */
nports = rte_eth_dev_count_avail();
for (pid = 0; pid < nports; pid++) {
dev = netif_port_get(pid);
if (!dev) {
RTE_LOG(WARNING, DPVS, "port %d not found\n", pid);
continue;
}
err = netif_port_start(dev);
if (err != EDPVS_OK)
rte_exit(EXIT_FAILURE, "Start %s failed, skipping ...\n",
dev->name);
}
- 启动数据面处理:rte_eal_mp_remote_launch 在每个 slave lcore 调用 netif_loop 进入工作线程的永循环。
// src/main.c
/* start data plane threads */
netif_lcore_start();
// src/netif.c
int netif_lcore_start(void)
{
rte_eal_mp_remote_launch(netif_loop, NULL, SKIP_MASTER);
return EDPVS_OK;
}
// src/netif.c
static int netif_loop(void *dummy)
- 启动控制面处理:进入 master 主线程的永循环。
/* start control plane thread */
while (1) {
/* reload configuations if reload flag is set */
try_reload();
/* IPC loop */
sockopt_ctl(NULL);
/* msg loop */
msg_master_process(0);
/* timer */
now_cycles = rte_get_timer_cycles();
if ((now_cycles - prev_cycles) * 1000000 / cycles_per_sec > timer_sched_interval_us) {
rte_timer_manage();
prev_cycles = now_cycles;
}
/* kni */
kni_process_on_master();
/* process mac ring on master */
neigh_process_ring(NULL, 0);
/* increase loop counts */
netif_update_master_loop_cnt();
}
数据面 jobs 注册
前文提到,在 main 的 init 初始化流程中,会注册一系列的数据面的 jobs,并存储在全局变量 netif_lcore_jobs 中。这些 jobs 的本质是一个函数引用,在处理报文时会在不同的环节被调用。
- main->netif_init->netif_lcore_init 注册了 3 个 NETIF_LCORE_JOB_LOOP:
- lcore_job_recv_fwd
- lcore_job_xmit
- lcore_job_timer_manage
static void netif_lcore_init(void)
{
......
/* register lcore jobs*/
snprintf(netif_jobs[0].name, sizeof(netif_jobs[0].name) - 1, "%s", "recv_fwd");
netif_jobs[0].func = lcore_job_recv_fwd;
netif_jobs[0].data = NULL;
netif_jobs[0].type = NETIF_LCORE_JOB_LOOP;
snprintf(netif_jobs[1].name, sizeof(netif_jobs[1].name) - 1, "%s", "xmit");
netif_jobs[1].func = lcore_job_xmit;
netif_jobs[1].data = NULL;
netif_jobs[1].type = NETIF_LCORE_JOB_LOOP;
snprintf(netif_jobs[2].name, sizeof(netif_jobs[2].name) - 1, "%s", "timer_manage");
netif_jobs[2].func = lcore_job_timer_manage;
netif_jobs[2].data = NULL;
netif_jobs[2].type = NETIF_LCORE_JOB_LOOP;
}
- main->ctrl_init->msg_init 注册了 1 个 NETIF_LCORE_JOB_LOOP。
- slave_lcore_loop_func
/* register netif-lcore-loop-job for Slaves */
snprintf(ctrl_lcore_job.name, sizeof(ctrl_lcore_job.name) - 1, "%s", "slave_ctrl_plane");
ctrl_lcore_job.func = slave_lcore_loop_func;
ctrl_lcore_job.data = NULL;
ctrl_lcore_job.type = NETIF_LCORE_JOB_LOOP;
- main->inet_init->ipv4_init->ipv4_frag_init 注册了 1 个 NETIF_LCORE_JOB_SLOW。
- ipv4_frag_job
snprintf(frag_job.name, sizeof(frag_job.name) - 1, "%s", "ipv4_frag");
frag_job.func = ipv4_frag_job;
frag_job.data = NULL;
frag_job.type = NETIF_LCORE_JOB_SLOW;
frag_job.skip_loops = IP4_FRAG_FREE_DEATH_ROW_INTERVAL;
- main->inet_init -> neigh_init -> arp_init 也注册了 NETIF_LCORE_JOB_SLOW。
- neigh_process_ring
/*get static arp entry from master*/
snprintf(neigh_sync_job.name, sizeof(neigh_sync_job.name) - 1, "%s", "neigh_sync");
neigh_sync_job.func = neigh_process_ring;
neigh_sync_job.data = NULL;
neigh_sync_job.type = NETIF_LCORE_JOB_SLOW;
neigh_sync_job.skip_loops = NEIGH_PROCESS_MAC_RING_INTERVAL;
实际上,还有其他的 NETIF_LCORE_JOB_XXX 没有列出来,此处先不作展开。这些 jobs 都会在 netif_loop 中被调用,用于完成数据面的收包、处理、转发工作。
数据面 jobs 执行
netif_lcore_jobs 作为数据面的处理逻辑,采用了类似 Kernel netfilter 的 chain 链式处理模式。在 netif_loop 中 jobs 函数的调用顺序为: lcore_job_recv_fwd -> lcore_job_xmit -> lcore_job_timer_manage -> slave_lcore_loop_func -> ipv4_frag_job -> neigh_process_ring。
static int netif_loop(void *dummy)
{
struct netif_lcore_loop_job *job;
// 获取当前 lcore id
lcoreid_t cid = rte_lcore_id();
enum netif_principal_status stat = NETIF_PRINCIPAL_STAT_IDLE;
lb_cycle_t deadline;
int ret;
assert(LCORE_ID_ANY != cid);
// lcore 是否配置为了 isol_rx_cpu_ids 专职收包?是则永循环,否则继续。
// 此处的设计思想是将收包和处理包的 Core 分离,增加网卡的吞吐能力吧。
try_isol_rxq_lcore_loop();
if (0 == lcore_conf[lcore2index[cid]].nports) {
// 没有 lcore 对应的 port
RTE_LOG(INFO, NETIF, "[%s] Lcore %d has nothing to do.\n", __func__, cid);
return EDPVS_IDLE;
}
// 首先,立即运行 lcore 中注册的 NETIF_LCORE_JOB_INIT 任务
list_for_each_entry(job, &netif_lcore_jobs[NETIF_LCORE_JOB_INIT], list) {
do_lcore_job(job, 0);
}
while (1) {
lcore_stats[cid].lcore_loop++;
deadline = lcore_timer_hz_cycle() + rte_rdtsc();
// 运行 lcore 中注册的 NETIF_LCORE_JOB_HIGH 任务
do {
list_for_each_entry(job, &netif_lcore_jobs[NETIF_LCORE_JOB_HIGH], list) {
ret = do_lcore_job(job, 0);
if (ret) {
stat = ret;
}
}
} while (stat == NETIF_PRINCIPAL_STAT_FULL && rte_rdtsc() < deadline);
// 运行 lcore 中注册的 NETIF_LCORE_JOB_LOOP 任务
list_for_each_entry(job, &netif_lcore_jobs[NETIF_LCORE_JOB_LOOP], list) {
do_lcore_job(job, stat);
}
// 每隔一定时间,运行 lcore 中注册的 NETIF_LCORE_JOB_SLOW 任务
++netif_loop_tick[cid];
list_for_each_entry(job, &netif_lcore_jobs[NETIF_LCORE_JOB_SLOW], list) {
if (netif_loop_tick[cid] % job->skip_loops == 0) {
do_lcore_job(job, stat);
//netif_loop_tick[cid] = 0;
}
}
// 运行 lcore 中注册的 NETIF_LCORE_JOB_IDLE 任务
if (is_lcore_idle(cid)) {
/* TODO NETIF_PRINCIPAL_STAT_IDLE == stat is strict, consider lcore_stats[cid].opackets */
list_for_each_entry(job, &netif_lcore_jobs[NETIF_LCORE_JOB_IDLE], list) {
do_lcore_job(job, stat);
}
rec_lcore_tx_idle_credit(cid);
} else {
inc_lcore_tx_idle_credit(cid);
}
}
return EDPVS_OK;
}
转发流程分析
下面以 lcore_job_recv_fwd job 为例分析数据面的转发处理流程。
收包阶段
lcore_job_recv_fwd 收包。
static int lcore_job_recv_fwd(void *arg __rte_unused, int high_stat __rte_unused)
{
int i, j;
portid_t pid;
lcoreid_t cid;
uint32_t nic_type;
struct netif_queue_conf *qconf;
enum netif_principal_status stat = NETIF_PRINCIPAL_STAT_IDLE;
cid = rte_lcore_id();
assert((LCORE_ID_ANY != cid) && cid < DPVS_MAX_LCORE);
for (i = 0; i < lcore_conf[lcore2index[cid]].nports; i++) {
pid = lcore_conf[lcore2index[cid]].pqs[i].id;
nic_type = lcore_conf[lcore2index[cid]].pqs[i].nic_type;
assert(pid <= bond_pid_end);
for (j = 0; j < lcore_conf[lcore2index[cid]].pqs[i].nrxq; j++) {
qconf = &lcore_conf[lcore2index[cid]].pqs[i].rxqs[j];
// 从 arp_ring 获取 arp 报文
lcore_process_arp_ring(qconf, cid);
lcore_process_redirect_ring(qconf, cid);
qconf->len = netif_rx_burst(pid, qconf, nic_type);
stat = lcore_update_rx_principal_status(qconf->len, stat);
lcore_stats_burst(&lcore_stats[cid], qconf->len);
lcore_process_marked_flow(qconf);
lcore_stats[cid].impackets += qconf->marked_cnt;
lb_redirect_ring_proc(qconf, cid);
// 读取网卡队列数据之后,调用 lcore_process_packets 对数据包进行处理。
lcore_process_packets(qconf, qconf->mbufs, cid, qconf->len, 0);
kni_send2kern_loop(pid, qconf);
}
}
return stat;
}
L2 处理阶段
lcore_process_packets 处理 L2 报文。
void lcore_process_packets(struct netif_queue_conf *qconf, struct rte_mbuf **mbufs,
lcoreid_t cid, uint16_t count, bool pkts_from_ring)
{
......
/* L2 filter */
for (i = 0; i < count; i++) {
struct rte_mbuf *mbuf = mbufs[i];
struct netif_port *dev;
if (t < count) {
rte_prefetch0(rte_pktmbuf_mtod(mbufs[t], void *));
t++;
}
dev = netif_port_get(mbuf->port);
if (unlikely(!dev)) {
rte_pktmbuf_free(mbuf);
lcore_stats[cid].dropped++;
continue;
}
if (dev->type == PORT_TYPE_BOND_SLAVE) {
dev = dev->bond->slave.master;
mbuf->port = dev->id;
}
eth_hdr = rte_pktmbuf_mtod(mbuf, struct rte_ether_hdr *);
/* reuse mbuf.packet_type, it was RTE_PTYPE_XXX */
mbuf->packet_type = eth_type_parse(eth_hdr, dev);
/*
* In NETIF_PORT_FLAG_FORWARD2KNI mode.
* All packets received are deep copied and sent to KNI
* for the purpose of capturing forwarding packets.Since the
* rte_mbuf will be modified in the following procedure,
* we should use mbuf_copy instead of rte_pktmbuf_clone.
*/
if (dev->flag & NETIF_PORT_FLAG_FORWARD2KNI) {
if (likely(NULL != (mbuf_copied = mbuf_copy(mbuf,
pktmbuf_pool[dev->socket]))))
kni_ingress(mbuf_copied, dev, qconf);
else
RTE_LOG(WARNING, NETIF, "%s: Failed to copy mbuf\n",
__func__);
}
/*
* do not drop pkt to other hosts (ETH_PKT_OTHERHOST)
* since virtual devices may have different MAC with
* underlying device.
*/
/*
* handle VLAN
* if HW offload vlan strip, it's still need vlan module
* to act as VLAN filter.
*/
if (eth_hdr->ether_type == htons(ETH_P_8021Q) ||
mbuf->ol_flags & PKT_RX_VLAN_STRIPPED) {
if (vlan_rcv(mbuf, netif_port_get(mbuf->port)) != EDPVS_OK) {
rte_pktmbuf_free(mbuf);
lcore_stats[cid].dropped++;
continue;
}
dev = netif_port_get(mbuf->port);
if (unlikely(!dev)) {
rte_pktmbuf_free(mbuf);
lcore_stats[cid].dropped++;
continue;
}
eth_hdr = rte_pktmbuf_mtod(mbuf, struct rte_ether_hdr *);
}
if (lb_sync_lcore_is_backup() && lb_sync_process_message(mbuf) == 0) {
/*mbuf is freed in lb_sync_process_message */
deliver_mbuf = false;
} else {
deliver_mbuf = true;
}
// 链路层的过滤处理完之后,调用 netif_deliver_mbuf 进入 IP 层
if (likely(deliver_mbuf)) {
/* handler should free mbuf */
netif_deliver_mbuf(mbuf, eth_hdr->ether_type, dev, qconf,
(dev->flag & NETIF_PORT_FLAG_FORWARD2KNI) ? true : false,
cid, pkts_from_ring);
}
lcore_stats[cid].ibytes += mbuf->pkt_len;
lcore_stats[cid].ipackets++;
}
}
L3 处理阶段
- netif_deliver_mbuf 处理 L3 IP 包:
static inline int netif_deliver_mbuf(struct rte_mbuf *mbuf,
uint16_t eth_type,
struct netif_port *dev,
struct netif_queue_conf *qconf,
bool forward2kni,
lcoreid_t cid,
bool pkts_from_ring)
{
......
/* Remove ether_hdr at the beginning of an mbuf */
data_off = mbuf->data_off;
if (unlikely(NULL == rte_pktmbuf_adj(mbuf, sizeof(struct rte_ether_hdr))))
return EDPVS_INVPKT;
// IP 层的 pkt_type 只注册了 2 种类型 ip4_pkt_type 和 arp_pkt_type。
// ip4_pkt_type 在 ipv4_init 中注册。
// arp_pkt_type 在 arp_init 中注册。
err = pt->func(mbuf, dev);
......
// 对于 ipv4 包,实际上 pt->func 调用的就是 ipv4_rcv。
static struct pkt_type ip4_pkt_type = {
//.type = rte_cpu_to_be_16(ETHER_TYPE_IPv4),
.func = ipv4_rcv,
.port = NULL,
};
static struct pkt_type arp_pkt_type = {
//.type = rte_cpu_to_be_16(ETHER_TYPE_ARP),
.func = neigh_resolve_input,
.port = NULL,
};
- ipv4_rcv
static int ipv4_rcv(struct rte_mbuf *mbuf, struct netif_port *port)
{
......
// ipv4_rcv 完成一系列错误检查后调用了 INET_HOOK 函数
return INET_HOOK(INET_HOOK_PRE_ROUTING, mbuf, port, NULL, ipv4_rcv_fin);
- INET_HOOK
int INET_HOOK(unsigned int hook, struct rte_mbuf *mbuf,
struct netif_port *in, struct netif_port *out,
int (*okfn)(struct rte_mbuf *mbuf))
{
......
// inet_hooks 在 dp_vs_init 中注册
state.hook = hook;
hook_list = &inet_hooks[hook];
ops = list_entry(hook_list, struct inet_hook_ops, list);
if (!list_empty(hook_list)) {
verdict = INET_ACCEPT;
list_for_each_entry_continue(ops, hook_list, list) {
repeat:
// 先后执行 dp_vs_in 和 dp_vs_pre_routin
verdict = ops->hook(ops->priv, mbuf, &state);/*g*/
if (verdict != INET_ACCEPT) {
if (verdict == INET_REPEAT)
goto repeat;
break;
}
}
}
}
L4 处理阶段
dp_vs_in 的主体逻辑是判断 IP 包的 src/dst 是否存在 conn,若存在则直接转发;否则 prot->conn_sched 创建一个新的 conn 然后转发。
static int dp_vs_in(void *priv, struct rte_mbuf *mbuf,
const struct inet_hook_state *state)
{
......
/* packet belongs to existing connection ? */
conn = prot->conn_lookup(prot, &iph, mbuf, &dir, false);
// 如果是 tcp 协议,则会调用到 conn_sched->tcp_conn_sched
if (unlikely(!conn)) {
/* try schedule RS and create new connection */
if (prot->conn_sched(prot, &iph, mbuf, &conn, &verdict) != EDPVS_OK) {
/* RTE_LOG(DEBUG, IPVS, "%s: fail to schedule.\n", __func__); */
return verdict;
}
/* only SNAT triggers connection by inside-outside traffic. */
if (conn->dest->fwdmode == DPVS_FWD_MODE_SNAT)
dir = DPVS_CONN_DIR_OUTBOUND;
else
dir = DPVS_CONN_DIR_INBOUND;
}
......
// xmit_inbound 将包转发给 RS
// xmit_outbound 从 RS 回包
/* holding the conn, need a "put" later. */
if (dir == DPVS_CONN_DIR_INBOUND)
return xmit_inbound(mbuf, prot, conn);
else
return xmit_outbound(mbuf, prot, conn);
}
高级特性
大象流转发优化
现代 DPDK 程序都会基于 RSS 收包多核扩展技术来将不同的 IP 5-tuple Traffic 映射到特定的 Core 上进行处理。
但当出现大象流时,10% 的大象流就占据了总流量的 90%,继而造成某些 Core 忙死,而另外一些 Core 则闲死的情况。更甚者,即便忙死了某个 Core 也依旧无法满足大象流的处理需求,而导致丢包。
为了解决大象流问题,HDSLB 基于以下思路进行了 3 方面的优化:
- 大象流识别:首先,要识别出大象流(Heavy)和老鼠流(Light)。
- 大象流拆分:然后,将大象流能够拆分并映射到多个 Cores 上并行处理,而不仅仅映射到一个 Core 上。
- 大象流重排:最终,还需要将拆分到多个 Cores 上并行处理的流量再进行合法性排序。
从上述原理图可以看出,这里面的关键技术是由 Intel CPU 硬件提供的 DLB(Dynamic Load Balancer)特性。基于 DLB 可以实现:
- 收包时:将大象流切分到多个 Cores 中进行处理。
- 发包时:将多个 Cores 上的流量进行汇聚并合法化排序。
更具体而言,HDSLB 的大象流处理方案需要在 Main Core 上实现一个基于 Intel NIC FDIR 硬件特性的 Switch Filter,用于完成大象流和老鼠流的识别、标记并分类映射到不同的 Core,通过硬件的方式减少了软件上的匹配和查表,性能更高;而在 Worker Cores 上还需要实现基于 Intel CPU DLB 硬件特性的大象流拆分。如下图所示:
-
Main Core
-
Worker Cores
了解更多的细节,推荐阅读 Intel 的官方文档:https://networkbuilders.intel.com/docs/networkbuilders/intel-dynamic-load-balancer-intel-dlb-accelerating-elephant-flow-technology-guide-1677672283.pdf
快慢路径分离转发优化
快慢路径分离现在已然是高性能转发模式的标配了,HDSLB 为性能敏感且处理逻辑复杂的 TCP 流量实现了一套 Session/Conn 快路径。
报文基础转发优化
在基础的报文转发方面,HDSLB 做了 2 方面的努力:
-
Vectorize:向量转发的思路来自于 VPP,批量处理同类报文可以有效提高 icache/dcache 的命中率。详细推荐浏览:《FD.io/VPP — VPP 的实现原理解析》
-
microjobs:将原来的 jobs 进一步的细化为了多个符合 icache size 对齐的 microjobs。结合 pipeline nodes prefetch 的方式,可以进一步减少 icache/dcache miss 带来的性能损耗。
最后
通过本系列的文章,笔者希望向对 DPDK 数据面开发感兴趣的读者们推荐 HDSLB 这个优秀的开源项目。实际上,DVPS 本身就已经是一个足够优秀的数据面项目,HDSLB 更是在其基础上叠加了 Intel 多年积累的软硬件融合加速技术。难能可贵的是,HDSLB 不仅仅满足于作为一个研究项目,而是针对大象流此类在生产环境中存在的典型问题,给出了一个完整可落地的解决方案。这一点非常值得大多数开源项目学习!
参考文档
- https://cloud.tencent.com/developer/article/1180256
- https://cloud.tencent.com/developer/article/1180838
- https://cloud.tencent.com/developer/article/1182928
- https://www.jianshu.com/p/d8ee301f9122
- https://static.sched.com/hosted_files/dpdksummitapac2021/35/Handling Elephant Flow on a DPDK-Based Load Balancer.pdf
- https://zhuanlan.zhihu.com/p/416992198