BIC主要包括两个部分: binary search increase 和 additive increase,这两个部分又统称为binary increase,即BIC算法名称的由来。另外,BIC还包括一个Slow-Start和Fast-Convergence阶段。
Binary search increase阶段: BIC以丢包作为拥塞窗口是否合适的判断依据,将发送丢包时的CWND设置为最大窗口值,并且将当前窗口值降低β倍(0.2),作为最小窗口值,BIC在两者之间执行二分查找获得目标窗口值。如果在新的目标窗口没有发生丢包,将其设置为新的最小窗口值,再次执行二分查找。一直到最小窗口值与最大窗口值之间的差值小于最小阈值(minimum increment - Smin)时,拥塞窗口恢复到最大窗口值。
Additive Increase阶段:在Binary search increase阶段,如果二分查找选取的中间窗口值大于最大增长值(maximum increment - Smax),为防止在一个RTT内给网络带来太大的压力,拥塞窗口增长Smax的量,直到选取的中间值小于Smax后,执行以上的Binary search increase阶段的算法。
BIC Slow Start阶段:当拥塞窗口超过当前的最大窗口值时,新的最大窗口变得未知,BIC执行Slow-Start探测最大窗口,一直到探测窗口增加值到Smax之后,BIC执行Additive Increase算法,停止Slow-Start,每个RTT周期拥塞窗口增长Smax的量。在Slow-Start阶段,每个RTT周期,拥塞窗口变化为:cwnd+1, cwnd+2, cwnd+4,…, cwnd+Smax。
Fast convergence阶段:在binary search increase阶段,发生丢包时,如果当前拥塞窗口小于上一次丢包时设置的最大拥塞窗口,表明窗口处于下降阶段,为实现快速收敛,BIC并不是将当前拥塞窗口设置为最大窗口,而是设置为:(max_win-min_win)/2),进一步降低窗口,让出网络带宽。
具体算法参见文档:Binary Increase Congestion Control for Fast,Long Distance Networks。
BIC预设参数
fast_convergence - 是否启用快速收敛。启用的话将会修正选择的最大拥塞窗口,尽快达到合理的窗口值。
low_window - 只有当拥塞窗口大于此值时,BIC算法才开始生效。默认值为14,拥塞窗口小于此值时,采用传统的Reno TCP拥塞算法控制窗口。
max_increment - 在二分查找增长拥塞窗口时,可增长的最大限值。防止拥塞窗口过快的增长给网络带来的压力,默认值为16。
beta - 窗口减少因子(Multiplicative window decrease factor),在发生丢包时,用于选择最小窗口,即最小窗口等于当前拥塞窗口*beta/BICTCP_BETA_SCALE,beta默认值为819。
initial_ssthresh - 初始的慢启动阈值ssthresh。
smooth_part - 定义在丢包发生点执行的RTT周期数量。
宏定义:
BICTCP_B - 用于寻找最大窗口和最小窗口之间的值,算法为: (max+min)/BICTCP_B。
BICTCP_BETA_SCALE - 默认值为1024。 beta/BICTCP_BETA_SCALE的结果等于(1-β)。
#define BICTCP_BETA_SCALE 1024 /* Scale factor beta calculation
* max_cwnd = snd_cwnd * beta */
#define BICTCP_B 4 /* * In binary search, go to point (max+min)/N */
static int beta = 819; /* = 819/1024 (BICTCP_BETA_SCALE) */
static int smooth_part = 20;
BIC初始化
如果指定了初始ssthresh,使用其作为初始慢启动阈值,而不是系统默认的TCP_INFINITE_SSTHRESH(0x7fffffff)。在bictcp_init初始化函数中,将其赋值给套接口的发送ssthresh变量(snd_ssthresh)。
默认情况下,延迟ACK的速率设置为2,变量delayed_ack中保存的为估算的Packets/ACKs的16倍值(ACK_RATIO_SHIFT)。
static inline void bictcp_reset(struct bictcp *ca)
{
ca->cnt = 0;
ca->last_max_cwnd = 0;
ca->last_cwnd = 0;
ca->last_time = 0;
ca->epoch_start = 0;
ca->delayed_ack = 2 << ACK_RATIO_SHIFT;
}
static void bictcp_init(struct sock *sk)
{
struct bictcp *ca = inet_csk_ca(sk);
bictcp_reset(ca);
if (initial_ssthresh)
tcp_sk(sk)->snd_ssthresh = initial_ssthresh;
BIC拥塞窗口更新
如下函数bictcp_cong_avoid,其在ACK报文处理的最后被调用。首先如果拥塞窗口足够使用,例如应用层发送数据不足;或者在慢启动阶段,发送窗口大于翻倍之后的发送速率,无需进行拥塞窗口调整。否则,如果处于慢启动阶段,由tcp_slow_start函数处理拥塞窗口。
以上情况都不去成立的话,由BIC算法函数bictcp_update处理拥塞窗口。
static void bictcp_cong_avoid(struct sock *sk, u32 ack, u32 acked)
{
struct tcp_sock *tp = tcp_sk(sk);
struct bictcp *ca = inet_csk_ca(sk);
if (!tcp_is_cwnd_limited(sk))
return;
if (tcp_in_slow_start(tp))
tcp_slow_start(tp, acked);
else {
bictcp_update(ca, tp->snd_cwnd);
tcp_cong_avoid_ai(tp, ca->cnt, 1);
如果当前拥塞窗口等于上一次调整后的拥塞窗口,并且调整时间小于1/32秒(大约31毫秒),不做调整。初始情况下epoch_start等于零,表示一个BIC周期的开始,记录下开始时间戳。如果当前拥塞窗口小于预设的low_window(默认为14),不做处理,仅更新计数cnt为当前拥塞窗口。在稍后介绍的函数tcp_cong_avoid_ai中,将会看到拥塞窗口的增加值等于ack数量与cnt的比值,例如,当接收到一个ack报文时,拥塞窗口的增加值为1/cnt,在拥塞窗口小于low_window时,窗口增加值等于1/cwnd,即每个RTT周期拥塞窗口增加1,负荷RENO-TCP的定义。
static inline void bictcp_update(struct bictcp *ca, u32 cwnd)
{
if (ca->last_cwnd == cwnd &&
(s32)(tcp_jiffies32 - ca->last_time) <= HZ / 32)
return;
ca->last_cwnd = cwnd;
ca->last_time = tcp_jiffies32;
if (ca->epoch_start == 0) /* record the beginning of an epoch */
ca->epoch_start = tcp_jiffies32;
/* start off normal */
if (cwnd <= low_window) {
ca->cnt = cwnd;
return;
}
线性增长阶段。如果当前拥塞窗口CWND小于最大拥塞窗口(发生丢包时的窗口值),计算二者之差的四分之一(BICTCP_B默认为4),即拥塞窗口要增加的目标值(变量dist)。如果dist大于max_increment(默认为16),BIC算法认为窗口增加值过大,将其钳制在max_increment值(默认16)。变量cnt使用cwnd / max_increment的值,即在下一个RTT周期,拥塞窗口值增加max_increment的量。
否则,进入二分查找增长阶段。如果dist小于等于1,变量cnt赋值为(cwnd * smooth_part) / BICTCP_B,默认情况下smooth_part为20个RTT,即在拥塞窗口增加到接近上次最大窗口值时(<=1),在此位置持续运行20个RTT周期,此阶段内窗口将仅增加BICTCP_B(4)的量,增加算法的稳定性,因为此处这是上次发生丢包的位置。
最后,以上两个条件都不成立的话,变量cnt赋值为cwnd / dist,即使用二分查找(内核实现为1/4)结果作为拥塞窗口增长值。
/* binary increase */
if (cwnd < ca->last_max_cwnd) {
__u32 dist = (ca->last_max_cwnd - cwnd) / BICTCP_B;
if (dist > max_increment)
/* linear increase */
ca->cnt = cwnd / max_increment;
else if (dist <= 1U)
/* binary search increase */
ca->cnt = (cwnd * smooth_part) / BICTCP_B;
else
/* binary search increase */
ca->cnt = cwnd / dist;
如果当前拥塞窗口CWND大于等于最大拥塞窗口,并且,小于最大拥塞窗口与BICTCP_B(4)之和,进入慢启动阶段,变量cnt的值为:(cwnd * smooth_part) / BICTCP_B,与上所述类似,为保持稳定性,在此处运行20个RTT周期(smooth_part),以观察是否发生丢包。否则,如果CWND值位于还没有超出最大窗口值与max_increment*(BICTCP_B-1)之和阶段,变量cnt的值为:(cwnd * (BICTCP_B-1))/ (cwnd - ca->last_max_cwnd),即在此3个RTT周期(BICTCP_B-1=3),拥塞窗口的涨幅为:(cwnd - ca->last_max_cwnd)。
最后,以上两个条件都不成立,进入线性增长阶段,变量cnt赋值为cwnd / max_increment,之后每个RTT周期拥塞窗口以max_increment为步长增加。
} else {
/* slow start AMD linear increase */
if (cwnd < ca->last_max_cwnd + BICTCP_B)
/* slow start */
ca->cnt = (cwnd * smooth_part) / BICTCP_B;
else if (cwnd < ca->last_max_cwnd + max_increment*(BICTCP_B-1))
/* slow start */
ca->cnt = (cwnd * (BICTCP_B-1))
/ (cwnd - ca->last_max_cwnd);
else
/* linear increase */
ca->cnt = cwnd / max_increment;
}
如果处于BIC慢启动阶段,或者链路使用率非常低,不宜过多的增加拥塞窗口,这里将在每个RTT增加5%窗口。
/* if in slow start or link utilization is very low */
if (ca->last_max_cwnd == 0) {
if (ca->cnt > 20) /* increase cwnd 5% per RTT */
ca->cnt = 20;
}
以下考虑接收端Delayed-ACK的影响,接收到的ACK报文必然减小,这里将cnt转换为对应的装换之后的ACK报文数量值。例如,初始情况下delayed_ack比值为2,如果算法计算得到的cnt值为20,那这里最终的cnt值为20/2等于10,即只要接收到10个ACK报文即可满足算法要求。
ca->cnt = (ca->cnt << ACK_RATIO_SHIFT) / ca->delayed_ack;
if (ca->cnt == 0) /* cannot be zero */
ca->cnt = 1;
}
最后,在函数bictcp_cong_avoid中调用tcp_cong_avoid_ai计算拥塞窗口,参数w为以上计算的变量cnt的值,参数acked固定为1,即每次进入此函数,将拥塞窗口计数变量(snd_cwnd_cnt)递增一。如果一开始snd_cwnd_cnt大于等于w,清空snd_cwnd_cnt,将拥塞窗口加一。
如果snd_cwnd_cnt递增一之后,大于等于w,拥塞窗口CWND增加tp->snd_cwnd_cnt / w 的整数值。
/* In theory this is tp->snd_cwnd += 1 / tp->snd_cwnd (or alternative w),
* for every packet that was ACKed.
*/
void tcp_cong_avoid_ai(struct tcp_sock *tp, u32 w, u32 acked)
{
/* If credits accumulated at a higher w, apply them gently now. */
if (tp->snd_cwnd_cnt >= w) {
tp->snd_cwnd_cnt = 0;
tp->snd_cwnd++;
}
tp->snd_cwnd_cnt += acked;
if (tp->snd_cwnd_cnt >= w) {
u32 delta = tp->snd_cwnd_cnt / w;
tp->snd_cwnd_cnt -= delta * w;
tp->snd_cwnd += delta;
}
tp->snd_cwnd = min(tp->snd_cwnd, tp->snd_cwnd_clamp);
}
ssthresh计算
套接口在进入TCP_CA_CWR、TCP_CA_Recovery或者TCP_CA_Loss状态时,调用bictcp_recalc_ssthresh函数计算慢启动阈值。首先,进入以上的三种拥塞状态,意味着拥塞的发生,结束BIC增长周期,epoch_start设置为零。
如果当前拥塞窗口CWND(当前的最大拥塞窗口),小于之前记录的最大拥塞窗口,表明链路可用带宽在减少,例如网络中增加了新的流,如果启用了快速收敛功能,将让出更多的空间给新增流,最大拥塞窗口降低:
(BICTCP_BETA_SCALE - beta) (BICTCP_BETA_SCALE + beta)
—————————————————————————— 倍,即得到的最大CWND的值为: ——————————————————————————— * snd_cwnd。
(2 * BICTCP_BETA_SCALE) (2 * BICTCP_BETA_SCALE)
反之,如果CWND大于等于之前的最大拥塞窗口,或者没有启动快速收敛,最大CWND设置为当前窗口值。
static u32 bictcp_recalc_ssthresh(struct sock *sk)
{
const struct tcp_sock *tp = tcp_sk(sk);
struct bictcp *ca = inet_csk_ca(sk);
ca->epoch_start = 0; /* end of epoch */
/* Wmax and fast convergence */
if (tp->snd_cwnd < ca->last_max_cwnd && fast_convergence)
ca->last_max_cwnd = (tp->snd_cwnd * (BICTCP_BETA_SCALE + beta))
/ (2 * BICTCP_BETA_SCALE);
else
ca->last_max_cwnd = tp->snd_cwnd;
如果当前拥塞窗口CWND小于预设的最低窗口,安装RENO算法,将CWND减低一半。否则,CWND减低:
BICTCP_BETA_SCALE - beta beta
————————————————————————— 倍, 即减低之后的CWND等于: ———————————————————— * snd_cwnd
BICTCP_BETA_SCALE BICTCP_BETA_SCALE
默认情况下beta等于819,等式为:819/1024 * snd_cwnd,约等于0.8*snd_cwnd。
if (tp->snd_cwnd <= low_window)
return max(tp->snd_cwnd >> 1U, 2U);
else
return max((tp->snd_cwnd * beta) / BICTCP_BETA_SCALE, 2U);
BIC拥塞状态处理
如下函数bictcp_state,仅处理TCP_CA_Loss拥塞状态,此状态下复位BIC相关变量,开始新的BIC增长周期。
static void bictcp_state(struct sock *sk, u8 new_state)
{
if (new_state == TCP_CA_Loss)
bictcp_reset(inet_csk_ca(sk));
}
数据报文与ACK比率
函数bictcp_acked中,新的数据报文数量与ACK报文数量的比值,等于旧值与新值的加权和,其中旧的比值占15/16,而新确认数据报文量占比为1/16。注意仅在TCP_CA_Open拥塞状态更新delayed_ack值。
采样变量pkts_acked表示当前ACK确认的数据报文数量,即当前的数据报文数量与相应ACK报文量的比值为pkts_acked/1,此值在最终值中占比1/16。
/* Track delayed acknowledgment ratio using sliding window
* ratio = (15*ratio + sample) / 16
*/
static void bictcp_acked(struct sock *sk, const struct ack_sample *sample)
{
const struct inet_connection_sock *icsk = inet_csk(sk);
if (icsk->icsk_ca_state == TCP_CA_Open) {
struct bictcp *ca = inet_csk_ca(sk);
ca->delayed_ack += sample->pkts_acked -
(ca->delayed_ack >> ACK_RATIO_SHIFT);
内核版本 5.0