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.

290 lines
24 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 12 | 重传的认识:重传到底是怎么回事?
你好,我是胜辉。
在前面的[第8讲](https://time.geekbang.org/column/article/484667)和[第9讲](https://time.geekbang.org/column/article/484923)我们先后介绍了两个TCP传输方面的案例。在刚过去的[第11讲](https://time.geekbang.org/column/article/486281)我们更是全面了解了TCP的拥塞控制机制。其中有一个词经常被提到就是“重传”。
在我看来TCP最核心的价值如果说只有一个的话那就是**对可靠传输的保证**。而要实现可靠的传输,可能需要这样做:如果我的报文丢了,应该在一定次数内持续尝试,直到传输完成;而如果这些重传都失败了,那就及时放弃传输,避免陷入死循环。
所以为了应对不同的情况TCP又发展出了两种不同的重传类型**超时重传**和**快速重传**。它们在各自的场景下都有不可替代的作用。不过,它们本身也只是外在的表现,触发它们的条件又分别是什么呢?
另外你可能在Wireshark里也见过Spurious retransmission这个又是什么意思会对传输有什么影响吗
这节课,我就通过对几个案例中的抓包文件的解读,带你学习这些重传家族的成员,了解它们的性格脾气,以后你在日常网络排查中看到重传,也就能顺利搞定了。
## 超时重传
我们先来学习下超时重传Timeout Retransmission。在TCP传输中以下两种情况都可能会导致发送方收不到确认
* 报文在发送途中丢失,没有到达接收方,那接收方也不会回复确认包。
* 报文到达接收方,接收方也回复了确认,但确认包在途中丢失。
![](https://static001.geekbang.org/resource/image/71/6f/71e453a2e8690941b9198bcb192e8d6f.jpg?wh=2000x979)
没有收到确认怎么办?发送方为了避免自己陷入“尬等”的境地,选择在等待某段时间后重新发送同样这份报文,这个等待的时间就是**重传超时**Retransmission Timeout简称RTO。这个Timeout其实是基于一个计时器在报文发送出去后就开始计时在时限内对方回复ACK的话计时器就清零而如果达到时限对方还没回复ACK的话重传操作就被触发。
当然超时重传也还是可能会丢包此时发送方一般会以RTO为基数的2倍、4倍、8倍等时间倍数去尝试多次。
我们来看一个例子,熟悉一下这种重传。
### 超时重传案例
有一次我们一个客户访问HTTPS站点的服务时常报错失败我们就做了抓包。这个问题的原因已经不重要了但是这个抓包文件倒是很适合用来给我们学习TCP重传。
我们直接选一个典型的失败事务的TCP流来看一下
![图片](https://static001.geekbang.org/resource/image/e7/c8/e785dd65d5649f3aa7c0320900be5dc8.jpg?wh=1212x470)
> 示例文件已经上传至[Gitee](https://gitee.com/steelvictor/network-analysis/tree/master/12),建议你结合示例文件和文稿来学习,效果更好。
显然图中黑底红字的报文就是一系列的重传而且都是超时重传。你如果仔细看了这个文件可能会指出“老师不对这里的12号报文是Spurious重传不是超时重传”。但是听完我后面的分析你应该会同意我的观点这个本质上也是超时重传。
好,我们开始分析。
* 第一阶段1~3号报文是TCP握手。
* 第二阶段4~9号报文是TLS握手。
* 第三阶段11~20号报文是连续重传以及夹杂的DupAck和Spurious重传。
* 第四阶段连接关闭。它的触发点是21号报文这个TLS Alert消息它的类型是21。要知道它的具体报错信息需要解密才能知道在第20讲我会介绍TLS解密的细节。不过通常来说在这种TLS Alert消息之后就是TCP挥手了。
![图片](https://static001.geekbang.org/resource/image/33/c4/3352ae9d3ca7ddc638b04dcea61491c4.jpg?wh=1920x747)
我们重点关注下第三阶段也就是11~20号报文这些重传。那么问题来了这些重传都是重传了谁呢也就是如何找到原始报文呢
方法就是:**先找到重传报文的序列号,然后到前面找到同样这个序列号的报文**。那个就是原始报文。
比如11号报文是Wireshark提示我们的第一个重传报文我们看到它的序列号是272。在11号报文前面序列号同为272的是10号报文。那么显然11就是10的重传后面的14到20号报文也是如此。
![图片](https://static001.geekbang.org/resource/image/1a/17/1ab83ea403c9325652688f287349e617.jpg?wh=1920x403)
这里还有一个我们熟悉的现象。因为这一系列的重传是对同一个原始报文的重传,所以它们的发送时间也遵循了“[指数退避](https://en.wikipedia.org/wiki/Exponential_backoff)”的原则,比如:
* #11和#10隔了233ms
* #14和#11隔了467ms
* #15和#14隔了936ms
* ……
到20号报文的时候已经重传了第8次也是最后一次。那么为什么没有第9次呢有下面这两种可能
* 客户端本来就最多只重传8次所以后续也不再重传。
* 服务端的TLS Alert报文过来并发起了挥手这样后续也没机会重传了。
然后我们再来理解一下整个的传输过程。不过因为抓包文件是单侧的,所以你要注意,**Wireshark里的信息是需要跟你“抓包发生在哪一侧”这个信息结合起来解读的**。
我画了下面这张图,供你参考这个过程。请注意,这是从抓包视角(也就是客户端)来解读的。如果服务端有抓包,很可能是不同的景象。比如,有可能服务端确实收到了这些重传报文,但确认报文一直没能成功传过来,这个可能性也是存在的。那样的话,这张图就会非常不同了。
![](https://static001.geekbang.org/resource/image/01/56/01fe829f2b84f4a1f0167452c4a77f56.jpg?wh=1971x1125)
12号报文的序列号是1200跟9号报文相同而且由于客户端9号报文已经收到并且回复了确认所以Wireshark认为它是**Spurious重传**。这又是什么重传呢?
### Spurious重传
Wireshark如果识别到某个报文已经被确认过但又再次发送那么这次就是Spurious重传。简单来说就是已经成功了不需要再传。
这可以发生在两个方向上。具体来说假设两端分别是A和B我们在A端抓包发现下面任何一种情况Wireshark就会标记X为Spurious重传
* A发送了报文XB回复了确认A再次发送X。
* A收到了B发过来的报文XA也回复了确认但B再次发送X。
那么我们在Wireshark里看到这种Spurious重传该怎么办呢我觉得一般不用特别处理集中关注超时重传和快速重传就好了。
> 补充这个建议也是基于概率。Spurious重传大部分时候不是问题只有极少数情况下是问题所以不去重点关注它是“划算的”。相关的案例会在专栏的后半程里介绍。
不过我们仔细观察报文还是发现了一些问题。10号报文的确认号是1251也就是9号报文的下个序列号1251=序列号1200+载荷长度51所以10号报文就是客户端对9号的确认报文。那么9号既然已经被确认了为什么还要12号报文这个重传呢
只有一种解释:**这个抓包是在客户端做的,所以看不到服务端的情况**。这个10号报文一定是没有到达服务端所以后者认为9号未被客户端收到于是在234ms后重传了9号的副本12号报文。这里的234ms就是11号报文的233ms加上12号报文的1ms。
从发送端客户端的抓包来看是收到了Spurious重传
![图片](https://static001.geekbang.org/resource/image/df/10/df3d94a038aaf30ba643ac33b3db1910.jpg?wh=1920x406)
但从接收端服务端来看我推测是认为9号丢失所以200多ms后进行超时重传
![](https://static001.geekbang.org/resource/image/96/80/96fef7102d6cc81e0f969c1ddd31d880.jpg?wh=2000x720)
另外你会发现10号报文本身也携带数据它对9号报文的确认信息是跟着10号报文自己的数据一起过来的。反正确认信息只是一份元数据不占用额外的空间那么跟随数据报文一起发送是最高效的。
实际的重传的例子我们解读完了,接下来了解一下我们可能最关心的问题。
### 重传超时究竟是多长呢?
[RFC6298](https://datatracker.ietf.org/doc/html/rfc6298)规定:**在一条TCP连接刚刚开始还没有收到任何回复的时候这时的超时RTO为1秒**。在更早以前的规范里这个值是3秒。你可以参考RFC6298的[这个部分](https://datatracker.ietf.org/doc/html/rfc6298#appendix-A),了解这个改变的来龙去脉。
**在连接成功建立后Linux会根据RTT的实际情况动态计算出RTO。**实际场景中RTO为200ms出头最为常见。
而且,**RTO有上限值和下限值**(仿佛有语音:“我们不是没有下限的~”)。一般情况下,**Linux的这两个值分别是120秒和200毫秒**。那么这个能否修改呢?
你可能想起了sysctl命令。但是很可惜这两个值不能像sysctl那样调整好像不太方便其实这也是一种“幸运”操作系统把一些比较敏感、改错后影响比较大的参数没有做成可以灵活调整的方式也可以避免我们随便调整引发问题。
## 快速重传
上面的超时重传虽然避免了“干等”的尴尬局面,但不可避免地带来了另外的问题:“干等”的时间还是不短的,这段时间被白白浪费了。快速重传的出现就是为了解决这个问题。
它的思路是这样的:**如果对端回复连续3个DupAck即重复确认我就把序列号等于这个ACK号的包重传。**
### 快速重传案例
我在公有云工作的时候有个客户对我们机房的网络可用性进行测试结果发现测试情况不容乐观很多HTTP请求没有得到及时回复。因为是相对简单的HTTP请求本来期望在几个RTT之内就得到HTTP响应的但实际上很多次都是超过了1秒。
于是我们做了抓包然后过滤出了有问题的TCP流。我们看一下这条流的专家信息Expert Information
![图片](https://static001.geekbang.org/resource/image/fb/ff/fb8eb4b226bfa64d17667289d139bbff.jpg?wh=1820x342)
这里我选几个值得关注的信息,做一下解读。
Error级别的一条信息是**New fragment overlaps old data (retransmission?)**。这是说这个报文跟前面的报文有重合。这里的“重合”如何理解呢比如前面的报文是字节100到200新的报文是字节150到250那么两者在150到200字节之间就是重合的。这也不是很大的问题。
Note级别的3类信息分别是
* **This frame is a (suspected) fast retransmission**这是快速重传报文那为什么还要加个suspected字样呢我的理解是Wireshark是根据一些条件来综合判断这个报文属于什么类型的但这仅仅是一种参考的信息。由于TCP报文本身没有表示重传的字段所以Wireshark对它的解读只能作为参考所以是suspected。其实这个信息一般都比较准确很少有错的时候用suspected这个词更多地体现了Wireshark开发人员的谦虚和严谨。
* **This frame is a (suspected) retransmission**这里就是超时重传了Wireshark发现抓包文件中没有相关的DupAck就推断出这个是超时重传。
* **Duplicate ACK (#1)**这里有28个重复确认报文而且如果我们点开的话会发现这28个DupAck指向的都是同一个报文56号报文。
![图片](https://static001.geekbang.org/resource/image/a1/ea/a1c4452a6541b632ee4162e6be316bea.jpg?wh=1272x1060)
**记住DupAck经常跟快速重传相关。因为有3个或以上数量的DupAck就可以触发快速重传**。
那么问题来了为什么这里会有28个DupAck呢不是有3个就足够触发了吗
我们回到主界面来分析,你可以参考下图:
![图片](https://static001.geekbang.org/resource/image/63/d5/63dd2ca0a046487c059d54020d9a05d5.png?wh=1066x755)
报文58~60是3个DupAck显然也直接触发了61号快速重传报文。这样不是已经重传了吗为什么还会有后面连续25个之多的DupAck呢
答案可能不在这片区域。我们把视线往上挪看一下57号及之前的报文情况。
![图片](https://static001.geekbang.org/resource/image/bc/8d/bc1b274905bd14f582f3b59dd345318d.png?wh=910x750)
我们可以看到57号报文之前整个传输就像一片宁静的草原。然而在风平浪静的表面之下能找到我们要的答案吗
61号快速重传报文的原始报文是哪个呢不难找到它就是32号报文因为**它的序列号就是42061也就是连续DupAck的确认号**。
正因为抓包在服务端抓取所以一定能抓取到由这个服务端发出的报文但是对端有没有收到我们是看不到的。只有当3个的DupAck到达的时候我们才知道这个报文一定是丢失了然后会快速重传。
我直接给你揭晓答案就是因为从32号报文之后服务端还继续发送了14个数据报文远不止3个所以触发的DupAck也远不止是3个。你可以直接看下图来理解这里的逻辑
![](https://static001.geekbang.org/resource/image/43/0d/4343074e10dc89634c8f3540ac72ba0d.jpg?wh=1843x1035)
不过你仔细看这个抓包文件的话可能还是发现了一个漏洞服务端从32号报文之后发送的数据报文一共是14个为啥客户端要回复的DupAck有28个呢这数量对不上老师你是不是搞错了
我在解读这个抓包的时候,其实也在这里卡住过,确实是个有点烧脑的问题:逻辑都对,就是数量不对,到底哪里出了问题?
![](https://static001.geekbang.org/resource/image/f8/f6/f81e98de0b231a5f6c1d9f2d0561cef6.jpg?wh=1528x965)
这种时候,你会怎么做呢?
* 宽慰自己:“应该有什么别的原因吧,就不追究了,这个小问题不影响大方向。”
* 鼓励自己:“真相只有一个!再查一下。”
其实你如果是连续学习课程过来的应该会对TSO有印象。这是我们在[第8讲](https://time.geekbang.org/column/article/484667)提到的概念。有了TSO操作系统就可以把大于MSS的TCP段比如2个MSS或更大尺寸的段交给网卡驱动来处理后者会利用其硬件芯片做分段工作重新组装成新的符合MTU的报文后发送出去。
那么“为什么14个报文触发了28个DupAck”的答案就在这里了
![图片](https://static001.geekbang.org/resource/image/c2/d4/c2a683b06747d39f92e6f9639e6663d4.jpg?wh=910x511)
这14个报文每个都是2804字节大小这就显然是TSO在起作用了。服务端把2804字节发给网卡后网卡拆分为2个报文后发出所以14个数据报文实际到达接收端的时候就是28个报文
![](https://static001.geekbang.org/resource/image/0c/6f/0c41957d3144dcb5a24c941dee64be6f.jpg?wh=1709x489)
> 补充实际TSO工作起来比这个图要复杂。为了突出重点这个图里并没有展示TCP头部和IP头部的封装工作。
![](https://static001.geekbang.org/resource/image/f3/cf/f397d9e75c93facc2fyy4ae6d59bd7cf.jpg?wh=1481x950)
因为tcpdump在内核里靠近网卡这一侧所以tcpdump抓取到的还是TSO处理之前的大报文只有到达了网卡并且被TSO机制做了分段处理后才变成小报文。我们抓包文件里看到14个报文实际发送出去的是28个报文也就触发了28次DupAck。
遇到难题,努力一下,往往就有新的发现。让我们每天进步一点点。
## SACK跟重传的关系
其实在第8讲“MTU引发的血案”里我们就发现了SACK现象。这次我们研究重传那就有必要回顾一下这个SACK部分把它的含义和作用都搞清楚。
在那次案例里服务端向客户端发送了3个HTTP响应报文但是因为MTU的问题其中一个大的报文在路径上丢失了只有后面2个报文到达了客户端从而引发了客户端发送了两次DupAck。
> 示例文件已经上传至[Gitee](https://gitee.com/steelvictor/network-analysis/blob/master/12/SACK.pcap),建议结合示例文件和文稿来深入学习。
我们来看看这两个DupAck的详情先看第一个
![图片](https://static001.geekbang.org/resource/image/d3/56/d37ef038ac854b213495d345a71e8556.jpg?wh=1920x1213)
你可以直接进入关键部分也就是这里框出来的TCP Option SACK 1389-2077。**SACK全称是Selective Acknowlegement中文叫“选择性确认”**。但是在中文里,貌似带“选择性”这个前缀的词都不是很正面,比如像“选择性忽视”“选择性失忆”什么的,我们好像都不想摊上这种事。
不过在TCP的世界里“选择性确认”这个概念就很不一样了它还真的是我们实实在在需要的一个特性能帮助TCP运作得更好。
我们先说说没有SACK会怎么样。在前面的五个报文的例子里接收端在TCP里我们只能对收到的连续报文进行确认。我们可以看个例子
![](https://static001.geekbang.org/resource/image/af/c9/af653ea11041d94c418d24a98c4f2cc9.jpg?wh=2000x951)
![](https://static001.geekbang.org/resource/image/27/31/272b1714ab31190eaa6f2da1c9fa6331.jpg?wh=1622x1051)
接收端回复的报文的确认号只能是连续字节数的最后一个位置。因为发送端的序列号为201、长度为100的TCP报文丢失了那么服务端收到的连续字节数的最后一个位置就是第201字节。这还不是最糟糕的后续发送过来的序列号为301和401这两个报文服务端回复的确认报文的确认号也**仍然只能是201**。
现在有3个确认报文的确认号为201。对于早期TCP实现来说这个时候发送端只能把序列号从201开始的报文也就是序列号分别为201、301、401的这3个报文全部重传。但是301和401报文实际已经到达接收端却也要跟着201一起被重传这未免太浪费了301和401号报文表示“201真是我们的猪队友”。
那有没有办法只重传序列号为201的报文而避免重传301和401呢
于是TCP增加了SACK这个特性。SACK机制可以告诉发送端“虽然我的确认号是201但是我的TCP Option里面还有更详细的信息在那里我会告诉你在断点后我又收到了哪些数据”。这段话比较晦涩我们直接看这次的这个报文
![图片](https://static001.geekbang.org/resource/image/49/a9/49bcae74a56d2db4aed257cc4bd1e9a9.jpg?wh=758x222)
SACK最关键的部分就是 **left edge和 right edge**也就是左右边界。上图告诉我们我收到了从第1389字节开始直到2076字节也就是不含2077的数据。这样我们可以把SACK和确认号结合起来知道了通过这个报文接收端这里是客户端明白了什么样的信息。我用示意图把这个信息表示了出来
![](https://static001.geekbang.org/resource/image/47/e4/47f91621fa7aa6de9fdb9be953ddd2e4.jpg?wh=2000x471)
类似的,我们看一下第二个重复确认报文([示例文件](https://gitee.com/steelvictor/network-analysis/blob/master/12/SACK.pcap)的10号报文的SACK详情
![图片](https://static001.geekbang.org/resource/image/fe/fb/fe6eda6c07f74efafef0bb14a06d6bfb.jpg?wh=770x220)
可见第二个重复确认报文的SACK把实际收到的报文边界又往右“推”了一些到了第2134字节之前是第2077字节。这个差额是2134-2077=57。
![](https://static001.geekbang.org/resource/image/9c/7f/9ccffc7bf2bd185299633b694857d77f.jpg?wh=2000x545)
你有没有发现这57个字节其实正是前面抓包文件截图里的8号报文的大小也就是说这57字节的报文确实被服务端收到了并在随后回复的SACK报文中得以体现。这里我们再看一眼这57字节的报文在抓包文件中的位置
![图片](https://static001.geekbang.org/resource/image/19/71/191ab7df2b07697956a3f486165dd471.jpg?wh=934x290)
既然SACK这么好是不是TCP传输都能用上它呢其实**SACK要能工作还需要SACK permitted这个TCP扩展属性的支持**。这个字段只有在TCP握手的SYN和SYN+ACK报文中出现表示自己是否支持SACK特性。比如下图
![图片](https://static001.geekbang.org/resource/image/4b/d2/4bba1dedd65f20966a820a1dcab8f9d2.jpg?wh=1174x324)
知道对方支持SACK那我们就可以在后续报文里带上SACK也就是上面的含有left edge和right edge来告诉对方我实际收到的TCP段了就可以避免这部分报文也被连带重传。201报文表示还好有了SACK队友们你们继续往前冲吧我虽然这次掉队了但我还会回来的。
那么有了SACK是不是所有的这种零星到达的报文都不用重传了呢答案是没有那么乐观。受限于TCP Option长度**SACK部分最多只能容纳4个块**。当然这比没有SACK的情况还是好多了。
![](https://static001.geekbang.org/resource/image/67/29/67c5546e23639f2dfa276d0289c84129.jpg?wh=2000x711)
## 小结
这节课我们学习了TCP重传的两种类型超时重传和快速重传。然后也通过实际案例看到了这两种重传在实际情况中的特点。这里我再给你小结一下这些知识点你可要好好掌握。
对于超时重传:
* TCP对于每条连接都维护了一个超时计时器当数据发送出去后一定时限内还没有收到确认就认为是发生了超时然后重传这部分数据。
* RTO的初始值是1秒在发送SYN但未收到SYN+ACK阶段
* 在连接建立后TCP会动态计算出RTO。
* RTO有上限值和下限值常见值分别为2分钟和200ms。
* 实际场景中RTO为200ms出头最为常见。
对于快速重传:
* 快速重传的触发条件是收到3个或者3个以上的重复确认报文DupAck
* 在快速重传中SACK选择性确认也起到了避免一部分已经到达的数据被重传。不过也由于TCP头部长度的限制SACK只能放置4个块再多也不行了。
* 快速重传只要3个DupAck就可以触发实际上我们还可能观察到远多于3个DupAck的情况这也是正常现象。
* **Spurious重传**对TCP传输的影响比快速重传和超时重传小很多总体来说是一种影响不大的重传。
另外在案例拆解的过程中我们也进一步学习了Wireshark的使用技巧包括
* **Wireshark里的信息是需要跟你“抓包发生在哪一侧”这个信息结合起来解读的。**这对你的排查会起到很关键的作用。
* 如何定位到被重传的原始报文的方法:**先找到重传报文的序列号,然后到前面找到同样这个序列号的报文。**
* 如果在专家信息里看到 **New fragment overlaps old data (retransmission?)**,这意味着多个报文之间的数据有重叠,但一般不是严重的问题。
* Wireshark提示的 **(suspected) fast retransmission** 就是快速重传报文。
* Wireshark提示的 **(suspected) retransmission** 就是超时重传报文。
* 如果发现有数据报文和DupAck数量不对等的情况可以**看一下是否有TSO的存在**。
## 思考题
最后再给你留两个思考题:
* TCP的确认报文如果丢失了发送端还会不会重传呢为什么
* 你有没有遇到过重传引发的问题,你是怎么处理的呢?
欢迎你把答案分享到留言区,我们一起进步、成长。