gitbook/网络排查案例课/docs/486281.md

323 lines
25 KiB
Markdown
Raw Permalink Normal View History

2022-09-03 22:05:03 +08:00
# 11 | 拥塞TCP是如何探测到拥塞的
你好,我是胜辉。
前面两节课我们通过真实的案例一起学习了TCP传输方面的知识比如其中的核心概念往返时间、接收窗口和发送窗口、在途字节数还有推导出来的核心公式。
当然在实际场景里我们可以直接利用Wireshark的I/O Graph查看速度趋势图这样最方便并且有不同时段的速度方便我们对整体状况做全面的评估。
不过不知道你有没有发现TCP传输的起始阶段速度都是从低到高升上来的很少有一上来就直接以最终速度运行的情形。其实这个行为跟 **TCP拥塞控制**有着密切的关系。所以这节课我会带你了解什么是拥塞窗口、TCP是如何检测和避开拥塞的。这样呢以后你处理TCP拥塞相关的问题时候就能有的放矢做到有针对性的分析了。
好,让我们来看一个具体的案例吧。
## 案例
在公有云服务的时候我们有个银行的客户他们有一次需要跨机房拷贝一个大文件也就是用SCP命令把文件从公有云机房拷贝到他们的自建机房。但是客户发现速度比较慢让我们看一下原因。
![](https://static001.geekbang.org/resource/image/c8/5b/c88359c7b0889fe31436bd1424b0015b.jpg?wh=1686x539)
架构上说,客户自己的机房在上海,公有云上的资源则在北京。地理位置相差很远,而且机房所属的性质也完全不同,那两者如何通信呢?走公网的话不太安全,而且速度和质量没有保障。
如果你熟悉网络的话可能知道可以在两个机房之间搭建VPN。而从可靠性角度考虑客户选择了使用公有云的专线产品也就是在上海自有机房和北京公有云之间打通了专线。这样客户在上海的自有机房就可以直接以10.x.x.x这种内网地址直达他们在北京公有云的资源就好像真的在同一个内网一样。是不是挺酷的
> 补充上传的抓包文件我做了脱敏修改所以IP也是随机值而不是10.x.x.x。
可是他们在传输文件的时候发现速度只有200KB/s左右达不到购买的专线带宽值。正好他们也在传输过程做了tcpdump抓包我们来看看这个抓包文件的具体情况。
> 抓包示例文件已上传至 [Gitee](https://gitee.com/steelvictor/network-analysis/tree/master/11)你可以用Wireshark打开这个文件跟随我的分析步骤来同步学习。
跟我们在[第9讲](https://time.geekbang.org/column/article/484923)和[第10讲](https://time.geekbang.org/column/article/485689)学过的一样,我们可以用 **I/O Graph** 这个小工具来直观地看一下传输速度:
![图片](https://static001.geekbang.org/resource/image/1c/cd/1c9eb4698d2fa4cf0fccac8ac2413ccd.jpg?wh=896x624)
从图上看速度大体上在100~200KB/s之间浮动有少数几个时段的速度在230KB/s左右。这是传输速度方面的整体概览。
另外就是要查看TCP传输过程中的一些行为了这些行为没办法在I/O Graph上体现出来但是它们很可能就是“因”正是因为它们才有了传输速度这个“果”。
现在课程快学到一半了你应该对Expert Information很熟悉了吧这次也不例外我们来看一下Expert Information
![图片](https://static001.geekbang.org/resource/image/12/60/125d10e8d65429639yy87875a4e91360.jpg?wh=1818x296)
可见信息量还挺大的,包括了多种行为。
* Warning级别有两种分别是乱序Out-of-Order和前面报文未抓取的情形。这两者本质上都是乱序引起的现象。
* Note级别一共有四种分别是
* **Spurious重传**:这是已经被确认过的数据再一次被重传。
* **快速重传**收到3次及以上次数的重复确认后不等超时就做出的重传。
* **重传**:超时计时器到点而触发的重传,这就是有名的超时重传。
* **重复确认**:确认号重复的多个报文,重复确认是引发快速重传的原因。
* Chat级别的 **TCP Window update**这里主要是客户端上海向SSH服务端北京通告自己的接收窗口的变化是比较正常的行为。
既然还是跟传输速度相关的话题你应该还记得上节课刚深入讨论过的TCP Window Full了吧但是这里为什么一条这样的信息都没有呢
对于TCP传输来说其速度大体上是窗口/往返时间。而这里的窗口在不同情境下就有着不同的含义。我们用CWCongestion Window来指代自身的拥塞窗口而用RWReceive Window指代对端的接收窗那么
* 当RW<CW时这里的窗口就是RW
* 当RW>CW时这里的“窗口”就是CW。
对于情况1也就是对端的接收窗口小于自身拥塞窗口的情况一般意味着传输过程中没有或者很少有“拥塞”发生因而拥塞窗口能增长到较高的值。所以传输速度的上限就是对端接收窗口值决定的。这样呢也就容易在Wireshark里观察到TCP Window Full这样的现象。当然也不是每次都一定有TCP Window Full像[第9讲](https://time.geekbang.org/column/article/484923)的案例就没有。
对于情况2也就是对端的接收窗口大于自身拥塞窗口的情况这一般意味着传输过程中遇到了“拥塞”因而拥塞窗口进行了适配也就是往下调整这往往会使得拥塞窗口变得比较小。
事实上我们在第9讲也已经初步介绍了上面这些知识。我做一下搬运工同时也做一下美工对第9讲的图补充了Wireshark可能解读出来的信息供你参考。下次你在Wireshark看到TCP Window Full、Out-of-Order、或者retransmission时就可以跟这里的图对应起来协助你排查传输速度方面的问题了。
![](https://static001.geekbang.org/resource/image/7e/a3/7edf726e8a4a678996ce79067e9700a3.jpg?wh=2000x695)
在当前这个案例里,因为乱序、重传等都有大几千个,非常多,所以我们初步判断,应该就是这些事件导致传输遇到了拥塞,也进而限制了传输速度。那么到这里,我们也就正式进入拥塞机制的学习了。我会给你概括其关键部分,也会结合案例里的抓包文件,来带你获得一个更加感性的认识。
## TCP拥塞控制
为了应对错综复杂的互联网网络环境TCP使用了**拥塞控制机制**来确保传输速度和稳定性。这里你也要注意,总的来说,拥塞控制主要是通信两端自己需要实现的功能,而途中的网络设备,比如交换机、路由器等等,除了可能会发出拥塞通知报文以外,其他时候它们只管转发报文,都是不会担负更多的拥塞控制的责任的。
TCP拥塞控制主要有四个重要阶段
* 慢启动;
* 拥塞避免;
* 快速重传;
* 快速恢复。
其中还包括拥塞窗口这个概念,接下来我给你逐一介绍一下。
### 慢启动
Slow Start是指TCP传输的开始阶段是从一个相对低的速度“慢”一词的由来开始的。事实上在这个阶段拥塞窗口会以翻倍的方式增长所以从增长过程来看叫“快启动”也未尝不可。
具体来说在这个阶段每次TCP收到一个确认了数据的ACK拥塞窗口就增加一个MSS。比如下面这样
![](https://static001.geekbang.org/resource/image/10/8c/107e4ef0ec736a9b71dbe5968efa768c.jpg?wh=1728x951)
不过这里的“确认了数据的ACK”怎么理解呢
它说的是有确认数据的ACK报文而不是重复的ACK报文。比如收到2个ACK但确认号一样那第二个ACK就不是“确认了数据的ACK”了拥塞窗口不会增加2个MSS而是只增加1个MSS。
那么,这个过程什么时候终止呢?是下面两件事中有一件发生时:
* 遇到了拥塞;
* 拥塞窗口增长到慢启动阈值。
#### 慢启动阈值
慢启动阈值也有人称之为慢启动门限英文简称ssthresh。过了这个阈值拥塞窗口的增长速度立刻就放缓了变成了每过一个RTT拥塞窗口就只增长一个MSS此前是每个确认数据的ACK增长一个MSS
![图片](https://static001.geekbang.org/resource/image/b4/c9/b4a0028f02c61b1c8c1afeyya2cf44c9.jpg?wh=715x599)
比如上图的例子中假设ICW是4个MSSssthresh是32个MSS。在慢启动阶段经过一个RTT后CW扩大为8个MSS然后是16个MSS32个MSS以指数级上升。
那么到了这个阈值后TCP就进入了**拥塞避免阶段**每过一个RTT拥塞窗口只增加一个MSS于是在图上看就又变成了一条平直的斜率比较低的直线了。
![](https://static001.geekbang.org/resource/image/b2/f2/b2ebefaeb3070dfdffd7fe024b0dc6f2.jpg?wh=1663x963)
那么,如果拥塞窗口正好等于慢启动阈值,发送方应该选择继续慢启动过程(指数性增长),还是拥塞避免过程(线性增长)呢? [RFC5681](https://datatracker.ietf.org/doc/html/rfc5681) 的规定是“没有规定”,两种都可以。
#### 间隔确认
这里有个情况必须要提一下。很多TCP实现里比如Windows系统确认报文是这样工作的如果收到连续多个报文确认报文是一个隔一个回复。也就是
* 收到1、2对2进行确认
* 收到3、4对4进行确认。
比如就在这个案例里,我们很容易就发现有这种隔一个报文再确认的现象:
![图片](https://static001.geekbang.org/resource/image/09/4d/090550397c591a01a4afeb7b1965354d.jpg?wh=920x184)
上图中我们选中了17号报文Wireshark自动找到了被它确认的16号报文也在它的左边打上了一个小小的勾。当然我们也可以通过对比NextSeq和ACK来找到这种关系。我在图中就用红框和箭头找到了这里的3对TCP确认关系。
这样的间隔ACK可能会使得拥塞窗口的增长速度比每次都ACK要更低一些。
### 拥塞窗口
Congestion Window缩写是CWND或者CW。拥塞窗口是不是操作系统全局统一的配置呢其实这是比较常见的误解。拥塞窗口是每个连接分开维护的比如同一个主机有两个TCP连接在传输数据的话那么这两个连接就各自维护自己的拥塞窗口比如一个很大而一个很小都没有关系。
下图中我用CW指代拥塞窗口图中CW1到CW8都是各自不同、独立维护的拥塞窗口
![](https://static001.geekbang.org/resource/image/26/b1/267cfac4b58d69ef8a4e8568ca8d34b1.jpg?wh=2000x836)
这里还有一个子概念很重要,叫**初始拥塞窗口**英文是Initial Congestion Window或者Initial Window缩写为ICW或者IW
在Linux内核3.0以前初始拥塞窗口的大小比较小在2到4个MSS。2010年谷歌[提出](https://datatracker.ietf.org/doc/html/draft-hkchu-tcpm-initcwnd-01)为了充分利用现代互联网的传输能力Linux应该把ICW从2~4个MSS提升到10个MSS。这也被应用到了Linux内核3.0版本及以后的版本中比如在include/net/tcp.h中就定义了TCP\_INIT\_CWND的值为10。
```plain
/* TCP initial congestion window as per rfc6928 */
#define TCP_INIT_CWND 10
```
前面刚介绍过在慢启动阶段每过一个RTT拥塞窗口就翻倍。那么不同的ICW就会造成不同的传输速度比如
![](https://static001.geekbang.org/resource/image/31/a2/3114816492a2e9d6da4074f9fd7541a2.jpg?wh=2000x591)
看着ICW的变迁我真的就觉得很多知识是相通的。还记得我们在[第3讲](https://time.geekbang.org/column/article/479163)学习TCP握手的时候介绍的Window Scale概念吗为什么已经有Window字段设计者们还要创造Window Scale呢本质原因还是互联网发展很快原先设计的Window不够用了。
那么现在ICW的增加也是如此既然互联网条件好了那么多咱就不要过于谨慎了吧天地大得很上来就迈大点的步子后面跑起来就快上加快了
### 拥塞避免
前面我说过传输过了慢启动阈值ssthresh之后就进入了**拥塞避免**阶段。这个阶段的特征是“**和性增长乘性降低**”英文是Addictive increase/mutiplicative decrease缩写为AIMD。它也翻自英文怪不得这中文念起来略有不顺特别是“和性增长”。其实说是“佛系增长”也许更容易理解吧因为增长很慢挺佛系的。
那么因为AIMD的关系每一个RTT里拥塞窗口只增长一个MSS所以这个阶段的拥塞窗口的增长是线性的。直到探测到拥塞然后拥塞窗口就要往下降。这个下降是直接减半的所以叫**乘性降低**。我画了一个示意图,给你做参考:
![](https://static001.geekbang.org/resource/image/1b/fc/1b8d403aace12f2742f6054078e273fc.jpg?wh=2000x1125)
当然,图中的第二个拥塞点比第一个低只是一种可能的情况,现实场景里什么情况都可能有,因为网络状况本身就是动态变化的。
### 窗口和MSS的关系
本来这也属于拥塞算法相关的知识点但因为确实是比较常见的误区所以我在这里单独拎出来介绍一下。首先窗口一般比MSS大而且大很多可能会有个别同学以为“MSS就是窗口最大值”。其实这是反过来的窗口值一般比MSS大很多相当于就是MSS的某个倍数比如2倍、10倍、50倍等等。
MSS是有确定上限的我在前面课程里都多次提到过MSS一般为1460当然根据实际情况也经常会有更低的值。比如开启TCP timestamp等Option的话肯定要相应地从1460字节里扣去这部分字节数这样的话MSS就会低于1460比如可能是1440字节。
你可以理解为:**窗口就是n个MSS**。
> 补充确切来说窗口的单位是字节数所以也经常不是MSS的整数倍这也都是正常的。
### 快速重传
TCP每发送一个报文就启动一个超时计时器。如果在限定时间内没收到这个报文的确认那么发送方就会认为这个报文已经在网络上丢失了于是需要重传这个报文这种形式叫做**超时重传**。
一般来说TCP的最小超时重传时间为200ms。这样的超时重传的机制虽然解决了丢包的问题但也带来了一个新的问题如果每次丢包都要等200ms或者更长时间那应用不是就不能及时处理了吗特别是对于有些时间敏感型的应用来说影响更为严重。
所以TCP会用另外一种方式来解决超时重传带来的时间空耗的问题就是用**快速重传**。在这个机制里一旦发送方收到3次重复确认加上第一次确认就一共是4次就不用等超时计时器了直接重传这个报文。
### 快速恢复
这是TCP Reno算法引入的一个阶段它是跟随快速重传一起工作的。跟之前的“慢启动->拥塞避免->慢启动->拥塞避免”这种做法不同的是,在遇到拥塞点之后,通过快速重传,就不再进入慢启动,**而是从这个减半的拥塞窗口开始**,保持跟拥塞避免一样的线性增长,直到遇到下一个拥塞点。你可以参考下面的图片来理解,橙色线就是快速恢复阶段:
![](https://static001.geekbang.org/resource/image/34/54/34cf2de4yy170ayyc8cd107efeacbb54.jpg?wh=2000x1125)
那么,是不是有了快速重传和快速恢复,传输过程中都不用反复进入慢启动了呢?其实并不是这样。如果遇到超时,也一样要回到慢启动阶段,重新开始。
## 回到案例
好了,拥塞控制相关的知识点告一段落。这里,正好我们结合实际案例,来学习一下这部分知识。
在这次从北京传输文件到上海的过程中有没有慢启动呢要查看这个信息用Wireshark的TCP Stream Graphs再合适不过了。我们打开Statistics菜单下的TCP Stream Graphs -> Time Sequence (Stevens),得到下图:
![图片](https://static001.geekbang.org/resource/image/ec/9f/ece79da2cf0f158bb5d7f5a322732e9f.jpg?wh=849x767)
这里的斜率还是比较平稳的说明虽然有很多乱序和重传但整体速度还算稳定。当然如果我们放大这条线就能看到不同的景象了。比如放大到前10秒明显曲线的波动幅度加大了不少
![图片](https://static001.geekbang.org/resource/image/f9/be/f99c0dd7032fda26942946d028e19abe.jpg?wh=853x765)
我们可以看到在起点的时候这条曲线的斜率是很高的过了0.5秒以后开始平缓然后到了2.5秒开始又陡峭了,然后大体上在循环着这个过程。这样看来,你可能会想:“陡的部分都是在慢启动吗?”
整体来说,确实可以这么理解。陡的部分是慢启动过程,所以斜率比较高;缓的部分是拥塞避免阶段,斜率比较低一些。
所以我们也发现,**慢启动不是只在TCP连接启动时候发生而是可能在传输过程中发生多次**。前面的示意图里就显示一旦拥塞避免阶段探测到了拥塞TCP还是会回到慢启动过程只是这次的慢启动阈值跟之前的不同。然后如果多次遇到拥塞就会重复这个过程直到传输结束。
而要查看最初的慢启动的过程最初的慢启动也称为冷启动Cold Start还得继续放大。比如下图中我们在0~0.4秒区间内,才终于明显找到它了。
![图片](https://static001.geekbang.org/resource/image/9c/30/9cf144995d235700b3546a31f3341930.jpg?wh=791x798)
这里我用红色方框标注出了每一次往返时间RTT那么在每个RTT内22端口都连续发送了很多报文给到客户端每次报文的序列号都是图上的一个点。显然每个RTT后发送的报文数量都比前一轮RTT里发送的更多。
那你可能会有疑问:为什么这里的慢启动,并没有严格按照“翻倍”这样的原则来进行,比如说第二个红框的高度应该是第一个红框高度的两倍,但为什么图上并不是这样呢?
我个人的理解是这样的在慢启动阶段并不一定是“每次RTT就翻倍”也可能会比翻倍低一些。这是因为**在慢启动阶段TCP每收到一个ACK拥塞窗口就增加一个MSS**。假设初始拥塞窗口是2 MSS发送2个数据报文后
* 收到1个ACK也就是间隔确认那么在这次RTT内拥塞窗口就变成了3 MSS这是“不翻倍”的。
* 收到2个ACK那么在这次RTT内拥塞窗口就变成了4 MSS这是“翻倍”的。
然后我们再看一下拥塞。在这个Graph中也明显有几个点出现了异常。因为Y轴代表序列号那么这些低处的点位**它们的序列号自然就是比前一个更低(也就是更小)的值,说明这些是老的报文再次发了出去,所以可以判断为是重传**。
![图片](https://static001.geekbang.org/resource/image/b1/ea/b1f5314f1616ffb8b04497f9e0933eea.jpg?wh=789x658)
比如我们选中最下面这个点。这里的点都比较小,定位起来稍麻烦一些。你如果没有一点外科医生的手艺,那还是多一些耐心吧,慢慢移动鼠标才能定位到它。
![图片](https://static001.geekbang.org/resource/image/2d/b2/2dbce0ed33a895dd6b327938a43d50b2.jpg?wh=1153x889)
定位到这个点之后发现它是201号报文。在主窗口里我们可以看到201号报文是一个TCP Fast Retransmission也就是快速重传报文。那么通过这个重传TCP拥塞控制机制就感知到了拥塞然后进入了拥塞避免阶段。
好了,拥塞的知识点介绍得差不多了,案例也快结束了。那么案例的结论又是什么呢?
其实就是**专线上的限速设置失误**造成的。这个限速应该是通过网络硬件设备完成的,它单位时间内只允许一定字节数的报文通过,如果超过限制,就会丢弃这些报文。
现在我们学习了TCP拥塞控制机制应该已经明白了丢包对于发送端来说就是“拥塞”然后它就根据拥塞控制机制主动进入拥塞避免阶段以确保传输速度不至于大面积丢包。事实上就自动降低了速率达到了我们想要的“限速”的效果。
我们的同事把这个限速设置值给搞错了,并不是客户购买的那个带宽值。修正之后就解决了问题。
虽然说这个排查很简单不过通过这些抓包文件的拆解分析还是可以对我们理解TCP拥塞控制的机制有很大的帮助。
## 实验一下
拥塞控制机制对TCP传输十分重要技术细节也很复杂所以是由内核实现的。这也是内核实现TCP栈的巨大优势应用程序可以集中于业务逻辑而不需要操心传输和拥塞这种底层细节了。
那么这是不是意味着TCP拥塞这个东西有点“高冷”咱们也就看看评论一下好像啥都做不了
并不是,接下来的实验,就是可以改变一些拥塞控制行为的。
### 实验1修改初始拥塞窗口
有些时候我们也有修改拥塞行为的需求。比如修改初始拥塞窗口ICW。这样当你认为某条链路网络状况比较糟糕用更低的ICW更合理时就可以这么做。
在Linux操作系统上修改初始拥塞窗口的方法是这样的
* 运行`ip route`命令找到当前的路由条目把整行都进行复制记为item
```plain
$ ip route
default via 10.0.2.2 dev enp0s3 proto dhcp src 10.0.2.15 metric 100
```
* 运行`ip route change item initcwnd n`把路由项item的初始拥塞窗口修改为n比如改为2
```plain
$sudo ip route change default via 10.0.2.2 dev enp0s3 proto dhcp src 10.0.2.15 metric 100 initcwnd 2
```
### 实验2改变TCP拥塞控制算法
有时候我们还需要调整TCP拥塞控制算法。在Linux里我们可以通过 **sysctl命令**查看或者修改这个算法。比如Linux默认是用cubic算法那你可以运行下面这条命令看看是不是这样
```plain
$ sysctl net.ipv4.tcp_congestion_control
net.ipv4.tcp_congestion_control = cubic
```
那如果你想用最新的BBR算法该怎么做呢如果内核大于4.9Linux里就已经默认带有BBR了。执行下面的命令即可
```plain
$ sudo sysctl net.ipv4.tcp_congestion_control=bbr #配置拥塞算法为BBR
net.ipv4.tcp_congestion_control = bbr
$ sudo sysctl net.core.default_qdisc=fq #调整缓存队列算法
net.core.default_qdisc = fq
```
> 补充:把这些配置写入到/etc/sysctl.conf即使机器重启配置也会保持不变。
TCP BBR拥塞控制算法是Google于2016年提出的新的算法。它的开发背景是当今网络设备的缓存越来越大导致丢包这个行为不像以前缓存小的时代那么频繁但是报文延迟的问题比以前严重了。所以要更加准确地探测拥塞我们应该更多地关注延迟并基于延迟的变化作出拥塞窗口的调整。
## 小结
这节课我们学习了TCP传输中非常核心的一块内容拥塞控制。拥塞控制的实现主要依靠这几个环节。
* **慢启动**每收到一个ACK拥塞窗口CW增加一个MSS。
* **拥塞避免**策略是“和性增长乘性降低”每一个RTTCW增加一个MSS。
* **快速重传**接收到3次或者以上的重复确认后直接重传这个丢失的报文。
* **快速恢复**:结合快速重传,在遇到拥塞点后,跳过慢启动阶段,进入线性增长。
另外我们也复习了拥塞窗口CW和接收窗口RW是如何决定了传输速度上限的简单来说
* 当RW<CW时速度由RW决定
* 当RW>CW时速度由CW决定。
然后我们也知道了拥塞窗口CW是每条连接分开各自维护的以及初始拥塞窗口ICW的概念并且知道从Linux 3.0内核开始ICW已经提升到10个MSS。
在Wireshark使用技巧上你也要清楚如何用 **TCP Stream Graphs** 的Time sequence (Stevens)小工具,来观察慢启动和拥塞避免等现象,包括其中发生的快速重传等行为,都可以在图上看到,这非常有利于你的排查工作。
除此之外,我们也可以对拥塞控制做一些调整:
* 用`ip route change`命令,调整某个网卡接口的初始拥塞窗口。
* 用`sysctl net.ipv4.tcp_congestion_control`命令,查看和修改内核使用的拥塞控制算法。
## 思考题
你在工作中有没有遇到拥塞引起的问题,或者有没有在抓包分析过程中,观察到过拥塞现象呢?欢迎在留言区分享你的经验,我们一同成长、进步。
## 附件
抓包示例文件:[https://gitee.com/steelvictor/network-analysis/tree/master/11](https://gitee.com/steelvictor/network-analysis/tree/master/11)