解读TCP Window Full

案例:tcp window full 是导致异地拷贝速度低的原因吗?也是在公有云服务的时候,有个客户有这么一个需求,就是要把文件从北京机房拷贝到上海机房。但是他们发现传输速度比较慢,就做了抓包。在查看抓包文件的时候,发现 wireshark 有很多 tcp window full 这样的提示,不明白这些是否跟速度慢有关系,于是找我们来协助分析。
解读 expert information 我们先要了解一下抓包文件的整体状况。怎么看呢?当然是看 expert information 了:
它确实提醒我们,有 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 号报文,主界面也自动定位到了这个报文:
我们可以看到,除了 224 报文,也确实有很多其他报文也报了 tcp window full 的警告信息。
解读 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 字节。所以,我把前面的示意图改进一下供你参考。这里面的信息比较多,建议你耐心多花几分钟时间来充分理解其中的机制:
整个过程是这样的:
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 是这个报文的数据截止点,自然也是下一个报文的起始点,你可以参考这个示意图:
在 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 这种提示呢?我们看一下当时的报文状况吧。
在 2239.067477,接收端的确认号为 7105632:
然后在 2239.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 segment》,可能会对上面这些信息感到疑惑,建议先去看完,再来看这一篇,效果更好。
tcp window full 对传输的影响
好了,现在我们已经对 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,看看会怎么样:
这样看起来精确了很多。
补充:如果你用 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 的趋势图了:
我就直接给你把关键信息标注出来了。这里主要是两个关键点。
数据流的方向要找对:比如这次传输是从客户端向 ssh 服务端发送数据,所以要确认这是从一个高端口向 22 端口发送数据的流向。如果搞反了,那图就变成了 ssh 服务端回复的 ack 报文了,不是你要分析的传输速度了。定位 tcp window full:在这里,receive window 是“阶梯”式的,每次变化后会保持在一个“平台”一小段时间,那么这时候 bytes out(发送的数据,也就是 bytes in flight)就有可能触及这个“平台”,每次真的碰上的时候,就是一次 tcp window full。我们可以看一个例子。图中的蓝线代表 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 窗口里,我们要添加这么几个自定义列,以便进行数据比对:
acknowledgement number:确认号 next sequence number:下个序列号 caculated window size:计算后的接收窗口 bytes in flight:在途字节数 另外,因为我们要集中检查发送端的 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 字节了。
像图上这种情况:
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 的差值,还有时间的差值,然后两个差值相除。
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 人 / 分钟,也就可以用“速度 = 窗口 / 往返时间”来计算了。


中科院微电子所专访:连接实验室与产业化的“最后一公里”
自动驾驶大事记:苹果无人车出事故、捷豹路虎给车辆装上“眼睛”、Waymo自动驾驶车麻烦不断
如何解决pogo pin测试出现不稳定现象?
标准与电路保护和电磁兼容技术
基因区块链解读
解读TCP Window Full
数据存储技术之nand_flash结构和原理剖析
2022机器人企业创新50强公布:九号机器人登榜前十
全球基本面的逻辑、梳理和展望
HC89F0431A HC89F0421A 带ADC的20引脚8位FLASH微控制器外设功能端口总映射
实时定位技术助力自动引导小车高效运输
基于振弦采集仪的工程监测技术探索
虹科运输冲击和GPS定位数据记录仪可以迅速确定并记录其位置
安森美推出新的高功率图腾柱PFC控制器,满足具挑战的能效标准
美的空调“双十一”推出四款新品,引领行业加速变革
工位ANDON呼叫拉绳按钮终端介绍
隧道人员定位系统如何做到精准
愚人节! 锤子科技发布十大黑科技,苹果根本想象不到的新功能
华为 nova 2s要搞事情?葫芦里到底卖的什么药
中兴“跌倒”倒逼中国“补芯”