服务质量 (QoS) 框架
本章介绍 DPDK 服务质量 (QoS) 框架。
21.1 带有 QoS 支持的数据包流水线
下图显示了一个具有 QoS 支持的复杂数据包处理流水线的示例
表21.1:带有 QoS 支持的复杂数据包处理流水线
这个流水线可以使用可重用的 DPDK 软件库构建。在这个流水线中实现 QoS 的主要模块有:policer(警察器)、dropper(丢包器)和scheduler(调度器)。下表提供了每个模块的功能描述
# | 模块 | 功能描述 |
---|---|---|
1 | Packet I/O | 从/向多个 NIC 端口接收/传输数据包。适用于 Intel 1 GbE/10 GbE NIC 的轮询模式驱动程序 (PMDs)。 |
2 | Packet parser | 识别输入数据包的协议栈。检查数据包头的完整性。 |
3 | Flow classification | 将输入数据包映射到已知流量流之一。使用可配置哈希函数(如 jhash、CRC 等)进行精确匹配表查找和桶逻辑处理碰撞。 |
4 | Policer | 使用 srTCM(RFC 2697)或 trTCM(RFC 2698)算法对数据包进行计量。 |
5 | Load Balancer | 将输入数据包分发给应用程序工作线程。为每个工作线程提供均匀的负载。保持流量流与工作线程之间的亲和性以及每个流的数据包顺序。 |
6 | Worker threads | 客户特定应用工作负载的占位符(例如,IP 栈等)。 |
7 | Dropper | 使用随机早期检测(RED)算法(由 Sally Floyd - Van Jacobson 论文指定)或加权 RED 进行拥塞管理。根据当前调度器队列负载水平和数据包优先级丢弃数据包。遇到拥塞时,首先丢弃较低优先级的数据包。 |
8 | Hierarchical Scheduler | 5级分层调度器(级别包括:输出端口、子端口、管道、流量类别和队列),具有成千上万(通常为64K)叶节点(队列)。实现流量整形(对子端口和管道级别)、严格优先级(对流量类别级别)和加权循环轮询(对每个管道流量类别内的队列)。 |
以下是在数据包处理流程中使用的基础设施模块列表
表21.2: 数据包处理流程使用的基础设施模块
# | 模块 | 功能描述 |
---|---|---|
1 | Buffer manager | 支持全局缓冲池和每个线程私有的缓冲区缓存。 |
2 | Queue manager | 支持流水线模块之间的消息传递。 |
3 | Power saving | 在低活动时期支持节能功能。 |
数据包处理流程模块与 CPU 核心的映射可以根据每个特定应用程序所需的性能水平以及为每个模块启用的特性集进行配置。一些模块可能会消耗多个 CPU 核心(每个 CPU 核心在不同的输入数据包上运行相同模块的不同实例),而其他几个模块可能映射到同一个 CPU 核心上。
21.2 分层调度器
当存在分层调度器模块时,通常位于传输阶段之前的发送端(TX)位置。其目的是根据每个网络节点的服务级别协议(SLAs)指定的策略,优先传输来自不同用户和不同流量类别的数据包。
21.2.1 概述
分层调度器模块类似于网络处理器使用的流量管理器模块,通常实现每个流(或一组流)的数据包排队和调度。它通常充当缓冲区,能够在传输之前暂时存储大量数据包(入列操作)。随着网络接口控制器(NIC TX)请求更多要传输的数据包,这些数据包稍后会被移除并传递给 NIC TX,同时数据包选择逻辑会遵循预定义的SLAs(出列操作)。
图 21.2:分层调度程序块内部图
调度层次结构优化了大量数据包队列的分层调度器。当只需要少量队列时,应使用消息传递队列而不是这个模块。详细讨论请参阅性能最差情况部分。
21.2.2 调度层次结构
调度层次结构如图21.3所示。层次结构的第一级是以太网TX端口 1/10/40 GbE,随后的层次级别被定义为子端口、管道、流量类和队列。
通常,每个子端口代表一个预定义的用户组,而每个管道代表一个单独的用户/订阅者。每个流量类是不同流量类型的表示,具有特定的丢包率、延迟和抖动要求,例如语音、视频或数据传输。每个队列托管来自同一用户的同一类型的一个或多个连接的数据包。
图 21.3:每个端口的调度层次结构
下表详细介绍了每个层级的功能。
表 21.3:端口调度层次结构
# | Level Siblings per Parent Functional Description |
---|---|
1 | Port |
• Output Ethernet port 1/10/40 GbE. | |
• 多个端口按循环轮询顺序进行调度,所有端口具有相等的优先级。 | |
2 | Subport (可配置,默认为 8) |
1. 使用令牌桶算法进行流量整形(每个子端口一个令牌桶)。 | |
2. 在子端口级别对每个流量类别(TC)施加上限。 | |
3. 低优先级的 TC 能够重用当前被高优先级 TC 未使用的子端口带宽。 | |
3 | Pipe (可配置,默认为 4K) |
1. 使用令牌桶算法进行流量整形(每个管道一个令牌桶)。 | |
4 | Traffic Class (TC) 4 |
1. 相同管道的 TC 按严格优先级顺序处理。 | |
2. 在管道级别对每个 TC 施加上限。 | |
3. 低优先级的 TC 能够重用当前被高优先级 TC 未使用的管道带宽。 |
21.2.3 应用程序编程接口 (API)
端口调度器配置 API
rte_sched.h
文件包含用于端口、子端口和管道的配置函数。
端口调度器入队 API
端口调度器入队 API 与 DPDK PMD TX 函数的 API 非常相似。
int rte_sched_port_enqueue(struct rte_sched_port *port,
struct rte_mbuf **pkts,
uint32_t n_pkts);
端口调度器出队 API
端口调度器出队 API 与 DPDK PMD RX 函数的 API 非常相似
int rte_sched_port_dequeue(struct rte_sched_port *port,
struct rte_mbuf **pkts,
uint32_t n_pkts);
用法示例
/* 文件 "application.c" */
#define N_PKTS_RX 64
#define N_PKTS_TX 48
#define NIC_RX_PORT 0
#define NIC_RX_QUEUE 0
#define NIC_TX_PORT 1
#define NIC_TX_QUEUE 0
struct rte_sched_port *port = NULL;
struct rte_mbuf *pkts_rx[N_PKTS_RX], *pkts_tx[N_PKTS_TX];
uint32_t n_pkts_rx, n_pkts_tx;
/* 初始化 */
<初始化代码>
/* 运行时 */
while (1) {
/* 从 NIC RX 队列读取数据包 */
n_pkts_rx = rte_eth_rx_burst(NIC_RX_PORT, NIC_RX_QUEUE, pkts_rx, N_PKTS_RX);
/* 分层调度器入队 */
rte_sched_port_enqueue(port, pkts_rx, n_pkts_rx);
/* 分层调度器出队 */
n_pkts_tx = rte_sched_port_dequeue(port, pkts_tx, N_PKTS_TX);
/* 将数据包写入 NIC TX 队列 */
rte_eth_tx_burst(NIC_TX_PORT, NIC_TX_QUEUE, pkts_tx, n_pkts_tx);
}
21.2.4 实现
每个端口的内部数据结构
内部数据结构的示意图显示在详细信息中
图 21.4:每个端口的内部数据结构
表 21.4:每个端口的调度程序内部数据结构
# | Data structure | Size (bytes) | # per port | Access type | Description |
---|---|---|---|---|---|
1 | Subport table entry | 64 | # subports per port | Rd, Wr | 持久子端口数据(信用、等等)。 |
2 | Pipe table entry | 64 | # pipes per port | Rd, Wr | 管道及其 TCs 和队列的持久数据(信用等),在运行时更新。 管道配置参数在运行时不会更改。相同的管道配置参数被多个管道共享,因此它们不是管道表条目的一部分。 |
3 | Queue table entry | 4 | # queues per port | Rd, Wr | 持久队列数据(读取和写入指针)。对于所有队列,每个 TC 的队列大小相同,允许使用快速公式计算队列基地址,因此这两个参数不是队列表条目的一部分。队列表条目 |
21.2. 层次调度器
多核扩展策略
多核扩展策略如下:
- 在不同的线程上运行不同的物理端口。同一端口的入队和出队由同一个线程处理。
- 通过在不同的线程上运行同一物理端口的不同子端口集合(虚拟端口)来将同一物理端口拆分到不同的线程上。类似地,一个子端口可以被拆分为多个由不同线程运行的子端口。同一端口的入队和出队由同一个线程处理。只有在性能原因上无法使用单个核处理完整个端口时才需要这样做。
同一输出端口的入队和出队
从不同核心对同一输出端口进行入队和出队操作可能会对调度器的性能造成显著影响,因此不建议这样做。
端口入队和出队操作共享以下数据结构的访问: - 包描述符
- 队列表
- 队列存储区域
- 活动队列的位图
性能下降的预期原因包括: - 需要使队列和位图操作线程安全,这要求使用锁原语进行访问序列化(例如自旋锁/信号量),或者使用原子原语进行无锁访问(例如测试和设置,比较和交换等)。在前一种情况下,影响要大得多。
- 在两个核心的缓存层次结构之间存储共享数据结构的缓存行的来回传输(由MESI协议缓存一致性CPU硬件透明地完成)。
因此,调度器的入队和出队操作必须从同一个线程运行,这允许队列和位图操作不是线程安全的,并将调度器数据结构保持在同一个核心内部。
性能扩展
增加NIC端口数量只需按比例增加用于流量调度的CPU核心数量。
入队管道
每个数据包的步骤顺序如下: - 访问mbuf以读取标识数据包目标队列所需的数据字段。这些字段通常由分类阶段设置,包括:端口、子端口、流量类和流量类内的队列。
- 访问队列结构以确定队列数组中的写入位置。如果队列已满,则丢弃数据包。
- 访问队列数组位置以存储数据包(即写入mbuf指针)。
应当注意到这些步骤之间的强数据依赖关系,因为步骤2和3在步骤1和2的结果可用之前无法开始,这阻止了处理器乱序执行引擎提供任何重要的性能优化。
由于输入数据包的高速率和大量队列,预计用于入队当前数据包的数据结构不在当前核心的L1或L2数据缓存中,因此以上3个内存访问(平均)将导致L1和L2数据缓存未命中。出于性能原因,每个数据包的3个L1/L2缓存未命中是不可接受的。
解决方法是提前预取所需的数据结构。预取操作具有执行延迟期间,在此期间,处理器不应尝试访问当前处于预取状态的数据结构,因此处理器应执行其他工作。唯一可用的其他工作是在其他输入数据包上执行入队操作的不同阶段,从而实现入队操作的流水线化实现。
图21.5展示了入队操作的流水线化实现,具有4个流水线阶段,每个阶段执行2个不同的输入数据包。在给定时间内,没有任何输入数据包可以成为多个流水线阶段的一部分。
图 21.5:分层调度程序入队操作的预取管道
队列拥塞管理方案与出队状态机
上述入队管道实现的拥塞管理方案非常基本:数据包被入队直到特定队列变满,然后同一队列的所有数据包被丢弃,直到数据包被消耗(由出队操作完成)。这可以通过在入队管道中启用RED/WRED来改进,该方案会考虑队列占用情况和数据包优先级,从而决定特定数据包的入队/丢弃(而不是不加选择地入队所有数据包或丢弃所有数据包)。
出队状态机
从当前管道调度下一个数据包的步骤顺序如下:
- 使用位图扫描操作标识下一个活动管道,预取管道。
- 读取管道数据结构。更新当前管道及其子端口的信用额度。确定当前管道内的第一个活动流量类别,使用WRR选择下一个队列,预取当前管道的所有16个队列的队列指针。
- 从当前WRR队列中读取下一个元素,并预取其数据包描述符。
- 从数据包描述符(mbuf结构)中读取数据包长度。根据数据包长度和可用信用额度(当前管道、管道流量类、子端口和子端口流量类的信用额度),为当前数据包做出调度是/否的决定。
为避免缓存未命中,上述数据结构(管道、队列、队列数组、mbufs)在被访问之前会提前预取。隐藏预取操作的延迟策略是在为当前管道发出预取操作后立即切换到另一个管道(在磨坊B中的管道),这样可以给预取操作足够的时间完成,然后在执行再次切换回这个管道(在磨坊A中)之前。
出队管道状态机利用了处理器缓存中的数据存在性,因此它试图在移动到同一管道TC和管道中尽可能多地发送数据包(达到可用数据包和信用额度上限)之前,尽可能发送尽可能多的数据包,然后再移动到同一管道中的下一个活动TC(如果有)或移动到另一个活动管道。
图 21.6:分层调度程序出队操作的管道预取状态机
时序与同步
输出端口被建模为需要由调度器填充数据进行传输的字节槽的传送带。对于10GbE,每秒需要端口调度器填充12.5亿个字节槽。如果调度器填充速度不够快以填满这些槽(假设有足够的数据包和信用额度),则会有一些槽未被使用,造成带宽浪费。
原则上,分层调度器的出队操作应该由NIC TX触发。通常情况下,一旦NIC TX输入队列的占用率降至预定义的阈值以下,端口调度器就会被唤醒(基于中断或轮询,通过持续监控队列占用率)以向队列推送更多数据包。
内部时间参考
调度器需要跟踪时间的推进以进行信用逻辑的更新,这需要基于时间进行信用更新(例如,子端口和管道的流量整形、流量类上限强制等)。
每当调度器决定将数据包发送到NIC TX进行传输时,调度器将相应地增加其内部时间参考。因此,方便将内部时间参考单位设定为字节,其中一个字节表示物理接口在传输介质上发送一个字节所需的时间。这样,当调度数据包进行传输时,时间将以(n + h)增加,其中n是字节长度,h是每个数据包的帧开销字节数。
内部时间参考重新同步
调度器需要将其内部时间参考与端口传送带的速度同步。原因在于确保调度器不会向NIC TX提供比物理介质的线速率更多的字节,以防止数据包丢失(由调度器造成,因为NIC TX输入队列已满,或稍后由NIC TX内部造成)。
调度器在每次出队调用时读取当前时间。CPU时间戳可以通过读取时间戳计数器(TSC)寄存器或高精度事件计时器(HPET)寄存器来获取。当前CPU时间戳从CPU时钟数转换为字节数:time_bytes = time_cycles / cycles_per_byte,其中cycles_per_byte是等效于在传输介质上发送一个字节所需的CPU周期数(例如对于2GHz的CPU频率和10GbE端口,cycles_per_byte = 1.6)。
调度器保持NIC时间的内部时间参考。每当调度数据包时,NIC时间都会随数据包长度(包括帧开销)增加。在每次出队调用时,调度器将其NIC时间的内部参考与当前时间进行比较:
- 如果NIC时间在未来(NIC时间 >= 当前时间),则不需要调整NIC时间。这意味着调度器能够在NIC实际需要这些数据包之前进行调度,因此NIC TX已得到充分提供。
- 如果NIC时间在过去(NIC时间 < 当前时间),则应通过将其设置为当前时间来调整NIC时间。这意味着调度器无法跟上NIC字节传送带的速度,因此由于无法向NIC TX提供足够的数据包,NIC带宽被浪费。
调度器准确性和粒度
调度器往返延迟(SRTD)是调度器对同一管道连续检查之间的时间(CPU周期数)。
为了跟上输出端口(即避免带宽损失),调度器应能够比NIC TX传输同样数量的数据包更快。
调度器需要跟上每个单独管道的速率,按照管道令牌桶的配置,假设没有端口超额订阅。这意味着管道令牌桶的大小应设置得足够高,以防止由于大的SRTD导致溢出,因为这将导致管道的信用损失(因此会损失管道的带宽)。
信用逻辑
调度决策
发送来自(子端口S,管道P,流量类TC,队列Q)的下一个数据包的调度决策是有利的(发送数据包)当满足以下所有条件时:
• 子端口S的管道P目前被一个端口磨坊所选中;
• 流量类TC是管道P的最高优先级的活动流量类别;
• 队列Q是管道P内流量类TC中由WRR选择的下一个队列;
• 子端口S有足够的信用来发送数据包;
• 子端口S的流量类TC有足够的信用来发送数据包;
• 管道P有足够的信用来发送数据包;
• 管道P的流量类TC有足够的信用来发送数据包。
如果所有上述条件都满足,则选择该数据包进行传输,并从子端口S、子端口S的流量类TC、管道P、管道P的流量类TC中减去所需的信用。
帧开销
由于所有数据包长度的最大公约数为一个字节,所选信用单位为一个字节。传输n字节的数据包所需的信用数等于(n+h),其中h等于每个数据包的帧开销字节数。
表 21.5:以太网帧开销字段
# | Packet field | Length (bytes) | Comments |
---|---|---|---|
1 | Preamble | 7 | |
2 | Start of Frame Delimiter (SFD) | 1 | |
3 | Frame Check Sequence (FCS) | 4 | 只有当未包含在mbuf数据包长度字段中时才考虑为开销。 |
4 | Inter Frame Gap (IFG) | 12 | |
5 | Total | 24 |
通信流量整形
子端口和管道的通信流量整形是使用每个子端口/每个管道一个令牌桶来实现的。每个令牌桶使用一个饱和计数器来记录可用信用的数量。
令牌桶的通用参数和操作如下表所示(表6和表7):
表 21.6:令牌桶通用操作
# | Token Bucket Parameter | Unit | Description |
---|---|---|---|
1 | bucket_rate | Credits per second | 添加到桶中的信用速率。 |
2 | bucket_size | Credits | 桶中可存储的最大信用数量。 |
表 21.7:令牌桶通用参数
# | Token Bucket Operation | Description |
---|---|---|
1 | Initialization | 将桶设置为预定义的值,例如零或桶大小的一半。 |
2 | Credit update | 根据桶速率在现有信用的基础上周期性或按需添加信用。信用不能超过由桶大小定义的上限,因此在桶已满时要添加到桶中的任何信用都会被丢弃。 |
3 | Credit consumption | 作为数据包调度的结果,从桶中删除所需数量的信用。只有当桶中有足够的信用来发送完整数据包(数据包字节和数据包的帧开销)时,才能发送该数据包。 |
实现令牌桶通用操作
为了实现上述描述的令牌桶通用操作,当前设计使用了所提供的持久化数据结构,在表9中描述了令牌桶操作的实现。
表 21.8:令牌桶持久数据结构
# | Token bucket field | Unit | Description |
---|---|---|---|
1 | tb_time | Bytes | 上次信用更新的时间。以字节表示,而不是秒或 CPU 周期,便于信用消耗操作(因为当前时间也以字节形式维护)。请参阅内部时间参考部分,了解为何时间以字节单位维护的解释。 |
2 | tb_period | Bytes | 自上次信用更新以来应经过的时间段,以授予 tb_credits_per_period 的信用值。 |
3 | tb_credits_per_period | Bytes | 每个 tb_period 的信用津贴。 |
4 | tb_size | Bytes | 桶的大小,即 tb_credits 的上限。 |
5 | tb_credits | Bytes | 当前桶中的信用数量。 |
令牌桶速率计算公式
令牌桶速率(以每秒字节数计)可以使用以下公式计算:
bucket_rate = (tb_credits_per_period / tb_period) * r
其中,
- ( r ) = 端口线速率(以每秒字节数计)。
- (\text{tb_credits_per_period}) = 令牌桶每周期产生的信用数量。
- (\text{tb_period}) = 令牌桶周期(以秒为单位)。
表 21.9:令牌桶操作
# | Token bucket operation | Description |
---|---|---|
1 | Initialization | tb_credits = 0; 或者 tb_credits = tb_size / 2; |
2 | Credit update | 信用更新选项: • 每次为端口发送数据包时,更新该端口的所有子端口和管道的信用。不可行。 • 每次发送数据包时,更新管道和子端口的信用。非常准确,但不需要(需要大量计算)。 • 每次选择管道(即由研磨器之一选择)时,更新管道及其子端口的信用。当前实现使用选项3。根据出队状态机章节,在实际使用管道和子端口信用之前,每次由出队过程选择管道时都会更新管道和子端口信用。 实现在准确性和速度之间进行权衡,仅在至少经过一个完整的 tb_period 自上次更新以来才更新桶信用。 • 通过选择 tb_credits_per_period = 1 的 tb_period 值可以实现完全的准确性。 • 当不需要完全的准确性时,通过将 tb_credits 设置为较大的值可以实现更好的性能。 更新操作: • n_periods =(时间 - tb_time)/ tb_period; • tb_credits += n_periods * tb_credits_per_period; • tb_credits = min(tb_credits,tb_size); • tb_time += n_periods * tb_period; |
严格优先级调度和上限强制
严格优先级调度实现
同一管道内的流量类别的严格优先级调度由管道出队状态机实现,它按升序选择队列。因此,处理队列0到3(与最高优先级TC 0相关联的队列)先于处理队列4到7(TC 1,优先级低于TC 0),而这些队列又先于处理队列8到11(TC 2),依此类推先于处理队列12到15(TC 3,优先级最低的TC)。
上限强制
管道和子端口级别的流量类别没有进行流量整形,因此在这个情境下没有维护令牌桶。子端口和管道级别的流量类别的上限由定期重新填充子端口/管道流量类别信用计数器来执行。每当为该子端口/管道调度数据包时,就会消耗其中的信用,如表10和表11所描述。
表21.10:子端口/管道流量类别上限强制持久化数据结构
# | Subport or pipe field | Unit | Description |
---|---|---|---|
1 | tc_time | Bytes | 当前子端口/管道的4个 TC 的下一次更新时间(上限重新填充)。请参阅内部时间参考部分,了解时间为何以字节单位维护的解释。 |
2 | tc_period | Bytes | 当前子端口/管道的4个 TC 之间的连续更新时间。预期这个值会比令牌桶 tb_period 的典型值大很多倍。 |
3 | tc_credits_per_period | Bytes | 在每个强制执行周期 tc_period 中允许当前 TC 消耗的信用数量的上限。 |
4 | tc_credits | Bytes | 当前流量类别可在当前强制执行周期剩余时间内消耗的信用数量的当前上限。 |
表 21.11:子端口/管道流量类别上限强制执行操作
# | Traffic Class Operation | Description |
---|---|---|
1 | Initialization | tc_credits = tc_credits_per_period; tc_time = tc_period; |
2 | Credit update | 更新操作: 如果(时间 >= tc_time){ tc_credits = tc_credits_per_period; tc_time = time + tc_period; } |
3 | Credit consumption (on packet scheduling) | 作为数据包调度的结果,TC 限制随着所需的信用数量减少。只有在当前 TC 限制中有足够的信用来发送完整数据包(数据包字节和数据包的帧开销)时,才能发送该数据包。调度操作: pkt_credits = pk_len + frame_overhead; 如果(tc_credits >= pkt_credits){tc_credits -= pkt_credits;} |
加权循环法 (WRR)
WRR 设计解决方案从简单到复杂的演变如表 12 所示。
表 21.12:加权循环法 (WRR)
# | All Queues Active? | Equal Weights for All Queues? | All Packets Equal? | Strategy |
---|---|---|---|---|
1 | Yes | Yes | Yes | 字节级循环轮询(Byte level round robin) Next queue: queue #i,i = (i + 1) % n |
2 | Yes | Yes | No | 分组级循环轮询(Packet level round robin) 每从队列 #i 消耗一个字节需要消耗队列 #i 的一个令牌。T(i) = 之前从队列 #i 消耗的令牌总数。每次从队列 #i 消耗一个数据包时,T(i) 更新为:T(i) += pkt_len。 Next queue:具有最小 T 的队列。 |
3 | Yes | No | No | 分组级加权轮询(Packet level weighted round robin) 通过为每个队列引入不同的每字节成本,将此情况简化为前一情况。具有较低权重的队列每字节的成本较高。这样,仍然有意义地比较不同队列之间的消耗,以选择下一个队列。w(i) = 队列 #i 的权重 t(i) = 队列 #i 的每字节令牌,定义为队列 #i 的倒数权重。例如,如果 w[0…3] = [1:2:4:8],则 t[0…3] = [8:4:2:1];如果 w[0…3] = [1:4:15:20],则 t[0…3] = [60:15:4:3]。每从队列 #i 消耗一个字节需要为队列 #i 消耗 t(i) 个令牌。T(i) = 之前从队列 #i 消耗的令牌总数。每次从队列 #i 消耗一个数据包时,T(i) 更新为:T(i) += pkt_len * t(i)。Next queue:具有最小 T 的队列。 |
4 | No | No | No | 可变队列状态的分组级加权轮询(Packet level weighted round robin with variable queue status) 通过将不活动队列的消耗设置为一个较高的数字,将此情况简化为前一情况,以使不活动队列永远不会被最小 T 逻辑选中。为防止连续累加导致 T 溢出,每次数据包消耗后,对所有队列截断 T(i)。例如,T[0…3] = [1000, 1100, 1200, 1300] 截断为 T[0…3] = [0, 100, 200, 300],通过从 T(i) 中减去最小的 T,i = 0…n。这需要在输入队列集中至少有一个活动队列,由出队状态机永远不会选择不活动流量类别来保证。mask(i) = 队列 #i 的饱和掩码,定义为:mask(i) = (队列 #i 是否活动)? 0 : 0xFFFFFFFF; w(i) = 队列 #i 的权重 t(i) = 队列 #i 的每字节令牌,定义为队列 #i 的倒数权重。T(i) = 之前从队列 #i 消耗的令牌总数。 |
子端口流量类别超额订阅
问题陈述
对于子端口流量类别X,超额订阅是在配置时发生的事件。这种情况发生在子端口成员管道级别为流量类别X分配的带宽比父子端口级别为相同流量类别分配的带宽更多时。
对于特定子端口和流量类别的超额订阅的存在,纯粹是由于管道和子端口级别的配置而不是由于运行时流量负载的动态演变(就像拥塞一样)。
当流量类别X的整体需求较低时
对于当前子端口的流量类别X的整体需求较低时,超额订阅条件的存在并不代表问题,因为对于所有成员管道,流量类别X的需求得到了完全满足。然而,当所有子端口成员管道的流量类别X的总需求超过了在子端口级别配置的限制时,这就无法再实现了。
解决方案空间
解决这个问题的一些可能方法被总结如下,其中第三种方法被选定用于实现。
表21.13:子端口流量类别超额订阅
No. | Approach | Description |
---|---|---|
1 | Don’t care | 首来先服务。这种方法在子端口成员管道之间不公平,因为首先服务的管道将根据它们需要的 TC X 的带宽使用尽可能多的带宽,而稍后服务的管道由于子端口级别的 TC X 带宽稀缺而接收到较差的服务。 |
2 | Scale down all pipes | 子端口内的所有管道的 TC X 的带宽限制按相同比例缩减。这种方法在子端口成员管道之间不公平,因为低端管道(即配置带宽较低的管道)可能会遭受严重的服务降级,可能导致其服务不可用(如果这些管道的可用带宽降至可用服务的最低要求以下),而高端管道的服务降级可能根本不可察觉。 |
3 | Cap the high demand pipes | 每个子端口成员管道在子端口级别的 TC X 可用带宽上收到相等份额。任何未被低需求管道使用的带宽会以相等份额重新分配给高需求管道。这样,高需求管道被截断,而低需求管道不受影响。 |
子端口流量类别超额订阅实现概述
典型情况下
通常情况下,子端口流量类别(TC)超额订阅功能仅对最低优先级流量类别(TC 3)启用,该流量类别通常用于尽力而为的流量,并且管理平面可以防止此条件发生于其他(优先级更高的)流量类别。
前提假设
为了简化实现,还假设子端口 TC 3 的上限设置为子端口速率的 100%,管道 TC 3 的上限对于所有子端口成员管道也设置为各自管道速率的 100%。
实现概述
算法计算一个水位线(watermark),它基于子端口成员管道当前需求而周期性更新,其目的是限制每个管道允许发送给 TC 3 的流量量。水位线在每个流量类别上限执行期间的开始时在子端口级别计算,并且相同的值在整个当前执行期间内被所有子端口成员管道使用。下图说明了水位线如何在每个期间开始时从子端口级别传播到所有子端口成员管道。
在当前执行期间的开始(与前一执行期间结束同时),水位线的值根据前一期间开始时分配给 TC 3 的带宽量进行调整,而该量在前一期间结束时未被子端口成员管道使用。
如果存在未使用的子端口 TC 3 带宽,当前期间水位线的值将增加,以鼓励子端口成员管道消耗更多带宽。否则,水位线的值将减少,以强制要求 TC 3 的带宽消耗在子端口成员管道之间平等。
水位线值的增加或减少以小幅度增量进行,因此可能需要多个执行周期才能达到平衡状态。这种状态可能随时发生变化,原因是子端口成员管道对 TC 3 的需求发生变化,例如需求增加时(需要降低水位线)或需求减少时(需要增加水位线)。
需求低时,水位线设置得较高,以防止其阻碍子端口成员管道消耗更多带宽。水位线的最高值是从子端口成员管道配置的最高速率中选择的。
表21.14:水位线从子端口级别传播到每个流量类别上限执行期间的成员管道开始时
No. | Subport Traffic Class Operation | Description |
---|---|---|
1 | Initialization | 子端口级别:subport_period_id = 0 管道级别:pipe_period_id = 0 |
2 | Credit update | 子端口级别:如果(时间 >= subport_tc_time){subport_wm = water_mark_update(); subport_tc_time = time + subport_tc_period; subport_period_id++;} 管道级别:如果(pipe_period_id != subport_period_id){pipe_ov_credits = subport_wm * pipe_weight; pipe_period_id = subport_period_id;} |
3 | Credit consumption (on packet scheduling) | 管道级别:pkt_credits = pk_len + frame_overhead; 如果(pipe_ov_credits >= pkt_credits){pipe_ov_credits -= pkt_credits;} |
表 21.15:水印计算
No. | Subport Traffic Class Operation | Description |
---|---|---|
1 | Initialization | 子端口级别:wm = WM_MAX |
2 | Credit update | 子端口级别(water_mark_update):tc0_cons = subport_tc0_credits_per_period - subport_tc0_credits; tc1_cons = subport_tc1_credits_per_period - subport_tc1_credits; tc2_cons = subport_tc2_credits_per_period - subport_tc2_credits; tc3_cons = subport_tc3_credits_per_period - subport_tc3_credits; tc3_cons_max = subport_tc3_credits_per_period - (tc0_cons + tc1_cons + tc2_cons); 如果(tc3_consumption > (tc3_consumption_max - MTU)){ wm -= wm >> 7; if(wm < WM_MIN) wm = WM_MIN; } else { wm += (wm >> 7) + 1; if(wm > WM_MAX) wm = WM_MAX; } |
21.2.5 性能最差情景
大量活跃队列但信用不足
调度器需要检查大量队列以选择一个包和信用时,其性能会降低。调度器维护活跃队列的位图,跳过非活跃队列,但为了检测特定管道是否有足够的信用,需要使用管道出队状态机深入探查管道,这会消耗周期,而不管调度结果如何(无包生成或至少生成一个包)。这种情况强调了对调度器性能的速度控制的重要性:如果管道没有足够的信用,其数据包应尽快被丢弃(在到达分层调度器之前),从而将管道队列渲染为非活跃状态,允许出队端跳过该管道而无需花费用于调查管道信用的周期,因为这将导致“信用不足”状态。
单个队列达到100%线速率
端口调度器的性能针对大量队列进行了优化。如果队列数较少,则相同活跃流量水平下,端口调度器的性能预计会比小型消息传递队列的性能差。
21.3 丢弃器
DPDK丢弃器的目的
DPDK丢弃器的目的是在分组调度器到达时丢弃分组,以避免拥塞。丢弃器支持随机早期检测(RED)、加权随机早期检测(WRED)和尾部丢弃算法。图21.7说明了丢弃器如何与调度器集成。目前DPDK不支持拥塞管理,因此丢弃器提供了唯一的拥塞避免方法。
图 21.7:DPDK Dropper 的高级框图
丢弃器使用的拥塞避免算法
丢弃器使用文献中记录的随机早期检测(RED)拥塞避免算法。RED算法的目的是监视数据包队列,确定队列的当前拥塞水平,并决定是否应该将到达的数据包入队或丢弃。RED算法使用指数加权移动平均(EWMA)滤波器来计算平均队列大小,从而指示队列的当前拥塞水平。
对于每个入队操作,RED算法将平均队列大小与最小和最大阈值进行比较。根据平均队列大小是低于、高于还是介于这些阈值之间的情况,RED算法计算到达数据包应该被丢弃的概率,并基于这一概率进行随机决策。
丢弃器还通过允许调度器在运行时为同一个数据包队列选择不同的RED配置来支持加权随机早期检测(WRED)。在严重拥塞情况下,丢弃器会使用尾部丢弃。当数据包队列达到最大容量并且无法存储更多数据包时,就会发生尾部丢弃。在这种情况下,所有到达的数据包都会被丢弃。
丢弃器的数据流程如图21.8所示。首先执行RED/WRED算法,其次执行尾部丢弃。
丢弃器支持的使用案例有:
- 初始化配置数据
- 初始化运行时数据
- 入队(决定是将到达的数据包入队还是丢弃)
- 标记为空(记录数据包队列变为空的时间)
配置用例在"配置"中解释,入队操作在"入队操作"中解释,标记为空操作在"队列为空操作"中解释。
21.3.1 配置
RED配置包含表21.16中给出的参数。
表21.16:RED配置参数
Parameter | Minimum | Maximum | Typical |
---|---|---|---|
Minimum Threshold | 0 | 1022 | 1/4 x queue size |
Maximum Threshold | 1 | 1023 | 1/2 x queue size |
Inverse Mark Probability | 1 | 255 | 10 |
EWMA Filter Weight | 1 | 12 | 9 |
这些参数的含义在后续章节中将会详细解释。这些参数按照其指定给丢弃器模块API的格式,对应于思科*在其RED实现中所使用的格式。最小和最大阈值参数以数据包数量的形式提供给丢弃器模块。标记概率参数以逆值指定,例如,标记概率参数值为10对应于标记概率为1/10(即,10个数据包中将丢弃1个)。EWMA滤波器权重参数以逆对数值指定,例如,滤波器权重参数值为9对应于滤波器权重为1/29。
21.3.2 入队操作
在图21.9中所示的示例中,q(实际队列大小)是输入值,avg(平均队列大小)和count(自上次丢弃以来的数据包数量)是运行时值,decision是输出值,其余数值为配置参数。
图 21.8:通过滴管的流量
图 21.9:通过 Dropper 的数据流示例
EWMA滤波器微块
EWMA滤波器微块的目的是对队列大小值进行滤波,以平滑“突发”流量所导致的瞬态变化。输出值为平均队列大小,从而更稳定地反映了队列当前的拥塞水平。
EWMA滤波器具有一个配置参数,即滤波器权重,它决定了平均队列大小输出对实际队列大小变化的快慢响应。滤波器权重值越高,平均队列大小对实际队列大小的变化响应就越快。
队列非空时的平均队列大小计算
EWMA滤波器的定义如下方程所示:
其中:
- avg = 平均队列大小
- wq = 滤波器权重
- q = 实际队列大小
- R = 固定整数,表示除法的位移量
队列为空时的平均队列大小计算
当队列为空时,EWMA滤波器不会读取时间戳,而是假定入队操作会相当定期地发生。当队列变为空时,需要进行特殊处理,因为队列可能短时间或长时间为空。队列变为空时,平均队列大小应该逐渐衰减到零,而不是突然降为零或停留在上次计算的值上。当在空队列上入队一个数据包时,平均队列大小使用以下公式计算:
其中:
- m = 在队列为空时可能发生的入队操作数
在dropper模块中,m被定义为:
其中:
- time = 当前时间
- qtime = 队列变为空的时间
- s = 在该队列上连续入队操作之间的典型时间
时间参考以字节为单位,其中每个字节表示物理接口在传输介质上发送一个字节所需的时间(参见"内部时间参考"部分)。参数s在dropper模块中被定义为一个常数,其值为:s=2^22。这对应于在具有64K个叶节点的层次结构中,每个叶节点传输一个64字节数据包到传输介质上所需的时间,并代表了最坏情况。对于规模较小的调度器层次结构,可能需要减小参数s,在red头文件源文件(rte_red.h)中定义为:
#define RTE_RED_S
由于时间参考是以字节为单位的,因此端口速度被包含在表达式time-qtime中。dropper无需配置实际端口速度,它会自动适应低速和高速链路。
实现
采用数值方法来计算方程2中出现的(1-wq)^m因子。这个方法基于以下恒等式:
这使我们能够表达如下:
在dropper模块中,使用查找表来为dropper模块支持的每个wq值计算log2(1-wq)。然后,可以通过将表值乘以m并进行位移操作来获得(1-wq)m因子。为了避免乘法中的溢出,值m和查找表值都限制在16位。查找表的总大小为56字节。一旦使用这种方法获得了(1-wq)m因子,就可以从方程2中计算出平均队列大小。
替代方法
考虑了其他计算在队列为空时(方程2)计算平均队列大小所需的因子(1-wq)^m的方法。这些方法包括:
- 浮点数计算
- 使用小查找表(512B)和最多16次乘法的定点数计算(这是FreeBSD* ALTQ RED实现中使用的方法)
- 使用小查找表(512B)和16个SSE乘法的定点数计算(FreeBSD* ALTQ RED实现的SSE优化版本)
- 大查找表(76 KB)
最终选择的方法(在上面的“实现”部分中描述)在运行时性能和内存需求方面优于所有这些方法,并且在精度上也达到了与浮点数计算相当的水平。表17列出了这些替代方法相对于dropper使用的方法的性能。可以看到,浮点数实现性能最差。
表21.17:替代方法的相对性能
Method | Relative Performance |
---|---|
Current dropper method (see Dropper) | 100% |
Fixed-point method with small (512B) look-up table | 148% |
SSE method with small (512B) look-up table | 114% |
Large (76KB) look-up table | 118% |
Floating-point | 595% |
注意:
在这种情况下,由于性能是以在特定条件下执行操作所花费的时间来表示的,任何超过100%的相对性能值都比参考方法运行得更慢。
丢弃决策模块
丢弃决策模块:
- 比较平均队列大小与最小和最大阈值
- 计算丢包概率
- 随机决定是否对到达的数据包进行入队或丢弃
丢包概率的计算分为两个阶段。首先根据平均队列大小、最小和最大阈值以及标记概率计算初始丢包概率。然后,从初始丢包概率计算实际丢包概率。实际丢包概率考虑了计数的运行时值,因此随着自上次丢包以来到达队列的数据包越来越多,实际丢包概率也会增加。
初始数据包丢包概率
初始丢包概率使用以下方程计算:
其中:
- maxp = 标记概率
- avg = 平均队列大小
- minth = 最小阈值
- maxth = 最大阈值
方程3中使用平均队列大小计算数据包丢失概率的过程如图21.10所示。如果平均队列大小低于最小阈值,则将到达的数据包入队。如果平均队列大小达到或超过最大阈值,则将到达的数据包丢弃。如果平均队列大小介于最小和最大阈值之间,则计算丢包概率以确定是否应将数据包入队或丢弃。
实际丢包概率
如果平均队列大小介于最小和最大阈值之间,则实际丢包概率由以下方程计算:
其中:
- Pb = 初始丢包概率(来自方程3)
- count = 自上次丢包以来到达的数据包数量
方程4中的常数2是与参考文档给出的丢包概率公式唯一的偏差,参考文档中使用了值1。需要注意的是,从计算得到的pa可能为负值或大于1。如果是这种情况,则应该使用值1。
图21.11显示了初始和实际丢包概率。实际丢包概率分别使用了参考文档中给出的公式(蓝色曲线)和dropper模块中实现的公式(红色曲线)。与用户指定的标记概率配置参数相比,参考文档中的公式导致了明显更高的丢包率。与参考文档的偏差只是一个设计决策,其他RED实现(例如FreeBSD* ALTQ RED)也做出了类似的选择。
图 21.10:给定 RED 配置的数据包丢弃概率
图 21.11:使用 a 计算的初始掉落概率 (pb)、实际掉落概率 (pa)
因子 1(蓝色曲线)和因子 2(红色曲线)
21.3.3 队列为空操作
记录数据包队列变为空的时间,并将其保存到RED运行时数据中,以便EWMA过滤器块在下一次入队操作时计算平均队列大小。通过API通知dropper模块队列已经变为空是调用应用程序的责任。
21.3.4 源文件位置
DPDK dropper的源文件位于:
- DPDK/lib/librte_sched/rte_red.h
- DPDK/lib/librte_sched/rte_red.c
21.3.5 与DPDK QoS调度器的集成
DPDK QoS调度器中的RED功能默认情况下是禁用的。要启用它,请使用DPDK配置参数:
CONFIG_RTE_SCHED_RED=y
必须将此参数设置为y。该参数位于DPDK/config目录中的构建配置文件中,例如,DPDK/config/common_linuxapp。在初始化过程中,RED配置参数在传递给调度器的rte_sched_port_params结构中的rte_red_params结构中进行指定。RED参数分别针对四个流量类别和三种数据包颜色(绿色、黄色和红色)进行指定,允许调度器实现加权随机早期检测(WRED)。
21.3.6 与DPDK QoS调度器示例应用程序的集成
DPDK QoS调度器应用程序在启动时读取一个配置文件。配置文件包括一个包含RED参数的部分。这些参数的格式在“配置”中有描述。下面是一个示例RED配置。在此示例中,队列大小为64个数据包。
注意:为了正确运行,应该在相同流量类别(tc)中的每种数据包颜色(绿色、黄色、红色)中使用相同的EWMA过滤器权重参数(wred weight)。
; RED params per traffic class and color (Green / Yellow / Red)
[red]
tc 0 wred min = 28 22 16
tc 0 wred max = 32 32 32
tc 0 wred inv prob = 10 10 10
tc 0 wred weight = 9 9 9
tc 1 wred min = 28 22 16
tc 1 wred max = 32 32 32
tc 1 wred inv prob = 10 10 10
tc 1 wred weight = 9 9 9
tc 2 wred min = 28 22 16
tc 2 wred max = 32 32 32
tc 2 wred inv prob = 10 10 10
tc 2 wred weight = 9 9 9
tc 3 wred min = 28 22 16
tc 3 wred max = 32 32 32
tc 3 wred inv prob = 10 10 10
tc 3 wred weight = 9 9 9
通过此配置文件,适用于绿色、黄色和红色数据包的 RED 配置
流量类别 0 的情况如表 18 所示。
表 21.18:与 RED 配置相对应的 RED 配置文件
Parameter Name | Green | Yellow | Red |
---|---|---|---|
Minimum Threshold | 28 | 22 | 16 |
Maximum Threshold | 32 | 32 | 32 |
Mark Probability | 10 | 10 | 10 |
EWMA Filter Weight | 9 | 9 | 9 |
21.3.7 应用程序编程接口(API)
Enqueue API
enqueue API 的语法如下:
int rte_red_enqueue(const struct rte_red_config *red_cfg,
struct rte_red *red,
const unsigned q,
const uint64_t time)
传递给 enqueue API 的参数包括配置数据、运行时数据、数据包队列的当前大小(以数据包为单位)以及表示当前时间的值。时间参考以字节为单位,其中一个字节表示物理接口在传输介质上发送一个字节所需的时间持续。(请参阅内部时间参考部分)。为了性能考虑,丢弃器重用调度器的时间戳。
Empty API
empty API 的语法如下:
void rte_red_mark_queue_empty(struct rte_red *red, const uint64_t time)
传递给 empty API 的参数包括运行时数据和以字节表示的当前时间。
21.4 交通计量
交通计量组件实现了由 IETF RFC 2697 和 2698 定义的单速率三色标记器(srTCM)和双速率三色标记器(trTCM)算法。这些算法根据预先为每个流量流定义的允许量来测量传入数据包流。因此,每个传入的数据包根据其所属流的监测消耗被标记为绿色、黄色或红色。
21.4.1 功能概述
srTCM 算法为每个流量流定义了两个令牌桶,这两个桶共享相同的令牌更新速率:
• 承诺(C)桶:以承诺信息速率(CIR)参数定义的速率提供令牌(以每秒 IP 数据包字节为单位)。C 桶的大小由承诺突发大小(CBS)参数定义(以字节为单位);
• 过剩(E)桶:以与 C 桶相同的速率提供令牌。E 桶的大小由过剩突发大小(EBS)参数定义(以字节为单位)。
trTCM 算法为每个流量流定义了两个令牌桶,这两个桶以独立的速率更新令牌:
• 承诺(C)桶:以承诺信息速率(CIR)参数定义的速率提供令牌(以每秒 IP 数据包字节为单位)。C 桶的大小由承诺突发大小(CBS)参数定义(以字节为单位);
• 峰值(P)桶:以峰值信息速率(PIR)参数定义的速率提供令牌(以每秒 IP 数据包字节为单位)。P 桶的大小由峰值突发大小(PBS)参数定义(以字节为单位)。
请参阅 RFC 2697(用于 srTCM)和 RFC 2698(用于 trTCM)以获取有关如何从桶中消耗令牌以及确定数据包颜色的详细信息。
色盲和色感知模式
对于这两种算法,色盲模式在功能上等同于输入颜色设置为绿色的色感知模式。对于色感知模式,具有红色输入颜色的数据包只能获得红色输出颜色,而具有黄色输入颜色的数据包只能获得黄色或红色输出颜色。
色盲模式仍然与色感知模式有明显区别的原因是,与色感知模式相比,色盲模式可以用更少的操作来实现。
21.4.2 实现概述
对于每个输入数据包,srTCM / trTCM 算法的步骤如下:
• 更新 C 和 E / P 令牌桶。这是通过读取当前时间(从 CPU 时间戳计数器获取),识别自上次桶更新以来的时间量,并根据预先配置的桶速率计算关联的令牌数量来完成的。桶中的令牌数量受到预先配置的桶大小的限制;
• 根据 IP 数据包的大小和 C 和 E / P 令牌桶中当前可用的令牌数量,确定当前数据包的输出颜色;对于仅色感知模式,还考虑数据包的输入颜色。当输出颜色不为红色时,从 C 或 E / P 桶中减去与 IP 数据包长度相等的令牌数量,取决于算法和数据包的输出颜色。
POWER MANAGEMENT
DPDK 的电源管理功能允许用户空间应用程序通过动态调整 CPU 频率或进入不同的 C 状态来节省电力。
• 根据 RX 队列的利用率动态调整 CPU 频率。
• 根据自适应算法进入不同的深度 C 状态,用于猜测短暂的挂起应用程序的时间,如果未收到数据包。
用于调整操作 CPU 频率的接口位于电源管理库中。C 状态控制根据不同的使用情况在应用程序中实现。
22.1 CPU 频率调节
Linux 内核为每个逻辑核心提供了一个 cpufreq 模块用于 CPU 频率调节。例如,对于 cpuX,/sys/devices/system/cpu/cpuX/cpufreq/ 包含以下与频率调节相关的 sys 文件:
• affected_cpus
• bios_limit
• cpuinfo_cur_freq
• cpuinfo_max_freq
• cpuinfo_min_freq
• cpuinfo_transition_latency
• related_cpus
• scaling_available_frequencies
• scaling_available_governors
• scaling_cur_freq
• scaling_driver
• scaling_governor
• scaling_max_freq
• scaling_min_freq
• scaling_setspeed
在 DPDK 中,scaling_governor 在用户空间中配置。然后,用户空间应用程序可以通过写入 scaling_setspeed 来提示内核根据用户空间应用程序定义的策略调整 CPU 频率。
22.2 通过 C 状态对核心负载进行节流
当指定的逻辑核心没有任务时,核心状态可以通过猜测性的睡眠来改变。在 DPDK 中,如果轮询后未收到任何数据包,则可以根据用户空间应用程序定义的策略触发猜测性睡眠。
22.3 电源库的 API 概述
电源库导出的主要方法是用于 CPU 频率调节的,包括以下内容:
• Freq up:提示内核增加特定逻辑核心的频率。
• Freq down:提示内核降低特定逻辑核心的频率。
• Freq max:提示内核将特定逻辑核心的频率调至最大。
• Freq min:提示内核将特定逻辑核心的频率调至最小。
• Get available freqs:从 sys 文件中读取特定逻辑核心的可用频率。
• Freq get:获取特定逻辑核心的当前频率。
• Freq set:提示内核设置特定逻辑核心的频率。
22.4 用户案例
电源管理机制用于在执行 L3 转发时节省功耗。
22.5 参考资料
• l3fwd-power:DPDK 中执行带有电源管理的 L3 转发的示例应用程序。
• DPDK 示例应用程序用户指南中的“带有电源管理的 L3 转发示例应用程序”章节。
PACKET CLASSIFICATION AND ACCESS CONTROL
DPDK 提供了一个访问控制库,它具有基于一组分类规则对输入数据包进行分类的能力。
ACL 库用于在具有多个类别的一组规则上执行 N 元搜索,并为每个类别找到最佳匹配(最高优先级)。库 API 提供了以下基本操作:
• 创建新的访问控制(AC)上下文。
• 将规则添加到上下文中。
• 对上下文中的所有规则,构建执行数据包分类所需的运行时结构。
• 执行输入数据包的分类。
• 销毁 AC 上下文及其运行时结构,并释放相关内存。
23.1 概述
23.1.1 规则定义
当前实现允许用户为每个 AC 上下文指定其自己的规则(字段集),以执行数据包分类。尽管规则字段布局上有一些限制:
• 规则定义中的第一个字段必须是一个字节长。
• 所有后续字段必须分组为连续 4 个字节的集合。
这主要是出于性能考虑 - 搜索函数将第一个输入字节作为流程设置的一部分处理,然后搜索函数的内部循环展开为一次处理四个输入字节。
为定义 AC 规则内的每个字段,使用以下结构:
struct rte_acl_field_def {
uint8_t type; /*< type - ACL_FIELD_TYPE. */
uint8_t size; /*< size of field 1,2,4, or 8. */
uint8_t field_index; /*< index of field inside the rule. */
uint8_t input_index; /*< 0-N input index. */
uint32_t offset; /*< offset to start of field. */
};
• 类型:字段类型有三种选择之一:
– _MASK - 用于具有值和定义相关位数的掩码的字段,例如 IP 地址。
– _RANGE - 用于具有字段的下限和上限值的字段,例如端口。
– _BITMASK - 用于具有值和位掩码的协议标识符字段。
• 大小:大小参数定义字段的字节长度。允许的值为 1、2、4 或 8 字节。注意,由于输入字节的分组,1 或 2 字节字段必须定义为组成 4 个连续输入字节的连续字段。此外,最好将 8 字节或更多字节的字段定义为 4 字节字段,以便构建过程可以消除所有为通配符的字段。
• 字段索引:表示规则内字段位置的从零开始的值;对于 N 个字段,范围为 0 到 N-1。
• 输入索引:如上所述,除了第一个字段外,所有输入字段必须分组为 4 个连续字节。输入索引指定该字段属于哪个输入组。
• 偏移量:偏移字段定义字段的偏移量。这是从搜索的 buffer 参数开始的偏移量。
例如,要定义以下 IPv4 5-tuple 结构的分类:
struct ipv4_5tuple {
uint8_t proto;
uint32_t ip_src;
uint32_t ip_dst;
uint16_t port_src;
uint16_t port_dst;
};
可以使用以下字段定义数组:
#include <rte_acl.h> // 假设相关的头文件
// 定义一个包含5个 rte_acl_field_def 结构的数组,用于 ACL 匹配
struct rte_acl_field_def ipv4_defs[5] = {
/* 第一个输入字段 - 总是一个字节长。 */
{
.type = RTE_ACL_FIELD_TYPE_BITMASK,
.size = sizeof(uint8_t),
.field_index = 0,
.input_index = 0,
.offset = offsetof(struct ipv4_5tuple, proto),
},
/* 下一个输入字段(IPv4 源地址)- 连续的4个字节。 */
{
.type = RTE_ACL_FIELD_TYPE_MASK,
.size = sizeof(uint32_t),
.field_index = 1,
.input_index = 1,
.offset = offsetof(struct ipv4_5tuple, ip_src),
},
/* 下一个输入字段(IPv4 目标地址)- 连续的4个字节。 */
{
.type = RTE_ACL_FIELD_TYPE_MASK,
.size = sizeof(uint32_t),
.field_index = 2,
.input_index = 2,
.offset = offsetof(struct ipv4_5tuple, ip_dst),
},
/*
* 接下来的2个字段(源和目标端口)共同占用4个连续字节。
* 它们共享相同的输入索引。
*/
{
.type = RTE_ACL_FIELD_TYPE_RANGE,
.size = sizeof(uint16_t),
.field_index = 3,
.input_index = 3,
.offset = offsetof(struct ipv4_5tuple, port_src),
},
{
.type = RTE_ACL_FIELD_TYPE_RANGE,
.size = sizeof(uint16_t),
.field_index = 4,
.input_index = 3,
.offset = offsetof(struct ipv4_5tuple, port_dst),
},
};
这种 IPv4 5 元组规则的典型示例如下:
source addr/mask destination addr/mask source ports dest ports protocol/mask
192.168.1.0/24 192.168.2.31/32 0:65535 1234:1234 17/0xff
协议 ID 17 (UDP)、源地址 192.168.1.[0-255]、目标地址的任何 IPv4 数据包
地址192.168.2.31,源端口[0-65535]和目标端口1234与上面匹配规则。
要定义 IPv6 2 元组的分类:<协议,IPv6 源地址> 基于以下内容
IPv6 标头结构:
struct struct ipv6_hdr {
uint32_t vtc_flow; /* IP version, traffic class & flow label. */
uint16_t payload_len; /* IP packet length - includes sizeof(ip_header). */
uint8_t proto; /* Protocol, next header. */
uint8_t hop_limits; /* Hop limits. */
uint8_t src_addr[16]; /* IP address of source host. */
uint8_t dst_addr[16]; /* IP address of destination host(s). */
} __attribute__((__packed__));
可以使用以下字段定义数组:
#include <rte_acl.h> // 可能需要包含相关的头文件
// 定义一个包含5个 rte_acl_field_def 结构的数组,用于 IPv6 两元组匹配
struct rte_acl_field_def ipv6_2tuple_defs[5] = {
/* 第一个输入字段 - 总是一个字节长,用于IPv6协议类型。 */
{
.type = RTE_ACL_FIELD_TYPE_BITMASK,
.size = sizeof(uint8_t),
.field_index = 0,
.input_index = 0,
.offset = offsetof(struct ipv6_hdr, proto),
},
/* 下一个输入字段(IPv6源地址的前4个字节)。 */
{
.type = RTE_ACL_FIELD_TYPE_MASK,
.size = sizeof(uint32_t),
.field_index = 1,
.input_index = 1,
.offset = offsetof(struct ipv6_hdr, src_addr[0]),
},
/* 接下来的输入字段(IPv6源地址的第5至第8字节)。 */
{
.type = RTE_ACL_FIELD_TYPE_MASK,
.size = sizeof(uint32_t),
.field_index = 2,
.input_index = 2,
.offset = offsetof(struct ipv6_hdr, src_addr[4]),
},
/* 后续的输入字段(IPv6源地址的第9至第12字节)。 */
{
.type = RTE_ACL_FIELD_TYPE_MASK,
.size = sizeof(uint32_t),
.field_index = 3,
.input_index = 3,
.offset = offsetof(struct ipv6_hdr, src_addr[8]),
},
/* 最后的输入字段(IPv6源地址的第13至第16字节)。 */
{
.type = RTE_ACL_FIELD_TYPE_MASK,
.size = sizeof(uint32_t),
.field_index = 4,
.input_index = 4,
.offset = offsetof(struct ipv6_hdr, src_addr[12]),
},
};
这种 IPv6 2 元组规则的典型示例如下:
source addr/mask protocol/mask
2001:db8:1234:0000:0000:0000:0000:0000/48 6/0xff
任何 IPv6 数据包的协议 ID 为 6(TCP),且源地址位于范围 [2001:db8🔢0000:0000:0000:0000:0000 - 2001:db8🔢ffff:ffff:ffff:ffff:ffff] 内,都符合上述规则。
在创建一组规则时,对于每个规则,还必须提供额外的信息:
• 优先级:用于衡量规则优先级的权重(值越高越好)。如果输入元组匹配多个规则,则返回优先级较高的规则。请注意,如果输入元组匹配多个具有相同优先级的规则,则未定义将返回哪个规则作为匹配。建议为每个规则分配唯一的优先级。
• 类别掩码:每个规则使用位掩码值来选择规则的相关类别。当执行查找时,将为每个类别返回结果。这有效地通过使单个搜索能够返回多个结果来提供“并行查找”,例如,如果有四个不同的 ACL 规则集,一个用于访问控制,一个用于路由等。每个集合可以分配其自己的类别,并通过将它们合并为单个数据库,使单个查找返回每个集合的结果。
• 用户数据:一个用户定义的字段,可以是除零之外的任何值。对于每个类别,成功匹配将返回具有最高优先级的匹配规则的 userdata 字段。
注意:将新规则添加到 ACL 上下文时,所有字段必须是主机字节顺序(LSB)。当对输入元组执行搜索时,元组中的所有字段必须是网络字节顺序(MSB)。
23.1.2 RT 内存大小限制
构建阶段(rte_acl_build())为给定规则集创建用于进一步运行时遍历的内部结构。使用当前实现,它是一组多位 Trie(stride == 8)。根据规则集,这可能会消耗大量内存。为了节省一些空间,ACL 构建过程尝试将给定的规则集拆分为几个不相交的子集,并为每个子集构建单独的 Trie。根据规则集,这可能会减少 RT 内存需求,但可能会增加分类时间。在构建时,可以为给定 AC 上下文的内部 RT 结构指定最大内存限制。可以通过 rte_acl_config 结构的 max_size 字段来完成。将其设置为大于零的值,指示 rte_acl_build():
• 尝试最小化 RT 表中 Trie 的数量,但
• 确保 RT 表的大小不超过给定值。
将其设置为零会使 rte_acl_build() 使用默认行为:尝试最小化 RT 结构的大小,但不会施加任何硬限制。
这使用户能够自行决定性能/空间折衷的方案。例如:
#include <rte_acl.h> // 可能需要包含相关的头文件
struct rte_acl_ctx *acx; // 定义指向 ACL 上下文的指针
struct rte_acl_config cfg; // 定义 ACL 配置结构体
int ret; // 定义用于存储函数返回值的变量
/*
* 假设 acx 指向已创建并填充了规则的 ACL 上下文,
* 并且 cfg 被正确填充。
*/
/* 尝试构建 ACL 上下文,RT 结构小于 8MB。*/
cfg.max_size = 0x800000; // 设置最大大小为 8MB
ret = rte_acl_build(acx, &cfg); // 尝试构建 ACL 上下文
/*
* 如果给定上下文的 RT 结构超出了 8MB。
* 尝试构建时不暴露任何硬限制。
*/
if (ret == -ERANGE) {
cfg.max_size = 0; // 设置最大大小为 0,不暴露硬限制
ret = rte_acl_build(acx, &cfg); // 再次尝试构建 ACL 上下文
}
23.1.3 分类方法
在给定的 AC 上下文上成功完成 rte_acl_build() 后,可以用于执行分类 - 在输入数据上搜索具有最高优先级的规则。有几种分类算法的实现:
• RTE_ACL_CLASSIFY_SCALAR:通用实现,不需要任何特定的硬件支持。
• RTE_ACL_CLASSIFY_SSE:矢量实现,可以并行处理多达 8 个流。需要 SSE 4.1 支持。
• RTE_ACL_CLASSIFY_AVX2:矢量实现,可以并行处理多达 16 个流。需要 AVX2 支持。
纯粹是一个运行时的决策选择哪种方法,没有构建时的差异。所有实现都在相同的内部 RT 结构上操作,并使用类似的原理。主要区别在于矢量实现可以手动利用 IA SIMD 指令,并并行处理多个输入数据流。在启动时,ACL 库确定给定平台的最高可用分类方法,并将其设置为默认方法。尽管用户可以覆盖给定 ACL 上下文的默认分类器函数或使用非默认的分类方法执行特定搜索。在这种情况下,用户有责任确保给定平台支持所选的分类实现。
23.2 应用程序编程接口(API)使用
注意:有关访问控制 API 的更多详细信息,请参阅 DPDK API 参考。
以下示例更详细地演示了上述定义的带有多个类别的 IPv4 5-tuple 分类规则的示例。
23.2.1 多类别分类
#include <rte_acl.h> // 可能需要包含相关的头文件
struct rte_acl_ctx *acx; // 定义指向 ACL 上下文的指针
struct rte_acl_config cfg; // 定义 ACL 配置结构体
int ret; // 用于存储函数返回值的变量
/* 定义一个包含多达5个字段的规则结构。*/
RTE_ACL_RULE_DEF(acl_ipv4_rule, RTE_DIM(ipv4_defs));
/* AC 上下文创建参数。*/
struct rte_acl_param prm = {
.name = "ACL_example", // ACL 上下文的名称
.socket_id = SOCKET_ID_ANY, // 指定的套接字 ID
.rule_size = RTE_ACL_RULE_SZ(RTE_DIM(ipv4_defs)), // 每个规则的大小
.max_rule_num = 8, // AC 上下文中规则的最大数量
};
/* 定义 ACL 规则数组。*/
struct acl_ipv4_rule acl_rules[] = {
/* 匹配前往 192.168.0.0/16 的所有数据包,适用于类别:0,1 */
{
.data = {.userdata = 1, .category_mask = 3, .priority = 1},
/* 目标 IPv4 */
.field[2] = {.value.u32 = IPv4(192,168,0,0), .mask_range.u32 = 16,},
/* 源端口 */
.field[3] = {.value.u16 = 0, .mask_range.u16 = 0xffff,},
/* 目标端口 */
.field[4] = {.value.u16 = 0, .mask_range.u16 = 0xffff,},
},
// 其他规则...
// 依次添加更多规则...
};
/* 创建一个空的 AC 上下文 */
if ((acx = rte_acl_create(&prm)) == NULL) {
/* 处理上下文创建失败的情况。*/
}
/* 向上下文中添加规则 */
ret = rte_acl_add_rules(acx, acl_rules, RTE_DIM(acl_rules));
if (ret != 0) {
/* 处理添加 ACL 规则时的错误。*/
}
/* 准备 AC 构建配置。*/
cfg.num_categories = 2; // 规定的类别数量
cfg.num_fields = RTE_DIM(ipv4_defs); // 规则中字段的数量
memcpy(cfg.defs, ipv4_defs, sizeof(ipv4_defs)); // 复制字段定义
/* 构建已添加规则的运行时结构,使用2个类别。 */
ret = rte_acl_build(acx, &cfg);
if (ret != 0) {
/* 处理为 ACL 上下文构建运行时结构时的错误。*/
}
对于源 IP 地址:10.1.1.1 和目标 IP 地址:192.168.1.15 的元组,一次
执行以下几行:
uint32_t results[4]; /* make classify for 4 categories. */
rte_acl_classify(acx, data, results, 1, 4);
那么 results[] 数组包含:
results[4] = {2, 3, 0, 0};
• 对于类别 0,规则 1 和规则 2 都匹配,但规则 2 优先级更高,因此 results[0] 包含规则 2 的 userdata。
• 对于类别 1,规则 1 和规则 3 都匹配,但规则 3 优先级更高,因此 results[1] 包含规则 3 的 userdata。
• 对于类别 2 和 3,没有匹配,因此 results[2] 和 results[3] 包含零,表示这些类别没有匹配项。
对于源 IP 地址为 192.168.1.1 和目标 IP 地址为 192.168.2.11 的元组,
一旦执行以下行:
uint32_t results[4]; /* make classify by 4 categories. */
rte_acl_classify(acx, data, results, 1, 4);
the results[] array contains:
results[4] = {1, 1, 0, 0};
• 对于类别 0 和 1,只有规则 1 匹配。
• 对于类别 2 和 3,没有匹配项。
对于源 IP 地址为 10.1.1.1 和目标 IP 地址为 201.212.111.12 的元组,
一旦执行以下行:
uint32_t results[4]; /* make classify by 4 categories. */
rte_acl_classify(acx, data, results, 1, 4);
the results[] array contains:
results[4] = {0, 3, 0, 0};
• 对于类别 1,只有规则 3 匹配。
• 对于类别 0、2 和 3,没有匹配项。
24.1 设计目标
DPDK 数据包框架的主要设计目标包括:
• 提供标准方法来构建复杂的数据包处理流水线。为常用的流水线功能模块提供可重用和可扩展的模板;
• 允许在同一个流水线功能模块中切换纯软件和硬件加速的实现方式;
• 在灵活性和性能之间寻求最佳平衡。硬编码的流水线通常提供最佳性能,但不灵活,而开发灵活的框架通常不成问题,但性能通常较低;
• 提供一个在逻辑上类似于 Open Flow 的框架。
24.2 概述
数据包处理应用通常被构建为多个阶段的流水线,每个阶段的逻辑都围绕着查找表进行。对于每个传入的数据包,该表定义了要应用于数据包的一组操作,以及发送数据包到下一个阶段的逻辑。
DPDK 数据包框架通过定义流水线开发的标准方法以及提供可重用模板库,最大限度地减少了构建数据包处理流水线所需的开发工作量。
流水线通过将一组输入端口与一组输出端口通过一组表连接在树状拓扑中构建。作为当前数据包在当前表中查找操作的结果,表的一个条目(查找命中时)或默认表条目(查找未命中时)提供了应用于当前数据包的一组操作,以及数据包的下一跳,可以是另一个表、一个输出端口或数据包丢弃。
数据包处理流水线的示例如图 24.1 所示:
图 24.1:连接输入端口 0 和 1 的数据包处理管道示例
具有输出端口 0、1 和 2 至表 0 和 1
24.3 端口库设计
24.3.1 端口类型
表 19 是可以使用数据包框架实现的端口的非详尽列表。
表 24.1:端口类型