浅谈TCP(2):流量控制与拥塞控制

news2025/1/18 17:10:05

上文浅谈TCP(1):状态机与重传机制介绍了TCP的状态机与重传机制。本文介绍流量控制(Flow Control,简称流控)与拥塞控制(Congestion Control)。TCP依此保障网络的QOS(Quality of Service)。

TCP的流量控制

RTT算法

根据前文对TCP超时重传机制的介绍,我们知道Timeout的设置对于重传非常重要:

  • 设长了,重发就慢,丢了老半天才重发,没有效率,性能差。

  • 设短了,会导致可能并没有丢就重发。于是重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。

而且,这个超时时间在不同的网络环境下不同,必须动态设置。为此,TCP引入了RTT(Round Trip Time,环回时间):一个数据包从发出去到回来的时间。这样,发送端就大约知道正常传输需要多少时间,据此计算RTO(Retransmission TimeOut,超时重传时间)。听起来似乎很简单:在发送方发包时记下t0,收到接收方的Ack时记一个t1,于是RTT = t1 – t0。然而,这只是一个采样,不能代表网络环境的普遍情况。

经典算法

RFC793 中定义了一个经典算法

  1. 首先,采样RTT,记下最近几次的RTT值。

  2. 然后,使用加权移动平均算法(Weighted Moving Average Method)做平滑,计算SRTT(Smoothed RTT):

     

    1

     

    SRTT = ( α * SRTT ) + ((1- α) * RTT) (通常取0.8≤α≤0.9,α越大收敛速度越快)

  1. 最后,计算RTO:
    RTO = min [ UBOUND, max [ LBOUND, (β * SRTT) ] ] (通常取1.3≤β≤2.0)

Karn / Partridge 算法

经典算法描述了RTO计算的基本思路,但还有一个重要问题:RTT的采样取“第一次发Seq+收Ack的时间”,还是“重传Seq+收Ack的时间”?

如图:

图片

  • 情况(a)中,RTT的采样取“第一次发Seq+收Ack的时间”。假设第一次发的Seq彻底丢包了,则采样到的时间多算了“从第一次发到重传的间隔”。

  • 情况(b)中,RTT的采样取“重传Seq+收Ack的时间”。假设是Ack传输的慢,导致恰好刚重传就收到了接收方之前发出的Ack,则采样到的时间少算了“从第一次发到重传的间隔”。

问题的本质是:发送方无法区分收到的Ack对应第一次发的Seq还是重传的Seq(进入网络就都一样了)。针对该问题,Karn / Partridge算法选择回避重传的问题:忽略重传的样本,RTT的采样只取未产生重传的样本

简单的忽略重传样本也有问题:假设当前的RTO很小,突然发生网络抖动,延时剧增导致要重传所有的包;由于忽略重传样本,RTO不会被更新,于是继续重传使网络更加拥堵;拥堵导致更多的重传,恶性循环直至网络瘫痪。Karn / Partridge算法用了一个取巧的办法:只要一发生重传,就将现有的RTO值翻倍(指数回退策略),待网络恢复后再仿照经典算法逐渐平滑以降低RTO

该算法已经做到可用,然而网络抖动对性能的影响比较大。

Jacobson / Karels 算法

前面两种算法均使用加权移动平均算法做平滑,这种方法的最大问题是:很难发现RTT值上的较大波动,因为被平滑掉了(1 - a比较小,即最新RTT的权重小)。针对该问题,Jacobson / Karels算法引入了最新采样的RTT值和平滑过的SRTT值的差距做因子,即DevRTT(Deviation RTT,RTT的偏离度),同时考虑SRTT带来的惯性和DevRTT带来的波动:

 

1

2

3

 

SRTT = SRTT + α(RTT – SRTT) —— 计算SRTT

DevRTT = (1-β)*DevRTT + β*(|RTT-SRTT|) —— 计算SRTT和最新RTT的差距(加权移动平均)

RTO= µ * SRTT + ∂ *DevRTT —— 同时考虑SRTT(惯性)与DevRTT(波动)

Linux 2.6采用该算法计算RTO,默认取α = 0.125, β = 0.25, μ = 1, ∂ = 4(玄学调参,你懂的)。

TCP滑动窗口

TCP使用滑动窗口(Sliding Window)做流量控制与乱序重排。乱序重排在TCP的重传机制中已经介绍,下面介绍流量控制。

TCP头里有一个字段叫Window(或Advertised Window),用于接收方通知发送方自己还有多少缓冲区可以接收数据。发送方根据接收方的处理能力来发送数据,不会导致接收方处理不过来,是谓流量控制。暂且把Advertised Window当做滑动窗口,更容易理解滑动窗口如何完成流量控制,后面介绍拥塞控制时再说明二者的区别。

