因为项目需要做Traffic Shaping,看了下DPDK的QoS框架,做一下简单翻译以加深学习理解。这篇翻译基于DPDK 21.02 版本,介绍了DPDK QoS 分级调度模块的详细设计、数据结构、状态机等。本系列一共4篇文章,这是第三篇。原文链接:DPDK Quality of Service (QoS) Framework。
1. 端口内部数据结构
下图展示了内部数据结构的细节。
# | 数据结构 | 大小(字节) | 数量 | 入队访问类型 | 出队访问类型 | 描述 |
---|---|---|---|---|---|---|
1 | 子端口表项(subport table entry) | 64 | 每个端口下的子端口数 | · | 读,写 | 子端口的配置数据(credit等)。credit可以理解为当前可用带宽的抽象,令牌桶以credit为单位做流控。 |
2 | 管道表项(pipe table entry) | 64 | 每个端口下的管道数 | · | 读,写 | 管道的配置数据,管道下面的TC和队列(credit等)有可能会在运行时被更新。 管道配置参数在运行时是不可变的。管道配置参数并不是管道表项的一部分,因为相同的管道配置参数可以被多个管道共享。 |
3 | 队列表项(queue table entry) | 4 | 每个端口下的队列数 | 读,写 | 读,写 | 队列的配置数据(读写指针)。对于所有队列,每个TC的队列大小是相同的,允许使用快速公式计算队列的基地址,因此这两个参数不是队列表项的一部分。 任何给定管道的队列表项都存储在相同的cache line中。cache line对齐可以很好的优化CPU缓存加载速度和减少cache miss。 |
4 | 队列存储区(queue storage area) | 可配(默认:64×8) | 每个端口下的队列数 | 写 | 读 | 每个队列的元素数组;每个元素的大小为8字节(mbuf指针)。 |
5 | 活跃队列位图(active queues bitmap) | 每队列1比特 | 1 | 写(设置) | 读,写(清除) | Bitmap为每个队列维护一个状态位: 非活跃队列(队列为空)或活跃队列(队列不为空)。 状态位由调度程序在入队时置位,如果出队时队列变为空,调度程序负责将状态位清除。 Bitmap扫描操作可以返回下一个非空管道及其状态(管道中活跃队列的16位状态掩码)。 |
6 | 处理区(grinder) | 约128 | 可配(默认:8) | · | 读,写 | 当前正在处理的管道的简短名单。处理区包含管道处理过程中的临时数据。 一旦当前管道处理完了所有数据包,或者消耗完了credit,它就被位图中的另一个活动管道所替换。 |
2. 多核扩展策略
- 不同的线程处理不同的物理端口,同一个端口的入队和出队由同一个线程运行。
- 通过在不同的线程上处理同一物理端口(虚拟端口)的不同子端口集,将同一物理端口划分到不同的线程。类似地,可以将一个子端口拆分为多个子端口,每个子端口由不同的线程运行。同一个端口的入队和出队由同一个线程运行。只有当出于性能原因,不可能用单个CPU核处理端口上的所有流量时,才需要考虑这种处理方式。
2.1. 同一个发送端口的入队和出队
在不同的CPU核上处理同一个发送端口的入队/出队操作可能会对调度器的性能造成重大影响,因此不建议这样做。
端口入队/出队操作需要共享对以下数据结构的访问:
- 包描述符
- 队列表项
- 队列存储区
- 活动队列Bitmap
以下操作会造成预期的性能下降:
- 需要使用锁原语(例如,自旋锁/信号量)保证队列和Bitmap操作的线程安全,或者使用原子操作原语进行无锁访问(例如,TAS、CAS等),前者对性能的影响要大得多。
- 多核共享数据结构造成的cache line乒乓效应(和CPU硬件支持的MESI缓存一致性协议有关)。多核对同一个cache line同时进行CAS等操作会造成反复互相使对方的cache line无效。
因此,调度程序的入队和出队操作必须在同一个线程中运行,从而允许队列和Bitmap操作可以是非线程安全的,并保持调度程序的数据结构在同一个CPU核内部。尽量将数据结构局部化,避免跨线程共享。
2.2. 可扩展性
增加NIC端口的数量仅仅需要按比例增加用于流量调度的CPU核的数量。
3. 入队流水线
包处理步骤如下:
- 读取mbuf中用于识别数据包的目标队列所需的字段,这些字段可以是端口、子端口、TC和TC中的队列,通常在classification阶段设置。
- 在队列数据结构中标识当前可写的位置,如果队列已满,则丢弃该报文。
- 将mbuf指针存储在队列数组中。
需要注意的是,这些步骤之间有很强的依赖关系,在步骤1和步骤2的结果可用之前,步骤2和步骤3不能启动,因此没法利用处理器乱序执行引擎提供的任何性能优化。
当有大量的数据包和队列需要处理时,很有可能处理当前包所需要的数据结构不在L1和L2缓存中,因此上述3个步骤对内存的访问结果会造成L1/L2 cache miss。考虑到性能因素,每个数据包都造成3次L1/L2 cache miss是不可接受的。
解决方案是提前预取需要的数据结构。预取操作有执行延迟,在此期间处理器不应该尝试访问当前正在预取的数据结构,因此处理器应该执行其他工作。唯一可做的其他工作是对其他输入包执行入队操作流程的不同步骤,从而以流水线的方式处理入队操作。
图2演示了入队操作的流水线实现,其中有4个流水线阶段,每个阶段处理2个不同的输入包。在给定的时间内,任何输入包都不能成为多个流水线阶段的一部分。
上面描述了非常基本的入队流水线拥塞管理方案:将数据包塞入队列,当队列被填满时,所有到达同一队列的数据包将被丢弃,直到有数据包通过出队操作被消费掉,才可以在队列中塞入新的数据包。如果用RED/WRED改进入队流水线,就可以通过查看队列占用率和包的优先级,为特定的包做出入队/丢弃决策(而不是不加选择地将所有包入队或者丢弃)。
4. 出队状态机
在当前管道调度处理下一个包的步骤为:
- 使用Bitmap扫描操作识别并预取下一个活动管道。
- 读取管道数据结构,更新当前管道及其子端口的credit,识别当前管道中的第一个活跃流量组,使用WRR选择下一个队列,预取当前管道中所有16个队列的队列指针。
- 从当前WRR队列中读取下一个元素并预取其包描述符。
- 从包描述符(mbuf)中读取包长度,根据包长度和credit(当前管道、管道流量组、子端口和子端口流量组分别有不同的设置),对当前包进行入队/丢包调度决策。
以上操作涉及到的数据结构(pipe, queue, queue array, mbufs)可以在被访问之前预取进缓存,从而避免cache miss。在对当前管道(管道A)的数据发出预取操作后,可以立即切换到另一个管道(管道B)进行处理,等到管道A的预取操作完成,再切换回去,从而利用预取本身的时延同步处理多个管道的数据。
出队状态机利用处理器缓存机制,尽可能同时处理多个活跃管道,从而发送更多的数据包。
5. 定时与同步
输出端口就是一个被调度器不断的用待传输数据填充的队列。对于10 GbE,每秒有12.5亿个字节需要被端口调度器填充。如果调度器的速度不够快,不能填满队列(假设有足够的待传输数据包和credit),那么必然有一些带宽会被闲置,从而产生浪费。
原则上,分层调度器出队操作应该由网卡发送器触发。通常情况下,一旦网卡发送队列的输入流量低于一个预定义的阈值,端口调度程序将被唤醒(基于中断或轮询的方式,通过持续监控队列占用决定是否被唤醒),填充更多的数据包进入队列。
5.1. 内部时间参考值
调度器需要跟踪随着时间推移而变化的credit信息,这需要基于时间进行credit的更新(例如,子端口和管道流量整形、流量组上限的强制执行,等等)。
每次调度器决定向网卡发送器传递数据包时,调度器将相应地调整其内部时间信息。因为在物理接口上每发送一个字节所需的时间是固定的,所以可以以字节为单位作为内部时间的参考值,这样处理起来也会比较方便。当一个包准备传输时,时间以(n + h)的形式递增,其中n是以字节为单位的包长度,h是每个包的帧数据字节数(包括帧头、CRC等)。
5.2. 内部时间参考值的再同步
调度器需要将其内部时间参考值与端口速率对齐,从而确保调度器不会填充超过网卡发送速率的数据,从而防止由于网卡发生队列已满而发生的丢包。
调度器需要读取每次出队调用的当前时间。CPU时间戳可以通过读取TSC寄存器(Time Stamp Counter)或HPET寄存器(High Precision Event Timer)来获得。当前CPU时间戳需要通过以下公式将CPU时钟转换为字节数:time_bytes = time_cycles / cycles_per_byte, cycles_per_byte是传输一个字节所消耗的CPU周期(例如,10GbE的端口以2GHz的CPU频率传输数据,cycles_per_byte = 2G/(10G/8) = 1.6)。
调度器会维护针对网卡时间的内部时间引用,网卡时间将随着被调度的数据包的长度(包含帧开销)的增加而增加。在每次出队调用时,调度程序会根据当前时间检查其对网卡时间的内部引用:
- 如果网卡时间为未来时间(网卡时间>=当前时间),则不需要调整网卡时间。这意味着调度器能够在网卡实际需要这些数据包之前准备好数据包,因此网卡发送器可以很好地处理数据包;
- 如果网卡时间是过去时间(网卡时间<当前时间),则网卡时间应调整为当前时间。这意味着调度器无法跟上网卡发送的速度,因此网卡发送器的数据包供应不足,网卡带宽被浪费了。
5.3. 调度器的准确性和粒度
调度器往返延迟(SRTD,Scheduler Round Trip Delay)是调度器连续两次检查同一管道之间的时间(以CPU周期为单位)。
为了跟上发送端口的速率(即避免网卡带宽的浪费),调度器应该能够比网卡发送器实际传输速度更快地调度数据包。
假设没有发生端口带宽超卖,调度器需要根据配置的令牌桶,跟踪每个管道的速率。这意味着管道的令牌桶的大小应该设置得足够高,以防止它因较大的SRTD而溢出,从而导致管道的可用带宽损失。
6. Credit处理逻辑
6.1. 调度决策
当以下所有条件都满足时,调度器可以为(子端口S、管道P、流量组TC、队列Q)做出发送下一个包的合理调度决策:
- 子端口S的管道P被一个端口处理区选中;
- 流量组TC是管道P中优先级最高的活跃流量组;
- 队列Q是由WRR算法在流量组TC内选择的下一个活跃队列;
- 子端口S有足够的credit用来发送数据包;
- 子端口S有足够的credit供流量组TC发送数据包;
- 管道P有足够的credit用来发送数据包;
- 管道P有足够的credit供流量组TC发送数据包。
如果以上条件都满足,则可以选择一个数据包进行传输,并从子端口S、子端口S的流量组TC、管道P、管道P的流量组TC中减去必要的credit。
6.2. 帧开销
由于所有数据包长度的最大公约数是一个字节,因此以字节作为credit的计量单位。传输n字节的数据包所需的credit数等于(n+h),其中h等于每个数据包的帧开销(以字节为单位)。
# | 帧字段 | 长度(字节) | 注释 |
---|---|---|---|
1 | 前缀(Preamble) | 7 | |
2 | 帧起始符(SFD,Start of Frame Delimiter) | 1 | |
3 | 帧校验序列(FCS,Frame Check Sequence) | 4 | 仅当这个字段没有被包含在mbuf包长度中时才需要被计算。 |
4 | 帧间隔(IFG,Inter Frame Gap) | 12 | |
5 | 总计 | 24 |
6.3. 流量整形(Traffic Shaping)
子端口和管道的流量整形是通过令牌桶来实现的,每个令牌桶使用一个饱和计数器(saturated counter)来实现,通过这个计数器跟踪credit的数值。
令牌桶泛型参数和操作如下表所示:
# | 令牌桶参数 | 单位 | 描述 |
---|---|---|---|
1 | bucket_rate | 每秒可用的credit | 向桶中添加credit的速率。 |
2 | bucket_size | credit | 桶中可以存储的最大credit数。 |
# | 令牌桶操作 | 描述 |
---|---|---|
1 | 初始化 | 将桶的大小设置为一个预定义的值,例如0或桶最大容量的一半。 |
2 | credit更新 | credit会根据bucket_rate定期或按需添加到桶中。credit不能超过bucket_size所定义的上限,因此在桶满时添加到桶中的任何credit都会被丢弃。 |
3 | credit消费 | 作为数据包调度的结果,需要从桶中减掉必要的credit。只有当桶中有足够的credit来发送完整的数据包(包括数据包本身和帧开销)时,数据包才能被发送。 |
为了实现上面描述的令牌桶通用操作,当前设计使用下表中给出的持久数据结构:
# | 字段 | 单位 | 描述 |
---|---|---|---|
1 | tb_time | 字节 | 上次credit更新的时间。以字节而不是秒或CPU周期来度量,以便于credit消费操作(因为当前时间也是以字节来维护的)。请参阅5.1节“内部时间参考值”以了解为什么时间以字节为单位。 |
2 | tb_period | 字节 | 自从上次credit更新之后应该经过tb_period定义时间,从而在bucket里添加tb_credits_per_period值定义的credit。 |
3 | tb_credits_per_period | 字节 | 每经过tb_period可以给bucket增加的credit数量。 |
4 | tb_size | 字节 | 桶大小的上限。 |
5 | tb_credits | 字节 | 当前桶内credit的数量。 |
可以用以下公式计算桶速率(单位为字节/秒):
bucket_rate = (tb_credits_per_period / tb_period) * r
式中,r = 端口线速(单位为字节/秒)。
令牌桶操作的实现如下表所示:
# | 操作 | 描述 |
---|---|---|
1 | 初始化 | tb_credits = 0; 或者 tb_credits = tb_size / 2; |
2 | credit更新 | credit更新选项: - 每次在一个端口上发送数据包时,更新该端口的所有子端口和管道的credit。不可行。 - 每次发送数据包时,更新管道和子端口的credit。非常精确,但没必要要(需要耗费大量计算)。 - 每次选择管道(即,由其中一个处理区选择)时,更新管道及其子端口的credit。 当前的实现采用的是选项3。根据第4节出队状态机的描述,在管道和子端口credit被实际使用之前,每次出队流程选择管道时,管道和子端口的credit都会被更新。 该实现在精度和速度之间进行了权衡,只在自上次更新以来至少经过了完整的tb_period时才更新桶的credit。 - 当tb_credits_per_period = 1时,通过调整tb_period的值,可以获得完全的准确性。 - 当不需要完全的准确性时,通过将tb_credits设置为更大的值可以获得更好的性能。 更新操作: - n_periods = (time - 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; |
3 | credit消费(当有包被调度时) | 作为数据包调度的结果,需要从桶中减掉必要的credit。只有当桶中有足够的credit来发送完整的数据包(包括数据包本身和帧开销)时,数据包才能被发送。 调度操作: pkt_credits = pkt_len + frame_overhead; if (tb_credits >= pkt_credits){tb_credits -= pkt_credits;} |
6.4. 流量组(Traffic Classes)
6.4.1. 基于优先级的调度实现
同一管道内TC的优先级是严格定义的,其调度是由管道出队状态机实现的,该状态机按照优先级升序排列选择队列。因此,队列0(与TC 0相关,优先级最高的TC)在队列1(优先级低于TC 0的TC 1)之前被处理,队列1在队列2(优先级低于TC 1的TC 2)之前被处理,并继续下去,直到除最低优先级TC以外的所有TC队列都被处理。最后,队列12-15(尽力而为TC, 最低优先级TC)被处理。
6.4.2. 强制流量上限
管道和子端口级别的TC不支持流量整形(traffic shaping),所以在这两个级别的上下文里没有维护令牌桶。管道和子端口级别的TC上限被强制限定为周期性回填的credit,每次在管道/子端口级别处理包的时候,credit就会被消费掉,如下两表所示。
子端口/管道TC强制上限的持久化数据结构:
# | 子端口/管道字段 | 单位 | 描述 |
---|---|---|---|
1 | tc_time | 字节 | 当前子端口/管道TC的下一次更新(填充上限)的时间。 请参阅内部时间参考章节,以了解为什么使用字节为单位来表示时间。 |
2 | tc_period | 字节 | 当前子端口/管道的所有TC连续两次更新之间的间隔。预计该值将比令牌桶tb_period的典型值大很多倍。 |
3 | tc_credits_per_period | 字节 | 当前TC在每个执行期间(tc_period)允许消费的credit数量的上限。 |
4 | tc_credits | 字节 | 当前执行期间的剩余时间内,当前TC可消耗的credit最大数量。 |
# | 流量组操作 | 描述 |
---|---|---|
1 | 初始化 | tc_credits = tc_credits_per_period; tc_time = tc_period; |
2 | Credit更新 | 更新操作: if (time >= tc_time) { tc_credits = tc_credits_per_period tc_time = time + tc_period; } |
3 | Credit消费(包调度的时候) | 通过分组调度,TC的带宽限制随着credit的增加而降低。只有在TC带宽限制中有足够的credit可以负担发送包的开销(包字节和包的帧开销)时,才能发送包。 调度操作: pkt_credits = pk_len + frame_overhead; if (tc_credits >= pkt_credits) {tc_credits -= pkt_credits;} |
6.5. 加权轮询(WRR,Weighted Round Robin)
最低优先级TC(尽力而为TC,best effort TC)的WRR设计方案从简单到复杂的演变如下表所示。
# | 所有队列都处于活跃状态? | 所有队列权重相等? | 所有数据包大小一样? | 策略 |
---|---|---|---|---|
1 | Yes | Yes | Yes |
字节级轮询 下一个队列:队列#i,i = (i + 1) % n |
2 | Yes | Yes | No |
报文级轮询 从队列#i中消费一个字节需要对应的从队列#i中消费掉一个令牌。 T(i) =之前从队列#i消费的令牌累计数量。每次队列#i消耗一个包时,T(i)被更新为:T(i) += pkt_len。 下一个队列:T最小的队列。 |
3 | Yes | No | No |
报文级加权轮询 这种场景可以通过为每个队列引入不同的每字节成本来实现。权重较低的队列每字节成本较高。这样,比较不同队列之间的消费情况以选择下一个队列仍然是可行的。 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)。下一个队列:T最小的队列。 |
4 | No | No | No |
可变队列状态的报文级加权轮询 通过将非活动队列的消耗设置为一个较高的数字,可以实现这种场景需求,这样通过选择最小的T的逻辑将永远不会选择非活动队列。 为了防止T由于连续累积而溢出,所有队列的每个包消耗后,T(i)被截断。例如,通过T(i)减去最小T,T[0..3]=[1000, 1100, 1200, 1300]被截断为T[0..3]=[0, 100, 200, 300]。 在接收队列集合中至少要有一个活动队列,发送队列状态机保证不会选择非活跃的TC。 mask(i) =队列#i的饱和掩码,定义为: mask(i) = (队列#i是否活跃) ? 0 : 0xFFFFFFFF; w(i) =队列#i的权重 t(i) =队列#i的每字节令牌,定义为队列#i的反权重值。 T(i) =之前从队列#i消费的令牌的累计数量。 下一个队列:T最小的队列。 在队列#i消费报文之前: T(i) |= mask(i) 在队列#i消费报文之后: T(j) -= T(i), j != i T(i) = pkt_len * t(i) 注意:T(j)使用T(i)更新前的值。 |
6.6. 子端口流量组超售
6.6.1. 问题描述
子端口流量组X的超售是一个配置问题,当子端口的成员管道为流量组X分配的带宽大于在上层子端口级别为同一流量组分配的带宽时发生。
特定子端口和流量组的超售完全是管道和子端口级配置的结果,而不是由于运行时流量负载的动态演化(如拥塞)造成的。
如果当前子端口对流量组X的总体需求较低,超售的存在并不代表问题,因为所有成员管道对流量组X的需求已经完全可以满足。然而,当所有子端口的成员管道的流量组X的需求合在一起超过了在子端口级别配置的限制时,这将不再能够实现。
6.6.2. 解决方案
下表总结了处理这个问题的一些可能的方案,当前实现是基于第三种方案。
No. | 方案 | 描述 |
---|---|---|
1 | 忽略 | 先到先处理。 这种方法对于子端口的成员管道是不公平的,因为首先服务的管道会占用足够多的TC X所需的带宽,而之后服务的管道只能获得糟糕的服务,因为在子端口级别的TC X的带宽是稀缺的。 |
2 | 按比例降低所有管道带宽 | 子端口的所有管道根据TC X的带宽限制按相同的比例缩小。 这种方法对于子端口的成员管道是不公平的,因为低端管道(即配置低带宽的管道)可能会遭受严重的服务降级,可能导致它们的服务不可用(如果这些管道的可用带宽低于可用服务的最低要求),而高端管道的服务退化可能根本不明显。 |
3 | 限制高需求管道 | 每个子端口成员管道在运行时为子端口级别的TC X接收相同份额的可用带宽。任何未被低需求管道使用的带宽都被等量分配给高需求管道。这样,高需求管道会被限制,而低需求管道不会受到影响。 |
通常,子端口TC超售只对最低优先级的流量组启用,这通常用于尽力而为(best effort)的流量,管理平面需要防止其他(更高优先级)的TC发生这种情况。
为了便于实现,还假设子端口的最低优先级TC的上限设置为子端口速率的100%,并且对于所有子端口的成员管道,管道最低优先级TC的上限设置为管道速率的100%。
6.6.3. 实现概述
该算法会先计算一个容量(watermark),容量会根据子端口的成员管道当前的需求定期更新,其目的是限制每个管道允许发送的最低优先级(best effort)TC的流量。在每个TC上限计算周期开始时,在子端口级别上计算该容量,并在当前实施周期内在所有子端口的成员管道上使用相同的容量值。下面说明在每个周期开始时作为子端口级别计算的容量如何传播到所有子端口的成员管道的。
当前计算周期开始(和上一个计算周期结束的时间相同)时,容量值需要基于上一周期开始时分配给尽力而为TC但没有被子端口的成员管道消费掉的带宽值做出调整。
如果子端口有尽力而为TC带宽没用掉,就增加当前时段的容量值,以鼓励子端口的成员管道消耗更多带宽。否则,降低容量值,以强制子端口成员管道的尽力而为TC带宽的消耗相等。
容量值的增加或减少会以很小的增量进行,因此可能需要几个执行周期才能达到平衡状态。由于子端口成员管道对尽力而为TC的需求的变化,这种状态可以在任何时候发生改变,例如,由于需求增加(当需要降低容量值时)或需求减少(当可以增加容量值时)。
在需求较低时,为防止子端口成员管道占用太多带宽,需要设置高容量值。容量的最高值被选为子端口成员管道配置的最高速率。下表说明了容量操作。
每个TC流量限制执行周期开始时,从子端口级别到成员管道的容量传递:
No. | 子端口流量组操作 | 描述 |
---|---|---|
1 | 初始化 |
子端口层:subport_period_id= 0 管道层:pipe_period_id = 0 |
2 | Credit更新 |
子端口层: if (time>=subport_tc_time) { subport_wm = water_mark_update(); subport_tc_time = time + subport_tc_period; subport_period_id++; } 管道层: if(pipe_period_id != subport_period_id) { pipe_ov_credits = subport_wm * pipe_weight; pipe_period_id = subport_period_id; } |
3 | Credit消费(包调度的时候) |
管道层: pkt_credits = pk_len + frame_overhead; if(pipe_ov_credits >= pkt_credits{ pipe_ov_credits -= pkt_credits; } |
容量值计算:
No. | 子端口流量组操作 | 描述 |
---|---|---|
1 | 初始化 |
子端口层: wm = WM_MAX |
2 | Credit更新 |
子端口层(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; tc4_cons = subport_tc4_credits_per_period - subport_tc4_credits; tc5_cons = subport_tc5_credits_per_period - subport_tc5_credits; tc6_cons = subport_tc6_credits_per_period - subport_tc6_credits; tc7_cons = subport_tc7_credits_per_period - subport_tc7_credits; tc8_cons = subport_tc8_credits_per_period - subport_tc8_credits; tc9_cons = subport_tc9_credits_per_period - subport_tc9_credits; tc10_cons = subport_tc10_credits_per_period - subport_tc10_credits; tc11_cons = subport_tc11_credits_per_period - subport_tc11_credits; tc_be_cons_max = subport_tc_be_credits_per_period - (tc0_cons + tc1_cons + tc2_cons + tc3_cons + tc4_cons + tc5_cons + tc6_cons + tc7_cons + tc8_cons + tc9_cons + tc10_cons + tc11_cons); if(tc_be_consumption > (tc_be_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; } |
7. 异常情况处理
7.1. 没有足够的Credit支持大量的活跃队列
为了调度一个报文,调度器必须在多个队列里查看报文和credit,当调度器需要查看的队列越多,其性能就越低。
调度器维护了活跃队列的Bitmap,从而可以直接跳过非活跃队列,但是为了检测特定的管道是否有足够的credit,必须使用管道发送状态机进行探测,无论最终调度结果如何(没有报文或者至少产生一个报文),总会消耗CPU时间。
这个场景强调了策略对调度器性能的重要性:如果管道没有足够的credit,其数据包应该尽快丢弃(在触发分层调度器之前),从而可以将管道队列标识为不活跃,发送端就可以跳过这个管道而不用消耗资源去查看管道credit。
7.2. 单队列100%线速
端口调度器的性能针对大量队列进行了优化。但如果队列的数量很少,那么对于相同级别的活动流量,端口调度器的性能预期会比一小组消息传递队列的性能更差。
DPDK QoS 框架系列:
DPDK QoS 框架 - 1. 简介
DPDK QoS 框架 - 2. 分级调度模块介绍
DPDK QoS 框架 - 3. 分级调度模块的实现
DPDK QoS 框架 - 4. 丢包器和流量计量