前言

本文主要阐述TCP拥塞控制中ssthresh的来历以及为什么拥塞避免探测到丢包的时候,ssthresh会被设置为当前窗口的一半。
进入证实内容之前,不得不再次吐槽!目前在网上搜的,任何资料上看的,甚至RFC上,都没有讲明白到底什么是ssthresh,它的值有什么讲究,几乎所有的资料都是在说,如果窗口大于ssthresh,那么就执行线性增窗的拥塞避免阶段,否则执行慢启动...这让几乎所有人记住了这个结论,并且在长期被洗涤之后,很多人对这个不知所以然的事实却表现的不以为然,其实也包括我自己。因此当我明白了ssthresh到底是怎么一回事的时候,当我知道了丢包后ssthresh的1/2系数与公平性之间的关系的时候,我便迫不及待地想把这些东西分享出来!

TCP数据段填满端节点之间的网络

假设端系统A和B之间在进行TCP通信,那么只要A和B之间存在空间距离,由于光速的传播时延,一定意味着A和B之间存在一定的容量,可以容纳若干的数据段,你可以将其想做是一般的缓存,另外,为了更具普遍性,我们认为A和B之间的所有路由器,交换机等中间节点的队列缓存,也包含在A和B的网络缓存之中。如下图所示:

为了达到最高的网络利用率,我们希望A和B之间的缓存(包括节点队列以及网络本身)中完全充盈着TCP的数据段,并且是持续维持。

TCP数据段无间隙地持续流动

如上图所示,当A,B之间的网络被填满时,A和B之间一共有N个数据段,发送端还可以继续发送数据吗?事实上是可以的,因为在网络被填满之后,发送端每发送一个数据段,接收端同时也会消费掉一个数据段,同时发出一个ACK,直到填满A,B间网络的那个数据段的ACK到达发送端为止,依照假定,ACK的速率和数据段的速率一致,我们算一下,发送端一共可以持续发送2*N个数据段,这2*N个数据段发送的开始时间点是第1个数据段发送的时间,结束时间点是第一个数据段的ACK回到发送端的时间,正好是一个RTT,设发送速率为r,那么以下的等式显而易见:
2*N = r*RTT
过程如下图所示:

我们还可以看到,前N个段是为了填满A,B之间的网络,后N个段是在“A,B之间已经满载的情况下”TCP的ACK clock驱动的pacing。紧随着这2*N个数据段的是一个新的周期,又是一个RTT内2*N个数据段,这就是理想情况下的情景,数据段充盈着网络,不间断地源源不断从发送端发出,ACK亦不间断地从接收端返回。

两个区域(safe & dangerous)

我其实不想现在就把谜底揭穿,但是我也不想卖关子,毕竟现在也不早了。
请注意上图中的t28这个时间点,在t28之前,A和B之间并不总是被充满,而在t28之后,却总是满的。这意味着,t28之前的数据段是可以缓冲的,即网络中还存在一些空闲的空间可以提供给数据进行缓冲,从而不至于数据被丢弃,然而在t28之后,网络满载了,我们看到没有丝毫的空白区域可供数据包缓冲,这意味一旦发生拥塞,数据包必然丢失!
显而易见,t28之前是安全的,而t28之后则是危险的,这就是safe area和dangerous area的由来!划分二者的是什么?正是网络的容量!在上述图示的例子中,就是4!当Flight的数据段小于4的时候,意味着可以激进的传输,当Flight数据一旦越过4,就必须保守传输了!
        何谓激进?何谓保守?激进就是可以让窗口最快的速度增加到safe和dangerous的边界,由于TCP由ACK来驱动,只要收到一个ACK,就意味着通道是畅通的,窗口就可以递增一个MSS,而所谓的保守就是,必须等待当前窗口的数据全部都被ACK了之后,说明刚才发送的数据是可以到达对端的,此时才能将窗口增加一个MSS。现在来总结以下两个区域的增窗方式:
