一、概述

在RFC2861中,区分了TCP连接数据传输的三种状态

network-limited:TCP的数据传输受限于拥塞窗口而不能发送更多的数据

application-limited:TCP的数据传输速率受限与应用层的数据写入速率,并没有到达拥塞窗口上限,有些文档也称呼这种场景为data-limited

idle:发送端没有额外的数据等待发送,当数据发送间隔超过一个RTO的时候就认为是ilde态。

之前我们介绍慢启动和拥塞避免的过程都是基于conservation of packets principle和Ack clocking建立的,cwnd代表了对网络拥塞状态的一个评估,很明显拥塞控制要根据ACK来更新cwnd的前提条件是,当前的数据发送速率真实的反映了cwnd的状况,也就是说当前传输状态是network-limited。想象一个场景,假如tcp隔了很长时间没有发送数据包,即进入idle,那么当前真实的网络拥塞状态很可能就会与cwnd反映的网络状况有差距。而application-limited的场景下,受限数据的ACK报文还可能把cwnd增长到一个异常大的值,显然是不合理的。

基于上面提到的这个问题,RFC2861引入了拥塞窗口校验(CWV,Congestion Window Validation)算法,协议中给出的伪代码如下,其中tcpnow表示获取当前时间,T_last表示上次数据发送的时间,T_prev表示TCP从network-limited切换到application-limited状态的时间点,W_used表示程序实际使用的窗口大小。

  1. Initially:
  2.       T_last = tcpnow, T_prev = tcpnow, W_used = 0
  3.   After sending a data segment:
  4.       If tcpnow - T_last >= RTO
  5.           (The sender has been idle.)
  6.           ssthresh =  max(ssthresh, 3*cwnd/4)
  7.           For i=1  To (tcpnow - T_last)/RTO
  8.               win =  min(cwnd, receiver's declared max window)
  9.               cwnd =  max(win/2, MSS)
  10.           T_prev = tcpnow
  11.           W_used = 0
  12.       T_last = tcpnow
  13.       If window is full
  14.           T_prev = tcpnow
  15.           W_used = 0
  16.       Else
  17.           If no more data is available to send
  18.               W_used =  max(W_used, amount of unacknowledged data)
  19.               If tcpnow - T_prev >= RTO
  20.                   (The sender has been application-limited.)
  21.                   ssthresh =  max(ssthresh, 3*cwnd/4)
  22.                   win =  min(cwnd, receiver's declared max window)
  23.                   cwnd = (win + W_used)/2
  24.                   T_prev = tcpnow
  25.                   W_used = 0

可以看到基本思路是:当TCP idle超过一个RTO时更新ssthresh =  max(ssthresh, 3*cwnd/4),cwnd 每隔一个RTO更新为 max(win/2, MSS)。当TCP处于application-limited超过一个RTO的时候,更新ssthresh =  max(ssthresh, 3*cwnd/4),cwnd = (win + W_used)/2。

CWV是在RFC2861引入的,目前RFC2861已经被RFC7661取代,RFC7661中提出了一个new CWV的算法,不在区分application-limited和idle两种状态,统称为Rate-Limited,RFC7661对于RFC2861的评价是It had the  correct motivation but the wrong approach to solving this problem。翻译过来就是说提出RFC2861 CWV算法的人虽然脑子笨了点没能解决好问题但是动机还是好的。虽然有new CWV的patch,但目前(2016.9)最新的linux内核代码仍然是使用的RFC2861。因此本篇也以介绍RFC2861的CWV算法为主(实际上对于RFC7661我也没看完,只是看了协议前面的基本介绍)。

linux中CWV功能受到/proc/sys/net/ipv4/tcp_slow_start_after_idle参数的控制,这个参数设置为非0的时候打开CWV功能,默认是打开的。linux中使用is_cwnd_limited标记当前为network-limited的状态,is_cwnd_limited可以看成是每个rtt更新一次。linux在实现上实际与RFC2861有些差异,例如协议伪代码中If window is full这个条件,在慢启动阶段,linux会判断如果上一个窗口发出的数据中packets_out的最大值(max_packets_out)超过了cwnd的一半,就认为窗口是满的不会按照application-limited来更新ssthresh和cwnd,如果不是慢启动阶段则直接根据is_cwnd_limited来判断上一个窗口是否满,窗口或者说rtt的维护与RTO计算中rtt_seq状态变量的维护类似,同样是使用snd.una和snd.nxt来更新的。 对于W_used是以packets_out来更新的。对于ilde态的处理是在应用层write操作时候进行判断的。

上面这些差异是可以理解的,但是还有一处差异就是,linux在application-limited场景下,只有拥塞状态处于Open状态的时候才会更新ssthresh和cwnd,但是另外一方面Open状态下每次收到ack number反馈确认了新的数据包的时候又会更新T_prev,这样在linux中基本很难满足tcpnow - T_prev >= RTO这个条件,这就导致linux在application-limited场景下的处理与RFC2861相差甚远,一般只会触发idle场景处理,而不能进入application-limited场景。相关代码在git上已经找不到最初的修改记录了,具体原因也就不清楚。但是好在reno拥塞控制算法中当TCP发送端处于application-limited状态时候并不会更新cwnd。有可能是因为在application-limited场景下,直接在具体拥塞控制算法中控制不更新cwnd比原始的RFC2861 CWV算法效果更好吧。

二、wireshark示例

同样在测试进行前我们如下设置,使得server端与127.0.0.2的连接,初始cwnd=2,初始ssthresh=8,拥塞控制算法选择reno。关闭tso、gso功能

  1. ******@Inspiron:~$ sudo ip route add local 127.0.0.2 dev lo congctl reno initcwnd 2 ssthresh lock 8      #参考本系列destination metric文章
  2. ******@Inspiron:~$ sudo ethtool -K lo tso off gso off  #关闭tso gso以方便观察cwnd变化

此处的两个示例重点关注cwnd和ssthresh的变化,因为没有SACK及重传等干扰,sacked_out、lost_out、retrans_out中间变量一直为0,对于下面示例的场景linux内部计算的in_flight = packets_out - ( sacked_out + lost_out) + retrans_out =packets_out,与wireshark中in_fligth列是一致的,因此下面不在详细解释这些中间变量。对于有SACK及重传时这些状态变量的变化庆请参考前面文章拥塞控制的综合示例。而is_cwnd_limited、max_packets_out变量的更新示例只会给出更新后的结果,更新的过程涉及到比较多的其他变量,即使贴上内核代码也需要大量的篇幅来解释清楚,因为本文不再详细介绍。感兴趣的可以自行对照本示例去学习内核代码。我会在补充说明中给出几个与下面示例相关联的关键函数。读者因此可能很难理解清楚下面的wireshark示例,但是重点观察两个宏观的现象就行了,一个是application-limited状态下reno不会更新cwnd,另外一个是ilde时间超过RTO后,CWV会更新cwnd和ssthresh。不必过于纠结更新的细节。

1、application-limited状态linux的处理以及idle后cwnd和ssthresh的更新

client与server端建立连接后先发送一个请求报文,然后server端内核正常回复ACK,另外server应用层在建立与client的连接后休眠50ms,然后发送4个数据包,每个数据包大小为50bytes间隔为5ms,接着以60ms为间隔,连续发送6个数据包,每个数据包的大小为50bytes,构造RFC2861中的application-limited的场景,接着server端休眠300ms,构造RFC2861的idle场景,最后server端再以5ms为间隔,连续发送15个数据包,每个数据包大小为50bytes。client端对于每个server端的报文都回复一个ACK,client与server端的rtt为50ms。

No1-No3:client与server通过三次握手建立连接,连接建立后根据路由表配置,初始化cwnd=2,ssthresh=8。连接建立后进入慢启动流程

No4-No5:client端发送请求,server端回复ACK报文

No6-No7:server端开始以5ms的间隔写入报文,可以看到受限cwnd,只能发出No6和No7两个报文。其余写入的报文只能暂时缓存在内核中。注意server端在发出No6和No7两个数据包后,发现当前in_flight的报文个数以cwnd是一致的,因此更新is_cwnd_limited=1,表示当前是处于network-limited状态。

No8-No10:server端正在进行慢启动,收到一个ACK后cwnd=cwnd+1=3,此时可以额外发出两个报文了。No10发出后更新max_packets_out=3。

No11:server端收到No11后,更新cwnd=cwnd+1=4,但是server端第一阶段以5ms间隔仅仅写入了4个数据包,然后休眠60s后在继续写入的,因此此时已经没有额外的数据可以发送了,所以server端在收到No11后并没有立即发出新的数据包。

No12:server端开始以60ms的间隔写入数据,No12是第一个写入的数据包,从这时候起,server端开始进入RFC2861所说的application-limited状态。

No13-No15:这几个报文是之前server端发出报文的ACK报文,注意收到No13报文后更新cwnd=cwnd+1=5,收到No14后更新cwnd=cwnd+1=6。收到No15后,reno拥塞控制算法发现当前处于慢启动阶段,而且cwnd=6>=2*max_packets_out=6,因此认为当前处于application-limited状态,虽然收到了ACK报文,但是不再更新cwnd。

No16-No25:server端发出No16的时候会更新max_packets_out=1,后面收到No17这个ACK报文的时候,同样会因为cwnd=6>=2*max_packets_out=2,reno拥塞控制算法并不更新cwnd。后面reno收到No19、No21、No23、No25对应的ACK报文时候同样是因为这个原因而不能更新cwnd。但是如上所说No17、No19、No21、No23、No25这些报文都会更新伪代码中T_prev这个变量,因此linux每次发送数据的时候If tcpnow - T_prev >= RTO这个判断条件都很难满足,因此也就不会进入application-limited下cwnd和ssthresh的更新流程了。因此在收到No25之后,cwnd=6,ssthresh=8。

No26:注意No26与No24之间的间隔大约为300ms。server端的应用层在进行write操作的时候,发现当前数据发送的时间间隔已经超过了RTO(从server端程序可以获取当前RTO为252ms),因此更新ssthresh =  max(ssthresh, 3*cwnd/4)=8,cwnd =  max(win/2, MSS)=3,实际linux更新cwnd的时候还要cwnd大于等于路由表中配置的2。

接着server端以5ms为间隔连续写入15个数据包

No27-No28:可以看到发出No28后,server端不能在额外发出新的数据,此时in_flight为150bytes,正好对应cwnd=3。

No29-No43:接着server端进入慢启动流程,可以看到直到发出No43后,in_flight=400bytes,对应cwnd=8,此时ssthresh=8,因此随后应该进入拥塞避免阶段

No44-No49:进入拥塞避免阶段,每收到一个ACK报文,cwnd_cnt=cwnd_cnt+1,直到收到No48后,cwnd_cnt=3

No50-No57:server端陆续收到其余的ACK报文,其中在收到No54报文的时候,cwnd_cnt增长到8,因此更新cwnd=cwnd+1=9,并重置cwnd_cnt=0。最终收到No57报文后,ssthresh=8,cwn=9,cwnd_cnt=3

2、构造application-limited下更新cwnd、ssthresh流程场景

在上一个示例中我们已经看到了在RFC2861表述的application-limited场景下,linux并不会进入更新cwnd和ssthresh的application-limited代码流程,下面我们人为根据代码构造一个这样的场景。在应用层数据受限的情况下,我们前面介绍过linux会在数据真实发送时刻判断是否进入更新cwnd和ssthresh的application-limited流程,而idle态的判断则是在应用层write操作的时候进行的。因为linux会在收到确认新数据包的ACK报文的时候更新T_prev,因此如果要在数据实际发出的时候满足tcpnow - T_prev >= RTO这个条件,数据应用层write时刻不满足tcpnow - T_last >= RTO才行。读者可能会想到通过前面介绍的nagle算法或者cork算法,使得应用层的write操作与tcp层真实的发送操作隔离。但是这两个都行不通的,原因是Nagle算法是ACK触发,收到ACK会更新T_prev,因此不会满足tcpnow - T_prev >= RTO。而cork算法数据包的超时发送是persist timer定时器触发的,这个定时器触发的数据包发送流程不会经过linux中更新cwnd和ssthresh中的application-limited流程。

最终构造的场景如下,client端对于server端的每个数据包都会触发ACK回复,server端的No26数据包是间隔5ms后收到的ACK确认报文,而server端的其余数据包都是在发出50ms后收到的ACK报文。可以看到No26与No28间隔时间大约为240ms,低于RTO时间,因此不会判断进入tcpnow - T_last >= RTO的流程,而No27和No29之间间隔275ms,高于RTO时间,因此会进入更新cwnd和ssthresh的application-limited流程。

在No29之前,ssthresh=8,cwnd=8,cwnd_cnt=4,注意server端在收到No27报文的时候判断并没有处于network-limited阶段,因此reno并不会更新cwnd_cnt。收到No29之后更新ssthresh =  max(ssthresh, 3*cwnd/4)=8,cwnd = (win + W_used)/2=(8+2)/2=5,cwnd_cnt不变。更新后ssthresh、cwnd、cwnd_cnt的值也可以从后面慢启动和拥塞避免的流程看出来。注意的是在No35到No43慢启动的过程中cwnd_cnt的值并没有清空。在No44-No50拥塞避免阶段,cwnd_cnt从4增长到8,收到No50后更新cwnd=cwnd+1=9,重置cwnd_cnt=0;

补充说明:

1、RFC7661 new CWV的patch可以参考 https://github.com/rsecchi/newcwv

2、https://riteproject.eu/resources/new-cwv/

3、is_cwnd_limited等变量的更新请参考tcp_cwnd_validate、tcp_cwnd_application_limited、tcp_is_cwnd_limited

4、idle的处理请参考tcp_slow_start_after_idle_check

5、第二版的TCPIP详解中对于application-limit的解释正好是错误的,application-limited是指受限应用层而没有更多的数据可以发送,从协议给出的伪代码示例中也可以看到这一点。

来自为知笔记(Wiz)

转载于:https://www.cnblogs.com/lshs/p/6038757.html

TCP系列43—拥塞控制—6、Congestion Window Validation(CWV)相关推荐

  1. TCP系列39—拥塞控制—2、拥塞相关算法及基础知识

    原文:https://www.cnblogs.com/lshs/p/6038722.html 一.拥塞控制的相关算法 早期的TCP协议只有基于窗口的流控(flow control)机制而没有拥塞控制机 ...

  2. TCP系列42—拥塞控制—5、Linux中的慢启动和拥塞避免(二)

    在本篇中我们继续上一篇文章wireshark的示例讲解,上一篇介绍了一个综合示例后,本篇介绍一些简单的示例,在读本篇前建议先把上一篇读完,为了节省篇幅,本篇只针对一些特殊的场景点报文进行讲解,不会像上 ...

  3. TCP系列48—拥塞控制—11、FRTO拥塞撤销

    一.概述 FRTO虚假超时重传检测我们之前重传章节的文章已经介绍过了,这里不再重复介绍,针对后面的示例在说明两点 1.FRTO只能用于虚假超时重传的探测,不能用于虚假快速重传的探测. 2.延迟ER重传 ...

  4. TCP系列51—拥塞控制—14、TLP、ER与拥塞控制

    一.概述 这里的重点是介绍TLP.ER与拥塞控制并不是介绍TLP和ER本身,因此TLP和ER的详细内容请翻前文. 在TLP与拥塞控制的交互中有几个点需要注意 1.TLP触发的重传后,TCP仍然处于Op ...

  5. TCP/IP协议族之运输层(TCP流量控制和拥塞控制 [1])

    TCP的流量控制 1. 利用滑动窗口实现流量控制 如果发送方把数据发送得过快,接收方可能会来不及接收,这就会造成数据的丢失.所谓流量控制就是让发送方的发送速率不要太快,要让接收方来得及接收. 利用滑动 ...

  6. TCP 慢启动 拥塞控制

    TCP 的数据流 TCP的数据流大致可以分为两类,交互数据流与成块的数据流.交互数据流就是发送控制命令的数据流,比如relogin,telnet,ftp命令等等:成块数据流是用来发送数据的包,网络上大 ...

  7. TCP流量控制和拥塞控制

    TCP 流量控制和拥塞控制 MSS:MAX Segement Size TCP 一次传输的最大数据长度 RTT: Roud Trip Time 从发送端发送开始到收到接收端的 ACK 的确认,总共经历 ...

  8. 计算机网络拥塞解决方法,对TCP/IP计算机网络拥塞控制的研究

    摘 要: 为提升计算机的网络性能,更好地避免拥塞现象的发生,需要对其进行必要的技术控制.鉴于此,对基于TCP/IP协议的网络拥塞控制方法进行分析.在TCP拥塞控制中主要采用TCP Tahoe,TCP ...

  9. DotNetty 实现 Modbus TCP 系列 (三) Codecs Handler

    DotNetty 实现 Modbus TCP 系列 (一) 报文类 DotNetty 实现 Modbus TCP 系列 (二) ModbusFunction 类图及继承举例 DotNetty 作为一个 ...

最新文章

  1. 从某一日期开始过day天的日期
  2. [hdu6434]Problem I. Count
  3. python软件怎么用-如何使用Python自动控制windows桌面
  4. java实现简单的二叉树ADT
  5. 产品经理这个角色真的泡沫越来越大吗?
  6. 《利用python进行数据分析》读书笔记--第十章 时间序列(二)
  7. 关于合成的拷贝控制成员的一点问题
  8. js 字符转换,小驼峰转大写字母开头并且加空格 changeDate -》 Change Date
  9. MySQL:查询条件
  10. access在sql中横向求和_如何在Access查询中增加总和、平均查询列
  11. 开源 ERP 软件 Odoo 提速指南
  12. Notification的使用,以及他的监听方法
  13. # 创业计划书-样例参考五千套(二)
  14. Excel 2010实战技巧精粹
  15. GB28181国标错误码
  16. IDEA中Terminal窗口中无法使用maven命令
  17. Surface Pro 4 无限重启的解决方法
  18. 顺序内聚和过程内聚的区别
  19. 谷歌开源 ClusterFuzz,自动化查找并修复 bug
  20. NLP逻辑回归模型(LR)实现分类问题实例详解

热门文章

  1. AI产品经理数据模型设计文档(简版)
  2. html5课程总结500字,考试后的反思500字(精选10篇)
  3. 钉钉氚云到金碟之三 DELPHI从氚云下载数据
  4. 洛谷 P1618 STL全排列方法
  5. 鸟哥的linux私房菜学习笔记《二十》bash简介
  6. 360 2015校园招聘
  7. 利用计算机可以干什么,打开电脑不知道干什么 多个领域运用广【图解】
  8. 三维可视化引擎 打造全息感知数字孪生智慧地铁站
  9. 赛博朋克宣言1993_赛博朋克2077和未来武器
  10. 浅谈SAML, OAuth, OpenID和SSO, JWT和Session