观察TCP协议的发送缓冲区和接收缓冲区:

图片

假设位置序号从左向右增长(常见的读、写缓冲区设计),解释一下:

  • 发送方:LastByteAcked指向收到的连续最大Ack的位置;LastByteSent指向已发送的最后一个字节的位置;LastByteWritten指向上层应用已写完的最后一个字节的位置。

  • 接收方:LastByteRead指向上层应用已读完的最后一个字节的位置;NextByteExpected指向收到的连续最大Seq的位置;LastByteRcvd指向已收到的最后一个字节的位置。可以看到NextByteExpected与LastByteRcvd中间有些Seq还没有到达,对应空白区。

据此在接收方计算AdvertisedWindow,在发送方计算EffectiveWindow

  • 接收方在Ack中记录自己的AdvertisedWindow = MaxRcvBuffer – (LastByteRcvd - LastByteRead),随Ack回复到发送方。

  • 发送方根据Ack中的AdvertisedWindow值,需保证LastByteSent - LastByteAcked ≤ AdvertisedWindow,则窗口内剩余可发送的数据大小EffectiveWindow = AdvertisedWindow - (LastByteSent - LastByteAcked),以保证接收方可以处理。

AdvertisedWindow与EffectiveWindow

AdvertisedWindow衡量接收方还能接收的数据量,发送方要根据AdvertisedWindow决定接下来发送的数据量上限,即EffectiveWindow(可能为0)。

ADVERTISEDWINDOW的计算

由于乱序问题的存在,LastByteRcvd可能指向Seq(LastByteSent),而Seq(LastByteAcked + 1)至Seq(LastByteSent - 1)都还在路上,即将到达接收方,最好的情况是不丢包(丢包后会重传),则LastByteRcvd之后、接收缓冲区边界之前的空间就是发送方下一次发送数据的长度上限(重传不属于下一次发送),因此,AdvertisedWindow = MaxRcvBuffer – (LastByteRcvd - LastByteRead)

EFFECTIVEWINDOW的计算

LastByteRcvd还可能指向Seq(LastByteAcked)(一个新包都没有收到),显然AdvertisedWindow的公式不变,而Seq(LastByteAcked + 1)至Seq(LastByteSent)都还在路上,未来将到达接收方,进入接收缓冲区,则“还在路上的Seq(LastByteAcked + 1)至Seq(LastByteSent)”不应超过接收缓冲区的剩余空间AdvertisedWindow(目前等于MaxRcvBuffer),这要求的是上一次发送满足LastByteSent - LastByteAcked ≤ AdvertisedWindow,那么LastByteSent之后、接收缓冲区剩余空间边界之前的空间就是发送方窗口内剩余可发送数据的长度上限,因此,EffectiveWindow = AdvertisedWindow - (LastByteSent - LastByteAcked)

当然,EffectiveWindow最小取0。

示例1

以下是一个发送缓冲区的滑动窗口:

图片

上图分为4个部分:

  • #1是已发送已确认的数据,即LastByteAcked之前的区域。

  • #2是已发送未确认的数据,即LastByteAcked与LastByteSent之间的区域,大小不超过AdvertisedWindow。

  • #3是窗口内未发送的数据,即LastByteSent与窗口右界之间的区域,大小等于EffectiveWindow(可能为0)。

  • #4是窗口外未发送的数据,即窗口右界与LastByteWritten之间的区域。

其中,#2 + #3组成了滑动窗口,总大小不超过AdvertisedWindow,二者比例受到接收方的处理速度与网络情况的影响(如果丢包严重或处理速度慢于发送速度,则#2:#3会越来越大)。

示例2

以下是一个AdvertisedWindow的调整过程,EffectiveWindow随之变化:

图片

Zero Window

理解有问题,不要求掌握。

上图,我们可以看到一个处理缓慢的Server(接收端)是怎么把Client(发送端)的发送窗口size给降成0的。对于接收方来说,此时接收缓冲区确实已经满了,因此令发送方的发送窗口size降为0以暂时禁止发送是合理的。那么,等接收方的接收缓冲区再空出来,怎么通知发送方新的window size呢?

针对这个问题,为TCP设计了ZWP技术(Zero Window Probe,零窗通告):发送方在窗口变成0后,会发ZWP的包给接收方,让接收方来Ack他的Window尺寸;ZWP的重传也遵循指数回退策略,默认重试3次;如果3次后window size还是0,则认为接收方出现异常,发RST重置连接(部分文章写的是重试到window size正常???)。

