TCP拥塞控制之ABC(Appropriate Byte Counting)
ABC 的来源
我们知道,TCP 发送端的数据发送速度受到本端拥塞窗口(cwnd)和对端通告的接收窗口(rwnd)的限制,只有同时在这两个窗口内的待发送数据才允许被发送到网络中。其中对端接收窗口是由对端接收缓冲区确定的,由于本文主要关注拥塞窗口,因此不考虑接收窗口的影响(视为接收窗口很大很大)。而拥塞窗口的大小则是由不停的拥塞控制算法计算而来。经典的拥塞控制算法都至少包含慢启动(Slow Start)和拥塞避免(Congestion Avoid)两个阶段。
通常我们这样描述这两个阶段的窗口增加规则:慢启动阶段,每收到一个 ACK,拥塞窗口增加 1 个 MSS;拥塞避免阶段,每收到前一整窗的 ACK,窗口增加 1 个 MSS,换句话说,也就是说,每收到一个ACK,窗口增加 1/cwnd 个 MSS !
那么问题就来了,对于像 telnet 这类大部分情况下报文很短的 TCP 连接来说,当发送方收到 ACK (通常只应答几个 Bytes) 时,应该让发送方的拥塞窗口增加 1 个 MSS 或者 1/cwnd 个 MSS 吗?
显然不太合理,一个例子是这种仅应答几个 Bytes 的 ACK 并不能反馈链路的拥塞情况:这些 ACK 可能将拥塞窗口撑得过大,而如果之后发送端真的发送拥塞窗口大小的数据,就有可能出现大量丢包。
倘若 TCP 接收端采用了 delay-ACK ,将多个 TCP 报文放在一个 ACK 中应答,或者有一些 ACK 丢失了( ACK 不会重传),又会使得发送端拥塞窗口增加地较慢,影响 TCP 的传输性能。
以上问题的本质在于,发送端是根据收到 ACK 报文的数量来调整拥塞窗口,而不是根据 ACK 实际应答的数据长度来调整。
RFC 3465 TCP Congestion Control with ABC 给出了一种更加恰当(Appropriate)的以应答数据字节数((Byte Counting))为基础的窗口调整方案
-
- 慢启动阶段:当 ACK 的数据长度达到 MSS 时,窗口增加 1 个 SMSS;
-
- 拥塞避免阶段:当 ACK 的数据长度达到整个拥塞窗口的大小时,窗口增加 1 个 SMSS。
这样,在 TCP 传输的报文是满 MSS 长度的报文时,是否使用 ABC 对拥塞窗口调整没有影响。
L = 2*SMSS
在RFC 3465 TCP Congestion Control with ABC中,定义了一个变量 L
, 它表示在慢启动阶段,收到 ACK 后拥塞窗口增加的最大值,其建议值为 2 倍 SMSS bytes,这一点严格来说是违反 RFC 5681 的,后者是这样描述的
We note that RFC3465 allows for cwnd increases of more than SMSS bytes for incoming acknowledgments during slow start on an experimental basis; however, such behavior is not allowed as part of the standard.
对此,RFC3465的理由是,选择 L=2*SMSS
是为了弥补 delayed-ACK 带来的多个性能影响
This document specifies that TCP implementations MAY use L=2SMSS bytes and MUST NOT use L > 2SMSS bytes. This choice balances between being conservative (L=1SMSS bytes) and being potentially very aggressive. In addition, L=2SMSS bytes exactly balances the negative impact of the delayed ACK algorithm
Linux 的实现
Linux 对 ABC 的实现分为三个时期:显式sysctl选项支持ABC、移除 ABC、隐式实现ABC
显式sysctl选项支持ABC
ABC 功能在[TCP]: Appropriate Byte Count support中引入内核。
下面以 2.6.32 版本为例,看看它是如何工作的
sysctl_tcp_abc: Appropriate Byte Counting 的开关
0 -- 关闭 ABC
1 -- 开启 ABC, 在慢启动阶段收到 ACK 时,拥塞窗口最多增加 1 个 SMSS
2 -- 开启 ABC, 在慢启动阶段收到 ACK 时,拥塞窗口最多增加 2 个 SMSS
内核使用tcp_sock->bytes_acked
来记录 ACK 报文应答的数据长度,该字段在收到 ACK 报文时更新。
static int tcp_ack(struct sock* sk, struct sk_buff *skb, int flag)
{
// code omitted...
if (sysctl_tcp_abc){
if (icsk->icsk_ca_state < TCP_CA_CWR)
tp->bytes_acked += ack - prior_snd_una;
else if (icsk->icsk_ca_state == TCP_CA_Loss)
tp->bytes_acked += min(ack - prior_snd_una, tp->mss_cache);
}
}
慢启动阶段时,当累积的bytes_acked
不够一个 SMSS 时,便会直接返回,不更新拥塞窗口。否则才更新,具体增加 1 个 SMSS 还是 2 个 SMSS,取决于sysctl_tcp_abc
的值以及是否累积应答的数据长度。
void tcp_slow_start(struct tcp_sock* tp)
{
int cnt;
if (sysctl_tcp_abc && tp->bytes_acked < tp->mss_cache)
return;
// code omitted
else
cnt = tp->snd_cwnd;
if (sysctl_tcp_abc > 1 && tp->bytes_acked >= 2*tp->mss_cache)
cnt <<= 1;
tp->bytes_acked = 0;
tp->snd_cwnd_cnt += cnt;
while(tp->snd_cwnd_cnt >= tp->snd_cwnd)
{
tp->snd_cwnd_cnt -= tp->snd_cwnd;
if (tp->snd_cwnd < tp->snd_cwnd_clamp)
tp->snd_cwnd++;
}
}
而在拥塞避免阶段,也有 ABC 专门的处理逻辑。
void tcp_reno_cong_avoid(struct sock* sk, u32 ack, u32 in_flight)
{
// code omitted...
if (tp->snd_cwnd <= tp->snd_ssthresh)
tcp_slow_start(tp);
else if (sysctl_tcp_abc){
if (tp->bytes_acked >= tp->snd_cwnd*tp->mss_cache){
tp->bytes_acked -= tp->snd_cwnd*tp->mss_cache;
if (tp->snd_cwnd < tp->snd_cwnd_clamp)
tp->snd_cwnd++;
}
}
else {
// code omitted...
}
}
当 ABC 引入内核时,sysctl_tcp_abc
的默认值是 1
(开启), 不过,在后来的一个补丁中,将其修改为了 0 (关闭)
移除 ABC
内核在 2013 年 2 月的一个补丁中 tcp: remove Appropriate Byte Count support
短暂移除了 ABC 功能.
隐式支持 ABC
同年 10 月,内核用一种新的隐式方式实现了 ABC tcp: properly handle stretch acks in slow start
以慢启动为例, 实际上是增加了一个 acked 参数,它表示本次收到的 ACK 完整应答的数据长度(以SMSS为单位)
-void tcp_slow_start(struct tcp_sock *tp)
+int tcp_slow_start(struct tcp_sock *tp, u32 acked)
{
- int cnt; /* increase in packets */
- unsigned int delta = 0;
- u32 snd_cwnd = tp->snd_cwnd;
-
- if (unlikely(!snd_cwnd)) {
- pr_err_once("snd_cwnd is nul, please report this bug.\n");
- snd_cwnd = 1U;
- }
+ u32 cwnd = tp->snd_cwnd + acked;
- if (sysctl_tcp_max_ssthresh > 0 && tp->snd_cwnd > sysctl_tcp_max_ssthresh)
- cnt = sysctl_tcp_max_ssthresh >> 1; /* limited slow start */
- else
- cnt = snd_cwnd; /* exponential increase */
-
- tp->snd_cwnd_cnt += cnt;
- while (tp->snd_cwnd_cnt >= snd_cwnd) {
- tp->snd_cwnd_cnt -= snd_cwnd;
- delta++;
- }
- tp->snd_cwnd = min(snd_cwnd + delta, tp->snd_cwnd_clamp);
+ if (cwnd > tp->snd_ssthresh)
+ cwnd = tp->snd_ssthresh + 1;
+ acked -= cwnd - tp->snd_cwnd;
+ tp->snd_cwnd = min(cwnd, tp->snd_cwnd_clamp);
+ return acked;
而在拥塞避免阶段,也同样使用了这个acked
以 4.4.0 内核为例,下面的 w
表示当前拥塞窗口大小
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);
}
关键的 acked
在收到 ACK 时计算,每从重传队列删除 1 个 skb,就视为该 ACK 报文应答了 1 个 SMSS 的报文。
/* See if we can take anything off of the retransmit queue. */
acked = tp->packets_out;
flag |= tcp_clean_rtx_queue(sk, prior_fackets, prior_snd_una,
&sack_state);
acked -= tp->packets_out;