cover_image

DPDK Graph API 在网络层转发的实践与性能分析

字节跳动STE团队 字节跳动SYS Tech
2025年02月19日 09:53

在 DPDK Graph Pipeline 框架简介与实现原理的文章中,我们介绍了 DPDK Graph Pipeline 框架以及其内部运行原理。本文我们来通过一个实际示例来说明 Graph API 可以如何在网络层转发,并为大家演示如何定义 node 以及在 graph 中如何使用。最后我们会针对 node 之间传输数据的方式 (pointer swap, memory copy 或pointer assignment) 以及批处理方式,对 libgraph 进行性能分析。


例子: l3fwd-graph

为了方便用户了解并使用 Graph API,DPDK 提供了一个示例应用程序 l3fwd-graph 来展示 Graph API 如何用在网络层进行转发。

图片

为了更好地展示如何在实践中使用 node,我们专门编写了一个 ACL node。而在深入探讨一个新的 node 设计方式之前,我们将首先介绍原生 l3fwd-graph 的运作。


注意:Access Control List (ACL) node 的目的是透过配置好的 ACL 规则,对命中规则(比如根据五元组)的报文作出定义好的操作,而本示例设计的 ACL node 只定义了最简单的规则 (accept 和 drop)。新添加的 ACL node 仅仅是用来展示 node 是如何被编写以及使用 Graph API , node 内部功能设计不是唯一的,一般根据业务需求来设计。


1. l3fwd-graph配置

2 个 DPDK port,其中 1 个负责 RX(port 0),另一个(port 1)负责 TX。


2. Packet flow in l3fwd-graph

上图描述了 l3fwd-graph 中处理报文的流程。每个 node 的功能可以按以下顺序概括:
  • ethdev-rx:通过 rte_eth_rx_burst(port 0)接收数据包
  • pkt cls:报文分类;对报文类型进行分类,例如 IPv4、IPv6 报文。类型未知的报文被排队到 pkt drop node。
  • ipv4 查找:根据目标 IP 地址查找路由表。若查找未命中会将相应的报文传送到 pkt drop node。
  • ipv4 rewrite:重写报文 headers,如 Time To Live (TTL) 和 checksum。
  • ethdev-tx:将报文发送到分配的接口(port 1),未发送的报文发送到 pkt drop node。
  • pkt drop:释放报文。

总体而言,l3fwd-graph 从 port 0 接收数据包,通过每个 node 处理一批报文,然后将它们从 port 1 发送出去。


3. ACL 节点设计

对于我们修改后的设计,l3fwd-graph 將新添了一个 ACL node。

struct {    char mapped[NB_SOCKETS];    struct rte_acl_ctx *acx_ipv4[NB_SOCKETS];} acl_config;
/* * Main process function */static uint16_tpkt_acl_node_process(struct rte_graph *graph, struct rte_node *node, void **objs, uint16_t nb_objs);
/* * Build ACL */static struct rte_acl_ctx*setup_acl(struct rte_acl_rule *route_base, struct rte_acl_rule *acl_base, unsigned int route_num, unsigned int acl_num, int socketid);/* * Add rules by reading input rule_ipv4.db file */int rte_node_acl_rules_setup(const char *rule_path, int numa_on, uint32_t enabled_port_mask);
/* * Init acl_config */static intpkt_acl_node_init(const struct rte_graph *graph, struct rte_node *node);
struct rte_node_register pkt_acl_node = { .process = pkt_acl_node_process, .name = "pkt_acl", .init = pkt_acl_node_init, .nb_edges = PKT_ACL_NEXT_MAX, .next_nodes = { [PKT_ACL_NEXT_PKT_CLS] = "pkt_cls", [PKT_ACL_NEXT_PKT_DROP] = "pkt_drop", },};
RTE_NODE_REGISTER(pkt_acl_node);

由于 .init 函数定义是固定的,而 ACL 设置需要输入规则 / 其他的参数,我们可以定义另一个函数 rte_node_acl_rules_setup,并在 graph 创建后(在 main.c 中使用)视为一个helper function 去配置 ACL 规则。

