为了解决各种限速的问题,我们先来解决TB的实现。
令牌桶(Token-Bucket)是目前最常采用的一种流量测量方法,用来评估流量速率是否超过了规定值。
关于令牌桶处理报文的方式,RFC 中定义了两种令牌桶算法:
- 单速率三色标记(single rate three color marker,srTCM,RFC2697定义,或称为单速双桶算法)算法,主要关注报文尺寸的突发。
- 双速率三色标记(two rate three color marker,trTCM,RFC2698定义,或称为双速双桶算法)算法,主要关注速率的突发。
实际中比较常见的有两种实现方式:
- 周期性的添加,添加的时间间隔就是令牌桶的容量与添加速率的比值:△t=CBS/CIR,每次添加的令牌数为 CBS 个。
- 一次性添加,添加令牌的数量是△t×CIR(△t 是当前时间与上次添加令牌的时间之差),且是一次添加完毕,并不是按照一定速率添加。
当前业界都采用的第二种方式,第一种方式涉及定时器精度不太好控制。
由于报文过令牌桶的时候我们很容易能获取到的是报文长度和时间,这两个度量我们分别取单位为:
1个字节和1个cycle,然后我们使用这两个单位来展开令牌桶的换算:
1秒时间单位内有CIR(CIR已经换算成字节了)个字节,而1秒的时间单位有HZ * 1个cycle,利用这层关系我们假设:
CIR * 1个字节 = HZ * 1个cycle,换算后我们可以得到两个公式:
- 1个字节 = HZ/CIR * 1个cycle
- 1个cycle = CIR/HZ * 1个字节
两种实现方式的基本思想是一直的,只是细节上有一些差别,我们下面具体来看
3.1 第一种实现方式
根据公式:1个字节 = HZ/CIR * 1个cycle 我们来实现第一种方式的令牌桶,也是DPDK当前的实现方式,代码分散在dequeue的各个出队状态机里面。
3.1.1 基本实现
当CIR太大的时候,1个字节计算出来的cycle数会为小数,为了解决这个问题,我们引入扩大因子,扩大因子越大,分辨率越高,同时需要考虑HZ和最大支持的CIR的关系和溢出的风险,扩大因子又不能太大,那么公式就变成如下:
1个字节 = (HZ/CIR * 1个cycle) << 5 (扩大因子) ①
以此为依据我们看下过srTCM的整个过程:
获取当前的cycle,记为new_cycle
获取这段期间的cycle数,记为diff_cycle = new_cycle – old_cycle
获取diff_cycle的字节数 diff_bytes = diff_cycle/① * 扩大因子
然后使用diff_bytes过桶:
1、计算TC 和TE:
IF tc + diff_bytes >= CBS,
tc = CBS;
diff_bytes -= (CBS – tc);
IF te + diff_bytes >= EBS
te = EBS;
ELSE
te += diff_bytes;
ELSE
tc += diff_bytes;
2、计算颜色
IF tc >= Pkt_len
Color = GREE;
Tc -= Pkt_len;
ELSE IF te >= Pkt_len
Color = YELLOW;
Te -= Pkt_len;
ELSE
Color = RED;
3.1.2 优化方法
上面的实现由两个地方可以优化:
1、diff_bytes 使用了除法,这个代码是很昂贵的,DPDK 实现了一个使用移位代替除法的算法,来提高性能,但没怎么看明白。
我们可以使用空间换时间的想法,使用以下的方法,提高整个过桶的过程:
- 根据公式:1个字节 = HZ/CIR * 1个cycle * << 5(扩大因子),我们已经知道了一个字节代表的cycle数,那么可以可以把所有的都转化为cycle的时间单位来计算。
创建令牌桶的时候,CBS、EBS 根据公式换算成 cycles数保存,同时计算数组(下标是字节数)L2T[1 … 512] = [x cycle …… ycycle],为过桶做好准备。
过桶的时候,获取报文长度pkt_len,然后计算cycle,公式如下:
tmp_len = pkt_len / 扩大因子,然后计算tc_cycle,这里考虑tmp_len的计算,扩大因子不能太大,32是一个比较好的值。
IF tmp_len > 512
tc_cycle = (tmp_len / 512) * L2T[512] + L2T[tmp_len & 511];
ELSE
tc_cycle = L2T[tmp_len];
然后在根据tc_cycle去过桶。
解决了除法的问题,同时使用数组和移位加快计算,不在乎内存的话,L2T的数组可以定义为MTU的大小,任何报文长度都可以根据数组一次得到tc_cycle。
2、过桶的时候,使用的是比较tc >= pkt_len,没有实现借贷,可能会导致报文不平滑,举个例子:
- 假设设备端口的CIR设置为1Mbps,CBS为2000bytes, EBS为2000bytes,初始状态时C桶和E桶满。
- 假设第1个到达的报文是1500 bytes 时,检查C桶发现令牌数大于数据包的长度,所以数据包被标为绿色,C桶减少1500 bytes,还剩500 bytes,E桶保持不变。
- 假设1ms之后到达第2个报文1500 bytes,新增令牌 CIR*1ms=1000bit=125bytes,此时C桶共有625 bytes,令牌不够。检查E桶有足够令牌,因此报文标记为黄色,E桶减少1500bytes,剩 500bytes,C桶不变。
- 假设又过1ms后到达第3个报文1000 bytes,新增令牌 CIR*1ms=1000bit=125bytes,此时C桶共有750bytes,令牌不够,检查E桶也不够,因此报文被标记为红色, C桶、 E桶令牌数不变。
- 假设又过20ms后到达第4个报文1500bytes,新增令牌CIR*20ms=20000bit=2500bytes,C桶此时令牌数3250 bytes,CBS=2000bytes,因此溢出1250bytes 添加到E桶,此时E桶有1750bytes。由于此时C桶大于报文长度,报文标记为绿色,C桶减少1500bytes剩500bytes,E桶不变。
……………………
整个过程如下表所示:
序号 |
时间(ms) |
报文长度 |
时间间隔 |
本轮新增令牌 |
新的后TC |
新的后TE |
剩余TC |
剩余TE |
颜色 |
- |
- |
- |
- |
- |
2000 |
2000 |
2000 |
2000 |
- |
1 |
0 |
1500 |
0 |
0 |
2000 |
2000 |
500 |
2000 |
GREE |
2 |
1 |
1500 |
1 |
125 |
625 |
2000 |
625 |
500 |
YELLOW |
3 |
2 |
1500 |
1 |
125 |
750 |
500 |
750 |
500 |
RED |
4 |
22 |
1500 |
20 |
2500 |
2000 |
1750 |
500 |
1750 |
GREE |
如果使用了借贷后,计算颜色的过程变成:
IF tc > 0
Color = GREE;
Tc -= Pkt_len;
ELSE IF te > 0
Color = YELLOW;
Te -= Pkt_len;
ELSE
Color = RED;
上面的过程变成:
序号 |
时间(ms) |
报文长度 |
时间间隔 |
本轮新增令牌 |
新的后TC |
新增后TE |
剩余TC |
剩余TE |
颜色 |
- |
- |
- |
- |
- |
2000 |
2000 |
2000 |
2000 |
- |
1 |
0 |
1500 |
0 |
0 |
2000 |
2000 |
500 |
2000 |
GREE |
2 |
1 |
1500 |
1 |
125 |
625 |
2000 |
-875 |
2000 |
GREE |
3 |
2 |
1500 |
1 |
125 |
-750 |
2000 |
-750 |
500 |
YELLOW |
4 |
22 |
1500 |
20 |
2500 |
1750 |
500 |
250 |
500 |
GREE |
20ms这个上对于上面的过程tc没有溢出1250bytes的令牌,整个报文输出表现的更为平滑。
3.2 第二种实现方式
根据公式:1个cycle = CIR/HZ * 1个字节,我们来实现第二种方式的令牌桶,也是VPP CAR当前的实现方式。
当CIR 太小的时候,这个计算出来的值会为小数,同第一种方式,我们引入扩大因子:
1个cycle * << 17 = CIR/HZ * 1个字节 * << 17, 扩大因子VPP使用的是 1 << 17
我们现在把1个cycle * 217 称为新的时间单位,后续计算都以这个新的时间单位为基准。
以此为依据我们看下过srTCM的整个过程:
获取当前的cycle,记为new_cycle,单位为新的时间单位,计算为cycle >> 17。
获取这段期间的cycle数,记为diff_cycle = new_cycle – old_cycle(单位也是新的时间单位)
获取diff_cycle的字节数 diff_bytes = diff_cycle * CIR/HZ * 1个字节 * << 17
然后在根据diff_bytes去过桶,实现和上面的流程一致。
为了考虑乘法溢出的可能,可以使用64位存储。
VPP 还把TC/TE/CIRHZ * 1个字节 * 217,同时扩大了 2N,N根据前面3个最大值的log2N计算来的,然后过桶的时候把报文长度也扩大2N次,然后在过桶,代码注释是:
// Scale if possible. Scaling helps rate accuracy, but is constrained
// by the scaled rates and limits fitting in 32-bits.
// In addition, we need to insure the scaled rate is no larger than
// 2^22 tokens per period. This allows the dataplane to ignore overflow
// in the tokens-per-period multiplication since it could only
// happen if the policer were idle for more than a year.
// This is not really a constraint because 100Gbps at 1Ghz is only
// 1.6M tokens per period.
// Scale packet length to support a wide range of speeds
其实也是扩大因子,为了支持更小的CIR,比如800 bps。而第一种方式就没有这种忧虑。