第16章 TCP拥塞控制
TCP的成块数据流、批量数据传输
典型的TCP通常是根据是否存在丢包,判断是否出现拥塞。
在TCP中,丢包被用作判断拥塞发生与否的指标,用来衡量是否实施相应的响应措施。
其他拥塞探测方法,包括时延测量和显示拥塞通知(ECN)
cwnd 拥塞窗口;awnd 接收端通知窗口;发送端实际窗口 W;cwnd初始值;W初始值;慢启动阈值ssthresh初始值 ========================================================================================================= cwnd 拥塞窗口:反映网络传输能力的变量(发送方的主动限流) cwnd:获得cwnd的最佳值的唯一办法就是以越来越快的速率不断发送数据,直到数据数据丢包(or网络拥塞) awnd 接收端通知窗口:(接收端对发送方的限流) awnd:通过一个数据包的交换,就可以获取awnd --------------------------------------------------------------------------------- 发送端实际窗口 W W = min(awnd,cwnd) 在外数据值:发送端已发送但还未收到ack的数据 在外数据值总是小于等于发送端实际窗口 W #问题:上面的公式看起来合理,但是因为网络和接收端的情况会随着时间变化,awnd、cwnd也会随着变化 --------------------------------------------------------------------------------- W的最佳窗口大小:带宽延迟积BDP(即速率*RTT) W反映了网络中可存储的待发送数据量大小,其计算值 = RTT与链路中最小通行速率的乘积(即传输路径中的瓶颈) --------------------------------------------------------------------------------- 初始窗口IW 发送方最大段大小SMSS(SENDER MAXIMUM SEGMENT SIZE) = min(接收方MSS,路径MTU) 当SMSS > 2190字节: IW = 2*SMSS 且小于等于2个数据段 当2190 >= SMSS > 1095字节: IW = 3*SMSS 且小于等于3个数据段 当1095字节 >= SMSS: IW = 4*SMSS 且小于等于4个数据段 ###为了简单,我们仅讨论IW = 1 SMSS的情况 即初始化情况下 cwnd = 1 SMSS IW = 1 SMSS --------------------------------------------------------------------------------- 慢启动阈值ssthresh 初始值可以任意设定,比如awnd或者更大。
慢启动算法;拥塞避免算法;拥塞发生算法;快速恢复 ================================================================================================================= 慢启动算法 慢启动的目的:使tcp在用拥塞避免探寻更多可用带宽之前得到cwnd值,以及帮助tcp建立ack时钟 即在tcp连接之初、RTO超时导致的丢包、tcp发送端长时间处于空闲状态等情况下,tcp连接会启动慢启动;慢启动期间,cwnd快速增长,直到确定一个慢启动阈值; 慢启动算法: 接收到一个数据段的ack之后,cswn值++,接下来会发送2个数据段 如果还能成功收到相应的新ack,cwnd值会由2变4,由4变8,以此类推, 换个说法,每发送一个W窗口大小的数据,并接收到相应的ack,cwnd值翻倍,呈指数上升。 cwnd在慢启动过程中不断增大 W = min(awnd,cwnd) W也在不断增大 ------------------------------------------------------------------------------------------------------------------- 拥塞避免算法 拥塞避免期间,cwnd采用另一种算法增长,相比较慢启动会慢的多 慢启动启动,收到ack,cwnd增加1个SMSS 而在拥塞避免期间,收到ack,cwnd增加 1/k 个SMSS;或者换一种说法,收到了完整的W窗口大小的ack之后,cwnd才增加1个SMSS 拥塞避免算法假设了由比特错误导致丢包的概率很小(远小于1%),因此有丢包意味着出现了拥塞 或者换一种说法拥塞避免算法建立在链路ok的基础上(没有硬件故障因素导致的丢包),认为丢包意味着出现了拥塞 ------------------------------------------------------------------------------------------ 拥塞发生算法 发生超时重传时: ssthresh=cwnd/2 cwnd=1 SMSS 发生超时重传就意味着“一夜回到解放前”,之后就重新开始了慢启动。 发生快速重传时: 快速重传算法是当接收方发现丢了一个中间包时,发送三次前一个包的ACK,于是发送单就会快速重传,不必等待超时再重传。 TCP认为这种情况不严重,因为大部分没有丢失,于是ssthresh和cwnd变化如下: cwnd=cwnd/2 ssthresh=cwnd 进入快速恢复算法(快速重传和快速恢复算法一般同时使用) ------------------------------------------------------------------------------------------ 快速恢复 快速重传和快速恢复算法一般同时使用,快速恢复算法认为,你还能收到3个重复ACK说明网络也不那么糟糕嘛,所以没有必要像RTO超时那样强烈 1.拥塞窗口cwnd=ssthresh+3(意思是确认有3个数据包收到了) 2.重传丢失的数据包 3.如果再收到重复的ACK,那么cwnd+1(需要cwnd+1,才能继续发数据包) 4.如果收到新数据的ACK后,就把cwnd重设为ssthresh的值(取消cwnd的临时膨胀,也称为“收缩”),原因是该ACK确认了新的数据,说明从duplicated ACK的数据都已经收到,该恢复过程已经结束,可以回到恢复之前的状态了,也就是再次进入拥塞避免状态。
慢启动阈值的计算;TCP Tahoe版本;TCP Reno版本 =========================================================================== 慢启动阈值ssthresh:在没有丢包的情况下,记住上一次"最好的"操作窗口预计值,即记录TCP最优窗口估计值的下界。 当发生重传时,ssthresh = max(在外数据值/2,2*SMSS)【无论超时重传还是快速重传】 在最近的微软下一代TCP/IP协议中,ssthresh = max(min(awnd,cwnd)/2,2*SMSS),即ssthresh = max(W/2,2*SMSS) cwnd < ssthresh,使用慢启动算法 cwnd > ssthresh,使用拥塞避免算法 相等时,两种算法都可以 ---------------------------------------------------------------------------------------------------------------- TCP Tahoe版本(个人总结) 1.TCP以慢启动开始,直至W = 慢启动阈值ssthresh, 2.开始拥塞避免算法 发生丢包(无论超时还是快速重传):cwnd置为1,重新开始慢启动 问题:链路利用率低 ---------------------------------------------------------------------------------------------------------------- Reno算法所包含的慢启动、拥塞避免和快速重传、快速恢复机制 TCP Reno版本: 0.初始化:cwnd=IW,ssthresh取一个较大值(至少为awnd) 1.TCP以慢启动开始,直至W = 慢启动阈值ssthresh, 2.开始拥塞避免算法 发生重传: 快速重传:cwnd = cwnd/2;sshthresh = cwnd,进入快速恢复算法,快速恢复之后继续拥塞避免算法 超时重传:ssthresh=cwnd/2;cwnd=1 SMSS,重新开始慢启动
TCP 初始慢启动阈值 ================================================================================= TCP 初始慢启动阈值 第二次慢启动阈值是基于第一次拥塞窗口减半得到的。 初始慢启动阈值: 在具体的实现上,Linux(应该也包括其他 OS)的第一次慢启动阈值,实际上是“无穷大”。它被定义为: #define TCP_INFINITE_SSTHRESH 0x7fffffff #值为2147483647,也就是 2G #发送窗口是拥塞窗口和对方的接收窗口之间的较小值。由于接收窗口理论最大值也只有 1G,因而发送窗口的最大值也是 1G,那么拥塞窗口超过 1G 也已经没有意义了,所以这里的 2G,事实上就是无穷大。 在 Linux 的实现里,初始慢启动阈值肯定在第一个拥塞点之上。 这就造成一个现象:Linux 的 TCP 连接在慢启动后,先碰到的是拥塞点,而不是初始慢启动阈值。 从现象上看就是,TCP 的拥塞窗口在慢启动过程中不断爬升,直到遇到第一个拥塞点(发生丢包或者超时),此时这个拥塞窗口的一半,就是第二次慢启动阈值了。 所以首次慢启动过程中,是不存在拥塞避免阶段的 Linux 为什么会选择这样的初始慢启动阈值呢? 主要原因,是当今的网络条件比多年前好了很多,所以为了“压榨”网络性能,让传输的启动阶段尽可能快地达到理想的传输速度, Linux 在传输刚开始还没有网络质量信息的时候,直接用了“碰到拥塞再快速恢复”的方式。 这比预设一个折中的初始慢启动阈值的情况,会更快地达到理想的传输速度。 《网络排查案例课》答疑(三)| 第11~15讲思考题答案
=====================================================================================================================
《网络排查案例课》09 | 长肥管道:为何文件传输速度这么慢?
该案例涉及TCP/IP多个章节的知识点
RTT出现在第14章
WSopt窗口缩放因子(Window Scale)出现在第13章
带宽时延积出现在第16章"W的最佳窗口大小"
接收窗口、拥塞窗口、发送窗口 出现在第16章
案例:长肥管道传输速度慢 ====================================================================== 案例场景: 美国到欧洲需要传递一个95MB的文件,但是速度比较慢,只有400KB+;带宽是有10Gbps的 传输是 scp 拷贝,是基于 TCP 的。 美国到欧洲的时延在 134ms 左右 wireshark I/O Graph 统计的 Bytes 是二层帧的大小,速度在 480KB/s 上下 TCP Stream Graphs 关注的是四层 TCP 段的大小,计算出的结果在456KB/s左右 时延是 134ms,带宽是 10Gbps,那么带宽时延积就是 0.134×10Gb。转换为 Byte 需要再除以 8,得到约 168MB。 所以带宽时延积BDP并不是本次案例限制速度的因素。 这种说法很拗口啊。。。其实就是最大在途数据能够达到BDP,这种就是比较理想的情况 根据Bytes in flight ,发现发送方的窗口大小一直差不多稳定在64KB大小 传输速度 = 64KB/ 0.141s = 453KB/s 根因:限制速度的最大因素,既不是带宽,也不是丢包,而是窗口,确切地说就是接收端(POP 点)的接收窗口。因为这些接收端的设备比较特殊,沿用了老旧的配置,导致 TCP 接收窗口过小。 Window Scale 确实启用了,抓包数据中的接口窗口值有几次的值是 65728,明确超过了 65535。另外,在握手包里也可以看到 Window Scale 被启用的信息: 在某些情况下,这里的 Window Scale 虽然启用了,但无法充分工作,导致实际上这台设备的接收窗口一直被压制在 64KB 附近。 其实本案例还有美国传递到美国的需求,因为距离近,所以延迟小,所以计算出的速度更快,没有引起注意。
RTT、在途数据、长肥网络、iRTT;带宽时延积BDP ============================================================================================== Round Trip Time(RTT),即往返时间,也叫时延 RTT 越长,报文在“空中”的时间就越长。这些报文就有了一个新的身份,叫做“在途数据”,它的大小,叫做在途字节数。英文是 Bytes in flight。 TCP 详情页的 SEQ/ACK analysis 部分就有 Bytes in flight 信息 在网络世界里,带宽很大、RTT 很长的网络,被冠以一个特定的名词,叫做长肥网络,英文是 Long Fat Network。在长肥网络中的 TCP 连接,叫做长肥管道,英文是 Long Fat Pipeline。 Wireshark在 TCP 详情的 SEQ/ACK analysis 部分,就有 iRTT 信息 iRTT 是 intial RTT 的缩写,Wireshark 就是从 TCP 握手阶段的报文里计算出这个值的。 对于客户端来说,就是发出 SYN 和收到 SYN+ACK 的间隔。(看了抓包文件,iRTT值确实不会变化) 对于服务端,就是发出 SYN+ACK 和收到 ACK 的间隔。 --------------------------------------------------------------------------------------------- 带宽时延积 Bandwidth Delay Product,缩写是 BDP;这是传输链路理论上一直处于传输的数据大小 带宽时延积= 带宽*往返时间(RTT) 但是这个实际大小还得取决于发送端的发送窗口大小 传输速递(带宽) = 发送窗口大小 / RTT 我的理解: 假设TCP的传输速度很稳定,且两端的处理也很稳定,那么同时能发送的数据大小取决于发送窗口大小,发送的速度则是数据大小/RTT,即发送窗口/RTT
接收窗口、拥塞窗口、发送窗口;Window Scale ============================================================================================= TCP 有 3 个窗口: 接收窗口:它代表的是接收端当前最多能接收的字节数。通过 TCP 报文头部的 Window 字段,通信双方能互相了解到对方的接收窗口。 拥塞窗口:发送端根据实际传输的拥塞情况计算出来的可发送字节数,但不公开在报文中。各自暗地里各维护各的,互相不知道,也不需要知道。 发送窗口:对方的接收窗口和自身的拥塞窗口两者中,值较小者。实际发送的在途字节数不会大于这个值。 接收窗口是明的,在抓包文件里就能看到;拥塞窗口和发送窗口是暗的,抓包文件里没有。 ---------------------------------------------------------------------------------------------- Window Scale 它是在RFC1323中引进的,使得 Window 值最大能达到 2 的 30 次方,即 1GB。(Window Scale值虽然占用了1个字节,但是最大值为14) Window Scale的作用是向对方声明一个Shift count,我们把它作为2的指数,再乘以TCP头中定义的接收窗口,就得到真正的TCP接收窗口。 接收的窗口=Window * Winsow size scaling factor,也就是304*2的7次方=38912 (wireshark已经帮助我们计算好了) TCP的 Calculated window size显示的是wireshark计算出的接收窗口大小 Calculated window size = window * windows size scaling factor = window * 2的window scale次方 Bytes in flight 就是wireshark计算出的发送方的在途数据大小,相当于发送窗口大小
小结: 传输速度的问题,可以说就是窗口和往返时间这两个大玩家在起作用。你只要抓住这两个主要矛盾,就能解决大部分传输速度的问题了。其他因素也可能有它的作用,但一般不是核心矛盾。 时延:也叫往返时间,是通信两端之间的一来一回的时间之和。 在途报文:发送端已经发出但还未被确认的报文,时延越长,发送窗口越大,在途报文可能越多,这两者是正比关系。 带宽时延积:带宽和时延的乘积,表示这个网络能承载的最多的在途数据量。 发送窗口:发送窗口是拥塞窗口和对方接收窗口两者中的较小值。 先获取时延,再定位发送窗口,最后用这个公式去得到速度的上限值。 Wireshark 的一些技巧,包括: 查看 I/O Graph 的方法,这个可以直观地展示数据传输的速度。 查看 TCP Stream Graphs 的方法,这个能看到 TCP 序列号随着时间的变化趋势,同时也可以经过简单计算推导出 TCP 载荷的传输速度(因为没有计入各种报文头部,这个值比 #1 的速度值要略低一些)。 查看 TCP Window Scale 是否启用,以及启用的话,Scale Factor 的值的查找方法。 如果tcp发送buffer也就是SO_SNDBUF只有16K的话,这些包很快都发出去了,但是这16K的buffer不能立即释放出来填新的内容进去,因为tcp要保证可靠,万一中间丢包了呢。 只有等到这16K中的某些包ack了,才会填充一些新包进来然后继续发出去。由于这里rt基本是20ms,也就是16K发送完毕后,等了20ms才收到一些ack,这20ms应用、内核什么都不能做,所以就是如前面第二个图中的大概20ms的等待平台。
超详细的wireshark笔记(3)-TCP窗口和TCP重传
=====================================================================================================================
《网络排查案例课》11 | 拥塞:TCP是如何探测到拥塞的?
该案例涉及TCP/IP多个章节的知识点
Spurious 重传、快速重传、超时重传、重复确认 《第14章》
拥塞窗口CW、接收窗口RW、发送窗口《第16章》
TCP 拥塞控制《第16章》
Spurious 重传、快速重传、超时重传、重复确认 ============================================================================= 乱序(Out-of-Order)和 TCP Previous segment not captured。这两者本质上都是乱序引起的现象。 Spurious 重传:这是已经被确认过的数据再一次被重传。 快速重传:收到 3 次及以上次数的重复确认后,不等超时就做出的重传。 超时重传:超时计时器到点而触发的重传。 重复确认:确认号重复的多个报文,重复确认是引发快速重传的原因。
拥塞窗口CW、接收窗口RW、发送窗口 ======================================================================================================== 自身的拥塞窗口CW(Congestion Window) 拥塞窗口Congestion Window,缩写是 CWND,或者 CW 拥塞窗口是每个连接分开维护的,比如同一个主机有两个 TCP 连接在传输数据的话,那么这两个连接就各自维护自己的拥塞窗口 一个TCP连接有2个拥塞窗口,client端拥塞窗口、server端拥塞窗口 初始拥塞窗口,英文是 Initial Congestion Window(或者 Initial Window),缩写为 ICW(或者 IW)。 在 Linux 内核 3.0 以前,初始拥塞窗口的大小比较小,在 2 到 4 个 MSS。 Linux 内核 3.0 版本及以后的版本中,比如在 include/net/tcp.h 中,就定义了 TCP_INIT_CWND 的值为 10。 对端的接收窗口RW(Receive Window) 发送窗口 = min(CW,RW) 当CW>RW时,一般意味着传输过程中没有或者很少有“拥塞”发生,因而拥塞窗口能增长到较高的值。 此时传输速度的上限就是对端接收窗口值决定的。 所以也就容易在 Wireshark 里观察到 TCP Window Full 这样的现象。当然,也不是每次都一定有 TCP Window Full 当CW<RW时,一般意味着传输过程中遇到了“拥塞”,因而拥塞窗口进行了适配,也就是往下调整,这往往会使得拥塞窗口变得比较小。 --------------------------------------------------------------- 拥塞窗口(CW)和接收窗口(RW)是如何决定了传输速度上限的, 简单来说: 当 RW<CW 时,速度由 RW 决定; 当 RW>CW 时,速度由 CW 决定。
TCP 拥塞控制主要有四个重要阶段:慢启动、拥塞避免、快速重传、快速恢复 ================================================================================================================== TCP 拥塞控制主要有四个重要阶段: 慢启动; Slow Start,是指 TCP 传输的开始阶段是从一个相对低的速度(“慢”一词的由来)开始的。事实上,在这个阶段,拥塞窗口会以翻倍的方式增长,所以从增长过程来看,叫“快启动”也未尝不可。 在这个阶段,每次 TCP 收到一个确认了数据的 ACK,拥塞窗口就增加一个 MSS。 确认数据的 ACK 报文,而不是重复的 ACK 报文。重复的ACK不会使拥塞窗口增加1个MSS 理论上,每发送一轮拥塞窗口大小的数据,拥塞窗口将会增大一倍;但是实际上由于接收端可能会每2个报文才回一个ack,所以拥塞窗口增大的速度会低于理论值 慢启动停止:1.遇到拥塞;2.达到慢启动阈值 拥塞避免; 慢启动阈值(也有人称之为慢启动门限),英文简称 ssthresh。 拥塞窗口的增长速度立刻就放缓了,变成了每过一个 RTT,拥塞窗口就只增长一个 MSS(此前是每个确认数据的 ACK,增长一个 MSS)。 拥塞避免这个阶段的特征是“和性增长乘性降低”,英文是 Addictive increase/mutiplicative decrease,缩写为 AIMD。 当探测到拥塞,拥塞窗口就要往下降。这个下降是直接减半的,所以叫乘性降低。 快速重传; 超时重传:TCP 每发送一个报文,就启动一个超时计时器。如果在限定时间内没收到这个报文的确认,那么发送方就会认为,这个报文已经在网络上丢失了,于是需要重传这个报文,这种形式叫做超时重传。 一般来说,TCP 的最小超时重传时间为 200ms。 快速重传:一旦发送方收到 3 次重复确认(加上第一次确认就一共是 4 次),就不用等超时计时器了,直接重传这个报文。 快速恢复。 这是 TCP Reno 算法引入的一个阶段,它是跟随快速重传一起工作的。 之前碰到拥塞之后,“慢启动 -> 拥塞避免 -> 慢启动 -> 拥塞避免” 在TCP Reno 算法下,在遇到拥塞点之后,通过快速重传,就不再进入慢启动,而是从这个减半的拥塞窗口开始,保持跟拥塞避免一样的线性增长,直到遇到下一个拥塞点。 哦哦哦,在TCP Reno 算法下,碰到拥塞有2种情况: 1.超时重传,拥塞窗口减半,传输速度降至一个很低的位置,开始慢启动。即"慢启动 -> 拥塞避免 -> 慢启动 -> 拥塞避免" 2.快速重传,拥塞窗口减半,传输速度降至拥塞窗口所在的位置,开始快速恢复,即以一个较高的速度继续拥塞避免阶段 ------------------------------------------------------------------------------------ 慢启动:每收到一个 ACK,拥塞窗口(CW)增加一个 MSS。 拥塞避免:策略是“和性增长乘性降低”,每一个 RTT,CW 增加一个 MSS。 快速重传:接收到 3 次或者以上的重复确认后,直接重传这个丢失的报文。 快速恢复:结合快速重传,在遇到拥塞点后,跳过慢启动阶段,进入线性增长。
传输速度慢分析技巧 --------------------------------------------------------------------------- 个人理解:(理想状态下的最终速度上限) TCP的传输速度受接口窗口和拥塞窗口大小的限制;也收到RTT的限制;还受到接收端处理速度的限制 传输速度 = 接收端的处理速度/RTT 速度上限是由接收端的处理速度和接收窗口共同决定的 所以可以查看wireshark中的在途字节数,若在途数据等于最大的通告窗口,那么速度上限就取决于接收窗口大小or接收端的处理速度 反之则可能是带宽、发送端发送效率等因素影响发送速度 很多 TCP 实现里(比如 Windows 系统),确认报文是这样工作的:如果收到连续多个报文,确认报文是一个隔一个回复。 即每接收2个报文,回复一个ACK Wireshark 自动找到了被它确认的 16 号报文,也在它的左边打上了一个小小的勾。(wireshark细节)
案例:自有机房与公有云之间使用scp传输文件速度慢 ================================================================================================= 案例背景与现象: 客户自己的机房在上海,公有云上的资源则在北京 客户选择了使用公有云的专线产品,也就是在上海自有机房和北京公有云之间,打通了专线。 使用scp传输文件的时候,发现速度只有 200KB/s 左右,达不到购买的专线带宽值。 wireshark I/O Graph查看,发现传输速度忽高忽低,很不稳定,可以基本判断存在链路拥塞现象 分析专家信息,发现很多"out-of-order"和"previous segment not captured",存在数据包乱序的情况 进一步造成了TCP虚假重传,TCP快速重传,但更多的是TCP超时重传;同时也存在很多重复ACK 根因:专线上的限速设置失误造成的 猜测:这个限速应该是通过网络硬件设备完成的,它单位时间内只允许一定字节数的报文通过,如果超过限制,就会丢弃这些报文。
实验 1:修改初始拥塞窗口;实验 2:改变 TCP 拥塞控制算法 ================================================================================================= 实验一下 拥塞控制机制对 TCP 传输十分重要,技术细节也很复杂,所以是由内核实现的。这也是内核实现 TCP 栈的巨大优势:应用程序可以集中于业务逻辑,而不需要操心传输和拥塞这种底层细节了。 实验 1:修改初始拥塞窗口 当你认为某条链路网络状况比较糟糕,用更低的 ICW 更合理时,可以修改初始拥塞窗口(ICW) 在 Linux 操作系统上,修改初始拥塞窗口的方法是这样的: 1.运行ip route命令,找到当前的路由条目,把整行都进行复制,记为 item: $ ip route default via 10.0.2.2 dev enp0s3 proto dhcp src 10.0.2.15 metric 100 2.运行ip route change item initcwnd n,把路由项 item 的初始拥塞窗口修改为 n,比如改为 2: $sudo ip route change default via 10.0.2.2 dev enp0s3 proto dhcp src 10.0.2.15 metric 100 initcwnd 2 ------------------------------------------------------------------------------------------------------------ 实验 2:改变 TCP 拥塞控制算法 在 Linux 里,我们可以通过 sysctl 命令,查看或者修改这个算法。比如,Linux 默认是用 cubic 算法 $ sysctl net.ipv4.tcp_congestion_control net.ipv4.tcp_congestion_control = cubic 如果内核大于 4.9,Linux 里就已经默认带有 BBR算法 $ sudo sysctl net.ipv4.tcp_congestion_control=bbr #配置拥塞算法为BBR net.ipv4.tcp_congestion_control = bbr $ sudo sysctl net.core.default_qdisc=fq #调整缓存队列算法 net.core.default_qdisc = fq 把这些配置写入到 /etc/sysctl.conf,即使机器重启,配置也会保持不变。