/*  * pkt_acl_node_process:pkt_acl.c */rte_acl_classify(    acl_config.acx_ipv4[socketid],    acl_search.data_ipv4,    acl_search.res_ipv4,    acl_search.num_ipv4,    DEFAULT_MAX_CATEGORIES);
for (i = 0; i < acl_search.num_ipv4; i++) { pkt = acl_search.m_ipv4[i]; acl_res = acl_search.res_ipv4[i]; if (likely((acl_res & ACL_DENY_SIGNATURE) == 0 && acl_res != 0)) { /* forward packets */ rte_node_enqueue_x1(graph, node, PKT_ACL_NEXT_PKT_CLS, pkt); } else{ /* in the ACL list, drop it */ rte_node_enqueue_x1(graph, node, PKT_ACL_NEXT_PKT_DROP, pkt); }}

ACL node .process 函数的主要功能


上面的代码展示了用户如何可以添加 ACL 功能到现有的 graph。而在数据传递方面,rte_node_enqueue_x1 根据 ACL 搜索结果将相应的数据包入队到 pkt drop node 或 pkt cls node。


性能测试

以下是我们在不同报文传递方式下,测试对 graph pipeline 性能带来的影响,以及当一批报文拆分为多个批次(batching)时的稳健性。我们使用了简单的 graph 来测试,并通过查看所有 destination node 的 throughput 来验证我们在上一篇文章中提到使用 graph pipeline 的优势。


图片

测试环境

  • Intel(R) Xeon(R) Platinum 8336C CPU @ 2.30GHz

  • 128 核
  • 1 个核绑定到 graph 示例程序
  • 54MB LLC 缓存


图片

Node 设计

由于我们只对基于不同传递报文方式的性能表现感兴趣,node 的设计保持到最简洁。


Node 大小

默认 node 大小为 256,这也代表當一个 graph walk 结束时,我们将返回得到 256 个报文。


Source node

__rte_experimentalstatic inline voidrte_node_next_stream_put(struct rte_graph *graph, struct rte_node *node,             rte_edge_t next, uint16_t idx){    uint16_t count;    int i;
RTE_SET_USED(objs); RTE_SET_USED(nb_objs);
/* Create a proportional stream for every next */ for (i = 0; i < node->ctx[0]; i++) { count = (node->ctx[i + 9] * RTE_GRAPH_BURST_SIZE) / 100; rte_node_next_stream_get(graph, node, node->ctx[i + 1], count); rte_node_next_stream_put(graph, node, node->ctx[i + 1], count); }
return RTE_GRAPH_BURST_SIZE;}


Worker node

/* Worker node function */static uint16_ttest_perf_node_worker(struct rte_graph *graph, struct rte_node *node,              void **objs, uint16_t nb_objs){    uint16_t next = 0;    uint16_t count;    void **to_next;    int i;
/* Move stream for single next node - swap pointer*/ if (node->ctx[0] == 1) { rte_node_next_stream_move(graph, node, node->ctx[1]); return nb_objs; }
/* Enqueue objects to next nodes proportionally */ for (i = 0; i < node->ctx[0]; i++) { next = node->ctx[i + 1]; count = (node->ctx[i + 9] * nb_objs) / 100; to_next = rte_node_next_stream_get(graph, node, next, nb_objs); while (count) { /* Pointer assignment */ // rte_node_enqueue_x1(graph, node, next, objs[0]); // objs += 1; // count -= 1;
rte_memcpy(to_next, objs, 8 * sizeof(objs[0])); to_next += 8; objs += 8; count -= 8; rte_node_next_stream_put(graph, node, next, 8); } }
return nb_objs;}

不同的报文传递机制 —— pointer swap、memory copy、pointer assignment。目前上面的代码显示了 memory copy 方法


对于我们的 worker node,该 node 仅负责基于上述提到的不同报文传递的方法来传递报文。


Destination node

对于我们的 destination node,它什么也不做,只是返回报文数量。

static uint16_ttest_perf_node_sink(struct rte_graph *graph, struct rte_node *node, void **objs, uint16_t nb_objs){    return nb_objs;}


图片

使用不同报文传递方法下对性能的影响

图片
用于分析不同报文传递机制下性能差异的 graph
图片

在实验中,我们把不同的报文传递方法作为唯一的变量来测试 graph pipeline 性能的影响变化。不同的报文传递方法就是指:两个节点之间的 pointer swap(优化的情况),使用 rte_memcpy 以及最简单的 pointer assignment 方法 。此外,我们还测试了 rte_memcpy  在拷贝不同报文数目下对性能的影响。


