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

本文主要阐述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,这是由下述代码保证的:

if (sysctl_tcp_abc > 1 && tp->bytes_acked >= 2*tp->mss_cache)
	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的进化,但是这种进化始终围绕着一个内核,这个内核就是我上面说的这些,简单,易懂,然而却令人惊讶的东西。

时间: 2024-10-08 09:13:59

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

TCP数据量--滑动窗口、拥塞窗口、慢启动、Negle算法 经受时延的确认等

TCP的数据流大致可以分为两类,交互数据流与成块的数据流.交互数据流就是发送控制命令的数据流,比如relogin,telnet,ftp命令等等:成块数据流是用来发送数据的包,网络上大部分的TCP包都是这种包. 很明显,TCP在传输这两种类型的包时的效率是不一样的,因此为了提高TCP的传输效率,应该对这两种类型的包采用不同的算法. 总之,TCP的传输原则是尽量减少小分组传输的数量. TCP的交互式数据流 ?         经受时延的确认技术 TCP的交互式数据流通常使用"经过时延的确认"

TCP怎么保证证包有序传输的,TCP的慢启动,拥塞避免,快速重传,快速恢复

TCP提供了最可靠的数据传输,它给发送的每个数据包做顺序化(这看起来非常烦琐),然而,如果TCP没有这样烦琐的操作,那么,可能会造成更多的麻烦.如造成数据包的重传.顺序的颠倒甚至造成数据包的丢失. 那么,TCP具体是通过怎样的方式来保证数据的顺序化传输呢? 主机每次发送数据时,TCP就给每个数据包分配一个序列号并且在一个特定的时间内等待接收主机对分配的这个序列号进行确认,如果发送主机在一个特定时间内没有收到接收主机的确认,则发送主机会重传此数据包.接收主机利用序列号对接收的数据进行确认,以便检测

ElasticSearch学习笔记-01 简介、安装、配置与核心概念

一.简介 ElasticSearch是一个基于Lucene构建的开源,分布式,RESTful搜索引擎.设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便.支持通过HTTP使用JSON进行数据索引. Lucene只是一个框架,要利用它的功能,需要使用JAVA,并且在程序中集成Lucene.更糟的是,Lucene非常复杂,需要做很多的学习了解,才能明白它是如何运行的. Elasticsearch使用Lucene作为内部引擎,但是在使用它做全文搜索时,只需要使用统一开发好的API即可,

ElasticSearch笔记整理(二):CURL操作、ES插件、集群安装与核心概念

[TOC] CURL操作 CURL简介 curl是利用URL语法在命令行方式下工作的开源文件传输工具,使用curl可以简单实现常见的get/post请求.简单的认为是可以在命令行下面访问url的一个工具.在centos的默认库里面是有curl工具的,如果没有请yum安装即可. curl -X 指定http的请求方法 有HEAD GET POST PUT DELETE -d 指定要传输的数据 -H 指定http请求头信息 curl创建索引库 curl -XPUT http://<ip>:9200

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

Docker简介 Docker是开源应用容器引擎,轻量级容器技术. 基于Go语言,并遵循Apache2.0协议开源 Docker可以让开发者打包他们的应用以及依赖包到一个轻量级.可移植的容器中,然后发布到任何流行的Linux系统上,也可以实现虚拟化 容器完全使用沙箱技术,相互之间不会有任何接口 类似于虚拟机技术(vmware.vitural),但docker直接运行在操作系统(Linux)上,而不是运行在虚拟机中,速度快,性能开销极低 白话文,简介就是: Docker支持将软件编译成一个镜像,然

ElasticSearch 全文检索— ElasticSearch 核心概念

ElasticSearch核心概念-Cluster 1)代表一个集群,集群中有多个节点,其中有一个为主节点,这个主节点是可以通过选举产生的,主从节点是对于集群内部来说的.es的一个概念就是去中心化,字面上理解就是无中心节点,这是对于集群外部来说的,因为从外部来看es集群,在逻辑上是个整体,你与任何一个节点的通信和与整个es集群通信是等价的. 2)主节点的职责是负责管理集群状态,包括管理分片的状态和副本的状态,以及节点的发现和删除. 3)注意:主节点不负责对数据的增删改查请求进行处理,只负责维护集

ZooKeeper 系列(一)—— ZooKeeper核心概念详解

一.Zookeeper简介 二.Zookeeper设计目标 三.核心概念 ????????3.1 集群角色 ????????3.2 会话 ????????3.3 数据节点 ????????3.4 节点信息 ????????3.5 Watcher ????????3.6 ACL 四.ZAB协议 ????????4.1 ZAB协议与数据一致性 ????????4.2 ZAB协议的内容 五.Zookeeper的典型应用场景 ????????5.1数据的发布/订阅 ????????5.2 命名服务 ??

Kubernetes核心概念总结(摘选)

1.基础架构 1.1 Master Master节点上面主要由四个模块组成:APIServer.scheduler.controller manager.etcd. APIServer.APIServer负责对外提供RESTful的Kubernetes API服务,它是系统管理指令的统一入口,任何对资源进行增删改查的操作都要交给APIServer处理后再提交给etcd.如架构图中所示,kubectl(Kubernetes提供的客户端工具,该工具内部就是对Kubernetes API的调用)是直接

通过实例快速掌握k8s(Kubernetes)核心概念

容器技术是微服务技术的核心技术之一,并随着微服务的流行而迅速成为主流.Docker是容器技术的先驱和奠基者,它出现之后迅速占领市场,几乎成了容器的代名词.但它在开始的时候并没有很好地解决容器的集群问题.Kubernetes抓住了这个机遇,以容器编排者(Container Orchestration)的身份出现,对容器集群进行管理和调度,现在已经打败了Docker成为了容器技术事实上的标准.当然K8s内部还是需要Docker的,但它的功能范围被大大压缩了,只是负责底层的容器引擎和镜像(Docker