扫一扫
分享文章到微信
扫一扫
关注官方公众号
至顶头条
在本页阅读全文(共3页)
承上启下
终于到了阐述问题的时候了,以上的TCP协议实现的非常简单,这也是TCP的标准实现,然而很快我们就会发现各种各样的问题。这些问题导致了标准化 协会对 TCP协议进行了大量的修补,这些修补杂糅在一起让人们有些云里雾里,不知所措。本文档就旨在分离这些杂乱的情况,实际上,根据RFC,这些杂乱的情况都 是可以找到其单独的发展轨迹的。
4.端到端意义上的TCP协议效率
4.1.三个问题以及解决
问题1描述:接收端处理慢,导致接收窗口被填满
这明显是速率不匹配引发的问题,然而即使速率不匹配,只要滑动窗口能协调好它们的速率就好,要快都快,要慢都慢,事实上滑动窗口在这一点上做的很 好。但是如果我们不得不从效率上来考虑问题的话,事实就不那么乐观了。考虑此时接收窗口已然被填满,慢速的应用程序慢腾腾的读取了一个字节,空出一个位 置,然后通告给TCP的发送端,发送端得知空出一个位置,马上发出一个字节,又将接收端填满,然后接收应用程序又一次慢腾腾...这就是糊涂窗口综合症, 一个大多数人都很熟悉的词。这个问题极大的浪费了网络带宽,降低了网络利用率。好比从大同拉100吨煤到北京需要一辆车,拉1Kg煤到北京也需要一辆车 (超级夸张的一个例子,请不要相信),但是一辆车开到北京的开销是一定的...
问题1解决:窗口通告
对于问题1,很显然问题出在接收端,我们没有办法限制发送端不发送小分段,但是却可以限制接收端通告小窗口,这是合理的,这并不影响应用程序,此时 经典的延迟/吞吐量反比律将不再适用,因为接收窗口是满的,其空出一半空间表示还有一半空间有数据没有被应用读取,和其空出一个字节的空间的效果是一样 的,因此可以限制接收端当窗口为0时,直接通告给发送端以阻止其继续发送数据,只有当其接收窗口再次达到MSS的一半大小的时候才通告一个不为0的窗口, 此前对于所有的发送端的窗口probe分段(用于探测接收端窗口大小的probe分段,由TCP标准规定),全部通告窗口为0,这样发送端在收到窗口不为 0的通告,那么肯定是一个比较大的窗口,因此发送端可以一次性发出一个很大的TCP分段,包含大量数据,也即拉了好几十吨的煤到北京,而不是只拉了几公 斤。
即,限制窗口通告时机,解决糊涂窗口综合症
问题2描述:发送端持续发送小包,导致窗口闲置
这明显是发送端引起的问题,此时接收端的窗口开得很大,然而发送端却不积累数据,还是一味的发送小块数据分段。只要发送了任和的分段,接收端都要无条件接收并且确认,这完全符合TCP规范,因此必然要限制发送端不发送这样的小分段。
问题2解决:Nagle算法
Nagel算法很简单,标准的Nagle算法为:
IF 数据的大小和窗口的大小都超过了MSS
Then 发送数据分段
ELSE
IF 还有发出的TCP分段的确认没有到来
Then 积累数据到发送队列的末尾的TCP分段
ELSE
发送数据分段
EndIF
EndIF
可是后来,这个算法变了,变得更加灵活了,其中的:
IF 还有发出的TCP分段的确认没有到来
变成了
IF 还有发出的不足MSS大小的TCP分段的确认没有到来
这样如果发出了一个MSS大小的分段还没有被确认,后面也是可以随时发送一个小分段的,这个改进降低了算法对延迟时间的影响。这个算法体现了一种自 适应的策略,越是确认的快,越是发送的快,虽然Nagle算法看起来在积累数据增加吞吐量的同时也加大的时延,可事实上,如果对于类似交互式的应用,时延 并不会增加,因为这类应用回复数据也是很快的,比如Telnet之类的服务必然需要回显字符,因此能和对端进行自适应协调。
注意,Nagle算法是默认开启的,但是却可以关闭。如果在开启的情况下,那么它就严格按照上述的算法来执行。
问题3.确认号(ACK)本身就是不含数据的分段,因此大量的确认号消耗了大量的带宽
这是TCP为了确保可靠性传输的规范,然而大多数情况下,ACK还是可以和数据一起捎带传输的。如果没有捎带传输,那么就只能单独回来一个ACK, 如果这样的分段太多,网络的利用率就会下降。从大同用火车拉到北京100吨煤,为了确认煤已收到,北京需要派一辆同样的火车空载开到大同去复命,因为没有 别的交通工具,只有火车。如果这位复命者刚开着一列火车走,又从大同来了一车煤,这拉煤的哥们儿又要开一列空车去复命了。
问题3的解决:
RFC 建议了一种延迟的ACK,也就是说,ACK在收到数据后并不马上回复,而是延迟一段可以接受的时间,延迟一段时间的目的是看能不能和接收方要发给发送方的 数据一起回去,因为TCP协议头中总是包含确认号的,如果能的话,就将ACK一起捎带回去,这样网络利用率就提高了。往大同复命的确认者不必开一辆空载火 车回大同了,此时北京正好有一批货物要送往大同,这位复命者搭着这批货的火车返回大同。
如果等了一段可以接受的时间,还是没有数据要发往发送端,此时就需要单独发送一个ACK了,然而即使如此,这个延迟的ACK虽然没有等到可以被捎带 的数据分段,也可能等到了后续到来的TCP分段,这样它们就可以取最大者一起返回了,要知道,TCP的确认号是收到的按序报文的最后一个字节的后一个字 节。最后,RFC建议,延迟的ACK最多等待两个分段的积累确认。
4.2.分析三个问题之间的关联
三个问题导致的结果是相同的,但是要知道它们的原因本质上是不同的,问题1几乎总是出现在接收端窗口满的情况下,而问题2几乎总是发生在窗口闲置的 情况下,问题3看起来是最无聊的,然而由于TCP的要求,必须要有确认号,而且一个确认号就需要一个TCP分段,这个分段不含数据,无疑是很小的。
三个问题都导致了网络利用率的降低。虽然两个问题导致了同样的结果,但是必须认识到它们是不同的问题,很自然的将这些问题的解决方案汇总在一起,形成一个全局的解决方案,这就是如今的操作系统中的解决方案。
4.3.问题的杂糅情况
疑难杂症11:糊涂窗口解决方案和Nagle算法
糊涂窗口综合症患者希望发送端积累TCP分段,而Nagle算法确实保证了一定的TCP分段在发送端的积累,另外在延迟ACK的延迟的那一会时间,发送端会利用这段时间积累数据。然而这却是三个不同的问题。Nagle算法可以缓解糊涂窗口综合症,却不是治本的良药。
疑难杂症12:Nagle算法和延迟ACK
延迟ACK会延长ACK到达发送端的时间,由于标准Nagle算法只允许一个未被确认的TCP分段,那无疑在接收端,这个延迟的ACK是毫无希望等 待后续数据到来最终进行积累确认的,如果没有数据可以捎带这个ACK,那么这个ACK只有在延迟确认定时器超时的时候才会发出,这样在等待这个ACK的过 程中,发送端又积累了一些数据,因此延迟ACK实际上是在增加延迟的代价下加强了Nagle算法。在延迟ACK加Nagle算法的情况下,接收端只有不断 有数据要发回,才能同时既保证了发送端的分段积累,又保证了延迟不增加,同时还没有或者很少有空载的ACK。
要知道,延迟ACK和Nagle是两个问题的解决方案。
疑难杂症13:到底何时可以发送数据
到底何时才能发送数据呢?如果单从Nagle算法上看,很简单,然而事实证明,情况还要更复杂些。如果发送端已经排列了3个TCP分段,分段1,分 段2,分段3依次被排入,三个分段都是小分段(不符合Nagle算法中立即发送的标准),此时已经有一个分段被发出了,且其确认还没有到来,请问此时能发 送分段1 和2吗?如果按照Nagle算法,是不能发送的,但实际上它们是可以发送的,因为这两个分段已经没有任何机会再积累新的数据了,新的数据肯定都积累在分段 3上了。问题在于,分段还没有积累到一定大小时,怎么还可以产生新的分段?这是可能的,但这是另一个问题,在此不谈。
Linux的TCP实现在这个问题上表现的更加灵活,它是这么判断能否发送的(在开启了Nagle的情况下):
IF (没有超过拥塞窗口大小的数据分段未确认 || 数据分段中包含FIN ) &&
数据分段没有超越窗口边界
Then
IF 分段在中间(上述例子中的分段1和2) ||
分段是紧急模式 ||
通过上述的Nagle算法(改进后的Nagle算法)
Then 发送分段
EndIF
EndIF
曾经我也改过Nagle算法,确切的说不是修改Nagle算法,而是修改了“到底何时能发送数据”的策略,以往都是发送端判断能否发送数据的,可是 如果此时有延迟ACK在等待被捎带,而待发送的数据又由于积累不够或者其它原因不能发送,因此两边都在等,这其实在某些情况下不是很好。我所做的改进中对 待何时能发送数据又增加了一种情况,这就是“ACK拉”的情况,一旦有延迟ACK等待发送,判断一下有没有数据也在等待发送,如果有的话,看看数据是否大 到了一定程度,在此,我选择的是MSS的一半:
IF (没有超过拥塞窗口大小的数据分段未确认 || 数据分段中包含FIN ) &&
数据分段没有超越窗口边界
Then
IF 分段在中间(上述例子中的分段1和2) ||
分段是紧急模式 ||
通过上述的Nagle算法(改进后的Nagle算法)
Then 发送分段
EndIF
ELSE IF 有延迟ACK等待传输 &&
发送队列中有待发送的TCP分段 &&
发送队列的头分段大小大于MSS的一半
Then 发送队列头分段且捎带延迟ACK
EndIF
另外,发送队列头分段的大小是可以在统计意义上动态计算的,也不一定非要是MSS大小的一半。我们发现,这种算法对于交互式网路应用是自适应的,你 打字越快,特定时间内积累的分段就越长,对端回复的越快(可以捎带ACK),本端发送的也就越快(以Echo举例会更好理解)。
疑难杂症14:《TCP/IP详解(卷一)》中Nagle算法的例子解读
这个问题在网上搜了很多的答案,有的说RFC的建议,有的说别的。可是实际上这就是一个典型的“竞态问题”:
首先服务器发了两个分段:
数据段12:ack 14
数据段13:ack 14,54:56
然后客户端发了两个分段:
数据段14:ack 54,14:17
数据段15:ack 56,17:18
可以看到数据段14本来应该确认56的,但是确认的却是54。也就是说,数据段已经移出队列将要发送但还未发送的时候,数据段13才到来,软中断处 理程序抢占了数据段14的发送进程,要知道此时只是把数据段14移出了队列,还没有更新任何的状态信息,比如“发出但未被确认的分段数量”,此时软中断处 理程序顺利接收了分段13,然后更新窗口信息,并且检查看有没有数据要发送,由于分段14已经移出队列,下一个接受发送检查的就是分段15了,由于状态信 息还没有更新,因此分段15顺利通过发送检测,发送完成。
可以看Linux的源代码了解相关信息,tcp_write_xmit这个函数在两个地方会被调用,一个是TCP的发送进程中,另一个就是软中断的 接收处理中,两者在调用中的竞态就会引起《详解》中的那种情况。注意,这种不加锁的发送方式是合理的,也是最高效的,因此TCP的处理语义会做出判断,丢 弃一切不该接收或者重复接收的分段的。
如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。
现场直击|2021世界人工智能大会
直击5G创新地带,就在2021MWC上海
5G已至 转型当时——服务提供商如何把握转型的绝佳时机
寻找自己的Flag
华为开发者大会2020(Cloud)- 科技行者