mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-16 14:13:46 +08:00
del
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
<audio id="audio" title="11 基础篇 | TCP连接的建立和断开受哪些系统配置影响?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/13/28/1322c2010ab477dc83599f02e1885e28.mp3"></audio>
|
||||
|
||||
你好,我是邵亚方。
|
||||
|
||||
如果你做过Linux上面网络相关的开发,或者分析过Linux网络相关的问题,那你肯定吐槽过Linux系统里面让人眼花缭乱的各种配置项,应该也被下面这些问题困扰过:
|
||||
|
||||
- Client为什么无法和Server建立连接呢?
|
||||
- 三次握手都完成了,为什么会收到Server的reset呢?
|
||||
- 建立TCP连接怎么会消耗这么多时间?
|
||||
- 系统中为什么会有这么多处于time-wait的连接?该这么处理?
|
||||
- 系统中为什么会有这么多close-wait的连接?
|
||||
- 针对我的业务场景,这么多的网络配置项,应该要怎么配置呢?
|
||||
- ……
|
||||
|
||||
因为网络这一块涉及到的场景太多了,Linux内核需要去处理各种各样的网络场景,不同网络场景的处理策略也会有所不同。而Linux内核的默认网络配置可能未必会适用我们的场景,这就可能导致我们的业务出现一些莫名其妙的行为。
|
||||
|
||||
所以,要想让业务行为符合预期,你需要了解Linux的相关网络配置,让这些配置更加适用于你的业务。Linux中的网络配置项是非常多的,为了让你更好地了解它们,我就以最常用的TCP/IP协议为例,从一个网络连接是如何建立起来的以及如何断开的来开始讲起。
|
||||
|
||||
## TCP连接的建立过程会受哪些配置项的影响?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/af/44/afc841ee3822fyye3ec186b28ee93744.jpg" alt="" title="TCP建连过程">
|
||||
|
||||
上图就是一个TCP连接的建立过程。TCP连接的建立是一个从Client侧调用connect(),到Server侧accept()成功返回的过程。你可以看到,在整个TCP建立连接的过程中,各个行为都有配置选项来进行控制。
|
||||
|
||||
Client调用connect()后,Linux内核就开始进行三次握手。
|
||||
|
||||
首先Client会给Server发送一个SYN包,但是该SYN包可能会在传输过程中丢失,或者因为其他原因导致Server无法处理,此时Client这一侧就会触发超时重传机制。但是也不能一直重传下去,重传的次数也是有限制的,这就是tcp_syn_retries这个配置项来决定的。
|
||||
|
||||
假设tcp_syn_retries为3,那么SYN包重传的策略大致如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/01/e4/012b9bf3e59f3abd5c5588a968e354e4.jpg" alt="" title="tcp_syn_retries示意图">
|
||||
|
||||
在Client发出SYN后,如果过了1秒 ,还没有收到Server的响应,那么就会进行第一次重传;如果经过2s的时间还没有收到Server的响应,就会进行第二次重传;一直重传tcp_syn_retries次。
|
||||
|
||||
对于tcp_syn_retries为3而言,总共会重传3次,也就是说从第一次发出SYN包后,会一直等待(1 + 2 + 4 + 8)秒,如果还没有收到Server的响应,connect()就会产生ETIMEOUT的错误。
|
||||
|
||||
tcp_syn_retries的默认值是6,也就是说如果SYN一直发送失败,会在(1 + 2 + 4 + 8 + 16+ 32 + 64)秒,即127秒后产生ETIMEOUT的错误。
|
||||
|
||||
我们在生产环境上就遇到过这种情况,Server因为某些原因被下线,但是Client没有被通知到,所以Client的connect()被阻塞127s才去尝试连接一个新的Server, 这么长的超时等待时间对于应用程序而言是很难接受的。
|
||||
|
||||
**所以通常情况下,我们都会将数据中心内部服务器的tcp_syn_retries给调小,这里推荐设置为2,来减少阻塞的时间。**因为对于数据中心而言,它的网络质量是很好的,如果得不到Server的响应,很可能是Server本身出了问题。在这种情况下,Client及早地去尝试连接其他的Server会是一个比较好的选择,所以对于客户端而言,一般都会做如下调整:
|
||||
|
||||
>
|
||||
net.ipv4.tcp_syn_retries = 2
|
||||
|
||||
|
||||
有些情况下1s的阻塞时间可能都很久,所以有的时候也会将三次握手的初始超时时间从默认值1s调整为一个较小的值,比如100ms,这样整体的阻塞时间就会小很多。这也是数据中心内部经常进行一些网络优化的原因。
|
||||
|
||||
如果Server没有响应Client的SYN,除了我们刚才提到的Server已经不存在了这种情况外,还有可能是因为Server太忙没有来得及响应,或者是Server已经积压了太多的半连接(incomplete)而无法及时去处理。
|
||||
|
||||
半连接,即收到了SYN后还没有回复SYNACK的连接,Server每收到一个新的SYN包,都会创建一个半连接,然后把该半连接加入到半连接队列(syn queue)中。syn queue的长度就是tcp_max_syn_backlog这个配置项来决定的,当系统中积压的半连接个数超过了该值后,新的SYN包就会被丢弃。对于服务器而言,可能瞬间会有非常多的新建连接,所以我们可以适当地调大该值,以免SYN包被丢弃而导致Client收不到SYNACK:
|
||||
|
||||
>
|
||||
net.ipv4.tcp_max_syn_backlog = 16384
|
||||
|
||||
|
||||
**Server中积压的半连接较多,也有可能是因为有些恶意的Client在进行SYN Flood攻击**。典型的SYN Flood攻击如下:Client高频地向Server发SYN包,并且这个SYN包的源IP地址不停地变换,那么Server每次接收到一个新的SYN后,都会给它分配一个半连接,Server的SYNACK根据之前的SYN包找到的是错误的Client IP, 所以也就无法收到Client的ACK包,导致无法正确建立TCP连接,这就会让Server的半连接队列耗尽,无法响应正常的SYN包。
|
||||
|
||||
为了防止SYN Flood攻击,Linux内核引入了SYN Cookies机制。SYN Cookie的原理是什么样的呢?
|
||||
|
||||
在Server收到SYN包时,不去分配资源来保存Client的信息,而是根据这个SYN包计算出一个Cookie值,然后将Cookie记录到SYNACK包中发送出去。对于正常的连接,该Cookies值会随着Client的ACK报文被带回来。然后Server再根据这个Cookie检查这个ACK包的合法性,如果合法,才去创建新的TCP连接。通过这种处理,SYN Cookies可以防止部分SYN Flood攻击。所以对于Linux服务器而言,推荐开启SYN Cookies:
|
||||
|
||||
>
|
||||
net.ipv4.tcp_syncookies = 1
|
||||
|
||||
|
||||
Server向Client发送的SYNACK包也可能会被丢弃,或者因为某些原因而收不到Client的响应,这个时候Server也会重传SYNACK包。同样地,重传的次数也是由配置选项来控制的,该配置选项是tcp_synack_retries。
|
||||
|
||||
tcp_synack_retries的重传策略跟我们在前面讲的tcp_syn_retries是一致的,所以我们就不再画图来讲解它了。它在系统中默认是5,对于数据中心的服务器而言,通常都不需要这么大的值,推荐设置为2 :
|
||||
|
||||
>
|
||||
net.ipv4.tcp_synack_retries = 2
|
||||
|
||||
|
||||
Client在收到Serve的SYNACK包后,就会发出ACK,Server收到该ACK后,三次握手就完成了,即产生了一个TCP全连接(complete),它会被添加到全连接队列(accept queue)中。然后Server就会调用accept()来完成TCP连接的建立。
|
||||
|
||||
但是,就像半连接队列(syn queue)的长度有限制一样,全连接队列(accept queue)的长度也有限制,目的就是为了防止Server不能及时调用accept()而浪费太多的系统资源。
|
||||
|
||||
全连接队列(accept queue)的长度是由listen(sockfd, backlog)这个函数里的backlog控制的,而该backlog的最大值则是somaxconn。somaxconn在5.4之前的内核中,默认都是128(5.4开始调整为了默认4096),建议将该值适当调大一些:
|
||||
|
||||
>
|
||||
net.core.somaxconn = 16384
|
||||
|
||||
|
||||
当服务器中积压的全连接个数超过该值后,新的全连接就会被丢弃掉。Server在将新连接丢弃时,有的时候需要发送reset来通知Client,这样Client就不会再次重试了。不过,默认行为是直接丢弃不去通知Client。至于是否需要给Client发送reset,是由tcp_abort_on_overflow这个配置项来控制的,该值默认为0,即不发送reset给Client。推荐也是将该值配置为0:
|
||||
|
||||
>
|
||||
net.ipv4.tcp_abort_on_overflow = 0
|
||||
|
||||
|
||||
这是因为,Server如果来不及accept()而导致全连接队列满,这往往是由瞬间有大量新建连接请求导致的,正常情况下Server很快就能恢复,然后Client再次重试后就可以建连成功了。也就是说,将 tcp_abort_on_overflow 配置为0,给了Client一个重试的机会。当然,你可以根据你的实际情况来决定是否要使能该选项。
|
||||
|
||||
accept()成功返回后,一个新的TCP连接就建立完成了,TCP连接进入到了ESTABLISHED状态:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e0/3c/e0ea3232fccf6bba8bace54d3f5d8d3c.jpg" alt="" title="TCP状态转换">
|
||||
|
||||
上图就是从Client调用connect(),到Server侧accept()成功返回这一过程中的TCP状态转换。这些状态都可以通过netstat或者ss命令来看。至此,Client和Server两边就可以正常通信了。
|
||||
|
||||
接下来,我们看下TCP连接断开过程中会受哪些系统配置项的影响。
|
||||
|
||||
## TCP连接的断开过程会受哪些配置项的影响?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1c/cf/1cf68d3eb4f07113ba13d84124f447cf.jpg" alt="" title="TCP的四次挥手">
|
||||
|
||||
如上所示,当应用程序调用close()时,会向对端发送FIN包,然后会接收ACK;对端也会调用close()来发送FIN,然后本端也会向对端回ACK,这就是TCP的四次挥手过程。
|
||||
|
||||
首先调用close()的一侧是active close(主动关闭);而接收到对端的FIN包后再调用close()来关闭的一侧,称之为passive close(被动关闭)。在四次挥手的过程中,有三个TCP状态需要额外关注,就是上图中深红色的那三个状态:主动关闭方的FIN_WAIT_2和TIME_WAIT,以及被动关闭方的CLOSE_WAIT状态。除了CLOSE_WAIT状态外,其余两个状态都有对应的系统配置项来控制。
|
||||
|
||||
我们首先来看FIN_WAIT_2状态,TCP进入到这个状态后,如果本端迟迟收不到对端的FIN包,那就会一直处于这个状态,于是就会一直消耗系统资源。Linux为了防止这种资源的开销,设置了这个状态的超时时间tcp_fin_timeout,默认为60s,超过这个时间后就会自动销毁该连接。
|
||||
|
||||
至于本端为何迟迟收不到对端的FIN包,通常情况下都是因为对端机器出了问题,或者是因为太繁忙而不能及时close()。所以,通常我们都建议将 tcp_fin_timeout 调小一些,以尽量避免这种状态下的资源开销。对于数据中心内部的机器而言,将它调整为2s足以:
|
||||
|
||||
>
|
||||
net.ipv4.tcp_fin_timeout = 2
|
||||
|
||||
|
||||
我们再来看TIME_WAIT状态,TIME_WAIT状态存在的意义是:最后发送的这个ACK包可能会被丢弃掉或者有延迟,这样对端就会再次发送FIN包。如果不维持TIME_WAIT这个状态,那么再次收到对端的FIN包后,本端就会回一个Reset包,这可能会产生一些异常。
|
||||
|
||||
所以维持TIME_WAIT状态一段时间,可以保障TCP连接正常断开。TIME_WAIT的默认存活时间在Linux上是60s(TCP_TIMEWAIT_LEN),这个时间对于数据中心而言可能还是有些长了,所以有的时候也会修改内核做些优化来减小该值,或者将该值设置为可通过sysctl来调节。
|
||||
|
||||
TIME_WAIT状态存在这么长时间,也是对系统资源的一个浪费,所以系统也有配置项来限制该状态的最大个数,该配置选项就是tcp_max_tw_buckets。对于数据中心而言,网络是相对很稳定的,基本不会存在FIN包的异常,所以建议将该值调小一些:
|
||||
|
||||
>
|
||||
net.ipv4.tcp_max_tw_buckets = 10000
|
||||
|
||||
|
||||
Client关闭跟Server的连接后,也有可能很快再次跟Server之间建立一个新的连接,而由于TCP端口最多只有65536个,如果不去复用处于TIME_WAIT状态的连接,就可能在快速重启应用程序时,出现端口被占用而无法创建新连接的情况。所以建议你打开复用TIME_WAIT的选项:
|
||||
|
||||
>
|
||||
net.ipv4.tcp_tw_reuse = 1
|
||||
|
||||
|
||||
还有另外一个选项tcp_tw_recycle来控制TIME_WAIT状态,但是该选项是很危险的,因为它可能会引起意料不到的问题,比如可能会引起NAT环境下的丢包问题。所以建议将该选项关闭:
|
||||
|
||||
>
|
||||
net.ipv4.tcp_tw_recycle = 0
|
||||
|
||||
|
||||
因为打开该选项后引起了太多的问题,所以新版本的内核就索性删掉了这个配置选项:[tcp: remove tcp_tw_recycle.](https://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git/commit/?id=4396e46187ca5070219b81773c4e65088dac50cc)
|
||||
|
||||
对于CLOSE_WAIT状态而言,系统中没有对应的配置项。但是该状态也是一个危险信号,如果这个状态的TCP连接较多,那往往意味着应用程序有Bug,在某些条件下没有调用close()来关闭连接。我们在生产环境上就遇到过很多这类问题。所以,如果你的系统中存在很多CLOSE_WAIT状态的连接,那你最好去排查一下你的应用程序,看看哪里漏掉了close()。
|
||||
|
||||
至此,TCP四次挥手过程中需要注意的事项也讲完了。
|
||||
|
||||
好了,我们这节课就到此为止。
|
||||
|
||||
## 课堂总结
|
||||
|
||||
这节课我们讲了很多的配置项,我把这些配置项汇总到了下面这个表格里,方便你记忆:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3d/de/3d60be2523528f511dec0fbc88ce1ede.jpg" alt="">
|
||||
|
||||
当然了,有些配置项也是可以根据你的服务器负载以及CPU和内存大小来做灵活配置的,比如tcp_max_syn_backlog、somaxconn、tcp_max_tw_buckets这三项,如果你的物理内存足够大、CPU核数足够多,你可以适当地增大这些值,这些往往都是一些经验值。
|
||||
|
||||
另外,我们这堂课的目的不仅仅是为了让你去了解这些配置项,最主要的是想让你了解其背后的机制,这样你在遇到一些问题时,就可以有一个大致的分析方向。
|
||||
|
||||
## 课后作业
|
||||
|
||||
课后请你使用tcpdump这个工具来观察下TCP的三次握手和四次挥手过程,巩固今天的学习内容。欢迎在留言区分享你的看法。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
183
极客时间专栏/geek/Linux内核技术实战课/TCP重传问题/12 基础篇 | TCP收发包过程会受哪些配置项影响?.md
Normal file
183
极客时间专栏/geek/Linux内核技术实战课/TCP重传问题/12 基础篇 | TCP收发包过程会受哪些配置项影响?.md
Normal file
@@ -0,0 +1,183 @@
|
||||
<audio id="audio" title="12 基础篇 | TCP收发包过程会受哪些配置项影响?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/db/b5/db010a0a728f81ebbf5ebc81e49abab5.mp3"></audio>
|
||||
|
||||
你好,我是邵亚方。我们这节课来讲一下,TCP数据在传输过程中会受到哪些因素干扰。
|
||||
|
||||
TCP收包和发包的过程也是容易引起问题的地方。收包是指数据到达网卡再到被应用程序开始处理的过程。发包则是应用程序调用发包函数到数据包从网卡发出的过程。你应该对TCP收包和发包过程中容易引发的一些问题不会陌生,比如说:
|
||||
|
||||
- 网卡中断太多,占用太多CPU,导致业务频繁被打断;
|
||||
- 应用程序调用write()或者send()发包,怎么会发不出去呢;
|
||||
- 数据包明明已经被网卡收到了,可是应用程序为什么没收到呢;
|
||||
- 我想要调整缓冲区的大小,可是为什么不生效呢;
|
||||
- 是不是内核缓冲区满了从而引起丢包,我该怎么观察呢;
|
||||
- …
|
||||
|
||||
想要解决这些问题呢,你就需要去了解TCP的收发包过程容易受到哪些因素的影响。这个过程中涉及到很多的配置项,很多问题都是这些配置项跟业务场景不匹配导致的。
|
||||
|
||||
我们先来看下数据包的发送过程,这个过程会受到哪些配置项的影响呢?
|
||||
|
||||
## TCP数据包的发送过程会受什么影响?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5c/5e/5ce5d202b7a179829f4c9b3863b0b15e.jpg" alt="" title="TCP数据包发送过程">
|
||||
|
||||
上图就是一个简略的TCP数据包的发送过程。应用程序调用write(2)或者send(2)系列系统调用开始往外发包时,这些系统调用会把数据包从用户缓冲区拷贝到TCP发送缓冲区(TCP Send Buffer),这个TCP发送缓冲区的大小是受限制的,这里也是容易引起问题的地方。
|
||||
|
||||
TCP发送缓冲区的大小默认是受net.ipv4.tcp_wmem来控制:
|
||||
|
||||
>
|
||||
net.ipv4.tcp_wmem = 8192 65536 16777216
|
||||
|
||||
|
||||
tcp_wmem中这三个数字的含义分别为min、default、max。TCP发送缓冲区的大小会在min和max之间动态调整,初始的大小是default,这个动态调整的过程是由内核自动来做的,应用程序无法干预。自动调整的目的,是为了在尽可能少的浪费内存的情况下来满足发包的需要。
|
||||
|
||||
tcp_wmem中的max不能超过net.core.wmem_max这个配置项的值,如果超过了,TCP 发送缓冲区最大就是net.core.wmem_max。通常情况下,我们需要设置net.core.wmem_max的值大于等于net.ipv4.tcp_wmem的max:
|
||||
|
||||
>
|
||||
net.core.wmem_max = 16777216
|
||||
|
||||
|
||||
对于TCP 发送缓冲区的大小,我们需要根据服务器的负载能力来灵活调整。通常情况下我们需要调大它们的默认值,我上面列出的 tcp_wmem 的 min、default、max 这几组数值就是调大后的值,也是我们在生产环境中配置的值。
|
||||
|
||||
我之所以将这几个值给调大,是因为我们在生产环境中遇到过TCP发送缓冲区太小,导致业务延迟很大的问题,这类问题也是可以使用systemtap之类的工具在内核里面打点来进行观察的(观察sk_stream_wait_memory这个事件):
|
||||
|
||||
```
|
||||
# sndbuf_overflow.stp
|
||||
# Usage :
|
||||
# $ stap sndbuf_overflow.stp
|
||||
probe kernel.function("sk_stream_wait_memory")
|
||||
{
|
||||
printf("%d %s TCP send buffer overflow\n",
|
||||
pid(), execname())
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
如果你可以观察到sk_stream_wait_memory这个事件,就意味着TCP发送缓冲区太小了,你需要继续去调大wmem_max和tcp_wmem:max的值了。
|
||||
|
||||
应用程序有的时候会很明确地知道自己发送多大的数据,需要多大的TCP发送缓冲区,这个时候就可以通过setsockopt(2)里的SO_SNDBUF来设置固定的缓冲区大小。一旦进行了这种设置后,tcp_wmem就会失效,而且这个缓冲区大小设置的是固定值,内核也不会对它进行动态调整。
|
||||
|
||||
但是,SO_SNDBUF设置的最大值不能超过net.core.wmem_max,如果超过了该值,内核会把它强制设置为net.core.wmem_max。所以,如果你想要设置SO_SNDBUF,一定要确认好net.core.wmem_max是否满足需求,否则你的设置可能发挥不了作用。通常情况下,我们都不会通过SO_SNDBUF来设置TCP发送缓冲区的大小,而是使用内核设置的tcp_wmem,因为如果SO_SNDBUF设置得太大就会浪费内存,设置得太小又会引起缓冲区不足的问题。
|
||||
|
||||
另外,如果你关注过Linux的最新技术动态,你一定听说过eBPF。你也可以通过eBPF来设置SO_SNDBUF和SO_RCVBUF,进而分别设置TCP发送缓冲区和TCP接收缓冲区的大小。同样地,使用eBPF来设置这两个缓冲区时,也不能超过wmem_max和rmem_max。不过eBPF在一开始增加设置缓冲区大小的特性时并未考虑过最大值的限制,我在使用的过程中发现这里存在问题,就给社区提交了一个PATCH把它给修复了。你感兴趣的话可以看下这个链接:[bpf: sock recvbuff must be limited by rmem_max in bpf_setsockopt()](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?h=v5.8&id=c9e4576743eeda8d24dedc164d65b78877f9a98c)。
|
||||
|
||||
tcp_wmem以及wmem_max的大小设置都是针对单个TCP连接的,这两个值的单位都是Byte(字节)。系统中可能会存在非常多的TCP连接,如果TCP连接太多,就可能导致内存耗尽。因此,所有TCP连接消耗的总内存也有限制:
|
||||
|
||||
>
|
||||
net.ipv4.tcp_mem = 8388608 12582912 16777216
|
||||
|
||||
|
||||
我们通常也会把这个配置项给调大。与前两个选项不同的是,该选项中这些值的单位是Page(页数),也就是4K。它也有3个值:min、pressure、max。当所有TCP连接消耗的内存总和达到max后,也会因达到限制而无法再往外发包。
|
||||
|
||||
因tcp_mem达到限制而无法发包或者产生抖动的问题,我们也是可以观测到的。为了方便地观测这类问题,Linux内核里面预置了静态观测点:sock_exceed_buf_limit。不过,这个观测点一开始只是用来观察TCP接收时遇到的缓冲区不足的问题,不能观察TCP发送时遇到的缓冲区不足的问题。后来,我提交了一个patch做了改进,使得它也可以用来观察TCP发送时缓冲区不足的问题:[net: expose sk wmem in sock_exceed_buf_limit tracepoint](https://github.com/torvalds/linux/commit/563e0bb0dc74b3ca888e24f8c08f0239fe4016b0) ,观察时你只需要打开tracepiont(需要4.16+的内核版本):
|
||||
|
||||
>
|
||||
$ echo 1 > /sys/kernel/debug/tracing/events/sock/sock_exceed_buf_limit/enable
|
||||
|
||||
|
||||
然后去看是否有该事件发生:
|
||||
|
||||
>
|
||||
$ cat /sys/kernel/debug/tracing/trace_pipe
|
||||
|
||||
|
||||
如果有日志输出(即发生了该事件),就意味着你需要调大tcp_mem了,或者是需要断开一些TCP连接了。
|
||||
|
||||
TCP层处理完数据包后,就继续往下来到了IP层。IP层这里容易触发问题的地方是net.ipv4.ip_local_port_range这个配置选项,它是指和其他服务器建立IP连接时本地端口(local port)的范围。我们在生产环境中就遇到过默认的端口范围太小,以致于无法创建新连接的问题。所以通常情况下,我们都会扩大默认的端口范围:
|
||||
|
||||
>
|
||||
net.ipv4.ip_local_port_range = 1024 65535
|
||||
|
||||
|
||||
为了能够对TCP/IP数据流进行流控,Linux内核在IP层实现了qdisc(排队规则)。我们平时用到的TC就是基于qdisc的流控工具。qdisc的队列长度是我们用ifconfig来看到的txqueuelen,我们在生产环境中也遇到过因为txqueuelen太小导致数据包被丢弃的情况,这类问题可以通过下面这个命令来观察:
|
||||
|
||||
>
|
||||
<p>$ ip -s -s link ls dev eth0<br>
|
||||
…<br>
|
||||
TX: bytes packets errors dropped carrier collsns<br>
|
||||
3263284 25060 0 0 0 0</p>
|
||||
|
||||
|
||||
如果观察到dropped这一项不为0,那就有可能是txqueuelen太小导致的。当遇到这种情况时,你就需要增大该值了,比如增加eth0这个网络接口的txqueuelen:
|
||||
|
||||
>
|
||||
$ ifconfig eth0 txqueuelen 2000
|
||||
|
||||
|
||||
或者使用ip这个工具:
|
||||
|
||||
>
|
||||
$ ip link set eth0 txqueuelen 2000
|
||||
|
||||
|
||||
在调整了txqueuelen的值后,你需要持续观察是否可以缓解丢包的问题,这也便于你将它调整到一个合适的值。
|
||||
|
||||
Linux系统默认的qdisc为pfifo_fast(先进先出),通常情况下我们无需调整它。如果你想使用[TCP BBR](https://github.com/google/bbr)来改善TCP拥塞控制的话,那就需要将它调整为fq(fair queue, 公平队列):
|
||||
|
||||
>
|
||||
net.core.default_qdisc = fq
|
||||
|
||||
|
||||
经过IP层后,数据包再往下就会进入到网卡了,然后通过网卡发送出去。至此,你需要发送出去的数据就走完了TCP/IP协议栈,然后正常地发送给对端了。
|
||||
|
||||
接下来,我们来看下数据包是怎样收上来的,以及在接收的过程中会受哪些配置项的影响。
|
||||
|
||||
## TCP数据包的接收过程会受什么影响?
|
||||
|
||||
TCP数据包的接收过程,同样也可以用一张图来简单表示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9c/56/9ca34a53abf57125334e0278edd10356.jpg" alt="" title="TCP数据包接收过程">
|
||||
|
||||
从上图可以看出,TCP数据包的接收流程在整体上与发送流程类似,只是方向是相反的。数据包到达网卡后,就会触发中断(IRQ)来告诉CPU读取这个数据包。但是在高性能网络场景下,数据包的数量会非常大,如果每来一个数据包都要产生一个中断,那CPU的处理效率就会大打折扣,所以就产生了NAPI(New API)这种机制让CPU一次性地去轮询(poll)多个数据包,以批量处理的方式来提升效率,降低网卡中断带来的性能开销。
|
||||
|
||||
那在poll的过程中,一次可以poll多少个呢?这个poll的个数可以通过sysctl选项来控制:
|
||||
|
||||
>
|
||||
net.core.netdev_budget = 600
|
||||
|
||||
|
||||
该控制选项的默认值是300,在网络吞吐量较大的场景中,我们可以适当地增大该值,比如增大到600。增大该值可以一次性地处理更多的数据包。但是这种调整也是有缺陷的,因为这会导致CPU在这里poll的时间增加,如果系统中运行的任务很多的话,其他任务的调度延迟就会增加。
|
||||
|
||||
接下来继续看TCP数据包的接收过程。我们刚才提到,数据包到达网卡后会触发CPU去poll数据包,这些poll的数据包紧接着就会到达IP层去处理,然后再达到TCP层,这时就会面对另外一个很容易引发问题的地方了:TCP Receive Buffer(TCP接收缓冲区)。
|
||||
|
||||
与 TCP发送缓冲区类似,TCP接收缓冲区的大小也是受控制的。通常情况下,默认都是使用tcp_rmem来控制缓冲区的大小。同样地,我们也会适当地增大这几个值的默认值,来获取更好的网络性能,调整为如下数值:
|
||||
|
||||
>
|
||||
net.ipv4.tcp_rmem = 8192 87380 16777216
|
||||
|
||||
|
||||
它也有3个字段:min、default、max。TCP接收缓冲区大小也是在min和max之间动态调整 ,不过跟发送缓冲区不同的是,这个动态调整是可以通过控制选项来关闭的,这个选项是tcp_moderate_rcvbuf 。通常我们都是打开它,这也是它的默认值:
|
||||
|
||||
>
|
||||
net.ipv4.tcp_moderate_rcvbuf = 1
|
||||
|
||||
|
||||
之所以接收缓冲区有选项可以控制自动调节,而发送缓冲区没有,那是因为TCP接收缓冲区会直接影响TCP拥塞控制,进而影响到对端的发包,所以使用该控制选项可以更加灵活地控制对端的发包行为。
|
||||
|
||||
除了tcp_moderate_rcvbuf 可以控制TCP接收缓冲区的动态调节外,也可以通过setsockopt()中的配置选项SO_RCVBUF来控制,这与TCP发送缓冲区是类似的。如果应用程序设置了SO_RCVBUF这个标记,那么TCP接收缓冲区的动态调整就是关闭,即使tcp_moderate_rcvbuf为1,接收缓冲区的大小始终就为设置的SO_RCVBUF这个值。
|
||||
|
||||
也就是说,只有在tcp_moderate_rcvbuf为1,并且应用程序没有通过SO_RCVBUF来配置缓冲区大小的情况下,TCP接收缓冲区才会动态调节。
|
||||
|
||||
同样地,与TCP发送缓冲区类似,SO_RCVBUF设置的值最大也不能超过net.core.rmem_max。通常情况下,我们也需要设置net.core.rmem_max的值大于等于net.ipv4.tcp_rmem的max:
|
||||
|
||||
>
|
||||
net.core.rmem_max = 16777216
|
||||
|
||||
|
||||
我们在生产环境中也遇到过,因达到了TCP接收缓冲区的限制而引发的丢包问题。但是这类问题不是那么好追踪的,没有一种很直观地追踪这种行为的方式,所以我便在我们的内核里添加了针对这种行为的统计。
|
||||
|
||||
为了让使用Linux内核的人都能很好地观察这个行为,我也把我们的实践贡献给了Linux内核社区,具体可以看这个commit:[tcp: add new SNMP counter for drops when try to queue in rcv queue](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?h=v5.9-rc1&id=ea5d0c32498e1a08ff5f3dbeafa4d74895851b0d)。使用这个SNMP计数,我们就可以很方便地通过netstat查看,系统中是否存在因为TCP接收缓冲区不足而引发的丢包。
|
||||
|
||||
不过,该方法还是存在一些局限:如果我们想要查看是哪个TCP连接在丢包,那么这种方式就不行了,这个时候我们就需要去借助其他一些更专业的trace工具,比如eBPF来达到我们的目的。
|
||||
|
||||
## 课堂总结
|
||||
|
||||
好了,这节课就讲到这里,我们简单回顾一下。TCP/IP是一个很复杂的协议栈,它的数据包收发过程也是很复杂的,我们这节课只是重点围绕这个过程中最容易引发问题的地方来讲述的。我们刚才提到的那些配置选项都很容易在生产环境中引发问题,并且也是我们针对高性能网络进行调优时必须要去考虑的。我把这些配置项也总结为了一个表格,方便你来查看:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8d/9b/8d4ba95a95684004f271677f600cda9b.jpg" alt="">
|
||||
|
||||
这些值都需要根据你的业务场景来做灵活的调整,当你不知道针对你的业务该如何调整时,你最好去咨询更加专业的人员,或者一边调整一边观察系统以及业务行为的变化。
|
||||
|
||||
## 课后作业
|
||||
|
||||
我们这节课中有两张图,分别是TCP数据包的发送过程 和 TCP数据包的接收过程,我们可以看到,在TCP发送过程中使用到了qdisc,但是在接收过程中没有使用它,请问是为什么?我们可以在接收过程中也使用qdisc吗?欢迎你在留言区与我讨论。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
@@ -0,0 +1,138 @@
|
||||
<audio id="audio" title="13 案例篇 | TCP拥塞控制是如何导致业务性能抖动的?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a5/28/a5691c482726b20a11ce541ea913af28.mp3"></audio>
|
||||
|
||||
你好,我是邵亚方。这节课我来跟大家分享TCP拥塞控制与业务性能抖动之间的关系。
|
||||
|
||||
TCP拥塞控制是TCP协议的核心,而且是一个非常复杂的过程。如果你不了解TCP拥塞控制的话,那么就相当于不理解TCP协议。这节课的目的是通过一些案例,介绍在TCP拥塞控制中我们要避免踩的一些坑,以及针对TCP性能调优时需要注意的一些点。
|
||||
|
||||
因为在TCP传输过程中引起问题的案例有很多,所以我不会把这些案例拿过来具体去一步步分析,而是希望能够对这些案例做一层抽象,把这些案例和具体的知识点结合起来,这样会更有系统性。并且,在你明白了这些知识点后,案例的分析过程就相对简单了。
|
||||
|
||||
我们在前两节课([第11讲](https://time.geekbang.org/column/article/284912)和[第12讲](https://time.geekbang.org/column/article/285816))中讲述了单机维度可能需要注意的问题点。但是,网络传输是一个更加复杂的过程,这中间涉及的问题会更多,而且更加不好分析。相信很多人都有过这样的经历:
|
||||
|
||||
- 等电梯时和别人聊着微信,进入电梯后微信消息就发不出去了;
|
||||
- 和室友共享同一个网络,当玩网络游戏玩得正开心时,游戏忽然卡得很厉害,原来是室友在下载电影;
|
||||
- 使用ftp上传一个文件到服务器上,没想到要上传很久;
|
||||
- ……
|
||||
|
||||
在这些问题中,TCP的拥塞控制就在发挥着作用。
|
||||
|
||||
## TCP拥塞控制是如何对业务网络性能产生影响的 ?
|
||||
|
||||
我们先来看下TCP拥塞控制的大致原理。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5c/3c/5c4504d70ce3abc939yyca54780dd43c.jpg" alt="" title="TCP拥塞控制">
|
||||
|
||||
上图就是TCP拥塞控制的简单图示,它大致分为四个阶段。
|
||||
|
||||
#### 1. 慢启动
|
||||
|
||||
TCP连接建立好后,发送方就进入慢速启动阶段,然后逐渐地增大发包数量(TCP Segments)。这个阶段每经过一个RTT(round-trip time),发包数量就会翻倍。如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/05/4d/0534ce8d1e3a09a1def9c27e387eb64d.jpg" alt="" title="TCP Slow Start示意图">
|
||||
|
||||
初始发送数据包的数量是由init_cwnd(初始拥塞窗口)来决定的,该值在Linux内核中被设置为10(TCP_INIT_CWND),这是由Google的研究人员总结出的一个经验值,这个经验值也被写入了[RFC6928](https://tools.ietf.org/html/rfc6928)。并且,Linux内核在2.6.38版本中也将它从默认值3修改为了Google建议的10,你感兴趣的话可以看下这个commit: [tcp: Increase the initial congestion window to 10](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=442b9635c569fef038d5367a7acd906db4677ae1)。
|
||||
|
||||
增大init_cwnd可以显著地提升网络性能,因为这样在初始阶段就可以一次性发送很多TCP Segments,更加细节性的原因你可以参考[RFC6928](https://tools.ietf.org/html/rfc6928)的解释。
|
||||
|
||||
如果你的内核版本比较老(低于CentOS-6的内核版本),那不妨考虑增加init_cwnd到10。如果你想要把它增加到一个更大的值,也不是不可以,但是你需要根据你的网络状况多做一些实验,从而得到一个较为理想的值。因为如果初始拥塞窗口设置得过大的话,可能会引起很高的TCP重传率。当然,你也可以通过ip route的方式来更加灵活地调整该值,甚至将它配置为一个sysctl控制项。
|
||||
|
||||
增大init_cwnd的值对于提升短连接的网络性能会很有效,特别是数据量在慢启动阶段就能发送完的短连接,比如针对http这种服务,http的短连接请求数据量一般不大,通常在慢启动阶段就能传输完,这些都可以通过tcpdump来进行观察。
|
||||
|
||||
在慢启动阶段,当拥塞窗口(cwnd)增大到一个阈值( ssthresh,慢启动阈值)后,TCP拥塞控制就进入了下一个阶段:拥塞避免(Congestion Avoidance)。
|
||||
|
||||
#### 2.拥塞避免
|
||||
|
||||
在这个阶段cwnd不再成倍增加,而是一个RTT增加1,即缓慢地增加cwnd,以防止网络出现拥塞。网络出现拥塞是难以避免的,由于网络链路的复杂性,甚至会出现乱序(Out of Order)报文。乱序报文产生原因之一如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0c/99/0c2ce093d74a1dc76f39b7cbdd386699.jpg" alt="" title="TCP乱序报文">
|
||||
|
||||
在上图中,发送端一次性发送了4个TCP segments,但是第2个segment在传输过程中被丢弃掉了,那么接收方就接收不到该segment了。然而第3个TCP segment和第4个TCP segment能够被接收到,此时3和4就属于乱序报文,它们会被加入到接收端的ofo queue(乱序队列)里。
|
||||
|
||||
丢包这类问题在移动网络环境中比较容易出现,特别是在一个网络状况不好的环境中,比如在电梯里丢包率就会很高,而丢包率高就会导致网络响应特别慢。在数据中心内部的服务上很少会有数据包在网络链路中被丢弃的情况,我说的这类丢包问题主要是针对网关服务这种和外部网络有连接的服务上。
|
||||
|
||||
针对我们的网关服务,我们自己也做过一些TCP单边优化工作,主要是优化Cubic拥塞控制算法,以缓解丢包引起的网络性能下降问题。另外,Google前几年开源的一个新的[拥塞控制算法BBR](https://github.com/google/bbr)在理论上也可以很好地缓解TCP丢包问题,但是在我们的实践中,BBR的效果并不好,因此我们最终也没有使用它。
|
||||
|
||||
我们再回到上面这张图,因为接收端没有接收到第2个segment,因此接收端每次收到一个新的segment后都会去ack第2个segment,即ack 17。紧接着,发送端就会接收到三个相同的ack(ack 17)。连续出现了3个响应的ack后,发送端会据此判断数据包出现了丢失,于是就进入了下一个阶段:快速重传。
|
||||
|
||||
#### 3.快速重传和快速恢复
|
||||
|
||||
快速重传和快速恢复是一起工作的,它们是为了应对丢包这种行为而做的优化,在这种情况下,由于网络并没有出现拥塞,所以拥塞窗口不必恢复到初始值。判断丢包的依据就是收到3个相同的ack。
|
||||
|
||||
Google的工程师同样对TCP快速重传提出了一个改进策略:[tcp early retrans](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=eed530b6c67624db3f2cf477bac7c4d005d8f7ba),它允许一些情况下的TCP连接可以绕过重传延时(RTO)来进行快速重传。3.6版本以后的内核都支持了这个特性,因此,如果你还在使用CentOS-6,那么就享受不到它带来的网络性能提升了,你可以将你的操作系统升级为CentOS-7或者最新的CentOS-8。 另外,再多说一句,Google在网络方面的技术实力是其他公司没法比的,Linux内核TCP子系统的maintainer也是Google的工程师(Eric Dumazet)。
|
||||
|
||||
除了快速重传外,还有一种重传机制是超时重传。不过,这是非常糟糕的一种情况。如果发送出去一个数据包,超过一段时间(RTO)都收不到它的ack,那就认为是网络出现了拥塞。这个时候就需要将cwnd恢复为初始值,再次从慢启动开始调整cwnd的大小。
|
||||
|
||||
RTO一般发生在网络链路有拥塞的情况下,如果某一个连接数据量太大,就可能会导致其他连接的数据包排队,从而出现较大的延迟。我们在开头提到的,下载电影影响到别人玩网络游戏的例子就是这个原因。
|
||||
|
||||
关于RTO,它也是一个优化点。如果RTO过大的话,那么业务就可能要阻塞很久,所以在3.1版本的内核里引入了一种改进来将RTO的初始值[从3s调整为1s](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?h=v5.9-rc2&id=9ad7c049f0f79c418e293b1b68cf10d68f54fcdb),这可以显著节省业务的阻塞时间。不过,RTO=1s 在某些场景下还是有些大了,特别是在数据中心内部这种网络质量相对比较稳定的环境中。
|
||||
|
||||
我们在生产环境中发生过这样的案例:业务人员反馈说业务RT抖动得比较厉害,我们使用strace初步排查后发现,进程阻塞在了send()这类发包函数里。然后我们使用tcpdump来抓包,发现发送方在发送数据后,迟迟不能得到对端的响应,一直到RTO时间再次重传。与此同时,我们还尝试了在对端也使用tcpdump来抓包,发现对端是过了很长时间后才收到数据包。因此,我们判断是网络发生了拥塞,从而导致对端没有及时收到数据包。
|
||||
|
||||
那么,针对这种网络拥塞引起业务阻塞时间太久的情况,有没有什么解决方案呢?一种解决方案是,创建TCP连接,使用SO_SNDTIMEO来设置发送超时时间,以防止应用在发包的时候阻塞在发送端太久,如下所示:
|
||||
|
||||
>
|
||||
ret = setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, len);
|
||||
|
||||
|
||||
当业务发现该TCP连接超时后,就会主动断开该连接,然后尝试去使用其他的连接。
|
||||
|
||||
这种做法可以针对某个TCP连接来设置RTO时间,那么,有没有什么方法能够设置全局的RTO时间(设置一次,所有的TCP连接都能生效)呢?答案是有的,这就需要修改内核。针对这类需求,我们在生产环境中的实践是:将TCP RTO min、TCP RTO max、TCP RTO init 更改为可以使用sysctl来灵活控制的变量,从而根据实际情况来做调整,比如说针对数据中心内部的服务器,我们可以适当地调小这几个值,从而减少业务阻塞时间。
|
||||
|
||||
上述这4个阶段是TCP拥塞控制的基础,总体来说,拥塞控制就是根据TCP的数据传输状况来灵活地调整拥塞窗口,从而控制发送方发送数据包的行为。换句话说,拥塞窗口的大小可以表示网络传输链路的拥塞情况。TCP连接cwnd的大小可以通过ss这个命令来查看:
|
||||
|
||||
```
|
||||
$ ss -nipt
|
||||
State Recv-Q Send-Q Local Address:Port Peer Address:Port
|
||||
ESTAB 0 36 172.23.245.7:22 172.30.16.162:60490
|
||||
users:(("sshd",pid=19256,fd=3))
|
||||
cubic wscale:5,7 rto:272 rtt:71.53/1.068 ato:40 mss:1248 rcvmss:1248 advmss:1448 cwnd:10 bytes_acked:19591 bytes_received:2817 segs_out:64 segs_in:80 data_segs_out:57 data_segs_in:28 send 1.4Mbps lastsnd:6 lastrcv:6 lastack:6 pacing_rate 2.8Mbps delivery_rate 1.5Mbps app_limited busy:2016ms unacked:1 rcv_space:14600 minrtt:69.402
|
||||
|
||||
```
|
||||
|
||||
通过该命令,我们可以发现这个TCP连接的cwnd为10。
|
||||
|
||||
如果你想要追踪拥塞窗口的实时变化信息,还有另外一个更好的办法:通过tcp_probe这个tracepoint来追踪:
|
||||
|
||||
```
|
||||
/sys/kernel/debug/tracing/events/tcp/tcp_probe
|
||||
|
||||
```
|
||||
|
||||
但是这个tracepoint只有4.16以后的内核版本才支持,如果你的内核版本比较老,你也可以使用tcp_probe这个内核模块(net/ipv4/tcp_probe.c)来进行追踪。
|
||||
|
||||
除了网络状况外,发送方还需要知道接收方的处理能力。如果接收方的处理能力差,那么发送方就必须要减缓它的发包速度,否则数据包都会挤压在接收方的缓冲区里,甚至被接收方给丢弃掉。接收方的处理能力是通过另外一个窗口——rwnd(接收窗口)来表示的。那么,接收方的rwnd又是如何影响发送方的行为呢?
|
||||
|
||||
## 接收方是如何影响发送方发送数据的?
|
||||
|
||||
同样地,我也画了一张简单的图,来表示接收方的rwnd是如何影响发送方的:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e9/27/e920b93740d9677c5419dee332086827.jpg" alt="" title="rwnd与cwnd">
|
||||
|
||||
如上图所示,接收方在收到数据包后,会给发送方回一个ack,然后把自己的rwnd大小写入到TCP头部的win这个字段,这样发送方就能根据这个字段来知道接收方的rwnd了。接下来,发送方在发送下一个TCP segment的时候,会先对比发送方的cwnd和接收方的rwnd,得出这二者之间的较小值,然后控制发送的TCP segment个数不能超过这个较小值。
|
||||
|
||||
关于接收方的rwnd对发送方发送行为的影响,我们曾经遇到过这样的案例:业务反馈说Server向Client发包很慢,但是Server本身并不忙,而且网络看起来也没有问题,所以不清楚是什么原因导致的。对此,我们使用tcpdump在server上抓包后发现,Client响应的ack里经常出现win为0的情况,也就是Client的接收窗口为0。于是我们就去Client上排查,最终发现是Client代码存在bug,从而导致无法及时读取收到的数据包。
|
||||
|
||||
对于这种行为,我同样给Linux内核写了一个patch来监控它:[tcp: add SNMP counter for zero-window drops](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=fb223502ec0889444965f602f57b1f45f9e9845e) 。这个patch里增加了一个新的SNMP 计数:TCPZeroWindowDrop。如果系统中发生了接收窗口太小而无法收包的情况,就会产生该事件,然后该事件可以通过/proc/net/netstat里的TCPZeroWindowDrop这个字段来查看。
|
||||
|
||||
因为TCP头部大小是有限制的,而其中的win这个字段只有16bit,win能够表示的大小最大只有65535(64K),所以如果想要支持更大的接收窗口以满足高性能网络,我们就需要打开下面这个配置项,系统中也是默认打开了该选项:
|
||||
|
||||
>
|
||||
net.ipv4.tcp_window_scaling = 1
|
||||
|
||||
|
||||
关于该选项更加详细的设计,你如果想了解的话,可以去参考[RFC1323](https://tools.ietf.org/html/rfc1323)。
|
||||
|
||||
好了,关于TCP拥塞控制对业务网络性能的影响,我们就先讲到这里。
|
||||
|
||||
## 课堂总结
|
||||
|
||||
TCP拥塞控制是一个非常复杂的行为,我们在这节课里讲到的内容只是其中一些基础部分,希望这些基础知识可以让你对TCP拥塞控制有个大致的了解。我来总结一下这节课的重点:
|
||||
|
||||
- 网络拥塞状况会体现在TCP连接的拥塞窗口(cwnd)中,该拥塞窗口会影响发送方的发包行为;
|
||||
- 接收方的处理能力同样会反馈给发送方,这个处理是通过rwnd来表示的。rwnd和cwnd会共同作用于发送方,来决定发送方最大能够发送多少TCP包;
|
||||
- TCP拥塞控制的动态变化,可以通过tcp_probe这个tracepoint(对应4.16+的内核版本)或者是tcp_probe这个内核模块(对应4.16之前的内核版本)来进行实时观察,通过tcp_probe你能够很好地观察到TCP连接的数据传输状况。
|
||||
|
||||
## 课后作业
|
||||
|
||||
通过ssh登录到服务器上,然后把网络关掉,过几秒后再打开,请问这个ssh连接还正常吗?为什么?欢迎你在留言区与我讨论。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
@@ -0,0 +1,113 @@
|
||||
<audio id="audio" title="14 案例篇 | TCP端到端时延变大,怎样判断是哪里出现了问题?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/aa/23/aa43be1fbc9bc24ef4d430db2544d923.mp3"></audio>
|
||||
|
||||
你好,我是邵亚方。
|
||||
|
||||
如果你是一名互联网从业者,那你对下面这个场景应该不会陌生:客户端发送请求给服务端,服务端将请求处理完后,再把响应数据发送回客户端,这就是典型的C/S(Client/Server)架构。对于这种请求-响应式的服务,我们不得不面对的问题是:
|
||||
|
||||
- 如果客户端收到的响应时间变大了,那么这是客户端自身的问题呢,还是因为服务端处理得慢呢,又或者是因为网络有抖动呢?
|
||||
- 即使我们已经明确了是服务端或者客户端的问题,那么究竟是应用程序自身引起的问题呢,还是内核导致的问题呢?
|
||||
- 而且很多时候,这种问题往往是一天可能最多抖动一两次,我们很难去抓现场信息。
|
||||
|
||||
为了更好地处理这类折磨人的问题,我也摸索了一些手段来进行实时追踪,既不会给应用程序和系统带来明显的开销,又可以在出现这些故障时能够把信息给抓取出来,从而帮助我们快速定位出问题所在。
|
||||
|
||||
因此,这节课我来分享下我在这方面的一些实践,以及解决过的一些具体案例。
|
||||
|
||||
当然,这些实践并不仅仅适用于这种C/S架构,对于其他应用程序,特别是对延迟比较敏感的应用程序,同样具备参考意义。比如说:
|
||||
|
||||
- 如果我的业务运行在虚拟机里面,那怎么追踪呢?
|
||||
- 如果Client和Server之间还有一个Proxy,那怎么判断是不是Proxy引起的问题呢?
|
||||
|
||||
那么,我们就先从生产环境中C/S架构的网络抖动案例说起。
|
||||
|
||||
## 如何分析C/S架构中的网络抖动问题?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/49/d8/49fa6f705df48e89bdfb46242f7f2ed8.jpg" alt="" title="典型的C/S架构">
|
||||
|
||||
上图就是一个典型的C/S架构,Client和Server之间可能经过了很复杂的网络,但是对于服务器开发者或者运维人员而言,这些中间网络可以理解为是一个黑盒,很难去获取这些网络的详细信息,更不用说到这些网络设备上去做debug了。所以,我在这里把它们都简化为了一个Router(路由器),然后Client和Server通过这个路由器来相互通信。比如互联网场景中的数据库服务(像MySQL)、http服务等都是这种架构。而当时给我们提需求来诊断网络抖动问题的也是MySQL业务。因此,接下来我们就以MySQL为例来进行具体讲解。
|
||||
|
||||
MySQL的业务方反馈说他们的请求偶尔会超时很长,但不清楚是什么原因引起了超时,对应到上图中,就是D点收到响应的时刻和A点发出请求的时刻,这个时间差会偶然性地有毛刺。在发生网络问题时,使用tcpdump来抓包是常用的分析手段,当你不清楚该如何来分析网络问题时,那就使用tcpdump先把事故现场保存下来吧。
|
||||
|
||||
如果你使用过tcpdump来分析问题,那你应该也吐槽过用tcpdump分析问题会很麻烦。如果你熟悉wireshark的话,就会相对容易一些了。但是对于大部分人而言呢,学习wirkshark的成本也是很高的,而且wireshark的命令那么多,把每一条命令都记清楚也是件很麻烦的事。
|
||||
|
||||
回到我们这个案例中,在MySQL发生抖动的时候,我们的业务人员也使用了tcpdump来抓包保存了现场,但是他们并不清楚该如何分析这些tcpdump信息。在我帮他们分析这些tcpdump信息的时候,也很难把tcpdump信息和业务抖动的时刻关联起来。这是因为虽然我们知道业务抖动发生的时刻,比如说21:00:00.000这个时刻,但是在这一时刻的附近可能会有非常多的TCP数据包,很难简单地依赖时间戳把二者关联起来。而且,更重要的原因是,我们知道TCP是数据流,上层业务的一个请求可能会被分为多个TCP包(TCP Segment),同样地,多个请求也可能被合并为一个TCP包。也就是说,TCP流是很难和应用数据关联起来的。这就是用tcpdump分析业务请求和响应的难点。
|
||||
|
||||
针对tcpdump难以分析应用协议的问题,有一个思路是,在tcpdump的时候把数据也保存下来,然后使用tcpdump再进一步去解析这些应用协议。但是你会发现,用这种方式来处理生产环境中的抖动问题是很不现实的,因为在生产环境中的 TCP 连接动辄数百上千条,我们往往并不清楚抖动发生在哪个TCP连接上。如果把这些TCP流都dump出来,data部分即使只dump与应用协议有关的数据,这对磁盘I/O也是个负担。那有什么好办法吗?
|
||||
|
||||
好办法还是需要和应用协议关联起来,不过我们可以把这些应用协议做一层抽象,从而可以更简单地来解析它们,甚至无需解析。对于MySQL而言呢,工具[tcprstat](https://www.percona.com/blog/2010/08/31/introducing-tcprstat-a-tcp-response-time-tool/)就是来做这件事的。
|
||||
|
||||
tcprstat的大致原理是利用MySQL的request-response特征来简化对协议内容的处理。request-response是指一个请求到达MySQL后,MySQL处理完该请求,然后回response,Client侧收到response后再去发下一个request,然后MySQL收到下一个request并处理。也就是说这种模型是典型的串行方式,处理完了一个再去处理下一个。所以tcprstat就可以以数据包到达MySQL Server侧作为起始时间点,以MySQL将最后一个数据包发出去作为结束时间点,然后这二者的时间差就是RT(Response Time),这个过程大致如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/62/d6/62666eab402a7d4ed9eb41ae8a2581d6.jpg" alt="" title="tcprstat追踪RT抖动">
|
||||
|
||||
tcprstat会记录request的到达时间点,以及request发出去的时间点,然后计算出RT并记录到日志中。当时我们把tcprstat部署到MySQL server侧后,发现每一个RT值都很小,并没有延迟很大的情况,所以看起来服务端并没有问题。那么问题是否发生在Client这里呢?
|
||||
|
||||
在我们想要把tcprstat也部署在Client侧抓取信息时,发现它只支持在Server侧部署,所以我们对tcprstat做了一些改造,让它也可以部署在Client侧。
|
||||
|
||||
这种改造并不麻烦,因为在Client侧解析的也是MySQL协议,只是TCP流的方向跟Server侧相反,Client侧是发请求收响应,而Server侧是收请求发响应。
|
||||
|
||||
在改造完成后,我们就开始部署tcprstat来抓取抖动现场了。在业务发生抖动时,通过我们抓取到的信息显示,Client在收到响应包的时候就已经发生延迟了,也就是说问题同样也不是发生在Client侧。这就有些奇怪了,既然Client和Server都没有问题,难道是网络链路出现了问题?
|
||||
|
||||
为了明确这一点,我们就在业务低峰期使用ping包来检查网络是否存在问题。ping了大概数小时后,我们发现ping响应时间忽然变得很大,从不到1ms的时间增大到了几十甚至上百ms,然后很快又恢复正常。
|
||||
|
||||
根据这个信息,我们推断某个交换机可能存在拥塞,于是就联系交换机管理人员来分析交换机。在交换机管理人员对这个链路上的交换机逐一排查后,最终定位到一台接入交换机确实有问题,它会偶然地出现排队很长的情况。而之所以MySQL反馈有抖动,其他业务没有反馈,只是因为这个接入交换机上的其他业务并不关心抖动。在交换机厂商帮忙修复了这个问题后,就再也没有出现过这种偶发性的抖动了。
|
||||
|
||||
这个结果看似很简单,但是分析过程还是很复杂的。因为一开始我们并不清楚问题发生在哪里,只能一步步去排查,所以这个分析过程也花费了几天的时间。
|
||||
|
||||
交换机引起的网络抖动问题,只是我们分析过的众多抖动案例之一。除了这类问题外,我们还分析过很多抖动是由于Client侧存在问题或者Server侧存在问题。在分析了这么多抖动问题之后,我们就开始思考,能否针对这类问题来做一个自动化分析系统呢?而且我们部署运行tcprstat后,也发现它存在一些不足之处,主要是它的性能开销略大,特别是在TCP连接数较多的情况下,它的CPU利用率甚至能够超过10%,这难以满足我们生产环境中长时间运行的需要。
|
||||
|
||||
tcprstat会有这么高的CPU开销,原因其实与tcpdump是类似的,它在旁路采集数据后会拷贝到用户空间来处理,这个拷贝以及处理时间就比较消耗CPU。
|
||||
|
||||
为了满足生产环境的需求,我们在tcprstat的基础上,做了一个更加轻量级的分析系统。
|
||||
|
||||
## 如何轻量级地判断抖动发生在哪里?
|
||||
|
||||
我们的目标是在10Gb网卡的高并发场景下尽量地降低监控开销,最好可以控制在1%以内,而且不能给业务带来明显延迟。要想降低CPU开销,很多工作就需要在内核里面来完成,就跟现在很流行的eBPF这个追踪框架类似:内核处理完所有的数据,然后将结果返回给用户空间。
|
||||
|
||||
能够达到这个目标的方案大致有两种:一种是使用内核模块,另一种是使用轻量级的内核追踪框架。
|
||||
|
||||
使用内核模块的缺点是它的安装部署会很不方便,特别是在线上内核版本非常多的情况下,比如说,我们的线上既有CentOS-6的操作系统,也有CentOS-7的操作系统,每个操作系统又各自有很多小版本,以及我们自己发布的版本。统一线上的内核版本是件很麻烦的事,这会涉及到很多变更,不太现实。所以这种现状也就决定了,使用内核模块的方式需要付出比较高的维护成本。而且内核模块的易用性也很差,这会导致业务人员和运维人员排斥使用它,从而增加它的推广难度。基于这些考虑,我们最终选择了基于systemtap这个追踪框架来开发。没有选择eBPF的原因是,它对内核版本要求较高,而我们线上很多都是CentOS-7的内核。
|
||||
|
||||
基于systemtap实现的追踪框架大致如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0e/ff/0e6bf3f396e9c89f8a35376075d8c5ff.jpg" alt="" title="TCP流的追踪">
|
||||
|
||||
它会追踪每一个TCP流,TCP流对应到内核里的实现就是一个struct sock实例,然后记录TCP流经过A/B/C/D这四个点的时刻,依据这几个时间点,我们就可以得到下面的结论:
|
||||
|
||||
- 如果C-B的时间差较大,那就说明Server侧有抖动,否则是Client或网络的问题;
|
||||
- 如果D-A的时间差较小,那就说明是Client侧问题,否则是Server或者网络的问题。
|
||||
|
||||
这样在发生RT抖动时,我们就能够区分出抖动是发生在Client,Server,还是网络中了,这会大大提升分析定位问题的效率。在定位到问题出在哪里后,你就可以使用我们在“[11讲](https://time.geekbang.org/column/article/284912)”、“[12讲](https://time.geekbang.org/column/article/285816)”和“[13讲](https://time.geekbang.org/column/article/286494)”里讲到的知识点,再去进一步分析具体的原因是什么了。
|
||||
|
||||
在使用systemtap的过程中我们也踩了不少坑,在这里也分享给你,希望你可以避免:
|
||||
|
||||
- systemtap的加载过程是一个开销很大的过程,主要是CPU的开销。因为systemtap的加载会编译systemtap脚本,这会比较耗时。你可以提前将你的systemtap脚本编译为内核模块,然后直接加载该模块来避免CPU开销;
|
||||
- systemtap有很多开销控制选项,你可以设置开销阈值来作为兜底方案,以防止异常情况下它占用太多CPU;
|
||||
- systemtap进程异常退出后可能不会卸载systemtap模块,在你发现systemtap进程退出后,你需要检查它是否也把对应的内核模块给卸载了。如果没有,那你需要手动卸载一下,以免产生不必要的问题。
|
||||
|
||||
C/S架构是互联网服务中比较典型的场景,那针对其他场景我们该如何来分析问题呢?接下来我们以虚拟机这种场景为例来看一下。
|
||||
|
||||
## 虚拟机场景下该如何判断抖动是发生在宿主机上还是虚拟机里?
|
||||
|
||||
随着云计算的发展,越来越多的业务开始部署在云上,很多企业或者使用自己定制的私有云,或者使用公有云。我们也有很多业务部署在自己的私有云中,既有基于KVM的虚拟机,也有基于Kubernetes和Docker的容器。以虚拟机为例,在Server侧发生抖动的时候,业务人员还想进一步知道,抖动是发生在Server侧的虚拟机内部,还是发生在Server侧的宿主机上。要想实现这个需求,我们只需要进一步扩展,再增加新的hook点,去记录TCP流经过虚拟机的时间点就好了,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/27/c1/27e5240900409a0848842f73704d3bc1.jpg" alt="" title="虚拟机场景下TCP流的追踪">
|
||||
|
||||
这样我们就可以根据F和E的时间差来判断抖动是否发生在虚拟机内部。针对这个需求,我们对tcprstat也做了类似的改造,让它可以识别出抖动是否发生在虚拟机内部。这个改造也不复杂,tcprstat默认只处理目标地址为本机的数据包,不会处理转发包,所以我们让它支持混杂模式,然后就可以处理转发包了。当然,虚拟机的具体网络配置是千差万别的,你需要根据你的实际虚拟网络配置来做调整。
|
||||
|
||||
总之,希望你可以举一反三,根据你的实际业务场景来做合理的数据分析,而不要局限于我们这节课所列举的这几个具体场景。
|
||||
|
||||
## 课堂总结
|
||||
|
||||
我们这堂课以典型的C/S架构为例,分析了RT发生抖动时,该如何高效地识别出问题发生在哪里。我来总结一下这节课的重点:
|
||||
|
||||
- tcpdump是分析网络问题必须要掌握的工具,但是用它分析问题并不容易。在你不清楚该如何分析网络问题时,你可以先使用tcpdump把现场信息保存下来;
|
||||
- TCP是数据流,如何把TCP流和具体的业务请求/响应数据包关联起来,是分析具体应用问题的前提。你需要结合你的业务模型来做合理的关联;
|
||||
- RT抖动问题是很棘手的,你需要结合你的业务模型来开发一些高效的问题分析工具。如果你使用的是Redhat或者CentOS,那么你可以考虑使用systemtap;如果是Ubuntu,你可以考虑使用lttng。
|
||||
|
||||
## 课后作业
|
||||
|
||||
结合这堂课的第一张图,在这张图中,请问是否可以用TCP流到达B点的时刻(到达Server这台主机的时间)减去TCP流经过A点的时刻(到达Client这台主机的时间)来做为网络耗时?为什么?
|
||||
|
||||
结合我们在“[13讲](https://time.geekbang.org/column/article/286494)“里讲到的RTT这个往返时延,你还可以进一步思考:是否可以使用RTT来作为这次网络耗时?为什么?欢迎你在留言区与我讨论。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
165
极客时间专栏/geek/Linux内核技术实战课/TCP重传问题/15 分析篇 | 如何高效地分析TCP重传问题?.md
Normal file
165
极客时间专栏/geek/Linux内核技术实战课/TCP重传问题/15 分析篇 | 如何高效地分析TCP重传问题?.md
Normal file
@@ -0,0 +1,165 @@
|
||||
<audio id="audio" title="15 分析篇 | 如何高效地分析TCP重传问题?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/95/96/95e8a4c8069dec64272259f2b054f796.mp3"></audio>
|
||||
|
||||
你好,我是邵亚方。
|
||||
|
||||
我们在基础篇和案例篇里讲了很多问题,比如说RT抖动问题、丢包问题、无法建连问题等等。这些问题通常都会伴随着TCP重传,所以我们往往也会抓取TCP重传信息来辅助我们分析这些问题。
|
||||
|
||||
而且TCP重传也是一个信号,我们通常会利用这个信号来判断系统是否稳定。比如说,如果一台服务器的TCP重传率很高,那这个服务器肯定是存在问题的,需要我们及时采取措施,否则可能会产生更加严重的故障。
|
||||
|
||||
但是,TCP重传率分析并不是一件很容易的事,比如说现在某台服务器的TCP重传率很高,那究竟是什么业务在进行TCP重传呢?对此,很多人并不懂得如何来分析。所以,在这节课中,我会带你来认识TCP重传是怎么回事,以及如何来高效地分析它。
|
||||
|
||||
## 什么是TCP重传 ?
|
||||
|
||||
我在“[开篇词](https://time.geekbang.org/column/article/273544)”中举过一个TCP重传率的例子,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ab/f6/ab358c52ede21f0983fe7dfb032dc3f6.jpg" alt="">
|
||||
|
||||
这是互联网企业普遍都有的TCP重传率监控,它是服务器稳定性的一个指标,如果它太高,就像上图中的那些毛刺一样,往往就意味着服务器不稳定了。那TCP重传率究竟表示什么呢?
|
||||
|
||||
其实TCP重传率是通过解析/proc/net/snmp这个文件里的指标计算出来的,这个文件里面和TCP有关的关键指标如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d5/e7/d5be65df068c3a2c4d181f492791efe7.jpg" alt="">
|
||||
|
||||
TCP重传率的计算公式如下:
|
||||
|
||||
>
|
||||
retrans = (RetransSegs-last RetransSegs) / (OutSegs-last OutSegs) * 100
|
||||
|
||||
|
||||
也就是说,单位时间内TCP重传包的数量除以TCP总的发包数量,就是TCP重传率。那我们继续看下这个公式中的RetransSegs和OutSegs是怎么回事,我画了两张示例图来演示这两个指标的变化:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ed/54/ed69e93e3c13f0e117021e399500e854.jpg" alt="" title="不存在重传的情况">
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0a/b6/0a28a0596bd56174feaec0d82245b5b6.jpg" alt="" title="存在重传的情况">
|
||||
|
||||
通过这两个示例图,你可以发现,发送端在发送一个TCP数据包后,会把该数据包放在发送端的发送队列里,也叫重传队列。此时,OutSegs会相应地加1,队列长度也为1。如果可以收到接收端对这个数据包的ACK,该数据包就会在发送队列中被删掉,然后队列长度变为0;如果收不到这个数据包的ACK,就会触发重传机制,我们在这里演示的就是超时重传这种情况,也就是说发送端在发送数据包的时候,会启动一个超时重传定时器(RTO),如果超过了这个时间,发送端还没有收到ACK,就会重传该数据包,然后OutSegs加1,同时RetransSegs也会加1。
|
||||
|
||||
这就是OutSegs和RetransSegs的含义:每发出去一个TCP包(包括重传包),OutSegs会相应地加1;每发出去一个重传包,RetransSegs会相应地加1。同时,我也在图中展示了重传队列的变化,你可以仔细看下。
|
||||
|
||||
除了上图中展示的超时重传外,还有快速重传机制。关于快速重传,你可以参考“[13讲](https://time.geekbang.org/column/article/286494)”,我就不在这里详细描述了。
|
||||
|
||||
明白了TCP重传是如何定义的之后,我们继续来看下哪些情况会导致TCP重传。
|
||||
|
||||
引起TCP重传的情况在整体上可以分为如下两类。
|
||||
|
||||
<li>**丢包**<br>
|
||||
TCP数据包在网络传输过程中可能会被丢弃;接收端也可能会把该数据包给丢弃;接收端回的ACK也可能在网络传输过程中被丢弃;数据包在传输过程中发生错误而被接收端给丢弃……这些情况都会导致发送端重传该TCP数据包。</li>
|
||||
<li>**拥塞**<br>
|
||||
TCP数据包在网络传输过程中可能会在某个交换机/路由器上排队,比如臭名昭著的Bufferbloat(缓冲膨胀);TCP数据包在网络传输过程中因为路由变化而产生的乱序;接收端回的ACK在某个交换机/路由器上排队……这些情况都会导致发送端再次重传该TCP数据包。</li>
|
||||
|
||||
总之,TCP重传可以很好地作为通信质量的信号,我们需要去重视它。
|
||||
|
||||
那么,当我们发现某个主机上TCP重传率很高时,该如何去分析呢?
|
||||
|
||||
## 分析TCP重传的常规手段
|
||||
|
||||
最常规的分析手段就是tcpdump,我们可以使用它把进出某个网卡的数据包给保存下来:
|
||||
|
||||
```
|
||||
$ tcpdump -s 0 -i eth0 -w tcpdumpfile
|
||||
|
||||
```
|
||||
|
||||
然后在Linux上我们可以使用tshark这个工具(wireshark的Linux版本)来过滤出TCP重传包:
|
||||
|
||||
```
|
||||
$ tshark -r tcpdumpfile -R tcp.analysis.retransmission
|
||||
|
||||
```
|
||||
|
||||
如果有重传包的话,就可以显示出来了,如下是一个TCP重传的示例:
|
||||
|
||||
```
|
||||
3481 20.277303 10.17.130.20 -> 124.74.250.144 TCP 70 [TCP Retransmission] 35993 > https [SYN] Seq=0 Win=14600 Len=0 MSS=1460 SACK_PERM=1 TSval=3231504691 TSecr=0
|
||||
|
||||
3659 22.277070 10.17.130.20 -> 124.74.250.144 TCP 70 [TCP Retransmission] 35993 > https [SYN] Seq=0 Win=14600 Len=0 MSS=1460 SACK_PERM=1 TSval=3231506691 TSecr=0
|
||||
|
||||
8649 46.539393 58.216.21.165 -> 10.17.130.20 TLSv1 113 [TCP Retransmission] Change Cipher Spec, Encrypted Handshake Messag
|
||||
|
||||
```
|
||||
|
||||
借助tcpdump,我们就可以看到TCP重传的详细情况。从上面这几个TCP重传信息中,我们可以看到,这是发生在10.17.130.20:35993 - 124.74.250.144: 443这个TCP连接上的重传;通过[SYN]这个TCP连接状态,可以看到这是发生在三次握手阶段的重传。依据这些信息,我们就可以继续去124.74.250.144这个主机上分析https这个服务为什么无法建立新的连接了。
|
||||
|
||||
但是,我们都知道tcpdump很重,如果直接在生产环境上进行采集的话,难免会对业务造成性能影响。那有没有更加轻量级的一些分析方法呢?
|
||||
|
||||
## 如何高效地分析TCP重传 ?
|
||||
|
||||
其实,就像应用程序实现一些功能需要调用对应的函数一样,TCP重传也需要调用特定的内核函数。这个内核函数就是tcp_retransmit_skb()。你可以把这个函数名字里的skb理解为是一个需要发送的网络包。那么,如果我们想要高效地追踪TCP重传情况,那么直接追踪该函数就可以了。
|
||||
|
||||
追踪内核函数最通用的方法是使用Kprobe,Kprobe的大致原理如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9f/c8/9f3f412208d8e17dd859a97b017228c8.jpg" alt="" title="Kprobe基本原理">
|
||||
|
||||
你可以实现一个内核模块,该内核模块中使用Kprobe在tcp_retransmit_skb这个函数入口插入一个probe,然后注册一个break_handler,这样在执行到tcp_retransmit_skb时就会异常跳转到注册的break_handler中,然后在break_handler中解析TCP报文(skb)就可以了,从而来判断是什么在重传。
|
||||
|
||||
如果你觉得实现内核模块比较麻烦,可以借助ftrace框架来使用Kprobe。Brendan Gregg实现的[tcpretrans](https://github.com/brendangregg/perf-tools/blob/master/net/tcpretrans)采用的就是这种方式,你也可以直接使用它这个工具来追踪TCP重传。不过,该工具也有一些缺陷,因为它是通过读取/proc/net/tcp这个文件来解析是什么在重传,所以它能解析的信息比较有限,而且如果TCP连接持续时间较短(比如短连接),那么该工具就无法解析出来了。另外,你在使用它时需要确保你的内核已经打开了ftrace的tracing功能,也就是/sys/kernel/debug/tracing/tracing_on中的内容需要为1;在CentOS-6上,还需要/sys/kernel/debug/tracing/tracing_enabled也为1。
|
||||
|
||||
```
|
||||
$ cat /sys/kernel/debug/tracing/tracing_on
|
||||
1
|
||||
|
||||
```
|
||||
|
||||
如果为0的话,你需要打开它们,例如:
|
||||
|
||||
```
|
||||
$ echo 1 > /sys/kernel/debug/tracing/tracing_on
|
||||
|
||||
```
|
||||
|
||||
然后在追踪结束后,你需要来关闭他们:
|
||||
|
||||
```
|
||||
$ echo 0 > /sys/kernel/debug/tracing/tracing_on
|
||||
|
||||
```
|
||||
|
||||
由于Kprobe是通过异常(Exception)这种方式来工作的,所以它还是有一些性能开销的,在TCP发包快速路径上还是要避免使用Kprobe。不过,由于重传路径是慢速路径,所以在重传路径上添加Kprobe也无需担心性能开销。
|
||||
|
||||
Kprobe这种方式使用起来还是略有些不便,为了让Linux用户更方便地观察TCP重传事件,4.16内核版本中专门添加了[TCP tracepoint](https://github.com/torvalds/linux/commit/e086101b150ae8e99e54ab26101ef3835fa9f48d)来解析TCP重传事件。如果你使用的操作系统是CentOS-7以及更老的版本,就无法使用该Tracepoint来观察了;如果你的版本是CentOS-8以及后续更新的版本,那你可以直接使用这个Tracepoint来追踪TCP重传,可以使用如下命令:
|
||||
|
||||
```
|
||||
$ cd /sys/kernel/debug/tracing/events/
|
||||
$ echo 1 > tcp/tcp_retransmit_skb/enable
|
||||
|
||||
```
|
||||
|
||||
然后你就可以追踪TCP重传事件了:
|
||||
|
||||
```
|
||||
$ cat trace_pipe
|
||||
<idle>-0 [007] ..s. 265119.290232: tcp_retransmit_skb: sport=22 dport=62264 saddr=172.23.245.8 daddr=172.30.18.225 saddrv6=::ffff:172.23.245.8 daddrv6=::ffff:172.30.18.225 state=TCP_ESTABLISHED
|
||||
|
||||
```
|
||||
|
||||
可以看到,当TCP重传发生时,该事件的基本信息就会被打印出来。多说一句,在最开始的版本中是没有“state=TCP_ESTABLISHED”这一项的。如果没有这一项,我们就无法识别该重传事件是不是发生在三次握手阶段了,所以我给内核贡献了一个PATCH来显示TCP连接状态,以便于问题分析,具体见[tcp: expose sk_state in tcp_retransmit_skb tracepoint](https://github.com/torvalds/linux/commit/af4325ecc24f45933d5567e72227cff2c1594764)这个commit。
|
||||
|
||||
追踪结束后呢,你需要将这个Tracepoint给关闭:
|
||||
|
||||
```
|
||||
$ echo 0 > tcp/tcp_retransmit_skb/enable
|
||||
|
||||
```
|
||||
|
||||
Tracepoint这种方式不仅使用起来更加方便,而且它的性能开销比Kprobe要小,所以我们在快速路径上也可以使用它。
|
||||
|
||||
因为Tracepoint对TCP重传事件的支持,所以tcpretrans这个工具也跟着进行了一次升级换代。它通过解析该Tracepoint实现了对TCP重传事件的追踪,而不再使用之前的Kprobe方式,具体你可以参考[bcc tcpretrans](https://github.com/iovisor/bcc/blob/master/tools/tcpretrans.py)。再多说一句,Brendan Gregg在实现这些基于ebpf的TCP追踪工具之前也曾经跟我讨论过,所以我对他的这个工具才会这么熟悉。
|
||||
|
||||
我们针对TCP重传事件的分析就先讲到这里,希望能给你带来一些启发,去开发一些更加高效的工具来分析你遇到的TCP问题或者其他问题。
|
||||
|
||||
## 课堂总结
|
||||
|
||||
这堂课我们主要讲了TCP重传的一些知识,关于TCP重传你需要重点记住下面这几点:
|
||||
|
||||
- TCP重传率可以作为TCP通信质量的信号,如果它很高,那说明这个TCP连接很不稳定;
|
||||
- 产生TCP重传的问题主要是丢包和网络拥塞这两种情况;
|
||||
- TCP重传时会调用特定的内核函数,我们可以追踪该函数的调用情况来追踪TCP重传事件;
|
||||
- Kprobe是一个很通用的追踪工具,在低版本内核上,你可以使用这个方法来追踪TCP重传事件;
|
||||
- Tracepoint是一个更加轻量级也更加方便的追踪TCP重传的工具,但是需要你的内核版本为4.16+;
|
||||
- 如果你想要更简单些,那你可以直接使用tcpretrans这个工具。
|
||||
|
||||
## 课后作业
|
||||
|
||||
请问我们提到的tracepoint观察方式,或者tcpretrans这个工具,可以追踪收到的TCP重传包吗?为什么?欢迎你在留言区与我讨论。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
138
极客时间专栏/geek/Linux内核技术实战课/TCP重传问题/16 套路篇 | 如何分析常见的TCP问题?.md
Normal file
138
极客时间专栏/geek/Linux内核技术实战课/TCP重传问题/16 套路篇 | 如何分析常见的TCP问题?.md
Normal file
@@ -0,0 +1,138 @@
|
||||
<audio id="audio" title="16 套路篇 | 如何分析常见的TCP问题?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a8/1e/a851071dcabc33ca758cb5d16d71101e.mp3"></audio>
|
||||
|
||||
你好,我是邵亚方。
|
||||
|
||||
对互联网服务而言, 网络问题是非常多的,而且很多问题的外在表现都是网络问题,这就需要我们从网络入手,分析清楚根本原因是什么。而要分析各种各样的网络问题,你必须掌握一些分析手段,这样在出现问题的时候,你就可以高效地找到原因。这节课我就带你来了解下TCP的常见问题,以及对应的分析套路。
|
||||
|
||||
## 在Linux上检查网络的常用工具
|
||||
|
||||
当服务器产生问题,而我们又不清楚问题和什么有关时,就需要运行一些工具来检查系统的整体状况。其中,dstat是我们常用的一种检查工具:
|
||||
|
||||
```
|
||||
$ dstat
|
||||
--total-cpu-usage-- -dsk/total- -net/total- ---paging-- ---system--
|
||||
usr sys idl wai stl| read writ| recv send| in out | int csw
|
||||
8 1 91 0 0| 0 4096B|7492B 7757B| 0 0 |4029 7399
|
||||
8 1 91 0 0| 0 0 |7245B 7276B| 0 0 |4049 6967
|
||||
8 1 91 0 0| 0 144k|7148B 7386B| 0 0 |3896 6971
|
||||
9 2 89 0 0| 0 0 |7397B 7285B| 0 0 |4611 7426
|
||||
8 1 91 0 0| 0 0 |7294B 7258B| 0 0 |3976 7062
|
||||
|
||||
```
|
||||
|
||||
如上所示,dstat会显示四类系统资源的整体使用情况和两个关键的系统指标。这四类系统资源分别是:CPU、磁盘I/O、 网络和内存。两个关键的系统指标是中断次数(int)和上下文切换次数(csw)。而每个系统资源又会输出它的一些关键指标,这里你需要注意以下几点:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/14/68/145508f238e794df5fbf84f200c7ce68.jpg" alt="">
|
||||
|
||||
如果你发现某一类系统资源对应的指标比较高,你就需要进一步针对该系统资源做更深入的分析。假设你发现网络吞吐比较高,那就继续观察网络的相关指标,你可以用dstat -h来查看,比如针对TCP,就可以使用dstat -tcp:
|
||||
|
||||
```
|
||||
$ dstat --tcp
|
||||
------tcp-sockets-------
|
||||
lis act syn tim clo
|
||||
27 38 0 0 0
|
||||
27 38 0 0 0
|
||||
|
||||
```
|
||||
|
||||
它会统计并显示系统中所有的TCP连接状态,这些指标的含义如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c9/a4/c91a94caf6f74b508bf3648e7e9197a4.jpg" alt="">
|
||||
|
||||
在得到了TCP连接的整体状况后,如果你想要看TCP连接的详细信息,你可以使用ss这个命令来继续观察。通过ss你可以查看到每个TCP连接都是什么样的:
|
||||
|
||||
```
|
||||
$ ss -natp
|
||||
State Recv-Q Send-Q Local Address:Port Peer Address:Port
|
||||
LISTEN0 100 0.0.0.0:36457 0.0.0.0:* users:(("test",pid=11307,fd=17))
|
||||
LISTEN0 5 0.0.0.0:33811 0.0.0.0:* users:(("test",pid=11307,fd=19))
|
||||
ESTAB 0 0 127.0.0.1:57396 127.0.1.1:34751 users:(("test",pid=11307,fd=106))
|
||||
ESTAB 0 0 127.0.0.1:57384 127.0.1.1:34751 users:(("test",pid=11307,fd=100))
|
||||
|
||||
```
|
||||
|
||||
如上所示,我们能查看到每个TCP连接的状态(State)、接收队列大小(Recv-Q)、发送队列大小(Send-Q)、本地IP和端口(Local Address:Port )、远端IP和端口(Peer Address:Port)以及打开该TCP连接的进程信息。
|
||||
|
||||
除了ss命令外,你也可以使用netstat命令来查看所有TCP连接的详细信息:
|
||||
|
||||
```
|
||||
$ netstat -natp
|
||||
|
||||
```
|
||||
|
||||
不过,我不建议你使用netstat,最好还是用ss。因为netstat不仅比ss慢,而且开销也大。netstat是通过直接读取/proc/net/下面的文件来解析网络连接信息的;而ss使用的是netlink方式,这种方式的效率会高很多。
|
||||
|
||||
netlink在解析时会依赖内核的一些诊断模块,比如解析TCP信息就需要tcp_diag这个诊断模块。如果诊断模块不存在,那么ss就无法使用netlink这种方式了,这个时候它就会退化到和netstat一样,也就是使用解析/proc/net/这种方式,当然了,它的效率也会相应变差。
|
||||
|
||||
另外,如果你去看netstat手册,通过man netstat,你会发现这样一句话“This program is obsolete. Replacement for netstat is ss”。所以,以后在分析网络连接问题时,我们尽量还是使用ss,而不是netstat。
|
||||
|
||||
netstat属于net-tools这个比较古老的工具集,而ss属于iproute2这个工具集。net-tools中的常用命令,几乎都可以用iproute2中的新命令来代替,比如:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ca/81/cac1d4a8592a72cd5f249449867ebb81.jpg" alt="">
|
||||
|
||||
除了查看系统中的网络连接信息外,我们有时候还需要去查看系统的网络状态,比如说系统中是否存在丢包,以及是什么原因引起了丢包,这时候我们就需要netstat -s或者它的替代工具nstat了:
|
||||
|
||||
```
|
||||
$ nstat -z | grep -i drop
|
||||
TcpExtLockDroppedIcmps 0 0.0
|
||||
TcpExtListenDrops 0 0.0
|
||||
TcpExtTCPBacklogDrop 0 0.0
|
||||
TcpExtPFMemallocDrop 0 0.0
|
||||
TcpExtTCPMinTTLDrop 0 0.0
|
||||
TcpExtTCPDeferAcceptDrop 0 0.0
|
||||
TcpExtTCPReqQFullDrop 0 0.0
|
||||
TcpExtTCPOFODrop 0 0.0
|
||||
TcpExtTCPZeroWindowDrop 0 0.0
|
||||
TcpExtTCPRcvQDrop 0 0.0
|
||||
|
||||
```
|
||||
|
||||
上面输出的这些信息就包括了常见的丢包原因,因为我的这台主机很稳定,所以你可以看到输出的结果都是0。
|
||||
|
||||
假如你通过这些常规检查手段没有发现异常,那你就需要考虑使用网络分析的必备工具——tcpdump了。
|
||||
|
||||
## 分析网络问题你必须要掌握的工具:tcpdump
|
||||
|
||||
tcpdump的使用技巧有很多,在这里我们不讲述这些使用技巧,而是讲述一下它的工作原理,以便于你理解tcpdump到底在干什么,以及它能够分析什么样的问题。
|
||||
|
||||
tcpdump的大致原理如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a2/27/a2a0cdc510d8e77677ba957e0408cf27.jpg" alt="" title="tcpdump基本原理">
|
||||
|
||||
tcpdump抓包使用的是libpacp这种机制。它的大致原理是:在收发包时,如果该包符合tcpdump设置的规则(BPF filter),那么该网络包就会被拷贝一份到tcpdump的内核缓冲区,然后以PACKET_MMAP的方式将这部分内存映射到tcpdump用户空间,解析后就会把这些内容给输出了。
|
||||
|
||||
通过上图你也可以看到,在收包的时候,如果网络包已经被网卡丢弃了,那么tcpdump是抓不到它的;在发包的时候,如果网络包在协议栈里被丢弃了,比如因为发送缓冲区满而被丢弃,tcpdump同样抓不到它。我们可以将tcpdump的能力范围简单地总结为:网卡以内的问题可以交给tcpdump来处理;对于网卡以外(包括网卡上)的问题,tcpdump可能就捉襟见肘了。这个时候,你需要在对端也使用tcpdump来抓包。
|
||||
|
||||
你还需要知道一点,那就是tcpdump的开销比较大,这主要在于BPF过滤器。如果系统中存在非常多的TCP连接,那么这个过滤的过程是非常耗时的,所以在生产环境中要慎用。但是,在出现网络问题时,如果你真的没有什么排查思路,那就想办法使用tcpdump来抓一下包吧,也许它的输出会给你带来一些意外的惊喜。
|
||||
|
||||
如果生产环境上运行着很重要的业务,你不敢使用tcpdump来抓包,那你就得去研究一些更加轻量级的追踪方式了。接下来,我给你推荐的轻量级追踪方式是TCP Tracepoints。
|
||||
|
||||
## TCP疑难问题的轻量级分析手段:TCP Tracepoints
|
||||
|
||||
Tracepoint是我分析问题常用的手段之一,在遇到一些疑难问题时,我通常都会把一些相关的Tracepoint打开,把Tracepoint输出的内容保存起来,然后再在线下环境中分析。通常,我会写一些Python脚本来分析这些内容,毕竟Python在数据分析上还是很方便的。
|
||||
|
||||
对于TCP的相关问题,我也习惯使用这些TCP Tracepoints来分析问题。要想使用这些Tracepoints,你的内核版本需要为4.16及以上。这些常用的TCP Tracepoints路径位于/sys/kernel/debug/tracing/events/tcp/和/sys/kernel/debug/tracing/events/sock/,它们的作用如下表所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e8/12/e8b54452ccff8545441e4b5c655b7d12.jpg" alt="">
|
||||
|
||||
这里多说两句,表格里的tcp_rcv_space_adjust是我在分析RT抖动问题时贡献给内核的,具体你可以看[net: introduce a new tracepoint for tcp_rcv_space_adjust](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?h=v5.9-rc3&id=6163849d289be6ff2acd2fb520da303dec3219f0)这个commit。还有inet_sock_set_state,该Tracepoint也是我贡献给Linux内核的,具体可详见[net: tracepoint: replace tcp_set_state tracepoint with inet_sock_set_state tracepoint](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?h=v5.9-rc3&id=563e0bb0dc74b3ca888e24f8c08f0239fe4016b0)。其实我对“inet_sock_set_state”这个名字不太满意,本来想使用“inet_sk_set_state”来命名的,因为后者更精炼,但是为了更好地匹配内核里的struct inet_sock结构体,我还是选择了现在这个略显臃肿的名字。
|
||||
|
||||
我们回到TCP Tracepoints这一轻量级的追踪方式。有一篇文章对它讲解得很好,就是Brendan Gregg写的[TCP Tracepoints](http://www.brendangregg.com/blog/2018-03-22/tcp-tracepoints.html),这里面还详细介绍了基于Tracepoints的一些工具,如果你觉得用Python脚本解析TCP Tracepoints的输出有点麻烦,你可以直接使用里面推荐的那些工具。不过,你需要注意的是,这些工具都是基于ebpf来实现的,而ebpf有一个缺点,就是它在加载的时候CPU开销有些大。这是因为有一些编译工作比较消耗CPU,所以你在使用这些命令时要先看下你的系统CPU使用情况。当ebpf加载起来后,CPU开销就很小了,大致在1%以内。在停止ebpf工具的追踪时,也会有一些CPU开销,不过这个开销比加载时消耗的要小很多,但是你同样需要注意一下,以免影响到业务。
|
||||
|
||||
相比于tcpdump的臃肿,这些TCP Tracepoints就很轻量级了,你有必要用一用它们。
|
||||
|
||||
好了, 我们这节课就讲到这里。
|
||||
|
||||
## 课堂总结
|
||||
|
||||
我们讲了TCP问题分析的惯用套路,我再次强调一下这节课的重点:
|
||||
|
||||
- 尽量不要使用netstat命令,而是多使用它的替代品ss,因为ss的性能开销更小,运行也更快;
|
||||
- 当你面对网络问题一筹莫展时,可以考虑使用tcpdump抓包看看,当系统中的网络连接数较大时,它对系统性能会产生比较明显的影响,所以你需要想办法避免它给业务带来实质影响;
|
||||
- TCP Tracepoints是比较轻量级的分析方案,你需要去了解它们,最好试着去用一下它们。
|
||||
|
||||
## 课后作业
|
||||
|
||||
请问tcpdump在解析内核缓冲区里的数据时,为什么使用PACKET_MMAP这种方式?你了解这种方式吗?这样做的好处是什么?欢迎你在留言区与我讨论。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
Reference in New Issue
Block a user