问题背景
笔者在看自己服务状态数据的时候,会发现有很多 tcp 的连接,也会发现有很多处于不同状态下的 tcp 连接,TIME_WAIT
的连接数有83 个,为了弄清楚这个 TIME_WAIT 是什么,整理了下面的笔记用于梳理概念
基础流程
TCP的三次握手和四次挥手是TCP协议建立和终止连接的基本过程。
三次握手过程如下:
- 客户端发送SYN包(同步序列编号)给服务器,等待服务器确认。
- 服务器收到SYN包后,会确认客户的SYN(发送一个ACK),同时也会发送一个SYN包,这个步骤称为SYN+ACK。
- 客户端收到服务器的SYN+ACK后,还需向服务器发送确认包ACK。至此,完成三次握手,客户端和服务器开始传送数据。
四次挥手过程如下:
- 当数据传送完毕后,客户端会发送一个FIN包给服务器,请求关闭连接。
- 服务器收到FIN包后,会发送一个ACK给客户端,确认接收到了FIN包,但不会立即关闭连接,因为服务器可能还有数据需要处理和发送。
- 当服务器数据发送完毕后,会向客户端发送FIN包,请求关闭连接。
- 客户端收到FIN包后,会发送一个ACK给服务器,然后进入
TIME_WAIT
状态。TIME_WAIT
状态持续一段时间后,如果没有再收到服务器的消息,那么就关闭连接。至此,完成四次挥手。
为什么有 TIME_WAIT
TIME_WAIT
状态在TCP四次挥手的最后阶段出现。在TCP连接被关闭后,操作系统会等待一段时间(通常是2倍的最大分段生存时间),以确保对方收到了关闭连接的确认。这个等待时间就是TIME_WAIT
。对于复杂的网络状态,TCP 的实现提出了多种应对措施,TIME_WAIT
状态的提出就是为了应对其中一种异常状况。 此状态的存在主要有两个原因:
- 保证最后一个确认消息能被对方收到。如果直接关闭连接,那么对方可能会因为没有收到确认消息而无法关闭连接。
- 避免"旧的重复分组"在新的连接中被错误接收。由于网络原因,有可能会有一些旧的重复分组在网络中滞留,如果直接开启新的连接,这些旧的重复分组可能会被新的连接误认为是自己的数据。
为了理解 TIME_WAIT 状态的必要性,我们先来假设没有这么一种状态会导致的问题。暂以 A、B 来代指 TCP 连接的两端,A 为主动关闭的一端。
-
四次挥手中,A 发 FIN, B 响应 ACK,B 再发 FIN,A 响应 ACK 实现连接的关闭。而如果 A 响应的 ACK 包丢失,B 会以为 A 没有收到自己的关闭请求,然后会重试向 A 再发 FIN 包。
如果没有 TIME_WAIT 状态,那么A回复ACK立刻关闭,所以B重发的会让A响应重置,A 不再保存这个连接的信息,收到一个不存在的连接的包,A 会响应 RST 包,导致 B 端异常响应。
此时, TIME_WAIT 是为了保证全双工的 TCP 连接正常终止。
-
我们还知道,TCP 下的 IP 层协议是无法保证包传输的先后顺序的。如果双方挥手之后,一个网络四元组(src/dst ip/port)被回收,而此时网络中还有一个迟到的数据包没有被 B 接收,A 应用程序又立刻使用了同样的四元组再创建了一个新的连接后,这个迟到的数据包才到达 B,那么这个数据包就会让 B 以为是 A 刚发过来的。
此时, TIME_WAIT 的存在是为了保证网络中迷失的数据包正常过期。
不同场景下的 TIME_WAIT 的具体影响
基于不同的使用场景,我们一般区分为"长连接"和"短连接"
长链接
长连接,指在一个连接上可以连续发送多个数据包,在连接保持期间,如果没有数据包发送,需要双方发链路检测包。
是一种在客户端和服务器之间维持长时间连接的通信机制。与传统的短链接请求-响应模型不同,长链接允许客户端向服务器发出请求并保持连接打开,以等待服务器在有数据更新时立即响应。
长链接的工作原理通常如下:
- 客户端发起连接请求: 客户端向服务器发送一个连接请求。
- 服务器保持连接打开: 服务器接收到连接请求后,保持连接打开并等待数据变化或其他事件的发生。
- 服务器响应: 当服务器有数据更新或满足其他条件时,它立即响应客户端请求,并发送数据给客户端。
- 客户端处理响应: 客户端接收到服务器的响应后,可以处理数据更新或执行其他操作。
- 连接保持开放: 连接保持开放,客户端和服务器可以继续在连接上进行通信。
长连接的优点是:
这种长链接机制的优势在于实时性更强,因为服务器可以立即将更新推送给客户端,而无需等待客户端发起新的请求。这对于实时通信、即时消息推送以及需要及时获取数据更新的应用场景非常有用。
- 减少了建立连接和断开连接的开销,提高了传输效率。
- 减少了连接数,节省了服务器资源。
- 便于维护和管理。
长连接的缺点是:
长链接也有一些缺点,例如在一些网络环境中可能存在连接超时的问题,而且在维持大量长连接时可能增加服务器的负载。因此,开发人员需要根据具体的应用场景和需求来选择适当的通信机制。
- 可能会导致连接泄漏,造成资源浪费。
- 可能会导致连接超时,影响传输效率。
长连接的应用场景包括:
- 聊天软件:客户端和服务器之间需要保持长连接,以便及时交换消息。
- 文件传输:服务器和客户端之间需要保持长连接,以便传输大文件。
- 实时数据传输:服务器和客户端之间需要保持长连接,以便实时传输数据。
jsx
package main
import (
"fmt"
"net"
"time"
)
func handleLongConnection(conn net.Conn) {
defer conn.Close()
for {
// 读取客户端发送的数据
buffer := make([]byte, 1024)
_, err := conn.Read(buffer)
if err != nil {
fmt.Println("Error reading:", err)
return
}
// 处理数据
fmt.Println("Received:", string(buffer))
// 模拟长连接,等待一段时间再回复客户端
time.Sleep(time.Second * 5)
// 向客户端发送响应
response := []byte("Server response")
conn.Write(response)
}
}
func main() {
listener, err := net.Listen("tcp", ":8080")
if err != nil {
fmt.Println("Error listening:", err)
return
}
defer listener.Close()
fmt.Println("Server listening on :8080 for long connection")
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("Error accepting connection:", err)
continue
}
// 启动长连接处理协程
go handleLongConnection(conn)
}
}
短连接
短连接是指在数据传送过程中,只在需要发送数据时,才去建立一个连接,数据发送完成后,则断开此连接,即每次连接只完成一项业务的发送。
在计算机网络中,短连接(short connection)是指在数据传送过程中,只在需要发送数据时,才去建立一个连接,数据发送完成后,则断开此连接,即每次连接只完成一项业务的发送。
短连接的优点是**:**
- 资源释放: 短连接在完成数据传输后会立即释放资源,不需要维持长时间的连接状态,因此能够更有效地释放系统资源。
- 简单易实现: 短连接模型相对简单,易于实现和维护。每个请求都是独立的,不需要维持连接状态。
- 连接灵活性: 短连接适用于一些场景,如HTTP请求,每次请求都是独立的,适合短暂的数据交互。
- 适用于并发: 短连接模型适用于并发连接,因为每个连接都是独立的,不会影响其他连接。
短连接的缺点是:
- 连接建立开销: 每次建立连接都需要进行握手过程,包括TCP的三次握手,这会增加网络开销。
- 频繁的连接断开和建立: 对于高频率的短连接,频繁的连接断开和建立可能会增加系统开销,尤其是在高并发的情况下。
- 维护开销: 如果应用中存在频繁的连接建立和断开,服务器需要维护大量的连接状态信息,可能会增加服务器的负担。
- 实时性差: 短连接可能无法满足实时性要求较高的应用场景,因为连接建立和断开的开销可能影响数据的实时传输。
短连接的应用场景包括:
- 浏览器和服务器之间的HTTP请求。
- 邮件服务器和客户端之间的SMTP/POP3/IMAP协议。
- 聊天软件客户端和服务器之间的聊天协议。
jsx
package main
import (
"fmt"
"net"
)
func handleShortConnection(conn net.Conn) {
defer conn.Close()
// 读取客户端发送的数据
buffer := make([]byte, 1024)
_, err := conn.Read(buffer)
if err != nil {
fmt.Println("Error reading:", err)
return
}
// 处理数据
fmt.Println("Received:", string(buffer))
// 向客户端发送响应
response := []byte("Server response")
conn.Write(response)
}
func main() {
listener, err := net.Listen("tcp", ":8081")
if err != nil {
fmt.Println("Error listening:", err)
return
}
defer listener.Close()
fmt.Println("Server listening on :8081 for short connection")
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("Error accepting connection:", err)
continue
}
// 启动短连接处理
handleShortConnection(conn)
}
}
正常的TCP客户端连接在关闭后,会进入一个TIME_WAIT
的状态,持续的时间一般在1~4分钟,如果短时间内(例如1s内)进行大量的短连接,则可能出现这样一种情况:客户端所在的操作系统的socket端口和句柄被用尽,系统无法再发起新的连接!如果 TIME_WAIT
连接过多,会消耗大量的系统资源,会耗尽可用的网络端口,从而阻止新的连接建立。因此,对于处于 TIME_WAIT
状态的连接,需要进行合理的管理和控制。
举例来说:假设每秒建立了1000个短连接(Web场景下是很常见的,例如每个请求都去访问),假设TIME_WAIT的时间是1分钟,则1分钟内需要建立6W个短连接,由于TIME_WAIT时间是1分钟,这些短连接1分钟内都处于TIME_WAIT状态,都不会释放,而Linux默认的本地端口范围配置是:net.ipv4.ip_local_port_range = 32768 ~ 61000 不到3W,因此这种情况下新的请求由于没有本地端口就不能建立了。
缓解办法
代码侧
GOLANG
go 里面 Transport 默认参数有个 MaxIdleConns 是100,还有 Timeout 是 90s,这两个参数会导致 client 自动发起 FIN,代码侧可以优化这边的逻辑,不同的语言应该有对应的优化方法。
系统侧
在TCP连接中,TIME_WAIT
状态是在连接关闭后等待一段时间的状态,以确保对方收到了最后的ACK。这是为了处理网络中的滞留数据报文(可能在网络中延迟到达),防止它们被新的连接误认为是旧的连接的问题。
降低TIME_WAIT
状态对于提高连接的重用速度是有风险的,因为它可能导致旧的数据报文在网络中被混淆。然而,如果你仍然想要降低TIME_WAIT
状态的等待时间,可以在系统上进行一些调整。请注意,对于这样的操作,你应该非常小心,因为它可能会对网络稳定性产生负面影响。
在Linux系统上,你可以通过修改内核参数来调整TIME_WAIT
状态的等待时间。
- net.ipv4.tcp_max_tw_buckets :指定系统同时保持
TIME_WAIT
的最大数量。默认值为 180000。 - net.ipv4.tcp_tw_reuse :如果设置为 1,则允许
TIME_WAIT
状态的连接被重用。默认值为 0。 - net.ipv4.tcp_tw_recycle :如果设置为 1,则允许
TIME_WAIT
状态的连接在 FIN_WAIT-2 状态时被快速回收。默认值为 0。
以下是一些可能的方法:
方法一:通过sysctl修改
通过sysctl可以动态地调整内核参数。
bash
sudo sysctl -w net.ipv4.tcp_tw_reuse=1
sudo sysctl -w net.ipv4.tcp_tw_recycle=1
net.ipv4.tcp_tw_reuse=1
允许将TIME_WAIT状态的连接端口重用。net.ipv4.tcp_tw_recycle=1
尝试根据时间戳来快速回收TIME_WAIT
状态的连接。
这种方法修改是暂时的,重启后会失效。
方法二:修改sysctl配置文件
将上述配置添加到 /etc/sysctl.conf
文件中,以便在系统启动时应用。
bash
echo "net.ipv4.tcp_tw_reuse=1" | sudo tee -a /etc/sysctl.conf
echo "net.ipv4.tcp_tw_recycle=1" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
这种方法在系统重启后仍然有效。
请注意,在一些情况下,使用 net.ipv4.tcp_tw_recycle
可能会导致问题,因为它可能与一些网络设备不兼容。在生产环境中,修改这些参数前应该进行充分测试,以确保它们不会对系统的稳定性和性能产生负面影响。
总的来说,TIME_WAIT
是TCP连接管理中的一个重要环节,尽管它可能会引发一些问题,但适当的管理和调优可以最大程度地减轻这些问题的影响,保证网络连接的正常运行。
总结
在 Go 中,HTTP客户端(例如**http.Client
**)发起的HTTP请求的完成过程可能涉及到TCP连接的正常关闭,从而触发发送TCP FIN(Finish)标志。这是正常的TCP连接关闭行为,其触发条件包括:
- HTTP请求完成: 当HTTP请求完成(例如,成功获取到响应或发生错误)时,客户端的**
http.Transport
**可能会将连接放回到连接池以便复用,或者决定是否关闭连接。 - 连接池空闲时: 如果连接池中没有空闲的连接,并且该连接也没有被标记为永久保持活跃(通过**
Transport.DisableKeepAlives
**设置),那么连接可能会被关闭。 - 达到最大空闲连接数: 如果连接池中的连接数量达到了
http.Transport
的MaxIdleConns
或MaxIdleConnsPerHost
设置的最大空闲连接数,那么一些连接可能会被关闭。 - 连接过期:
http.Transport
会根据连接的空闲时间进行管理。如果连接在一段时间内未被使用,可能会被关闭。
这些操作是为了维护连接池的健康状态,确保连接的新鲜性,并防止长时间空闲的连接占用资源。触发TCP FIN标志是连接关闭的一部分,用于通知对端连接即将关闭。
引用
CHATGPT
BARD
www.xiaolincoding.com/network/3_t...