注意:只要有等待的地方都可能出现DDoS攻击,Zero Window也不例外。一些攻击者会在和服务端建好连接发完GET请求后,就把Window设置为0,于是服务端就只能等待进行ZWP;然后攻击者再大量并发发送ZWP,把服务器端的资源耗尽。(客户端等待怎么耗服务端???)

TCP的拥塞控制

通信中的拥塞指:

到达通信子网中某一部分的分组数量过多,使得该部分网络来不及处理,以致引起这部分乃至整个网络性能下降的现象,严重时甚至会导致网络通信业务陷入停顿即出现死锁。

为什么要进行拥塞控制?假设网络已经出现拥塞,如果不处理拥塞,那么延时增加,出现更多丢包,触发发送方重传数据,加剧拥塞情况,继续恶性循环直至网络瘫痪。可知,拥塞控制与流量控制的适应场景和目的均不同。

拥塞发生前,可避免流量过快增长拖垮网络;拥塞发生时,唯一的选择就是降低流量。主要使用4种算法完成拥塞控制:

  1. 慢启动

  2. 拥塞避免

  3. 拥塞发生

  4. 快速恢复

算法1、2适用于拥塞发生前,算法3适用于拥塞发生时,算法4适用于拥塞解决后(相当于拥塞发生前)。

rwnd与cwnd

在正式介绍上述算法之前,先补充下rwnd(Receiver Window,接收者窗口)与cwnd(Congestion Window,拥塞窗口)的概念:

  • rwnd是用于流量控制的窗口大小,即上述流量控制中的AdvertisedWindow,主要取决于接收方的处理速度,由接收方通知发送方被动调整(详细逻辑见上)。

  • cwnd是用于拥塞处理的窗口大小,取决于网络状况,由发送方探查网络主动调整。

介绍流量控制时,我们没有考虑cwnd,认为发送方的滑动窗口最大即为rwnd。实际上,需要同时考虑流量控制与拥塞处理,则发送方窗口的大小不超过min{rwnd, cwnd}。下述4种拥塞控制算法只涉及对cwnd的调整,同介绍流量控制时一样,暂且不考虑rwnd,假定滑动窗口最大为cwnd;但读者应明确rwnd、cwnd与发送方窗口大小的关系。

4种拥塞控制算法

慢启动算法

慢启动算法(Slow Start)作用在拥塞产生之前:对于刚刚加入网络的连接,要一点一点的提速,不要妄图一步到位。如下:

  1. 连接刚建好,初始化cwnd = 1(当然,通常不会初始化为1,太小),表明可以传一个MSS大小的数据。

  2. 每收到一个ACK,cwnd++,线性增长。

  3. 每经过一个RTT,cwnd = cwnd * 2,指数增长(主要增长来源)。

  4. 还有一个ssthresh(slow start threshold),当cwnd >= ssthresh时,就会进入拥塞避免算法(见后)。

因此,如果网速很快的话,Ack返回快,RTT短,那么,这个慢启动就一点也不慢。下图说明了这个过程:

图片

拥塞避免算法

前面说过,当cwnd >= ssthresh(通常ssthresh = 65535)时,就会进入拥塞避免算法(Congestion Avoidance):缓慢增长,小心翼翼的找到最优值。如下:

  1. 每收到一个Ack,cwnd = cwnd + 1/cwnd,显然,cwnd > 1时无增长。

  2. 每经过一个RTT,cwnd++,线性增长(主要增长来源)。

慢启动算法主要呈指数增长,粗犷型,速度快(“慢”是相对于一步到位而言的);而拥塞避免算法主要呈线性增长,精细型,速度慢,但更容易在不导致拥塞的情况下,找到网络环境的cwnd最优值。

拥塞发生时的算法

慢启动与拥塞避免算法作用在拥塞发生前,采取不同的策略增大cwnd;如果已经发生拥塞,则需要采取策略减小cwnd。那么,TCP如何判断当前网络拥塞了呢?很简单,如果发送方发现有Seq发送失败(表现为“丢包”),就认为网络拥塞了。

丢包后,有两种重传方式,对应不同的网络情况,也就对应着两种拥塞发生时的控制算法:

  1. 超时重传。TCP认为这种情况太糟糕,调整力度比较大:

    1. ssthresh = cwnd /2

    2. cwnd = 1,重新进入慢启动过程(网络糟糕,要慢慢调整)

  2. 快速重传。TCP认为这种情况通常比RTO超时好一些,主流实现TCP Reno的调整力度更柔和(TCP Tahoe的实现和RTO超时一样暴躁):

    1. ssthresh = cwnd /2

    2. cwnd = cwnd /2,进入快速恢复算法(网络没那么糟,可以快速调整,见下)

