这篇文章是下篇,因此若是你对TCP不熟悉的话,还请你先看看上篇《TCP的那些事儿(1)》 上篇中,咱们介绍了TCP的协议头、状态机、数据重传中的东西。可是TCP要解决一个很大的事,那就是要在一个网络根据不一样的状况来动态调整本身的发包的速度,小则让本身的链接更稳定,大则让整个网络更稳定。在你阅读下篇以前,你须要作好准备,本篇文章有好些算法和策略,可能会引起你的各类思考,让你的大脑分配不少内存和计算资源,因此,不适合在厕所中阅读。html
从前面的TCP重传机制咱们知道Timeout的设置对于重传很是重要。算法
设长了,重发就慢,丢了老半天才重发,没有效率,性能差;windows
设短了,会致使可能并无丢就重发。因而重发的就快,会增长网络拥塞,致使更多的超时,更多的超时致使更多的重发。服务器
并且,这个超时时间在不一样的网络的状况下,根本没有办法设置一个死的值。只能动态地设置。 为了动态地设置,TCP引入了RTT——Round Trip Time,也就是一个数据包从发出去到回来的时间。这样发送端就大约知道须要多少的时间,从而能够方便地设置Timeout——RTO(Retransmission TimeOut),以让咱们的重传机制更高效。 听起来彷佛很简单,好像就是在发送端发包时记下t0,而后接收端再把这个ack回来时再记一个t1,因而RTT = t1 – t0。没那么简单,这只是一个采样,不能表明广泛状况。网络
RFC793 中定义的经典算法是这样的:数据结构
1)首先,先采样RTT,记下最近好几回的RTT值。并发
2)而后作平滑计算SRTT( Smoothed RTT)。公式为:(其中的 α 取值在0.8 到 0.9之间,这个算法英文叫Exponential weighted moving average,中文叫:加权移动平均)ssh
SRTT = ( α * SRTT ) + ((1- α) * RTT)electron
3)开始计算RTO。公式以下:socket
RTO = min [ UBOUND, max [ LBOUND, (β * SRTT) ] ]
其中:
UBOUND是最大的timeout时间,上限值
LBOUND是最小的timeout时间,下限值
β 值通常在1.3到2.0之间。
可是上面的这个算法在重传的时候会出有一个终极问题——你是用第一次发数据的时间和ack回来的时间作RTT样本值,仍是用重传的时间和ACK回来的时间作RTT样本值?
这个问题不管你选那头都是按下葫芦起了瓢。 以下图所示:
状况(a)是ack没回来,因此重传。若是你计算第一次发送和ACK的时间,那么,明显算大了。
状况(b)是ack回来慢了,可是致使了重传,但刚重传不一下子,以前ACK就回来了。若是你是算重传的时间和ACK回来的时间的差,就会算短了。
因此1987年的时候,搞了一个叫Karn / Partridge Algorithm,这个算法的最大特色是——忽略重传,不把重传的RTT作采样(你看,你不须要去解决不存在的问题)。
可是,这样一来,又会引起一个大BUG——若是在某一时间,网络闪动,忽然变慢了,产生了比较大的延时,这个延时致使要重转全部的包(由于以前的RTO很小),因而,由于重转的不算,因此,RTO就不会被更新,这是一个灾难。 因而Karn算法用了一个取巧的方式——只要一发生重传,就对现有的RTO值翻倍(这就是所谓的 Exponential backoff),很明显,这种死规矩对于一个须要估计比较准确的RTT也不靠谱。
前面两种算法用的都是“加权移动平均”,这种方法最大的毛病就是若是RTT有一个大的波动的话,很难被发现,由于被平滑掉了。因此,1988年,又有人推出来了一个新的算法,这个算法叫Jacobson / Karels Algorithm(参看RFC6289)。这个算法引入了最新的RTT的采样和平滑过的SRTT的差距作因子来计算。 公式以下:(其中的DevRTT是Deviation RTT的意思)
SRTT = SRTT + α (RTT – SRTT) —— 计算平滑RTT
DevRTT = (1-β)*DevRTT + β*(|RTT-SRTT|) ——计算平滑RTT和真实的差距(加权移动平均)
RTO= μ * SRTT + *DevRTT —— 神同样的公式
(其中:在Linux下,α = 0.125,β = 0.25, μ = 1, = 4 ——这就是算法中的“调得一手好参数”,nobody knows why, it just works…) 最后的这个算法在被用在今天的TCP协议中(Linux的源代码在:tcp_rtt_estimator)。
须要说明一下,若是你不了解TCP的滑动窗口这个事,你等于不了解TCP协议。咱们都知道,TCP必须要解决的可靠传输以及包乱序(reordering)的问题,因此,TCP必须要知道网络实际的数据处理带宽或是数据处理速度,这样才不会引发网络拥塞,致使丢包。
因此,TCP引入了一些技术和设计来作网络流控,Sliding Window是其中一个技术。 前面咱们说过,TCP头里有一个字段叫Window,又叫Advertised-Window,这个字段是接收端告诉发送端本身还有多少缓冲区能够接收数据。因而发送端就能够根据这个接收端的处理能力来发送数据,而不会致使接收端处理不过来。 为了说明滑动窗口,咱们须要先看一下TCP缓冲区的一些数据结构:
上图中,咱们能够看到:
接收端LastByteRead指向了TCP缓冲区中读到的位置,NextByteExpected指向的地方是收到的连续包的最后一个位置,LastByteRcved指向的是收到的包的最后一个位置,咱们能够看到中间有些数据尚未到达,因此有数据空白区。
发送端的LastByteAcked指向了被接收端Ack过的位置(表示成功发送确认),LastByteSent表示发出去了,但尚未收到成功确认的Ack,LastByteWritten指向的是上层应用正在写的地方。
因而:
接收端在给发送端回ACK中会汇报本身的AdvertisedWindow = MaxRcvBuffer – LastByteRcvd – 1;
而发送方会根据这个窗口来控制发送数据的大小,以保证接收方能够处理。
下面咱们来看一下发送方的滑动窗口示意图:
(图片来源)
上图中分红了四个部分,分别是:(其中那个黑模型就是滑动窗口)
#1已收到ack确认的数据。
#2发还没收到ack的。
#3在窗口中尚未发出的(接收方还有空间)。
#4窗口之外的数据(接收方没空间)
下面是个滑动后的示意图(收到36的ack,并发出了46-51的字节):
下面咱们来看一个接受端控制发送端的图示:
(图片来源)
上图,咱们能够看到一个处理缓慢的Server(接收端)是怎么把Client(发送端)的TCP Sliding Window给降成0的。此时,你必定会问,若是Window变成0了,TCP会怎么样?是否是发送端就不发数据了?是的,发送端就不发数据了,你能够想像成“Window Closed”,那你必定还会问,若是发送端不发数据了,接收方一下子Window size 可用了,怎么通知发送端呢?
解决这个问题,TCP使用了Zero Window Probe技术,缩写为ZWP,也就是说,发送端在窗口变成0后,会发ZWP的包给接收方,让接收方来ack他的Window尺寸,通常这个值会设置成3次,第次大约30-60秒(不一样的实现可能会不同)。若是3次事后仍是0的话,有的TCP实现就会发RST把连接断了。
注意:只要有等待的地方均可能出现DDoS***,Zero Window也不例外,一些***者会在和HTTP建好链发完GET请求后,就把Window设置为0,而后服务端就只能等待进行ZWP,因而***者会并发大量的这样的请求,把服务器端的资源耗尽。(关于这方面的***,你们能够移步看一下Wikipedia的SockStress词条)
另外,Wireshark中,你可使用tcp.analysis.zero_window来过滤包,而后使用右键菜单里的follow TCP stream,你能够看到ZeroWindowProbe及ZeroWindowProbeAck的包。
Silly Window Syndrome翻译成中文就是“糊涂窗口综合症”。正如你上面看到的同样,若是咱们的接收方太忙了,来不及取走Receive Windows里的数据,那么,就会致使发送方愈来愈小。到最后,若是接收方腾出几个字节并告诉发送方如今有几个字节的window,而咱们的发送方会义无反顾地发送这几个字节。
要知道,咱们的TCP+IP头有40个字节,为了几个字节,要达上这么大的开销,这太不经济了。
另外,你须要知道网络上有个MTU,对于以太网来讲,MTU是1500字节,除去TCP+IP头的40个字节,真正的数据传输能够有1460,这就是所谓的MSS(Max Segment Size)注意,TCP的RFC定义这个MSS的默认值是536,这是由于 RFC 791里说了任何一个IP设备都得最少接收576尺寸的大小(实际上来讲576是拨号的网络的MTU,而576减去IP头的20个字节就是536)。
若是你的网络包能够塞满MTU,那么你能够用满整个带宽,若是不能,那么你就会浪费带宽。(大于MTU的包有两种结局,一种是直接被丢了,另外一种是会被从新分块打包发送) 你能够想像成一个MTU就至关于一个飞机的最多能够装的人,若是这飞机里满载的话,带宽最高,若是一个飞机只运一我的的话,无疑成本增长了,也而至关二。
因此,Silly Windows Syndrome这个现像就像是你原本能够坐200人的飞机里只作了一两我的。 要解决这个问题也不难,就是避免对小的window size作出响应,直到有足够大的window size再响应,这个思路能够同时实如今sender和receiver两端。
若是这个问题是由Receiver端引发的,那么就会使用 David D Clark’s 方案。在receiver端,若是收到的数据致使window size小于某个值,能够直接ack(0)回sender,这样就把window给关闭了,也阻止了sender再发数据过来,等到receiver端处理了一些数据后windows size 大于等于了MSS,或者,receiver buffer有一半为空,就能够把window打开让send 发送数据过来。
若是这个问题是由Sender端引发的,那么就会使用著名的 Nagle’s algorithm。这个算法的思路也是延时处理,他有两个主要的条件:1)要等到 Window Size>=MSS 或是 Data Size >=MSS,2)收到以前发送数据的ack回包,他才会发数据,不然就是在攒数据。
另外,Nagle算法默认是打开的,因此,对于一些须要小包场景的程序——好比像telnet或ssh这样的交互性比较强的程序,你须要关闭这个算法。你能够在Socket设置TCP_NODELAY选项来关闭这个算法(关闭Nagle算法没有全局参数,须要根据每一个应用本身的特色来关闭)
1
|
setsockopt(sock_fd, IPPROTO_TCP, TCP_NODELAY, (
char
*)&value,
sizeof
(
int
));
|
另外,网上有些文章说TCP_CORK的socket option是也关闭Nagle算法,这不对。TCP_CORK实际上是更新激进的Nagle算汉,彻底禁止小包发送,而Nagle算法没有禁止小包发送,只是禁止了大量的小包发送。最好不要两个选项都设置。
上面咱们知道了,TCP经过Sliding Window来作流控(Flow Control),可是TCP以为这还不够,由于Sliding Window须要依赖于链接的发送端和接收端,其并不知道网络中间发生了什么。TCP的设计者以为,一个伟大而牛逼的协议仅仅作到流控并不够,由于流控只是网络模型4层以上的事,TCP的还应该更聪明地知道整个网络上的事。
具体一点,咱们知道TCP经过一个timer采样了RTT并计算RTO,可是,若是网络上的延时忽然增长,那么,TCP对这个事作出的应对只有重传数据,可是,重传会致使网络的负担更重,因而会致使更大的延迟以及更多的丢包,因而,这个状况就会进入恶性循环被不断地放大。试想一下,若是一个网络内有成千上万的TCP链接都这么行事,那么立刻就会造成“网络风暴”,TCP这个协议就会拖垮整个网络。这是一个灾难。
因此,TCP不能忽略网络上发生的事情,而无脑地一个劲地重发数据,对网络形成更大的伤害。对此TCP的设计理念是:TCP不是一个自私的协议,当拥塞发生的时候,要作自我牺牲。就像交通阻塞同样,每一个车都应该把路让出来,而不要再去抢路了。
关于拥塞控制的论文请参看《Congestion Avoidance and Control》(PDF)
拥塞控制主要是四个算法:1)慢启动,2)拥塞避免,3)拥塞发生,4)快速恢复。这四个算法不是一天都搞出来的,这个四算法的发展经历了不少时间,到今天都还在优化中。 备注:
1988年,TCP-Tahoe 提出了1)慢启动,2)拥塞避免,3)拥塞发生时的快速重传
1990年,TCP Reno 在Tahoe的基础上增长了4)快速恢复
首先,咱们来看一下TCP的慢热启动。慢启动的意思是,刚刚加入网络的链接,一点一点地提速,不要一上来就像那些特权车同样霸道地把路占满。新同窗上高速仍是要慢一点,不要把已经在高速上的秩序给搞乱了。
慢启动的算法以下(cwnd全称Congestion Window):
1)链接建好的开始先初始化cwnd = 1,代表能够传一个MSS大小的数据。
2)每当收到一个ACK,cwnd++; 呈线性上升
3)每当过了一个RTT,cwnd = cwnd*2; 呈指数让升
4)还有一个ssthresh(slow start threshold),是一个上限,当cwnd >= ssthresh时,就会进入“拥塞避免算法”(后面会说这个算法)
因此,咱们能够看到,若是网速很快的话,ACK也会返回得快,RTT也会短,那么,这个慢启动就一点也不慢。下图说明了这个过程。
这里,我须要提一下的是一篇Google的论文《An Argument for Increasing TCP’s Initial Congestion Window》Linux 3.0后采用了这篇论文的建议——把cwnd 初始化成了 10个MSS。 而Linux 3.0之前,好比2.6,Linux采用了RFC3390,cwnd是跟MSS的值来变的,若是MSS< 1095,则cwnd = 4;若是MSS>2190,则cwnd=2;其它状况下,则是3。
前面说过,还有一个ssthresh(slow start threshold),是一个上限,当cwnd >= ssthresh时,就会进入“拥塞避免算法”。通常来讲ssthresh的值是65535,单位是字节,当cwnd达到这个值时后,算法以下:
1)收到一个ACK时,cwnd = cwnd + 1/cwnd
2)当每过一个RTT时,cwnd = cwnd + 1
这样就能够避免增加过快致使网络拥塞,慢慢的增长调整到网络的最佳值。很明显,是一个线性上升的算法。
前面咱们说过,当丢包的时候,会有两种状况:
1)等到RTO超时,重传数据包。TCP认为这种状况太糟糕,反应也很强烈。
sshthresh = cwnd /2
cwnd 重置为 1
进入慢启动过程
2)Fast Retransmit算法,也就是在收到3个duplicate ACK时就开启重传,而不用等到RTO超时。
TCP Tahoe的实现和RTO超时同样。
TCP Reno的实现是:
cwnd = cwnd /2
sshthresh = cwnd
进入快速恢复算法——Fast Recovery
上面咱们能够看到RTO超时后,sshthresh会变成cwnd的一半,这意味着,若是cwnd<=sshthresh时出现的丢包,那么TCP的sshthresh就会减了一半,而后等cwnd又很快地以指数级增涨爬到这个地方时,就会成慢慢的线性增涨。咱们能够看到,TCP是怎么经过这种强烈地震荡快速而当心得找到网站流量的平衡点的。
TCP Reno
这个算法定义在RFC5681。快速重传和快速恢复算法通常同时使用。快速恢复算法是认为,你还有3个Duplicated Acks说明网络也不那么糟糕,因此没有必要像RTO超时那么强烈。 注意,正如前面所说,进入Fast Recovery以前,cwnd 和 sshthresh已被更新:
cwnd = cwnd /2
sshthresh = cwnd
而后,真正的Fast Recovery算法以下:
cwnd = sshthresh + 3 * MSS (3的意思是确认有3个数据包被收到了)
重传Duplicated ACKs指定的数据包
若是再收到 duplicated Acks,那么cwnd = cwnd +1
若是收到了新的Ack,那么,cwnd = sshthresh ,而后就进入了拥塞避免的算法了。
若是你仔细思考一下上面的这个算法,你就会知道,上面这个算法也有问题,那就是——它依赖于3个重复的Acks。注意,3个重复的Acks并不表明只丢了一个数据包,颇有多是丢了好多包。但这个算法只会重传一个,而剩下的那些包只能等到RTO超时,因而,进入了恶梦模式——超时一个窗口就减半一下,多个超时会超成TCP的传输速度呈级数降低,并且也不会触发Fast Recovery算法了。
一般来讲,正如咱们前面所说的,SACK或D-SACK的方法可让Fast Recovery或Sender在作决定时更聪明一些,可是并非全部的TCP的实现都支持SACK(SACK须要两端都支持),因此,须要一个没有SACK的解决方案。而经过SACK进行拥塞控制的算法是FACK(后面会讲)
TCP New Reno
因而,1995年,TCP New Reno(参见 RFC 6582 )算法提出来,主要就是在没有SACK的支持下改进Fast Recovery算法的——
当sender这边收到了3个Duplicated Acks,进入Fast Retransimit模式,开发重传重复Acks指示的那个包。若是只有这一个包丢了,那么,重传这个包后回来的Ack会把整个已经被sender传输出去的数据ack回来。若是没有的话,说明有多个包丢了。咱们叫这个ACK为Partial ACK。
一旦Sender这边发现了Partial ACK出现,那么,sender就能够推理出来有多个包被丢了,因而乎继续重传sliding window里未被ack的第一个包。直到再也收不到了Partial Ack,才真正结束Fast Recovery这个过程
咱们能够看到,这个“Fast Recovery的变动”是一个很是激进的玩法,他同时延长了Fast Retransmit和Fast Recovery的过程。
下面咱们来看一个简单的图示以同时看一下上面的各类算法的样子:
FACK全称Forward Acknowledgment 算法,论文地址在这里(PDF)Forward Acknowledgement: Refining TCP Congestion Control 这个算法是其于SACK的,前面咱们说过SACK是使用了TCP扩展字段Ack了有哪些数据收到,哪些数据没有收到,他比Fast Retransmit的3 个duplicated acks好处在于,前者只知道有包丢了,不知道是一个仍是多个,而SACK能够准确的知道有哪些包丢了。 因此,SACK可让发送端这边在重传过程当中,把那些丢掉的包重传,而不是一个一个的传,但这样的一来,若是重传的包数据比较多的话,又会致使原本就很忙的网络就更忙了。因此,FACK用来作重传过程当中的拥塞流控。
这个算法会把SACK中最大的Sequence Number 保存在snd.fack这个变量中,snd.fack的更新由ack带秋,若是网络一切安好则和snd.una同样(snd.una就是尚未收到ack的地方,也就是前面sliding window里的category #2的第一个地方)
而后定义一个awnd = snd.nxt – snd.fack(snd.nxt指向发送端sliding window中正在要被发送的地方——前面sliding windows图示的category#3第一个位置),这样awnd的意思就是在网络上的数据。(所谓awnd意为:actual quantity of data outstanding in the network)
若是须要重传数据,那么,awnd = snd.nxt – snd.fack + retran_data,也就是说,awnd是传出去的数据 + 重传的数据。
而后触发Fast Recovery 的条件是: ( ( snd.fack – snd.una ) > (3*MSS) ) || (dupacks == 3) ) 。这样一来,就不须要等到3个duplicated acks才重传,而是只要sack中的最大的一个数据和ack的数据比较长了(3个MSS),那就触发重传。在整个重传过程当中cwnd不变。直到当第一次丢包的snd.nxt<=snd.una(也就是重传的数据都被确认了),而后进来拥塞避免机制——cwnd线性上涨。
咱们能够看到若是没有FACK在,那么在丢包比较多的状况下,原来保守的算法会低估了须要使用的window的大小,而须要几个RTT的时间才会完成恢复,而FACK会比较激进地来干这事。 可是,FACK若是在一个网络包会被 reordering的网络里会有很大的问题。
这个算法1994年被提出,它主要对TCP Reno 作了些修改。这个算法经过对RTT的很是重的监控来计算一个基准RTT。而后经过这个基准RTT来估计当前的网络实际带宽,若是实际带宽比咱们的指望的带宽要小或是要多的活,那么就开始线性地减小或增长cwnd的大小。若是这个计算出来的RTT大于了Timeout后,那么,不等ack超时就直接重传。(Vegas 的核心思想是用RTT的值来影响拥塞窗口,而不是经过丢包) 这个算法的论文是《TCP Vegas: End to End Congestion Avoidance on a Global Internet》这篇论文给了Vegas和 New Reno的对比:
关于这个算法实现,你能够参看Linux源码:/net/ipv4/tcp_vegas.h, /net/ipv4/tcp_vegas.c
这个算法来自RFC 3649(Wikipedia词条)。其对最基础的算法进行了更改,他使得Congestion Window涨得快,减得慢。其中:
拥塞避免时的窗口增加方式: cwnd = cwnd + α(cwnd) / cwnd
丢包后窗口降低方式:cwnd = (1- β(cwnd))*cwnd
注:α(cwnd)和β(cwnd)都是函数,若是你要让他们和标准的TCP同样,那么让α(cwnd)=1,β(cwnd)=0.5就能够了。 对于α(cwnd)和β(cwnd)的值是个动态的变换的东西。 关于这个算法的实现,你能够参看Linux源码:/net/ipv4/tcp_highspeed.c
2004年,产内出BIC算法。如今你还能够查获得相关的新闻《Google:美科学家研发BIC-TCP协议 速度是DSL六千倍》 BIC全称Binary Increase Congestion control,在Linux 2.6.8中是默认拥塞控制算法。BIC的发明者发这么多的拥塞控制算法都在努力找一个合适的cwnd – Congestion Window,并且BIC-TCP的提出者们看穿了事情的本质,其实这就是一个搜索的过程,因此BIC这个算法主要用的是Binary Search——二分查找来干这个事。 关于这个算法实现,你能够参看Linux源码:/net/ipv4/tcp_bic.c
westwood采用和Reno相同的慢启动算法、拥塞避免算法。westwood的主要改进方面:在发送端作带宽估计,当探测到丢包时,根据带宽值来设置拥塞窗口、慢启动阈值。 那么,这个算法是怎么测量带宽的?每一个RTT时间,会测量一次带宽,测量带宽的公式很简单,就是这段RTT内成功被ack了多少字节。由于,这个带宽和用RTT计算RTO同样,也是须要从每一个样原本平滑到一个值的——也是用一个加权移平均的公式。 另外,咱们知道,若是一个网络的带宽是每秒能够发送X个字节,而RTT是一个数据发出去后确认须要的时候,因此,X * RTT应该是咱们缓冲区大小。因此,在这个算法中,ssthresh的值就是est_BD * min-RTT(最小的RTT值),若是丢包是Duplicated ACKs引发的,那么若是cwnd > ssthresh,则 cwin = ssthresh。若是是RTO引发的,cwnd = 1,进入慢启动。 关于这个算法实现,你能够参看Linux源码: /net/ipv4/tcp_westwood.c
更多的算法,你能够从Wikipedia的 TCP Congestion Avoidance Algorithm 词条中找到相关的线索
好了,到这里我想能够结束了,TCP发展到今天,里面的东西能够写上好几本书。本文主要目的,仍是把你带入这些古典的基础技术和知识中,但愿本文能让你了解TCP,更但愿本文能让你开始有学习这些基础或底层知识的兴趣和信心。
固然,TCP东西太多了,不一样的人可能有不一样的理解,并且本文可能也会有一些荒谬之言甚至错误,还但愿获得您的反馈和批评。