safe区域:

dangerous区域:

我们来比对一下窗口在这两个区域时的特性,如果说安全区域的目标是填满网络的话,那么在安全区域我们已知的是网络并未被填满,此时只要有ACK到来就可以增窗,直到网络被填满,现在我们来看危险区域,此时我们已知的事实是网络已经满了,我们希望让它继续满下去,能满则保持,提高网络的利用率,我们知道TCP的发送窗口(即可以发送多少数据)是ACK驱动的,ACK就像时钟一样,我们希望数据可以持续发送以保持网络满载,就要保证ACK源源不断地到来,因此在这个阶段继续发送数据的目标仅仅是为了消除ACK时钟的空白期,或者称作停摆期,因为只有这样,源源不断的ACK才能驱动数据源源不断地被发送。
        回过头来再看一下增窗过程图的t20这个时间点,网络被4,5,6,7这四个数据包已经充满,但是在下一个时刻t21,随着4被接收,由于没有到达的ACK,网络被清空了1个MSS,理论上,我们知道最终的窗口肯定就是2*N,此例中,N就是4,在例子中,最终也确实窗口增加到了8,然而在现实中,拥塞随时都会发生,也就是说在窗口从4增加到8的期间,随时会发生丢包,这也就意味着窗口可能永远都增加不到8!那么我们怎么可以知道当前的窗口是合理的,然后尝试继续增加窗口呢?答案就是前一个窗口的数据全部被确认!这就是拥塞避免的由来,这个过程很慢,并不是设计的问题,而是它必须这么慢。拥塞避免这个名字非常好,确实在避免!
        理解了上面的描述,我们可以给出TCP管道的概念了,然后所有的真相就大白了。

TCP管道的概念

事实上,TCP管道包含两个部分,按照公式2*N=r*RTT,2*N便是管道的容量,这两部分的第一部分是网络被填满之前的容量,只要知道尚未被填满,尽情地填,使用慢启动足矣,第二部分是网络被填满之后的容量,完全按照ACK来驱动,采用拥塞避免方式探测窗口,理想情况下,理论上,这两部分的容量是相等的。因此,我们可以就可以知道事情的真相了。
问题1:N是什么?
答:N就是ssthresh!
问题2:为什么探测到拥塞后,ssthresh要下降为当前窗口的一半?
答:探测到拥塞说明管道的容量为当前窗口C,而C=2*N,因此N=(1/2)*C!
问题3:为什么在拥塞避免阶段要加性增?即AI
答:只有当一窗的数据被确认,才可以确保之前的窗口是有效的,毕竟网络已经塞满,而ACK有可能不对称返回,拥塞随时可能发生。
问题4:为什么乘性减?
答:见问题2.
问题5:慢启动阶段为什么可以指数级增窗?
答:因为此时可以确保窗口小于N,即网络还没有塞满,即便发生拥塞,也还有富余的缓存可用。
问题6:还有问题吗?
答:如果一切都如上那般美好,当然就没有问题了,问题是,ssthresh从来就没有估计准确过。

回到现实中的TCP

TCP在设计之处的愿景十分美好,然而现实世界并不是一个友好的世界,不过,TCP本身就是自适应的,它并没有规定ssthresh的值的大小,甚至都没有建议,它完全靠在TCP自行发现丢包或者拥塞后,以当前窗口的一半作为ssthresh的当前值,随着连接的继续,ssthresh也会动态调整,因为不管现实多么残酷,理想中的反馈系统总归是一个万物收敛的目标,那就是实际的带宽总是趋向于ssthresh的2倍的大小,令人惊讶的是,ssthresh就是依靠拥塞避免算法计算出来的。当然,随着TCP的发展,这个C=2*N=r*RTT经典公式也历经了诸多的变化形成了各种变体,比如cubic就不再以2作为ssthresh的系数来计算管道容量。

慢启动的hystatr优化

