一、状态的产生
要解决TIME_WAIT状态过多的问题,先来研究下TIME_WAIT状态的产生,下面是TCP连接断开时的四次挥手状态转换图,说明一点,途中显示的是客户端主动断开连接,tcp连接也可以由服务器端主动断开连接。我们先来描述一下断开的状态:
1)客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
2)服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
3)客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
4)服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
5)客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2MSL(最长报文段寿命,RFC规定一个MSL为2min,linux中一般设置为30s)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
6)服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
可以看到TIME_WAIT状态产生是在tcp连接主动关闭的一端产生的正常tcp状态,超过两个MSL之后,就会关闭,释放占用的端口。基于以上的分析我们可以推断,在我们的应用中产生大量TIME_WAIT状态的根本原因是频繁创建断开连接TCP连接。要解决TIME_WATIT状态过多的问题,就要分析我们的应用把频繁创建的短连接改为长连接。
二、常见的短连接产生的场景
1.服务连接服务
后台业务服务器,通常需要调用redis、mysql以及其他http服务和grpc服务,在服务相互调用中,如果使用的是短连接,高并发时就会产生大量TIME_WAIT,如何解决呢?一般情况下,redis等客户端会有连接池,我们要做的是设置好相关的连接服用参数,一般会有连接数、连接重用时间、连接空闲数等。所以在应用中通过设置合理的连接池参数可以避免TIME_WAIT状态过多的问题:
1.检查http连接池
2.检查grpc连接池
3.检查redis连接池
4.检查mysql连接池
...
我们来查看一个mysql连接池配置信息,最大连接数100,最大空闲连接数10,测试的并发数50,产生的效果如下:
可以看到TIME_WAIT状态快速上升,我们查看redis客户端的连接情况:
{MaxOpenConnections:100 OpenConnections:1 InUse:0 Idle:1 WaitCount:0 WaitDuration:0s MaxIdleClosed:0 MaxLifetimeClosed:0}
{MaxOpenConnections:100 OpenConnections:17 InUse:15 Idle:2 WaitCount:0 WaitDuration:0s MaxIdleClosed:48 MaxLifetimeClosed:0}
{MaxOpenConnections:100 OpenConnections:51 InUse:44 Idle:7 WaitCount:0 WaitDuration:0s MaxIdleClosed:82 MaxLifetimeClosed:0}
{MaxOpenConnections:100 OpenConnections:51 InUse:50 Idle:1 WaitCount:0 WaitDuration:0s MaxIdleClosed:90 MaxLifetimeClosed:0}
{MaxOpenConnections:100 OpenConnections:50 InUse:49 Idle:1 WaitCount:0 WaitDuration:0s MaxIdleClosed:126 MaxLifetimeClosed:0}
{MaxOpenConnections:100 OpenConnections:51 InUse:49 Idle:2 WaitCount:0 WaitDuration:0s MaxIdleClosed:131 MaxLifetimeClosed:0}
{MaxOpenConnections:100 OpenConnections:50 InUse:49 Idle:1 WaitCount:0 WaitDuration:0s MaxIdleClosed:181 MaxLifetimeClosed:0}
{MaxOpenConnections:100 OpenConnections:51 InUse:51 Idle:0 WaitCount:0 WaitDuration:0s MaxIdleClosed:233 MaxLifetimeClosed:0}
{MaxOpenConnections:100 OpenConnections:51 InUse:51 Idle:0 WaitCount:0 WaitDuration:0s MaxIdleClosed:240 MaxLifetimeClosed:0}
{MaxOpenConnections:100 OpenConnections:46 InUse:38 Idle:8 WaitCount:0 WaitDuration:0s MaxIdleClosed:296 MaxLifetimeClosed:0}
{MaxOpenConnections:100 OpenConnections:51 InUse:50 Idle:1 WaitCount:0 WaitDuration:0s MaxIdleClosed:313 MaxLifetimeClosed:0}
{MaxOpenConnections:100 OpenConnections:51 InUse:50 Idle:1 WaitCount:0 WaitDuration:0s MaxIdleClosed:363 MaxLifetimeClosed:0}
{MaxOpenConnections:100 OpenConnections:51 InUse:50 Idle:1 WaitCount:0 WaitDuration:0s MaxIdleClosed:409 MaxLifetimeClosed:0}
{MaxOpenConnections:100 OpenConnections:50 InUse:48 Idle:2 WaitCount:0 WaitDuration:0s MaxIdleClosed:438 MaxLifetimeClosed:0}
{MaxOpenConnections:100 OpenConnections:49 InUse:49 Idle:0 WaitCount:0 WaitDuration:0s MaxIdleClosed:494 MaxLifetimeClosed:0}
分析发现MaxIdleClosed数据持续上升,此为mysql客户端连接池配置不合理产生大量TIME_WAIT状态的例子
2.网络抖动
网络情况不好时,如果主动方无TIME_WAIT等待,关闭前个连接后,主动方与被动方又建立起新的TCP连接,这时被动方重传或延时过来的FIN包过来后会直接影响新的TCP连接。同样网络情况不好并且无TIME_WAIT等待,关闭连接后无新连接,当接收到被动方重传或延迟的FIN包后,会给被动方回一个RST包,可能会影响被动方其它的服务连接。
网络抖动问题比较好排查,直接使用ping命令可以观察到。
首先,我们需要明确, 只有主动断开的那一方才会进入 TIME_WAIT 状态 ,且会在那个状态持续 2 个 MSL(Max Segment Lifetime)。
为了讲清楚 TIME_WAIT,需要先介绍一下 MSL 的概念。
MSL(报文最大生存时间)是 TCP 报文在网络中的最大生存时间。这个值与 IP 报文头的 TTL 字段有密切的关系。
IP 报文头中有一个 8 位的存活时间字段(Time to live, TTL)如下图。 这个存活时间存储的不是具体的时间,而是一个 IP 报文最大可经过的路由数,每经过一个路由器,TTL 减 1,当 TTL 减到 0 时这个 IP 报文会被丢弃。
TTL 经过路由器不断减小的过程如下图所示,假设初始的 TTL 为 12,经过下一个路由器 R1 以后 TTL 变为 11,后面每经过一个路由器以后 TTL 减 1
从上面可以看到 TTL 说的是「跳数」限制而不是「时间」限制,尽管如此我们依然假设 最大跳数的报文在网络中存活的时间不可能超过 MSL 秒 。
Linux 的套接字实现假设 MSL 为 30 秒,因此在 Linux 机器上 TIME_WAIT 状态将持续 60秒。
要构造一个 TIME_WAIT 非常简单,只需要建立一个 TCP 连接,然后断开某一方连接,主动断开的那一方就会进入 TIME_WAIT 状态,我们用 Linux 上开箱即用的 nc 命令来构造一个。
过程如下图:
在机器 c2 上用nc -l 8888启动一个 TCP 服务器
在机器 c1 上用 nc c2 8888 创建一条 TCP 连接
在机器 c1 上用 Ctrl+C 停止 nc 命令,随后在用netstat -atnp | grep 8888查看连接状态。
第一个原因是:数据报文可能在发送途中延迟但最终会到达,因此要等老的“迷路”的重复报文段在网络中过期失效,这样可以避免用相同源端口和目标端口创建新连接时收到旧连接姗姗来迟的数据包,造成数据错乱。
比如下面的例子
假设客户端 10.211.55.2 的 61594 端口与服务端 10.211.55.10 的 8080 端口一开始建立了一个 TCP 连接。
假如客户端发送完 FIN 包以后不等待直接进入 CLOSED 状态,老连接 SEQ=3 的包因为网络的延迟。过了一段时间 相同 的 IP 和端口号又新建了另一条连接,这样 TCP 连接的四元组就完全一样了。
恰好 SEQ 因为回绕等原因 也正好相同,那么 SEQ=3 的包就无法知道到底是旧连接的包还是新连接的包了,造成新连接数据的混乱。
TIME_WAIT 等待时间是 2 个 MSL,已经足够让一个方向上的包最多存活 MSL 秒就被丢弃,保证了在创建新的 TCP 连接以后,老连接姗姗来迟的包已经在网络中被丢弃消逝,不会干扰新的连接。
第二个原因是确保可靠实现 TCP 全双工终止连接。
关闭连接的四次挥手中,最终的 ACK 由主动关闭方发出,如果这个 ACK 丢失,对端(被动关闭方)将重发 FIN,如果主动关闭方不维持 TIME_WAIT 直接进入 CLOSED 状态,则无法重传 ACK,被动关闭方因此不能及时可靠释放。
如果四次挥手的第 4 步中客户端发送了给服务端的确认 ACK 报文以后不进入 TIME_WAIT 状态,直接进入 CLOSED状态,然后重用端口建立新连接会发生什么呢?
如下图所示
主动关闭方如果马上进入 CLOSED 状态,被动关闭方这个时候还处于LAST-ACK状态,主动关闭方认为连接已经释放,端口可以重用了, 如果使用相同的端口三次握手发送 SYN 包,会被处于 LAST-ACK状态状态的被动关闭方返回一个 RST,三次握手失败。
为什么时间是两个 MSL?
1 个 MSL 确保四次挥手中主动关闭方最后的 ACK 报文最终能达到对端
1 个 MSL 确保对端没有收到 ACK 重传的 FIN 报文可以到达
2MS = 去向 ACK 消息最大存活时间(MSL) + 来向 FIN 消息的最大存活时间(MSL)
在一个非常繁忙的服务器上,如果有大量 TIME_WAIT 状态的连接会怎么样呢?
连接表无法复用
socket 结构体内存占用
连接表无法复用 因为处于 TIME_WAIT 的连接会存活 2MSL(60s),意味着相同的TCP 连接四元组(源端口、源 ip、目标端口、目标 ip)在一分钟之内都没有办法复用,通俗一点来讲就是“占着茅坑不拉屎”。
假设主动断开的一方是客户端,对于 web 服务器而言,目标地址、目标端口都是固定值(比如本机 ip + 80 端口),客户端的 IP 也是固定的,那么能变化的就只有端口了,在一台 Linux 机器上,端口最多是 65535 个( 2 个字节)。
如果客户端与服务器通信全部使用短连接,不停的创建连接,接着关闭连接,客户端机器会造成大量的 TCP 连接进入 TIME_WAIT 状态。
可以来写一个简单的 shell 脚本来测试一下,使用 nc 命令连接 redis 发送 ping 命令以后断开连接。
如果在 60s 内有超过 65535 次 redis 短连接 *** 作,就会出现端口不够用的情况,这也是使用 连接池 的一个重要原因。
针对 TIME_WAIT 持续时间过长的问题,Linux 新增了几个相关的选项,net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_tw_recycle。
下面我们来说明一下这两个参数的用意。 这两个参数都依赖于 TCP 头部的扩展选项:timestamp
TCP 头部时间戳选项(TCP Timestamps Option,TSopt)
除了我们之前介绍的 MSS、Window Scale 还有以一个非常重要的选项:时间戳(TCP Timestamps Option,TSopt)
它由四部分构成:类别(kind)、长度(Length)、发送方时间戳(TS value)、回显时间戳(TS Echo Reply)。
时间戳选项类别(kind)的值等于 8,用来与其它类型的选项区分。长度(length)等于 10。两个时间戳相关的选项都是 4 字节。
如下图所示:
是否使用时间戳选项是在三次握手里面的 SYN 报文里面确定的。
下面的包是 curl github.com 抓包得到的结果:
发送方发送数据时,将一个发送时间戳 1734581141 放在发送方时间戳 TSval 中
接收方收到数据包以后,将收到的时间戳 1734581141 原封不动的返回给发送方,放在 TSecr 字段中,同时把自己的时间戳 3303928779 放在 TSval 中
后面的包以此类推
有几个需要说明的点:
1. 时间戳是一个单调递增的值,与我们所知的 epoch 时间戳不是一回事。 这个选项不要求两台主机进行时钟同步
2. timestamps 是一个双向的选项,如果只要有一方不开启,双方都将停用 timestamps。
比如下面是curl www.baidu.com得到的包
可以看到客户端发起 SYN 包时带上了自己的TSval,服务器回复的SYN+ACK 包没有TSval和TSecr,从此之后的包都没有带上时间戳选项了。
有了这个选项,我们来看一下 tcp_tw_reuse 选项。
缓解紧张的端口资源,一个可行的方法是重用“浪费”的处于 TIME_WAIT 状态的连接,当开启 net.ipv4.tcp_tw_reuse 选项时,处于 TIME_WAIT 状态的连接可以被重用。
下面把主动关闭方记为 A, 被动关闭方记为 B,它的原理是:
如果主动关闭方 A 收到的包时间戳比当前存储的时间戳小,说明是一个迷路的旧连接的包,直接丢弃掉
如果因为 ACK 包丢失导致被动关闭方还处于LAST-ACK状态,并且会持续重传 FIN+ACK。这时 A 发送SYN 包想三次握手建立连接,此时 A 处于SYN-SENT阶段。 当收到 B 的 FIN 包时会回以一个 RST 包给 B,B 这端的连接会进入 CLOSED 状态,A 因为没有收到 SYN 包的 ACK,会重传 SYN,后面就一切顺利了。
tcp_tw_recyle 是一个比 tcp_tw_reuse 更激进的方案, 系统会缓存每台主机(即 IP)连接过来的最新的时间戳。
对于新来的连接,如果发现 SYN 包中带的时间戳与之前记录的来自同一主机的同一连接的分组所携带的时间戳相比更旧,则直接丢弃;如果更新则接受复用 TIME-WAIT 连接。
这种机制在客户端与服务端一对一的情况下没有问题,如果经过了 NAT 或者负载均衡,问题就很严重了。
什么是 NAT呢?
NAT(Network Address Translator)的出现是为了缓解 IP 地址耗尽的临时方案,IPv4 的地址是 32 位,全部利用最 多只能提 42.9 亿个地址,去掉保留地址、组播地址等剩下的只有 30 多亿,互联网主机数量呈指数级的增长,如果给每个设备都分配一个唯一的 IP 地址,那根本不够。于是 1994 年推出的 NAT 规范,NAT 设备负责维护局域网私有 IP 地址和端口到外网 IP 和端口的映射规则。
它有两个明显的优点:
出口 IP 共享:通过一个公网地址可以让许多机器连上网络,解决 IP 地址不够用的问题
安全隐私防护:实际的机器可以隐藏自己真实的 IP 地址 当然也有明显的弊端:NAT 会对包进行修改,有些协议无法通过 NAT。
当 tcp_tw_recycle 遇上 NAT 时,因为客户端出口 IP 都一样,会导致服务端看起来都在跟同一个 host 打交道。
不同客户端携带的 timestamp 只跟自己相关,如果一个时间戳较大的客户端 A 通过 NAT 与服务器建连,时间戳较小的客户端 B 通过 NAT 发送的包服务器认为是过期重复的数据,直接丢弃,导致 B 无法正常建连和发数据。
TIME_WAIT 状态是最容易造成混淆的一个概念,这个状态存在的意义是:
1. 可靠的实现 TCP 全双工的连接终止(处理最后 ACK 丢失的情况)
2. 避免当前关闭连接与后续连接混淆(让旧连接的包在网络中消逝)
假设 MSL 是 60s,请问系统能够初始化一个新连接然后主动关闭的最大速率是多少(忽略1~1024区间的端口)?
2MSL = 120s,(65535 - 1024) / 120 = 537.6 次/秒
每120秒可以初始化(65535-1024 )个
“时间戳是一个单调递增的值,与我们所知的 epoch 时间戳不是一回事” 这个epoch和时间戳分别是什么差异?
不是一回事,跟时间没有什么关系,只是随着时钟信号CPU中断递增。
SO_REUSEADDR是针对服务端的,tcp_tw_reuse和tcp_tw_recyle是针对客户端的,可以这样理解吗?
SO_REUSEADDR 两端都可以用,不过服务端上因为经常要固定端口,不设置,下次重启就bind 失败 。
tcp_tw_reuse和tcp_tw_recyle 也是主要用于繁忙的“服务端”,“客户端”和“服务端”这个说法是在不同的场景下可以互相转换的,服务端也可以发起请求充当客户端 。
深入理解 TCP 协议:从原理到实战
https://juejin.cn/book/6844733788681928712/section/6844733788837117959
从SO_REUSEADDR选项说起
https://zhuanlan.zhihu.com/p/31329253
在使用Connection之后,是否关闭了。再检查一下Statement和ResultSet是否关闭。只是出现错误提示吗?是否可以对数据库进行 *** 作?
(我在每次查询完都在Finally内调用了连接的Close()方法)。能否给我看看你的代码。
Cannot get a connection, pool error Timeout waiting for idle object.这种错误,说明池中连接用尽,而用户获取连接等待超时。你只给这么一段连接池的配置是不行的。配置应该没有问题,是你的代码有问题。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)