You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

14 KiB

11 基础篇 | TCP连接的建立和断开受哪些系统配置影响

你好,我是邵亚方。

如果你做过Linux上面网络相关的开发或者分析过Linux网络相关的问题那你肯定吐槽过Linux系统里面让人眼花缭乱的各种配置项应该也被下面这些问题困扰过

  • Client为什么无法和Server建立连接呢
  • 三次握手都完成了为什么会收到Server的reset呢
  • 建立TCP连接怎么会消耗这么多时间
  • 系统中为什么会有这么多处于time-wait的连接该这么处理
  • 系统中为什么会有这么多close-wait的连接
  • 针对我的业务场景,这么多的网络配置项,应该要怎么配置呢?
  • ……

因为网络这一块涉及到的场景太多了Linux内核需要去处理各种各样的网络场景不同网络场景的处理策略也会有所不同。而Linux内核的默认网络配置可能未必会适用我们的场景这就可能导致我们的业务出现一些莫名其妙的行为。

所以要想让业务行为符合预期你需要了解Linux的相关网络配置让这些配置更加适用于你的业务。Linux中的网络配置项是非常多的为了让你更好地了解它们我就以最常用的TCP/IP协议为例从一个网络连接是如何建立起来的以及如何断开的来开始讲起。

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包重传的策略大致如下

在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在收到Server的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状态

上图就是从Client调用connect()到Server侧accept()成功返回这一过程中的TCP状态转换。这些状态都可以通过netstat或者ss命令来看。至此Client和Server两边就可以正常通信了。

接下来我们看下TCP连接断开过程中会受哪些系统配置项的影响。

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.

对于CLOSE_WAIT状态而言系统中没有对应的配置项。但是该状态也是一个危险信号如果这个状态的TCP连接较多那往往意味着应用程序有Bug在某些条件下没有调用close()来关闭连接。我们在生产环境上就遇到过很多这类问题。所以如果你的系统中存在很多CLOSE_WAIT状态的连接那你最好去排查一下你的应用程序看看哪里漏掉了close()。

至此TCP四次挥手过程中需要注意的事项也讲完了。

好了,我们这节课就到此为止。

课堂总结

这节课我们讲了很多的配置项,我把这些配置项汇总到了下面这个表格里,方便你记忆:

当然了有些配置项也是可以根据你的服务器负载以及CPU和内存大小来做灵活配置的比如tcp_max_syn_backlog、somaxconn、tcp_max_tw_buckets这三项如果你的物理内存足够大、CPU核数足够多你可以适当地增大这些值这些往往都是一些经验值。

另外,我们这堂课的目的不仅仅是为了让你去了解这些配置项,最主要的是想让你了解其背后的机制,这样你在遇到一些问题时,就可以有一个大致的分析方向。

课后作业

课后请你使用tcpdump这个工具来观察下TCP的三次握手和四次挥手过程巩固今天的学习内容。欢迎在留言区分享你的看法。

感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。