可以看到,不管是哪种重传方式,ssthresh都会变成cwnd的一半,仍然是指数回退,待拥塞消失后再逐渐增长回到新的最优值,总体上在最优值(动态)附近震荡。

回退后,根据不同的网络情况,可以选择不同的恢复算法。慢启动已经介绍过了,下面介绍快速恢复算法。

快速恢复算法

如果触发了快速重传,即发送方收到至少3次相同的Ack,那么TCP认为网络情况不那么糟,也就没必要提心吊胆的,可以适当大胆的恢复。为此设计快速恢复算法(Fast Recovery),下面介绍TCP Reno中的实现。

回顾一下,进入快速恢复之前,cwnd和sshthresh已被更新:

  1. ssthresh = cwnd /2

  2. cwnd = cwnd /2

然后,进入快速恢复算法:

  1. cwnd = ssthresh + 3 * MSS (尝试一步到位)

  2. 重传重复Ack对应的Seq

  3. 如果再收到该重复Ack,则cwnd++,线性增长(缓慢调整)

  4. 如果收到了新Ack,则cwnd = ssthresh ,然后就进入了拥塞避免的算法了(为什么收到新Ack要降低sshthresh???)

可暂时忽略的内容:

这种实现也有问题:依赖于3个重复Ack。回忆上文讨论的“重传一个还是重传所有的问题”,3个重复Ack并不代表只丢了一个数据包,很有可能是丢了好多包。显然快速恢复算法选择“重传一个”,而剩下的那些包只能等到RTO超时。于是,超时一个窗口就减半一下,多个超时会超成TCP的传输速度指数级下降;同时,超时重传不会触发快速恢复算法,慢启动很容易加剧超时的情况,进入恶性循环。

示例

下面看一个简单的图示,感受拥塞控制过程中的cwnd变化:

图片

一个有趣的面试题

问:

使用wget的时候,通常刚开始速度慢一些,然后逐渐上升到满速。可能是什么原因导致?

猴子面试时遇到这个问题,懵逼了几秒钟,想到一个CDN pre load的原因,面试官看我懵逼就提醒“可能跟tcp协议有关”,瞬间明白考点:

  • wget使用HTTP协议构建,传输层使用TCP协议,而TCP的拥塞控制会从慢启动开始,逐渐试探,增长到一个足够大,又不至导致拥塞的速度。

接下来,还可以补充CDN的原因,但不是主要原因了:

  • 很多下载服务都使用了CDN,CDN属于缓存,一定也存在一些pre load策略,显然运行一段时间,缓存命中率才能上升到最高。


参考:

  • TCP 的那些事儿(下)

  • 网络基础3-传输层

原文地址: 浅谈TCP(2):流量控制与拥塞控制

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1569061.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

sparksql执行流程

1. SparkSQL的自动优化 我们前面的文章已经说过spark RDD定义好后,执行经过DAG sechduler划分号内存管道、逻辑任务,然后经由task scheduler来分配到具体worker来管理运行,RDD的运行会完全按照开发者的代码执行 如果开发者水平有限&#xff…

Transformers in Vision:A Survey 阅读笔记

ACM上的一篇综述,讨论Transformer在CV上的应用。 摘要: Among their salient benefits,Transformers enable modeling long dependencies between inputsequence elements and support parallel processing of sequence as compared to recurrent networ…

MFC通用静态库制作与使用

开发环境VS2013 1、新建工程,选择Win32 Project,命名,选择路径等 2、选择Static library ,勾选MFC 3、点击完成。在工程中添加相应的头文件、源文件等通用功能函数或者类。 4、在其他工程引入使用。在使用的工程项目设置中Linker…

C语言-----数据在内存中的存储(1)

1.整数在内存中的存储 我们之前就了解过整数的二进制写法分别有3种,分别为原码,反码,补码。整型在内存中存储的是补码。 原码,反码,补码都有自己的符号位和数值位,符号位为1时,则表示负数&…

AI绘画:使用Stable Diffusion ComfyUI进行换脸:IPAdapter FaceID全面教程

在数字艺术和媒体编辑领域,换脸技术已经成为一种流行且强大的工具。它允许创作者将一个人物的面部特征无缝地转移到另一个人物上,创造出令人信服的视觉作品。Stable Diffusion ComfyUI提供了一个高效的平台,让用户能够轻松地实现换脸。本文将…

python和pip中常见命令和方法

玩python的同学想必没有不用pip的吧,pip是python包管理工具,和Nodejs的npm、Java的maven类似,这些依靠开源力量建立起的庞大软件库极大提高了开发的效率,下面是整理和总结pip中的常见命令和方法。 pip更新版本 python -m pip inst…