我们知道,ssthresh的设置是以丢包作为反馈信号的,现在问题是,连接刚刚建立的时候,没有丢包作为反馈信号的时候,如何来设置ssthresh?
一般而言,默认的实现都是将其设置为一个巨大的值,然后最快的速度历经一次丢包,然后设置ssthresh为丢包时窗口的一半,然后像ssthresh的2倍缓慢逼近。但是这会带来问题,由于没有ssthresh作为阈值限制,用丢包作为代价,太高昂。因此在慢启动过程中如果可以探测到ssthresh的值,那就可以随时退出慢启动状态了。那么我们如何来探测呢?
还是C=2*N=r*RTT这个公式,关键看看我们怎么使用它。由于我们只是探测网络被塞满时的情况,即N的值,因此:
N=r*(RTT/2)
我们看看r是什么?所谓速率其实就是一定的事务量除以做这些事务的时间,如果说我们发出去了N'个数据包,一共用了时间段T,那么:
r=N'/T
代入后得到:
N=(N'/T)*(RTT/2)
理想情况下,在毕竟网络容量的时候,N=N',那么就可以很简单得到T等于RTT/2的时候,就说明达到了ssthresh,该退出慢启动了!
那么如何来实现它呢?由于我们无法单独探测N个数据段到达接收端并计时,我们可以变相等价使用ACK来计算,以一个窗口的第一个数据段作为计时开始Tstart,每收到一个ACK即更新以下数值:
RTTmin:采样周期内最小的RTT,以最大限度地表示A和B之间的理想往返时延。
Tcurr:当前时间

如果下列条件成立,则可以退出慢启动了:
Tcurr - Tstart >= RTTmin/2
非常简单易懂。然而现实并不是理想的,大多数情况下,以上的算法并没有带来比较好的效果,为什么呢?因为整个带宽不是一个TCP连接独享的,而是全世界的所有TCP连接甚至包括UDP共享的,因此以上的公式基本上无法表示任何真实的情况,所以实际当中,更倾向于使用RTT来预估网络已经被塞满。使用RTT来估算网络容量ssthresh更加实际一些,因为它充分考虑了拥塞时的排队延时,因此在该方法下,退出慢启动的条件便成了:
Tcurr_rtt > RTTmin + fixed_value

以上旨在解决首次慢启动在还没有ssthresh值的时候预测ssthresh的方式,其实在此后的任何时候,只要是慢启动,都可以用以上的算法来预测当前的ssthresh,而不是说必须要用拥塞算法给出的ssthresh或者说仅仅是1/2丢包窗口(虽然你已经看到,这个1/2是多么地合理!)

ssthresh的快速穿越问题

我们知道,慢启动的时候,增窗速度非常快(基本就是根据ACK的反馈,将数据段翻倍突突出去的),那么在窗口增加到接近到仍然小于ssthresh的时候,会出现如下图所示的情况:

然而这在实现中是不易发生的,是什么限制住它的发生呢?以下几点:

1.TCP的延迟确认机制最多只能延迟2个MSS

慢启动增窗,收到一个ACK递增1个MSS,即便在使用ABC的时候,也就是说窗口最多只能超越ssthresh 2个MSS,这是由下述代码保证的:

 
  1. if (sysctl_tcp_abc > 1 && tp->bytes_acked >= 2*tp->mss_cache)

  2. cnt <<= 1;

2.即便发生了ACK大量丢失,TCP的默认实现也是数ACK的个数,而不是数被ACK的字节数

3.发生大量ACK丢失又启用ABC时,见方法1.

4.两段处理方式

Linux的4.x版本内核中默认使用ACK的字节数来计数增窗值(ABC方案),在穿越ssthresh的时候,TCP拥塞控制逻辑会将被ACK的字节数分为两个部分,ssthresh以下的部分用来计数慢启动,而ssthresh以上的部分用来计数拥塞避免。综上所述,下图总结了ssthresh穿越的情况:

AIMD的公平性收敛

为了简单起见,我们假设有TCP1和TCP2两个连接共享一个链路,现在看它们是怎么“收敛到公平”的,下面的图示清晰显示了一切:

如果你看不懂这个图,请自行google。我们可以肯定,在公平线的下方,红色的减窗线的斜率是恒小于公平线(斜45度角)的斜率的,两个链接的每一次降窗,其降窗线的斜率都会越来越接近公平线的斜率,即收敛到公平,最终,它将在绿色粗线上震荡,永葆公平(虽然利用率不是那么高!)。

我们还可以看到,TCP1和TCP2是等比例降窗的,在此例中比例是0.5,它们非得是0.5吗?非也!只要保持等比例,图中的注解1就永远成立,最终的收敛也会永远成立,不同的仅仅是最终收敛额绿色粗线的长度和范围!虽然说按照最初的Reno TCP,保持0.5的降窗比例是多么得合理(见上述推论),然而考虑到现实的复杂情况,比例不再是0.5也是合理的。
         现在我们来看看如果TCP1和TCP2的降窗比例不同会怎样。假设TCP2降窗依然为0.5的比例,而TCP2则小于0.5,那么上图将会变成下面的样子:

我们可以看到,竞争者中降窗比率最小的将会最终抢占几乎所有的带宽,它会将所有的其它连接的带宽逐渐往左上角挤兑,最终归零。这么说来,如果想让自己的TCP具有侵略性,减少降窗比率是不是就可以了呢?没这么这简单!要知道,我上面的两幅图有一个共同的前提,那就是竞争者的RTT是相等的!但是现实中,会这样吗??非常难!如果RTT相等,比如它们的源头和目标都在同一个地点,那么它们十有八九是合作关系,而不是竞争!爆炸!

那么,RTT将会是一个十分重要的角色!确实是这样,实际的TCP在运行中,RTT的波动非常大,这就几乎将我上面的论述全部推翻了,显然很令人心碎!然而,上述的分析作为一个理论模型还是有意义的,它起码让你理解了TCP的本质行为。至于说实际情况,RTT的波动是一个有意义的信号,它让端系统看到了中间路由器交换机的排队行为,因此会出现RTT所谓的“噪点”,很多人想除掉它们,平滑掉它们,但是这同时也意味着你屏蔽了重要的信号。
        RTT的波动非常具有动感且性感,它用数值表征了整个排队理论,或者你可以推出马尔科夫到达过程,或者你只是觉得它们是令人难过的噪点...于是,现实中的TCP几乎完全改进了Reno的指导,除了Reno几乎没有什么拥塞算法在发现丢包时把ssthresh降为当前窗口的一半。这就是TCP的进化,但是这种进化始终围绕着一个内核,这个内核就是我上面说的这些,简单,易懂,然而却令人惊讶的东西。

转载自 大神:dog250

TCP核心概念-慢启动,ssthresh,拥塞避免,公平性的真实含义相关推荐

  1. TCP的拥塞控制--慢启动,拥塞避免,快重传,快速恢复

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

  2. TCP的慢启动、拥塞避免、重传、快恢复乱七八糟总是记不清?11个连环问让你一次性打通任督二脉

    摘要:如果你的开发过程涉及数据传输,一直在重传.超时之类的方案里有困惑的话,不妨重新学一学可靠性最精致的TCP协议. 本文分享自华为云社区<TCP的慢启动.拥塞避免.重传.快恢复乱七八糟总是记不 ...

  3. TCPIP------慢启动与拥塞避免

    如果阅读过 TCP 协议相关的书籍,一定看到过慢启动.拥塞控制等名词.这些概念似乎离应用开发者很远,然而,如果没有拥塞控制,整个网络将会锁死,所有消息都无法传输.当接收主机的处理能力不足时,是通过滑动 ...

  4. 【大厂面试题精选】UDP 和 TCP 核心知识总结

    今天给大家总结其中的核心高频面试知识点,希望对你有帮助! 前言 网络层只把分组发送到目的主机,但是真正通信的并不是主机而是主机中的进程.传输层提供了进程间的逻辑通信,传输层向高层用户屏蔽了下面网络层的 ...

  5. Kafka:Kafka核心概念

    1 消息系统简介 1.1 为什么要用消息系统 ? 解耦 各位系统之间通过消息系统这个统一的接口交换数据,无须了解彼此的存在: 冗余 部分消息系统具有消息持久化能力,可规避消息处理前丢失的风险: 灵活性 ...

  6. 学习笔记Flink(二)—— Flink数据流模型、时间窗口和核心概念

    一.Flink编程数据流模型 1.1.Flink – API封装 Flink 提供不同级别的API封装来支持流/批处理应用程序. 1.2.Flink-编程数据流 Source:一个不会结束的数据记录流 ...

  7. Docker 核心概念、安装、端口映射及常用操作命令,详细到令人发指。

    Docker简介 Docker是开源应用容器引擎,轻量级容器技术. 基于Go语言,并遵循Apache2.0协议开源 Docker可以让开发者打包他们的应用以及依赖包到一个轻量级.可移植的容器中,然后发 ...

  8. docker 查看镜像_Docker 核心概念、安装、端口映射及常用操作命令,详细到令人发指!...

    来自小洋人最HAPPY投稿 一.Docker简介 Docker是开源应用容器引擎,轻量级容器技术. 基于Go语言,并遵循Apache2.0协议开源 Docker可以让开发者打包他们的应用以及依赖包到一 ...

  9. 一文了解Docker核心概念和安装配置

    核心概念 Docker 有三大核心概念,分别是镜像,容器,仓库. Docker 镜像 Docker 镜像类似于虚拟机的镜像,可以把它理解为一个只读的模板,例如一个镜像可以包含一个基本的操作系统环境,里 ...

最新文章

  1. java string出现次数_Java String方法获取字符出现次数及字符最大相同部分示例
  2. python数据分析报告的格式_Python数据报表之Excel操作模块用法分析
  3. 求助:一个物理专业的在做GPU的人
  4. 2020 前端技术发展回顾
  5. python怎么获取时间_Python:如何从datetime.timedelta对象中获取时间?
  6. JeecgBoot轻松解决ERP项目复杂布局需求,JVXETable高性能行表格效果和项目案例
  7. 提升效率的几个小绝招
  8. excel如何做出弧形_人民日报同款海报,只用线和字就能做出高大上的工作报告...
  9. maven 把依赖包一起打包
  10. CukeTest新版本公测邀请-Windows应用自动化
  11. Oracle解决Ora-01653无法扩展表空间问题
  12. 【WPF】Slider 任意位置拖动
  13. lede做无线打印服务器吗,OpenWrt/PandoraBox/LEDE做打印服务器连接HP1020问题汇总
  14. auto dvr_什么是“广播DVR服务器”,为什么在我的PC上运行它?
  15. 紫猫数据库使用 (二)
  16. android qq聊天图片无法显示,QQ聊天时无法打开图片
  17. 竖屏java转横屏_android设置横屏和竖屏的方法
  18. 一个主机多显示器的操作方法
  19. 小米路由器SSH资料攻略
  20. 微信小程序的冥冥中拥有的小细节

热门文章

  1. [转]九个Console命令,让js调试更简单
  2. CentOS 6.3 编译安装 Nginx(含:管理脚本)
  3. CS193P学习笔记(一)
  4. Javascript中{}+[]===0为true,而[]+{}===0为false
  5. Linux下的I/O复用与epoll详解
  6. 数字图像处理实验(2):PROJECT 02-02, Reducing the Number of Gray Levels in an Image
  7. html转word保留样式_译员必备 | 初识Word格式标记
  8. 复制构造函数 与 赋值函数 的区别
  9. js循环(for/for in/forEach/map/for of)详解
  10. python面试总结