总体而言,从上图中可以看出性能最高的方法是 pointer swap,其次是 memory copy 和简单的 pointer assignment。这是因为通过在 node 之间 pointer swap 利用了 node 本身的设计,即每一个 node 都有各自的内存来存放报文,pointer swap 就直接交换了报文以及报文数目的信息。Pointer swap 本质其实也是 pointer assignment 的一种特殊的优化用例,只不过是基于一批报文,而不是基于每一个报文。


图片

拆分批次的稳健性

图片

拆分批次的 graph


除了测试报文传递方法对性能的变化影响外,我们还测试了在批处理情况下使用 graph pipeline 的水平扩展性,即把一个批次分成多个不同的批次。如前文所述,graph 有利于灵活的设计,因为用户可以通过编写一个新 node 来设计一个新的功能,并将其放在 graph 中。另一方面,显然对于新引入的 node(新功能/新的批次)性能会有所下降,这是因为新加入的 node 也带来了额外的工作,例如更新 stack frame 等。

图片

图片

1 批次增加到 2 批次和 2 批次增加到 4 批次时的百分比变化


从以上信息可以看出 graph pipeline 在批处理方面具备水平扩展性,每当批处理数量增加一倍时,性能下降幅度相似。


与 pointer assignment 相比,memory copy 仍然能够实现更高的性能。


结论

在本文中,我们通过 l3fwd 演示了 graph pipeline 如何使用,还通过添加自编写的 ACL node 来修改原有的 graph。对于性能分析,我们对几个简单 graph 进行了实验,以了解不同的报文传递方式会如何影响整体性能,以及引入批处理时 graph pipeline 的稳健性。


从实验结果可以看出,graph pipeline 是高度灵活的。ACL node 可以模块化编写并应用于 graph,并且从我们在性能分析时所用的 graph 中,仅使用相同的 node (source、worker 和 detination node) ,我们就能够建立各种不同的 graph。测试结果展示了当我们有更多的待处理函数(批处理)时,性能下降是符合预期的。另一方面,对于不同的报文传递机制,在 node 之间实现 pointer swap 确实是优化的方式,因为它利用到 node 本身的设计,即每个 node 都有自己的内存区域来保存报文,而不是透过 pointer assignment 把每个报文传递到下一个 node。我们还发现:相比 pointer assignment,memory copy 仍为更好的选择,尽管 memory copy 的性能会因报文数目而异。


总体而言,从设计灵活度、报文累积优化、以及批处理的水平扩展性的方面来看,libgraph 框架相比传统的 pipeline 模型都更具优势。


往期精彩

一种基于套餐思想的数据中心建设规划方法

性能媲美裸金属,边缘场景高性能虚拟机技术揭秘

字节跳动数据中心应用给C++编译工具链带来的机会和挑战 | CPP Summit 分享回顾

mGPU 技术揭秘:mGPU 算力和显存隔离底层方案

字节跳动开源Linux内核网络抓包工具netcap

字节跳动STE团队6个议题中选Linux网络开发者会议——Netdev

字节跳动系统智能运维实践 | DataFun大会分享回顾

字节跳动STE团队2篇论文入选国际峰会 IEEE RAS in Data Centers Summit

OpenBMC:coredump自动发现及调试实践

大模型Agent在AIOps运维场景的实践 

性能神器的碰撞:io_uring与高性能网络IO框架Netpoll的邂逅


关于STE团队

字节跳动STE团队System Technologies&Engineering,系统技术与工程),一直致力于操作系统内核与虚拟化、系统基础软件与基础库的构建和性能优化、超大规模数据中心的系统稳定性和可靠性建设、新硬件与软件的协同设计等基础技术领域的研发与工程化落地,具备全面的基础软件工程能力,为字节上层业务保驾护航。同时,团队积极关注社区技术动向,拥抱开源和标准,欢迎更多同学加入我们,一起交流学习。扫描下方二维码了解职位详情,欢迎大家投递简历至huangxuechun.hr@bytedance.com wangan.hr@bytedance.com

继续滑动看下一个
字节跳动SYS Tech
向上滑动看下一个