This commit is contained in:
louzefeng
2024-07-09 18:38:56 +00:00
parent 8bafaef34d
commit bf99793fd0
6071 changed files with 1017944 additions and 0 deletions

View File

@@ -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包后就会发出ACKServer收到该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之前的内核中默认都是1285.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上是60sTCP_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的三次握手和四次挥手过程巩固今天的学习内容。欢迎在留言区分享你的看法。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。

View 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(&quot;sk_stream_wait_memory&quot;)
{
printf(&quot;%d %s TCP send buffer overflow\n&quot;,
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&amp;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 &gt; /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拥塞控制的话那就需要将它调整为fqfair 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的处理效率就会大打折扣所以就产生了NAPINew API这种机制让CPU一次性地去轮询poll多个数据包以批量处理的方式来提升效率降低网卡中断带来的性能开销。
那在poll的过程中一次可以poll多少个呢这个poll的个数可以通过sysctl选项来控制
>
net.core.netdev_budget = 600
该控制选项的默认值是300在网络吞吐量较大的场景中我们可以适当地增大该值比如增大到600。增大该值可以一次性地处理更多的数据包。但是这种调整也是有缺陷的因为这会导致CPU在这里poll的时间增加如果系统中运行的任务很多的话其他任务的调度延迟就会增加。
接下来继续看TCP数据包的接收过程。我们刚才提到数据包到达网卡后会触发CPU去poll数据包这些poll的数据包紧接着就会到达IP层去处理然后再达到TCP层这时就会面对另外一个很容易引发问题的地方了TCP Receive BufferTCP接收缓冲区
与 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&amp;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吗欢迎你在留言区与我讨论。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。

View File

@@ -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。这个阶段每经过一个RTTround-trip time发包数量就会翻倍。如下图所示
<img src="https://static001.geekbang.org/resource/image/05/4d/0534ce8d1e3a09a1def9c27e387eb64d.jpg" alt="" title="TCP Slow Start示意图">
初始发送数据包的数量是由init_cwnd初始拥塞窗口来决定的该值在Linux内核中被设置为10TCP_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。紧接着发送端就会接收到三个相同的ackack 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&amp;id=9ad7c049f0f79c418e293b1b68cf10d68f54fcdb)这可以显著节省业务的阻塞时间。不过RTO=1s 在某些场景下还是有些大了,特别是在数据中心内部这种网络质量相对比较稳定的环境中。
我们在生产环境中发生过这样的案例业务人员反馈说业务RT抖动得比较厉害我们使用strace初步排查后发现进程阻塞在了send()这类发包函数里。然后我们使用tcpdump来抓包发现发送方在发送数据后迟迟不能得到对端的响应一直到RTO时间再次重传。与此同时我们还尝试了在对端也使用tcpdump来抓包发现对端是过了很长时间后才收到数据包。因此我们判断是网络发生了拥塞从而导致对端没有及时收到数据包。
那么针对这种网络拥塞引起业务阻塞时间太久的情况有没有什么解决方案呢一种解决方案是创建TCP连接使用SO_SNDTIMEO来设置发送超时时间以防止应用在发包的时候阻塞在发送端太久如下所示
>
ret = setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &amp;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:((&quot;sshd&quot;,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这个字段只有16bitwin能够表示的大小最大只有6553564K所以如果想要支持更大的接收窗口以满足高性能网络我们就需要打开下面这个配置项系统中也是默认打开了该选项
>
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连接还正常吗为什么欢迎你在留言区与我讨论。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。

View File

@@ -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/SClient/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处理完该请求然后回responseClient侧收到response后再去发下一个request然后MySQL收到下一个request并处理。也就是说这种模型是典型的串行方式处理完了一个再去处理下一个。所以tcprstat就可以以数据包到达MySQL Server侧作为起始时间点以MySQL将最后一个数据包发出去作为结束时间点然后这二者的时间差就是RTResponse 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抖动时我们就能够区分出抖动是发生在ClientServer还是网络中了这会大大提升分析定位问题的效率。在定位到问题出在哪里后你就可以使用我们在“[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来作为这次网络耗时为什么欢迎你在留言区与我讨论。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。

View 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 = (RetransSegslast RetransSegs) (OutSegslast 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 -&gt; 124.74.250.144 TCP 70 [TCP Retransmission] 35993 &gt; https [SYN] Seq=0 Win=14600 Len=0 MSS=1460 SACK_PERM=1 TSval=3231504691 TSecr=0
3659 22.277070 10.17.130.20 -&gt; 124.74.250.144 TCP 70 [TCP Retransmission] 35993 &gt; https [SYN] Seq=0 Win=14600 Len=0 MSS=1460 SACK_PERM=1 TSval=3231506691 TSecr=0
8649 46.539393 58.216.21.165 -&gt; 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重传情况那么直接追踪该函数就可以了。
追踪内核函数最通用的方法是使用KprobeKprobe的大致原理如下
<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 &gt; /sys/kernel/debug/tracing/tracing_on
```
然后在追踪结束后,你需要来关闭他们:
```
$ echo 0 &gt; /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 &gt; tcp/tcp_retransmit_skb/enable
```
然后你就可以追踪TCP重传事件了
```
$ cat trace_pipe
&lt;idle&gt;-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 &gt; 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重传包吗为什么欢迎你在留言区与我讨论。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。

View 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:((&quot;test&quot;,pid=11307,fd=17))
LISTEN0 5 0.0.0.0:33811 0.0.0.0:* users:((&quot;test&quot;,pid=11307,fd=19))
ESTAB 0 0 127.0.0.1:57396 127.0.1.1:34751 users:((&quot;test&quot;,pid=11307,fd=106))
ESTAB 0 0 127.0.0.1:57384 127.0.1.1:34751 users:((&quot;test&quot;,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&amp;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&amp;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这种方式你了解这种方式吗这样做的好处是什么欢迎你在留言区与我讨论。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。