你好,我是胜辉。
有时候,不少知识点在过段时间重新回看的时候,又会有新的体会和发现。比如在第8讲里,我们回顾了一个MTU造成传输失败的案例,虽然整个排查过程的步骤不算很多,但也算是TCP传输问题的一个缩影了。尤其是其中那个失败的TCP流中的一些现象,比如客户端发出的重复确认(DupAck),还有服务端启动的超时重传,都值得我们继续深挖,所以我会在后续的课程里继续这个话题。
然后在上节课里,我们还探讨了传输速度的相关知识,也初步学习了窗口的概念。最后,我们终于推导出了TCP传输的核心公式:速度=窗口/往返时间。这个公式,对于我们理解传输本质和排查传输问题,都有很强的指导意义。
然而,如果你足够细心的话,其实可能会对上节课里的细节有一些疑问,比如:既然接收窗口满了,那为什么当时没有看到TCP Window Full这种提示呢?
其实,我这边也有不少内容按住没有展开,包括核心公式的理解,我们在这节课里将有一个新的认识。另外,我也将带你继续挖掘窗口这个细分领域,这样你以后遇到跟窗口相关的问题,就知道如何破解了。
也是在公有云服务的时候,有个客户有这么一个需求,就是要把文件从北京机房拷贝到上海机房。但是他们发现传输速度比较慢,就做了抓包。在查看抓包文件的时候,发现Wireshark有很多TCP Window Full这样的提示,不明白这些是否跟速度慢有关系,于是找我们来协助分析。
我们先要了解一下抓包文件的整体状况。怎么看呢?当然是看Expert Information了:
它确实提醒我们,有69个Warning级别报文,它们的问题是TCP Window specified by the receiver is now completely Full。在展开进一步排查之前,我们先对这个信息做一下解读。
好了,这个信息解读完毕,一句话说就是:发生了69次在途数据等于接收窗口的情况。
接下来我们看看TCP Window Full具体是个什么样子。比如我们选中224号报文,主界面也自动定位到了这个报文:
我们可以看到,除了224报文,也确实有很多其他报文也报了TCP Window Full的警告信息。
TCP Window Full这个信息非常直接明了,就是说“接收窗口满了”。不过,你可别以为这个信息是TCP报文里的某个字段。其实,它只是Wireshark通过分析得出的信息。你有没有注意到,TCP Window Full前后是有方括号的。一般来说,Wireshark自己分析得到的信息,都会用方括号括起来,而TCP报文本身的字段,是不会带这种方括号的。我们来看一个截图:
上面是224号报文的TCP详情,里面有不少信息也带上了方括号,比如其中的 [Bytes in flight: 112000],这也是解读出来的,而且它跟Window Full关系很大。
前面提到过,在途数据(或者叫在途字节数,Bytes in flight)等于接收窗口大小的时候,Wireshark就会解读为TCP Window Full了。不过,如果你在上图中找一下Window size,会发现它是19200,而不是Bytes in flight的112000,这又是为什么呢?
这是因为,我们把发送方和接收方的接收窗口搞混啦。这里你需要搞清楚:如果说在途数据的发送方是A,接收方是B,那么这里Window Full的窗口,是B的接收窗口,而不是A的接收窗口。上图是A的报文,自然没有我们要找的B的接收窗口信息了,那怎么找到B的接收窗口呢?
因为这次通信是SCP文件传输,那么A就是客户端,它的端口是38979;B就是服务端,它的端口是22。我们的具体做法是:在抓包文件里,找到B(也就是源端口为22)的报文,而且应该是这个TCP Window Full报文之前的最近的一个,在这个报文里就有B的最近的接收窗口值。
单纯用文字,可能未必容易理解,我给你画了一张示意图:
上图中,我还是用A指代发送端,用B指代接收端。当A的在途数据跟B的接收窗口大小相等时,Wireshark就会判断出,这个接收窗口满了,这意味着:A无法再从自己的发送缓冲区把数据发送出来了。只有当B回复ACK,确认了n字节的数据后,A才有可能发送最多n字节的数据(如果缓冲区有足够多的待发数据的话)。
让我们回到Wireshark窗口,找到离这个TCP Window Full最近的,从源端口22发送来的报文。我们发现,它就是下图这个222号报文:
可以看到,这个报文的接收窗口就是112000,正好等于前面224号报文的Bytes in flight的112000字节。所以,我把前面的示意图改进一下供你参考。这里面的信息比较多,建议你耐心多花几分钟时间来充分理解其中的机制:
整个过程是这样的:
对于在途数据,既然Wireshark可以解读出来,那只要理解了TCP的原理,我们同样可以自己来计算,这不仅可以考查我们对TCP知识的掌握程度,同时对日常排查也有帮助。有这么多好处,你是不是跃跃欲试了呢?不过在开始之前,我们要先学习一个新的概念。
下个序列号,也就是Next Sequence Number,缩写是NextSeq。它是指当前TCP段的结尾字节的位置,但不包含这个结尾字节本身。很显然,下个序列号的值就是当前序列号加上当前TCP段的长度,也就是NextSeq = Seq + Len。
这也不难理解,因为TCP字节流是连续的,那么既然Seq + Len是这个报文的数据截止点,自然也是下一个报文的起始点,你可以参考这个示意图:
在Wireshark里,我们也可以找到NextSeq这个解读值,比如下图这样:
明白了NextSeq,我们来看如何手工验证TCP Window Full。比如,还是分析224号报文的这次TCP Window Full,我们可以这么做,来验证一下在途数据是否真的是112000字节。
首先,跟上面的步骤类似,我们要找到222号报文。在这个报文里,服务端(源端口22)告诉客户端:“我确认你发送来的198854字节的数据”。我们先把这个数字记为X。
然后,我们查看224号报文里,客户端发送的数据到了哪个位置:
我们可以在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:
然后在22:30:39.209712,接收端的确认号为7169872:
这两个报文的时间跨度正好是141ms左右,也就是这次传输里面的往返时间。在这个往返时间里,接收端确认了多少数据呢?是7169872-7105632=64240,也就是64KB。这个就是99.9%逼近TCP Window Full了,但是因为还差小几十个字节,所以Wireshark并没有提示TCP Window Full!
你可能还想追问:那为什么不把这剩余的0.1%的窗口“榨干”,非要留一点呢?我们看一下当时的接收窗口和在途数据的具体情况,就以上面选择的5864报文附近为例:
接收端(源端口为22)的接收窗口为65728,发送端(源端口为59159)的在途数据为65700,两者相差只有28字节。对于发送端来说,没有必要为了这区区28字节再发送一个小报文了,等接收窗口空余出多一点的空间后再动身不迟。
如果你还没学习过上节课的内容,可能会对这些信息感到疑惑,建议先去把上节课学完,再来学习这一讲,效果更好。
好了,现在我们已经对TCP Window Full做了充分的分析,而且也明白了:这就是接收端的接收窗口小于发送端的发送能力而出现的状况。我们也很容易得出推论:瓶颈在接收端,TCP Window Full也确实会影响传输速度。
春节刚过,你可能对高速公路上的状况也感受深刻吧!很多路段出现了堵车,这就相当于TCP Window Full,更多的车辆上不了高速了,只好堵在外面。如果高速公路的路更宽、车速更快,那么就相当于接收窗口变得更大,车辆就能进更多,也就相当于Bytes in flight更大了。这么说来,TCP流量控制和高速车流控制这两个领域也有不少共通之处,说不定双方都互有借鉴呢。
回到客户这次的案例。我们看看,这次的传输速度是多少呢?在上节课里,我介绍了在Wireshark里查看TCP传输速度的两种方法。比如,我们现在用I/O Graph来看一下:
补充:如果你的I/O Graph显示的不是这种图,那需要像图中这样:
- 选中All Bytes指标;
- Y轴的单位选为Bytes。
这个图不能说不对,但柱子比较粗,看起来不是很精确。这是因为,它默认是以1秒为间隔而计算的速度。但是TCP传输中途,很可能每过几毫秒都有所变化,所以,如果我们要看更加精细的图,可以调整一下粒度,把Interval从1sec改为100ms,看看会怎么样:
这样看起来精确了很多。我已经把这个抓包文件上传到Gitee,你可以下载后,依照这里的步骤,把我做过的排查工作也演练一遍,这对你掌握课程知识是很有帮助的。
补充:如果你用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:
这时候就能看到Windows Scaling的趋势图了:
我就直接给你把关键信息标注出来了。这里主要是两个关键点。
我们可以看一个例子。图中的蓝线代表Bytes Out,绿线代表Receive Window。你可以像我这样,在这几个“平台”区域,找到蓝线和绿线的汇合点,然后在这些点上点击鼠标左键,就能定位到TCP Window Full事件了。
上图中,我用鼠标放大了一个“平台”,然后选中了一个Receive Window和Bytes Out重合的点,它是1200号报文,主窗口也自动定位到了这个报文,果然它也是一次TCP Window Full。
在上节课里,我们推导出了TCP传输的核心公式:速度=窗口/往返时间。既然当前案例里TCP Window Full的时候,Bytes in flight跟接收窗口相等,那么在这个公式里的窗口,是否就是Bytes in flight呢?我们来验证一下。
还是在Wireshark窗口里,我们要添加这么几个自定义列,以便进行数据比对:
补充:如果对怎么添加自定义列还不清楚,可以复习第5讲中的添加TTL自定义列的部分。
另外,因为我们要集中检查发送端的Bytes in flight,就需要把源端口38979的报文过滤出来,这样就不会被另一个方向的报文给干扰了。
在这里,我们看到的Bytes in flight是112000字节左右。从右边滚动条的位置来看,这是在传输过程的初期。让我们滚动到中间和后期,看看这些在途字节数是多少:
中期这里的Calculated Window Size明显增大了,到了445312字节。再看看后半程:
最后阶段已经达到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字节了。
像图上这种情况:
图中的t1到t4表示时间点。t2到t4就是一次往返的时间,在这个往返时间内,被传输的数据是1000字节吗?不是。因为被确认的只有300字节,所以传输完的也只有这300字节,速度也就不是1000/RTT,而是300/RTT!我们可以把核心公式做一下改进,变成下面这个:
velocity = acked_data/RTT
我们再用改进后的公式来计算这次的速度。我们可以选择传输中间偏后面一点的报文来做分析。比如下图中,我们选择1337号和1357号报文为起始和截止点,计算NextSeq的差值,还有时间的差值,然后两个差值相除。
33600/0.094 = 357KB/s。是不是很接近I/O Graph的值了?看来这样计算才是正确的!
那为什么在上节课里我们就可以用 窗口/往返时间 来计算速度,而且数值也很准呢?而这种方法用到这里就完全不对呢?
这是因为上节课的案例,在途数据一旦到了接收端,都被及时确认了。而当前这个案例里面并没有这样。也就是说,这次的案例,出现了“滞留”现象。
还是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工具使用技巧:
最后,我们还发现这个案例里的接收端有“数据滞留”的现象,这就导致“速度=窗口/往返时间”的公式遇到了挑战,而我们可以进一步优化为新的公式:速度=确认数据/往返时间。这个改进后的公式,可以兼容这种有“数据滞留”现象的传输场景。
给你留两道思考题:
欢迎你把答案分享到留言区,我们一同交流和进步。
抓包示例文件:https://gitee.com/steelvictor/network-analysis/tree/master/10