You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

285 lines
25 KiB
Markdown

2 years ago
# 10 | 窗口TCP Window Full会影响传输效率吗
你好,我是胜辉。
有时候,不少知识点在过段时间重新回看的时候,又会有新的体会和发现。比如在[第8讲](https://time.geekbang.org/column/article/484667)里我们回顾了一个MTU造成传输失败的案例虽然整个排查过程的步骤不算很多但也算是TCP传输问题的一个缩影了。尤其是其中那个失败的TCP流中的一些现象比如客户端发出的重复确认DupAck还有服务端启动的超时重传都值得我们继续深挖所以我会在后续的课程里继续这个话题。
然后在上节课里我们还探讨了传输速度的相关知识也初步学习了窗口的概念。最后我们终于推导出了TCP传输的核心公式速度=窗口/往返时间。这个公式,对于我们理解传输本质和排查传输问题,都有很强的指导意义。
然而如果你足够细心的话其实可能会对上节课里的细节有一些疑问比如既然接收窗口满了那为什么当时没有看到TCP Window Full这种提示呢
其实,我这边也有不少内容按住没有展开,包括核心公式的理解,我们在这节课里将有一个新的认识。另外,我也将带你继续挖掘窗口这个细分领域,这样你以后遇到跟窗口相关的问题,就知道如何破解了。
## 案例TCP Window Full是导致异地拷贝速度低的原因吗
也是在公有云服务的时候有个客户有这么一个需求就是要把文件从北京机房拷贝到上海机房。但是他们发现传输速度比较慢就做了抓包。在查看抓包文件的时候发现Wireshark有很多**TCP Window Full**这样的提示,不明白这些是否跟速度慢有关系,于是找我们来协助分析。
### 解读Expert Information
我们先要了解一下抓包文件的整体状况。怎么看呢当然是看Expert Information了
![图片](https://static001.geekbang.org/resource/image/72/8c/7285f181c54e6f464836e8c44f43c88c.jpg?wh=1818x234)
它确实提醒我们有69个Warning级别报文它们的问题是TCP Window specified by the receiver is now completely Full。在展开进一步排查之前我们先对这个信息做一下解读。
* TCP Window在上节课里我介绍了TCP的三种窗口分别是发送窗口、接收窗口、拥塞窗口。那么这里说的是哪个窗口呢一般说到TCP Window**如果没有特别指明,就是指接收窗口**。
* specified by the receiver这也很明确这个窗口是接收方的其实就是佐证了这个窗口就是接收窗口。
* is now completely Full窗口满了这又怎么理解呢你还记得在上节课里学过的在途数据也就是Bytes in flight吗**当在途数据的大小等于接收窗口的大小时,这个窗口就是“满了”**。
好了这个信息解读完毕一句话说就是发生了69次在途数据等于接收窗口的情况。
接下来我们看看TCP Window Full具体是个什么样子。比如我们选中224号报文主界面也自动定位到了这个报文
![图片](https://static001.geekbang.org/resource/image/49/4f/4957d1a108c78d3f73eee9b85213864f.jpg?wh=1920x960)
我们可以看到除了224报文也确实有很多其他报文也报了TCP Window Full的警告信息。
### 解读TCP Window Full
TCP Window Full这个信息非常直接明了就是说“接收窗口满了”。不过你可别以为这个信息是TCP报文里的某个字段。其实它只是Wireshark通过分析得出的信息。你有没有注意到TCP Window Full前后是有方括号的。一般来说**Wireshark自己分析得到的信息都会用方括号括起来**而TCP报文本身的字段是不会带这种方括号的。我们来看一个截图
![图片](https://static001.geekbang.org/resource/image/b7/a5/b76369d4ef7fe6f9dd49816247351fa5.jpg?wh=1630x1062)
上面是224号报文的TCP详情里面有不少信息也带上了方括号比如其中的 \[Bytes in flight: 112000\]这也是解读出来的而且它跟Window Full关系很大。
前面提到过在途数据或者叫在途字节数Bytes in flight等于接收窗口大小的时候Wireshark就会解读为TCP Window Full了。不过如果你在上图中找一下Window size会发现它是19200而不是Bytes in flight的112000这又是为什么呢
![图片](https://static001.geekbang.org/resource/image/82/98/829ee4eda487862ed761cd0bafff8d98.jpg?wh=674x396)
这是因为我们把发送方和接收方的接收窗口搞混啦。这里你需要搞清楚如果说在途数据的发送方是A接收方是B那么这里Window Full的窗口**是B的接收窗口**而不是A的接收窗口。上图是A的报文自然没有我们要找的B的接收窗口信息了那怎么找到B的接收窗口呢
因为这次通信是SCP文件传输那么A就是客户端它的端口是38979B就是服务端它的端口是22。我们的具体做法是在抓包文件里找到B也就是源端口为22的报文而且应该是这个TCP Window Full报文之前的最近的一个在这个报文里就有B的最近的接收窗口值。
单纯用文字,可能未必容易理解,我给你画了一张示意图:
![](https://static001.geekbang.org/resource/image/2e/95/2e594ee968a7a62b307e5f22c08c4095.jpg?wh=2000x1125)
上图中我还是用A指代发送端用B指代接收端。当A的在途数据跟B的接收窗口大小相等时Wireshark就会判断出这个接收窗口满了这意味着A无法再从自己的发送缓冲区把数据发送出来了。只有当B回复ACK确认了n字节的数据后A才有可能发送最多n字节的数据如果缓冲区有足够多的待发数据的话
让我们回到Wireshark窗口找到离这个TCP Window Full最近的从源端口22发送来的报文。我们发现它就是下图这个222号报文
![图片](https://static001.geekbang.org/resource/image/30/60/30d13a6c4e4730742fe1bed79b6b0a60.jpg?wh=1836x1404)
可以看到这个报文的接收窗口就是112000正好等于前面224号报文的Bytes in flight的112000字节。所以我把前面的示意图改进一下供你参考。这里面的信息比较多建议你耐心多花几分钟时间来充分理解其中的机制
![](https://static001.geekbang.org/resource/image/84/0f/84453f815b643f6ac29062a3b9ccf90f.jpg?wh=2000x1125)
整个过程是这样的:
* B发送了报文222给A其中带有B自己的接收窗口112000字节。由于这是一个纯的确认报文所以没有TCP载荷也没有在途数据。
* 报文抵达A端进入A的接收缓冲区。
* A从222号报文中得知B现在的接收窗口是112000字节由于发送缓冲区有足够多的待发的数据A选择用满这个接收窗口也就是连续发送112000字节。
* A把这112000字节的数据发送出来成为报文224其中还带有A自己的接收窗口值19200字节不过由于这次主要是A向B传送数据所以B发给A的基本都是纯确认报文这些报文的载荷都是0。极端情况下即使A的接收窗口为0只要B回复的报文没有载荷它们也是可以持续通信的。
* 224报文抵达B端正好填满B的接收窗口112000字节。Wireshark分别从222报文中读取到B的接收窗口值从224报文中读取到在途字节数由于两者相等所以Wiresahrk提示TCP Window Full。而这个信息是被Wiresahrk展示在224报文中的。
## 自己验证TCP Window Full
对于在途数据既然Wireshark可以解读出来那只要理解了TCP的原理我们同样可以自己来计算这不仅可以考查我们对TCP知识的掌握程度同时对日常排查也有帮助。有这么多好处你是不是跃跃欲试了呢不过在开始之前我们要先学习一个新的概念。
### 下个序列号
下个序列号,也就是**Next Sequence Number**,缩写是**NextSeq**。它是指当前TCP段的结尾字节的位置但不包含这个结尾字节本身。很显然下个序列号的值就是当前序列号加上当前TCP段的长度也就是**NextSeq = Seq + Len**。
这也不难理解因为TCP字节流是连续的那么既然Seq + Len是这个报文的数据截止点自然也是下一个报文的起始点你可以参考这个示意图
![](https://static001.geekbang.org/resource/image/f7/bf/f7864509fd7ff569ebbfa54e14a9e6bf.jpg?wh=2000x910)
在Wireshark里我们也可以找到NextSeq这个解读值比如下图这样
![图片](https://static001.geekbang.org/resource/image/bd/7e/bd1093a136119ebf4166989e64b3637e.jpg?wh=756x201)
明白了NextSeq我们来看如何手工验证TCP Window Full。比如还是分析224号报文的这次TCP Window Full我们可以这么做来验证一下在途数据是否真的是112000字节。
![图片](https://static001.geekbang.org/resource/image/0e/28/0ec8c879acb7526bbff8eb9650cyy928.jpg?wh=1558x550)
首先跟上面的步骤类似我们要找到222号报文。在这个报文里服务端源端口22告诉客户端“我确认你发送来的198854字节的数据”。我们先把这个数字记为X。
然后我们查看224号报文里客户端发送的数据到了哪个位置
![图片](https://static001.geekbang.org/resource/image/22/2e/22f94b3933fb841705b28d11a3106b2e.jpg?wh=1552x984)
我们可以在224号报文的TCP详情页看到Next sequence number: 310854而这个数字就是客户端发送的数据的最新的位置。我们把这个数字记为Y。当然你也可以像前面说的那样把Seq和Len加起来也就是308054 + 2800得到的自然也是310854。
最后我们做一个最简单的减法Y - X = 310854 - 198854 = 112000这正是前面说的在途数据的大小。
恭喜你你已经学会了如何手工计算在途数据的方法这也意味着你对TCP的了解又更深入了一点。你可以这么来总结计算在途数据的方法
> **Bytes\_in\_flight = latest\_nextSeq - latest\_ack\_from\_receiver**
不过你会不会觉得虽然这个计算方法对理解窗口有帮助但是既然Wireshark会给我们提示那这种计算也主要是自我练习而已应该不会真的用得上吧
这还真不好说。因为Wireshark在不少场景下并不会给你提示。比如在接收窗口接近满但又不是完全满的时候哪怕是离窗口满只差1个字节Wireshark也不会提示TCP Window Full了。但是在途数据都已经逼近接收窗口的99.9%了,你还觉得这个肯定没有问题,或者一定没有隐患吗?
要知道,这种临界状况也很可能跟问题根因有关。那么你掌握了这个方法,就可以把排查做得更彻底了。或者,如果你想预防性能瓶颈,那么提前找到这种窗口临界满的状况,也是有益的。
到这里我们可以回答开始时候的问题了为什么上节课里没有看到TCP Window Full这种提示呢我们看一下当时的报文状况吧。
在22:30:39.067477接收端的确认号为7105632
![图片](https://static001.geekbang.org/resource/image/95/15/95c7e978090178984b6cb27355021e15.jpg?wh=896x61)
然后在22:30:39.209712接收端的确认号为7169872
![图片](https://static001.geekbang.org/resource/image/4a/77/4ae7928be673fd6d2c43c7289aed0077.jpg?wh=901x59)
这两个报文的时间跨度正好是141ms左右也就是这次传输里面的**往返时间**。在这个往返时间里接收端确认了多少数据呢是7169872-7105632=64240也就是64KB。这个就是99.9%逼近TCP Window Full了但是因为还差小几十个字节所以Wireshark并没有提示TCP Window Full
你可能还想追问那为什么不把这剩余的0.1%的窗口“榨干”非要留一点呢我们看一下当时的接收窗口和在途数据的具体情况就以上面选择的5864报文附近为例
![图片](https://static001.geekbang.org/resource/image/14/d7/1439256885a585cebc96a139e41495d7.jpg?wh=900x91)
接收端源端口为22的接收窗口为65728发送端源端口为59159的在途数据为65700两者相差只有28字节。对于发送端来说没有必要为了这区区28字节再发送一个小报文了等接收窗口空余出多一点的空间后再动身不迟。
> 如果你还没学习过上节课的内容,可能会对这些信息感到疑惑,建议先去把上节课学完,再来学习这一讲,效果更好。
## TCP Window Full对传输的影响
好了现在我们已经对TCP Window Full做了充分的分析而且也明白了这就是**接收端的接收窗口小于发送端的发送能力而出现的状况**。我们也很容易得出推论:瓶颈在接收端,**TCP Window Full也确实会影响传输速度**。
春节刚过你可能对高速公路上的状况也感受深刻吧很多路段出现了堵车这就相当于TCP Window Full更多的车辆上不了高速了只好堵在外面。如果高速公路的路更宽、车速更快那么就相当于接收窗口变得更大车辆就能进更多也就相当于Bytes in flight更大了。这么说来TCP流量控制和高速车流控制这两个领域也有不少共通之处说不定双方都互有借鉴呢。
回到客户这次的案例。我们看看这次的传输速度是多少呢在上节课里我介绍了在Wireshark里查看TCP传输速度的两种方法。比如我们现在用I/O Graph来看一下
![图片](https://static001.geekbang.org/resource/image/50/5f/50bb15443878c7458035155f5a19425f.jpg?wh=1920x1287)
> 补充如果你的I/O Graph显示的不是这种图那需要像图中这样
>
> * 选中All Bytes指标
> * Y轴的单位选为Bytes。
这个图不能说不对但柱子比较粗看起来不是很精确。这是因为它默认是以1秒为间隔而计算的速度。但是TCP传输中途很可能每过几毫秒都有所变化所以如果我们要看更加精细的图可以调整一下粒度把Interval从1sec改为100ms看看会怎么样
![图片](https://static001.geekbang.org/resource/image/91/e1/91b410903c0b1f35fa0b88666ae012e1.jpg?wh=308x328)
![图片](https://static001.geekbang.org/resource/image/55/5b/5575a289a1aba0d303a0be0db968175b.jpg?wh=1920x1284)
这样看起来精确了很多。我已经把这个抓包文件上传到[Gitee](https://gitee.com/steelvictor/network-analysis/tree/master/10),你可以下载后,依照这里的步骤,把我做过的排查工作也演练一遍,这对你掌握课程知识是很有帮助的。
> 补充如果你用Wireshark查看到的I/O Graph跟我这里的不同那可以对比一下Interval和SMA period这两个配置是否跟图中的一致。
这里有个小的注意点因为我们选择的间隔是100ms所以Y轴的数字就是Bytes/100ms换算成Bytes/s的话要把Y轴的数字再**乘以10**。从图上看在一开始的8秒几乎没有数据传输发生从第8秒开始速度上到了400KB/s就是图上的40K\*10左右一直到结束都大致维持在300~400KB/s这个区间里。
## 继续深挖窗口
一般来说,接收窗口、拥塞窗口、发送窗口,这些都不是一上来就是一个很大的值的,而是跟汽车起步阶段类似,逐步跑起来的。那么这就产生了一个很有意思的话题:这些窗口之间都是怎么协调的呢?直观上感觉,无论哪个更快了,另外两个就要受影响。
我们很容易理解,假设起始值相同,如果接收窗口增长的速度小于拥塞窗口的增长速度,那么接收窗口就成了瓶颈;反过来说,拥塞窗口增速更小,那么它就成了瓶颈。
当接收窗口成为瓶颈的时候很容易就出现这里的TCP Window Full的现象。不过我们这么多讨论都是基于文字如果有更加直观的方式让我们理解这个现象就更好了。这里我们就可以再学一个Wireshark的小工具TCP Stream Graphs里面的 **Window Scaling**
我们还是打开Wireshark的Statistics下拉菜单找到TCP Stream Graphs在二级菜单中选择Window Scaling
![图片](https://static001.geekbang.org/resource/image/cf/e4/cfa2f41e1e0a52f9yyd4d226c441a4e4.jpg?wh=462x552)
这时候就能看到Windows Scaling的趋势图了
![图片](https://static001.geekbang.org/resource/image/0c/82/0cda89fe0232b8515007fe1fc5e7aa82.jpg?wh=922x817)
我就直接给你把关键信息标注出来了。这里主要是两个关键点。
* **数据流的方向要找对**比如这次传输是从客户端向SSH服务端发送数据所以要确认这是从一个高端口向22端口发送数据的流向。如果搞反了那图就变成了SSH服务端回复的ACK报文了不是你要分析的传输速度了。
* **定位TCP Window Full**在这里Receive Window是“阶梯”式的每次变化后会保持在一个“平台”一小段时间那么这时候Bytes Out发送的数据也就是Bytes in flight就有可能触及这个“平台”每次真的碰上的时候就是一次TCP Window Full。
我们可以看一个例子。图中的蓝线代表Bytes Out绿线代表Receive Window。你可以像我这样在这几个“平台”区域找到蓝线和绿线的汇合点然后在这些点上点击鼠标左键就能定位到TCP Window Full事件了。
![图片](https://static001.geekbang.org/resource/image/37/3e/37cb41aa86cce8a437c13db7374d2a3e.jpg?wh=832x573)
上图中我用鼠标放大了一个“平台”然后选中了一个Receive Window和Bytes Out重合的点它是1200号报文主窗口也自动定位到了这个报文果然它也是一次TCP Window Full。
## 验证传输公式
在上节课里我们推导出了TCP传输的核心公式**速度=窗口/往返时间**。既然当前案例里TCP Window Full的时候Bytes in flight跟接收窗口相等那么在这个公式里的窗口是否就是Bytes in flight呢我们来验证一下。
还是在Wireshark窗口里我们要添加这么几个自定义列以便进行数据比对
* Acknowledgement Number确认号
* Next Sequence Number下个序列号
* Caculated Window Size计算后的接收窗口
* Bytes in flight在途字节数
> 补充:如果对怎么添加自定义列还不清楚,可以复习[第5讲](https://time.geekbang.org/column/article/481042)中的添加TTL自定义列的部分。
另外因为我们要集中检查发送端的Bytes in flight就需要把源端口38979的报文过滤出来这样就不会被另一个方向的报文给干扰了。
![图片](https://static001.geekbang.org/resource/image/eb/52/ebf76c39e874a4350f8423485f0acd52.jpg?wh=1231x315)
在这里我们看到的Bytes in flight是112000字节左右。从右边滚动条的位置来看这是在传输过程的初期。让我们滚动到中间和后期看看这些在途字节数是多少
![图片](https://static001.geekbang.org/resource/image/56/eb/5633292b85320c4a309d67f06c94b0eb.jpg?wh=1152x315)
中期这里的Calculated Window Size明显增大了到了445312字节。再看看后半程
![图片](https://static001.geekbang.org/resource/image/55/d7/55cecd4faa33088df661d5058df379d7.jpg?wh=1158x314)
最后阶段已经达到863800字节。综合这三个阶段来看折中值差不多在400KB左右我们把它除以RTT 0.029秒得到的是400KB/0.029s=13790KB/s。显然这个数值远超过前面I/O Graph里看到的300~400KB/s。这是怎么回事呢难道我们的核心公式是错的吗
不知道你有没有考虑到这个问题Bytes in flight是指真的一直在网络上两头不着吗一般来说数据到了接收端接收端就发送ACK确认这部分数据然后TCP Window就往下降了。比如ACK 300字节那么TCP Window就又空出来300字节也就是发送端又可以新发送300字节了。
![](https://static001.geekbang.org/resource/image/ba/96/ba911b5a75b84f691d7c61821e759b96.jpg?wh=2000x871)
像图上这种情况:
* B通知A“我的接收窗口是1000”
* A向B发送了1000字节此时B的接收窗口满
* B向A确认了300字节的数据自身的接收窗口也扩大为300字节
* A的在途字节数也从1000字节变成700字节因为刚刚有300字节被B确认了。
图中的t1到t4表示时间点。t2到t4就是一次往返的时间在这个往返时间内被传输的数据是1000字节吗不是。因为被确认的只有300字节所以传输完的也只有这300字节速度也就不是1000/RTT而是300/RTT我们可以把核心公式做一下改进变成下面这个
> **velocity = acked\_data/RTT**
我们再用改进后的公式来计算这次的速度。我们可以选择传输中间偏后面一点的报文来做分析。比如下图中我们选择1337号和1357号报文为起始和截止点计算NextSeq的差值还有时间的差值然后两个差值相除。
![图片](https://static001.geekbang.org/resource/image/d7/ec/d7174698a1e5c56357f6bd559bbb83ec.jpg?wh=978x289)
33600/0.094 = **357KB/s**。是不是很接近I/O Graph的值了看来这样计算才是正确的
那为什么在上节课里我们就可以用 **窗口/往返时间** 来计算速度,而且数值也很准呢?而这种方法用到这里就完全不对呢?
这是因为上节课的案例,在途数据一旦到了接收端,都被及时确认了。而当前这个案例里面并没有这样。也就是说,这次的案例,出现了“滞留”现象。
![图片](https://static001.geekbang.org/resource/image/4e/e0/4e02af20ef67d67490ba5b2c00bb7de0.jpg?wh=1000x512)
还是1337到1357号报文我们去掉了过滤器 `tcp.srcport eq 38979`这样就展示了双向报文。可以看到服务端B端在这段时间内只确认了22400字节1495254 - 1472854而同样时刻的在途数据却一直维持在一个比较高的数字在660KB上下。所以**真正完成了传输的数据量是前者22400B而不是“虚浮”的660KB**。
那你可能又要问了既然已经确认了22400字节为什么客户端的在途字节数还是没有变化呢
这是因为客户端被确认了22400字节的数据马上又把这个尺寸的数据发送出去了事实上就**维持了这个在途字节数的尺寸**。
我可以再做一个比喻帮助你理解这个现象。我们如果去银行的一个窗口这可不是TCP窗口排队办业务现在排队人数为10人相当于Bytes in flight为10。每分钟都有一个人能完成业务办理原以为队列会减小为9人结果每当有一个人出来保安就喊“下一个”于是就立刻又补进来一个人所以队伍还是维持在10人这么长。
那么窗口的业务员的办理速度是多少呢显然不是10人/分钟而是1人/分钟了。这样是不是理解起来容易多了而上节课的情况相当于这里的“每次就处理一个人”所以处理速度就是1人/分钟,也就可以用“速度=窗口/往返时间”来计算了。
## 小结
这节课我们集中讨论了TCP传输中的窗口相关的知识特别是围绕TCP Window Full这个Wiresahrk中比较常见的信息展开了深入的讨论。你需要重点掌握以下这些知识点
* Wireshark报告TCP Window Full是因为一端的在途数据跟另一端的接收窗口相等。
* TCP的下个序列号Next Sequence Number等于序列号和段长度之和即**NextSeq = Seq + Len**。
* 我们可以自己人工计算出在途字节数,公式是**Bytes\_in\_flight = latest\_nextSeq - latest\_ack\_from\_receiver**。
* 人工计算在途字节数的方法是:
* 找到最近一次发送出去的报文的NextSeq记为X
* 找到在这次发送之前收到的最近的ACK记录它的ACK记为Y
* XY得到在途字节数。
* 另外我们也知道了TCP Window Full确实会影响到传输速度。
除了技术知识点我也带你学习了下面这些Wireshark工具使用技巧
* 在Statistics下拉菜单下的I/O Graph工具可以直观地展示传输速度图。
* 同是Statistics菜单下的TCP Stream Graphs的Window Scaling工具可以直观的展示TCP Window Full历史曲线图。
最后,我们还发现这个案例里的接收端有“数据滞留”的现象,这就导致“速度=窗口/往返时间”的公式遇到了挑战,而我们可以进一步优化为新的公式:**速度=确认数据/往返时间**。这个改进后的公式,可以兼容这种有“数据滞留”现象的传输场景。
## 思考题
给你留两道思考题:
* TCP的序列号和确认号最大可以到多少
* 接收端只确认部分数据,导致了“数据滞留”现象,这个现象背后的原因可能是什么呢?
欢迎你把答案分享到留言区,我们一同交流和进步。
## 附录
抓包示例文件:[https://gitee.com/steelvictor/network-analysis/tree/master/10](https://gitee.com/steelvictor/network-analysis/tree/master/10)