拥塞控制是TCP在设计之时就考虑到的全局配置,因为传输路径的节点上路由器是有瓶颈的,他不能接收无限大的数据包。当很多tcp连接都在试图发送更多的数据包之时,就有可能发生恶性拥塞事件,导致所有的tcp连接都无法正常工作,拥塞控制的出现使得tcp连接在侦测到网络拥塞事件后,会降低自己的发包速率。
为了实现拥塞控制,tcp引入了如下几个概念。
- 慢启动
- 拥塞避免
- 快速重传
- 快速恢复
TCP 拥塞控制
- 最初的样子 tahoe
在最初的时候是 tahoe 版本的拥塞控制算法。这种落伍的拥塞控制算法,在小幅度丢包时就会造成拥塞窗口 cwnd (congestion window)
直接降低为初始拥塞窗口大小(一般为1MSS),慢启动阈值 ssthresh (slow start threshold)
降低为当前拥塞窗口 cwnd 的一半。然后重新进行一次慢启动,再到拥塞避免状态,所以早已经淘汰。
-
教科书的最爱 reno
后来引入了reno。reno也是众多教科书上讲授拥塞控制算法的一个典范,reno的特点是当收到三个dup ack 时,表示可能存在丢包了。这时会触发快速重传(这时引入了快重传的概念)。拥塞窗口 cwnd (congestion window)
降低为当前拥塞窗口的一半。慢启动阈值 ssthresh (slow start threshold)
也降低为当前拥塞窗口的一半。然后进入快速恢复 (Fast Recovery)
阶段,当收到新的ACK后退出快速恢复阶段接着拥塞避免状态进行线性增长。
存在问题,一般而言,发生拥塞后大量丢包,reno算法在快速恢复阶段收到某个包重传完成会立即退出快速恢复,之后的丢包只能等待超时重传。 改革春风吹满地 new reno,sack
可以看出reno只是针对一个数据包的,而实际情况下都是很多个包都出现了丢失的情况。所以多个数据包的丢失重传问题就产生问题了,因此New Reno出现了,它在Reno快速恢复的基础上稍加了修改,可以恢复一个窗口内多个包丢失的情况。具体来讲就是:Reno在收到一个新的数据的ACK时就退出了快速恢复状态了,而New Reno需要收到该窗口内所有数据包的确认后才会退出快速恢复状态,从而更一步提高吞吐量。
针对 new reno 举个例子。当A给B传输了 1,2,3,4,5,6,7,8 号包时 2,3 号包丢了,
new reno 的做法是先重传 2 号包,传完之后B这下收到了2,所以回ACK3。
A就知道原来 3号包也丢了,A再重传3。B回复ACK9。A 就知道快恢复状态结束了,这下1-8 号包都传过去了。
而SACK呢,它改变TCP的确认机制,最初的TCP只确认当前已连续收到的数据,SACK则把乱序等信息会全部告诉对方,从而减少数据发送方重传的盲目性。比如说序号1,2,3,5,7的数据收到了,那么普通的ACK只会确认序列号4,而SACK会把当前的5,7已经收到的信息在SACK选项里面告知对端,从而提高性能,当使用SACK的时候,NewReno算法可以不使用,因为SACK本身携带的信息就可以使得发送方有足够的信息来知道需要重传哪些包,而不需要重传哪些包。
- 百花齐放
随着tcp拥塞控制算法的进化,大致可以分为以下几个阶段。
以丢包作为依据
1、New reno: RFC6582
2、BIC :Linux 2.6.8 - Linux 2.6.18
3、Cubic:RFC8321 Linux 2.6.19
这类算法通过ACK带回的丢包信息来调整发送侧的拥塞窗口。
以路径时延为依据
1、Google BBR:Linux 4.9
前面阶段的拥塞控制算法都是以丢包作为依据的。RTT相对于丢包信息反馈更加灵敏,能更及时的反映出一般的网络拥塞状态,但是太灵敏也有可能是劣势,会导致算法反应过激。
基于显式拥塞反馈
典型的ECN利用中间节点检测拥塞状态,如路由器的反馈,直接反馈给发送方,以此调节发送源的窗口值和发送速率。
慢启动
慢启动过程是为了在未知网络情况下的带宽,所以以一个非常小的初始拥塞窗口 cwnd (congestion window)
进行发送,这里的拥塞窗口会以每次×2的速度递增。
发送窗口 scwnd = Min(cwnd,rwnd) 。也就是实际的发送的窗口是拥塞窗口的与接收窗口的最小值。
慢启动的初始拥塞窗口
这里又要讲到过时的教科书了,过时的教科书上经常说初始窗口大小是1个MSS。实际上现在的初始窗口大小早就有10MSS了。
1、1MSS 初始拥塞窗口 RFC2001 (1997年定义,教科书的最爱)
2、2-4 MSS 初始拥塞窗口 RFC2421 (1998年定义,也是教科书的最爱)
3、10MSS 初始拥塞窗口 RFC6928 (2013年定义,也是目前最广泛的)
拥塞避免
慢启动是以每次x2的速度增大拥塞窗口的,这样激进的方式不能一直这么使用,所以需要在到达一个点的时候,将拥塞窗口的扩大变成线性的,这个点就是慢启动阈值 ssthresh (slow start threshold)
超过这个点就进入拥塞避免状态。而拥塞避免状态的线性递增也不是简单的加1(是不是又被过时的教科书给骗了)。
实际上的线性递增
cwnd += SMSS * SMSS / cwnd
快速重传与快速恢复
当发送方发送的数据包存在丢失,如果还没有触发超时重传,那么会触发快速重传,(超时重传对性能是影响最大的,因为他有一段时间RTO都没有任何数据传输,且拥塞窗口又要降低为0,又要进入一次慢启动,所以快重传和快恢复就是为了避免超时重传而设计出来的)
即当接收端收到比期望序号大的报文段时,便会重复发送最近一次确认的报文段的确认信号,我们称之为冗余ACK(duplicate ACK),如下图。2号包丢了,接收方会发送很多ACK 2 。当 dup ack 达到3个的时候就会触发快速重传。
快速重传是在RFC2581 中定义的 http://www.cnpaf.net/rfc/rfc2581.txt
快速重传配合快速恢复可以让丢包只是适当减少
拥塞窗口 cwnd (congestion window)
和慢启动阈值 ssthresh (slow start threshold)
而不是重新进入一次慢启动过程。
在 RFC2581 中定义,当接收到3个dup ack 的话将进入快速重传。
此时的
慢启动阈值 ssthresh (slow start threshold)
将设置为当前拥塞窗口 cwnd (congestion window)
的一半。而当前的拥塞窗口 cwnd (congestion window)
为 ssthresh + 3MSS。每收到一个重复的ACK就 拥塞窗口 cwnd + 1。当有新的ACK收到后,表示都重传OK了。所以退出快恢复阶段,此时将 cwnd 重新设置为 ssthresh 。
这里提一下,丢包对小文件的影响是比大文件的影响严重的,因为读写一个小文件需要的包数很少,
所以丢包时往往凑不满3个 Dup ACK。只能等待超时了,而大文件有较大的可能触发快速重传
传统TCP存在的问题
长距离高带宽(Long Fat Network)
- 高带宽延迟积 (Bandwidth * Latency)
假设 100Mbps的网络,时延为 100ms,那么BDP为 100,000,000 * 0.1 / 8 = 1,250,000B = 1220.7 KB ,TCP 的接收窗口是 16 bit,加上 window scale opetion,最大可以为 65,535B * 2^14。 - 传统的AIMD的增长方式导致达到最大带宽的时间过长。
在拥塞避免状态中,每一个RTT,拥塞窗口+1,而一个RTT在以太网中只能传 1500 B,想要将带宽为 1Gbps 打满,需要 8333 个 RTT
被udp挤占带宽
其实这个不应该是 tcp 存在的问题,因为很多视频媒体文件传输协议用的udp,udp毫无拥塞控制可言,疯狂挤占带宽,而TCP只能做退让。
百家争鸣
一个拥塞控制算法就是在对 慢启动,拥塞避免,快重传,快恢复 这4个阶段进行自己的改写。最多的改动就是对 拥塞避免,快重传,快恢复的改动,其实想一想一个tcp的拥塞算法也没什么大不了的,自己都有设计一套的冲动。
1、Westwood算法
当出现 快恢复 阶段时,就要将 拥塞窗口 cwnd (congestion window)
降低一半,这是不是降的有些狠了... 一位叫 Saverio Mascolo 的意大利人和我有一样的观点,这样直接砍一半太浪费了。
比如两个场景,当A 传了 16个包丢了1个,B传了16个包,丢了12个,他们都被快重传了 Duplicate ACK。而且他们的拥塞窗口都被重置为了8。这就太不科学了,A 显然不应该降低的这么狠。理想的算法是先推算出有多少个包已经被对方收到了,从而更为精确的估算发生拥塞时的带宽,Saverio 就设计出了Westwood算法。(具体实现当然没这么简单了)当丢包轻微的时候,westwood算法可以推断丢包不是特别严重,所以不会大幅度减小临界窗口值,这在经常发生非拥塞性丢包的环境中(如无线网络)就能体现出其优势。
2、Vegas算法
其他算法都是基于丢包驱动的,而vegas是通过监控网络状况来调整发包速率的,从而实现真正的“拥塞避免”。当数据包的RTT比较稳定,就可以增大拥塞窗口,而数据包开始排队阻塞就会引起RTT变大,这时就可以通过RTT预测到,进而减小拥塞窗口。
3、Compound
Windows 的解决方案,这个不讲了.....
不过可以自己设置玩一下。
windows 10 玩家
netsh int tcp set global dca=enabled
netsh int tcp set global autotuninglevel=normal
netsh int tcp set supplemental template=internet congestionprovider=ctcp
netsh int tcp set global timestamps=enabled
查看命令
netsh int tcp show global
BBR
2016 年 google 提出了 测量驱动的拥塞控制算法。Google 的BBR提出后,对整个网络的传输效率给出了巨大的提升。
BBR解决了什么问题?
现如今的网络中,随着内存设备的价格下降,路由器则都加上了大内存。这样就使得数据包开始排队。就像下面的管子,第一个就不说了,流量小,第二个,看起来没丢包,但是中间的节点也被挤的满满的,缓存都被填满了,时延肯定也随之增大,最理想的状态是带宽占满,每个路由器节点不要排队,以最高效的方式进行传输。
(一般来说丢包就是发生在排队之后的,所以尽量不要出现排队现象)
Google的BBR为Youtube在全球范围内将网络吞吐平均提高了4%,在某些国家和地区甚至提高了14%。
什么是BBR?
BBR("B 长度 B宽度 R往返时间"),自1980年以后,互联网大量使用基于丢包的拥塞控制算法。这种仅靠丢包作为信号减慢发包的速率,因为互联网交换机和路由器的小缓冲区可以很好匹配互联网的低带宽,因此很多年以来都是很好的效果,但是,当互联网迎来高速发展时,更快的发包速度以及更大的发送缓冲区,缓冲区往往会一直填充并丢弃多余的数据包。
改被动接收丢包信息为主动网络探测
但是如今的互联网还在基于丢包的拥塞控制算法是存在问题的
- 在浅层缓冲区中,数据包丢失发生在拥塞之前,基于丢包的拥塞控制算法导致吞吐量过低。因为他会反应过度,即使数据包丢失是因为瞬间的流量突发,也会因为数据包丢失而减半发送窗口。
- 在深层缓冲区中,拥塞发生在数据包丢失之前,在当今互联网的边缘,基于丢包的拥塞控制因为大缓冲区,所以不会发生丢包,但是RTT会增大。从而导致臭名昭著的“缓冲区膨胀问题”。
为了确定网络发送数据的速率,BBR考虑网络传递数据的速度,对于给定的网络连接,它使用对网络传输速率和往返时间的最新测量结果建立一个显式的模型,这个模型包含了链接可用的最大带宽和最小延时。
网络传输模型:正交的RTT和BW
- RTT不变,填充带宽
- RTT增大,带宽不变
- RTT急剧增大,带宽急剧减小
BBR的目的就是求得最大带宽和最小RTT
持续探测最大带宽和最小RTT,当最大带宽不再增加且最小RTT开始增加表明开始堆积管道。
RTT变高,带宽不变的情况下有一个点,在这个点控制发包速率可以有更好的效果,而不是等到丢包的时候。
那么就是怎么找到这个点的问题了。
BBR算法是如何找到RTprop和BtlBw的呢
- RTT 排队噪声去除掉就是 RTprop(噪声是指中间路由器的排队时间)
- BtlBw 按发送率最大的时候的带宽。
BBR会在路由器缓冲队列开始排队时降低发送速率,排空队列,而Cubic感知不到已经排队了,还是在缓慢的增加。
参考推荐 https://blog.apnic.net/2017/05/09/bbr-new-kid-tcp-block/
查看当前内核支持的拥塞控制算法
$ cat /proc/sys/net/ipv4/tcp_allowed_congestion_control
reno cubic
查看内核当前运行的拥塞控制算法
$ cat /proc/sys/net/ipv4/tcp_congestion_control
cubic
修改内核中运行的拥塞控制算法
sudo sysctl net.ipv4.tcp_congestion_control=cubic