图像分割-综述篇

文章目录 图像分割算法类型全卷积FCNSegNetUNetDeeplab v1PSPNetDeeplab v2Deeplab v3Deeplab v3 基于候选区Mask RCNNMS RNN 基于GAN基于RNNReSegViTSwin TransformerSAM(Segment Anything Model) 图像分割算法类型 正如我在目标检测系列中提到的,图像分割&#x…

flutter官方案例context_menus【搭建与效果查看】【省时】

案例地址 https://github.com/flutter/samples/tree/main/context_menus 1:运行查看有什么可以快捷使用的,更新了些什么,可不可以直接复制粘贴 主要内容:在web端中模拟手机类型的点击长按操作,不能直接运行在安卓与io…

代码随想录Day27:回溯算法Part3

Leetcode 39. 组合总和 讲解前: 这道题其实在掌握了之前的组合问题之后再看并不是那么难,其关键就在于我们这道题中没有一个特定需要的组合大小,并且列表中的元素是可以重复使用的,那么比如说给的例子中的 输入: candidates [2…

使用Python获取红某书笔记详情并批量无水印下载

根据红某手最新版 请求接口必须要携带x-s x-s-c x-t,而调用官方接口又必须携带cookie,缺一不可,获取笔记详情可以通过爬取网页的形式获取,虽然也是无水印,但是一些详情信息只能获取大概,并不是详细的数值,因此既不想自己破解x-s x…

【chrome扩展】简 Tab (SimpTab)‘每日一句名言’样式

背景:最初参考“每日诗词”发现总是那几句,可以更换API接口完成“每日一句名言” 声明:本人不会ajax及ccs样式,非专业人士,借助CHATGPT代码生成完成。请友善交流。 每一句名言API: "https://api.xygeng.cn/open…

【MATLAB第102期】基于MATLAB的BRT增强回归树多输入单输出回归预测模型

【MATLAB第102期】基于MATLAB的BRT增强回归树多输入单输出回归预测模型 BRT,即Boosted Regression Trees(增强回归树),是一种用于回归问题的集成学习方法。它结合了多个决策树模型,通过逐步改进的方式来提高整体模型的…

【保姆级介绍Oracle】

🎥博主:程序员不想YY啊 💫CSDN优质创作者,CSDN实力新星,CSDN博客专家 🤗点赞🎈收藏⭐再看💫养成习惯 ✨希望本文对您有所裨益,如有不足之处,欢迎在评论区提出…

开放威胁情报社区

AlienVault - Open Threat ExchangeLearn about the latest cyber threats. Research, collaborate, and share threat intelligence in real time. Protect yourself and the community against todays emerging threats.https://otx.alienvault.com/

Kotlin:for循环的几种示例

一、 打印 0 到 2 1.1 方式一:0 until 3 /*** 打印 0 到 2*/ fun print0To2M1(){for (inex in 0 until 3){// 不包含3print("$inex ")} }运行结果 1.2 方式二:inex in 0 …2 /*** 打印 0 到 2*/ fun print0To2M2(){for (inex in 0 ..2){//…

哈哈哈哈哈

欢迎使用Markdown编辑器 你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。 222 我们对Markdown编辑器进行了一些功能拓展与语法支持,…

【Linux】详解动态库链接和加载对可执行程序底层的理解

一、动静态库链接的几种情况 如果我们同时提供动态库和静态库,gcc默认使用的是动态库。如果我们非要使用静态库,要加-static选项。如果我们只提供静态库,那可执行程序没办法,只能对该库进行静态链接,但程序不一定整体…

Ds18B20理解与运用

在51单片机中,DS18B20是一个十分重要的外设,它是一个温度传感器,可以读取温度并以16位的数据输出。这个数据由四位符号位,7位整数位和4位小数位组成。当符号位都是0的时候,温度是零上温度;当符号位都是1的时…

基于隐私保护的可追踪可撤销密文策略属性加密方案论文阅读

论文是2022年发表的A Traceable and Revocable Ciphertext-Policy Attribute-based Encryption Scheme Based on Privacy Protection 摘要 本篇论文提出了一种具有用户撤销、白盒追踪、策略策略隐藏功能的CP-ABE方案。在该方案中密文被分为两个部分:第一个部分是和…

ros小问题之rosdep update time out问题

在另外一篇ROS 2边学边练系列的文章里有写碰到这种问题的解决方法(主要参考了其他博主的文章,只是针对ROS 2做了些修改调整),此处单拎出来方便查找。 在ROS 2中执行rosdep update时,报出如下错误: 其实原因…