mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
489 字
1 分钟
为什么 TCP 协议有 TIME_WAIT 状态
2023-07-05

TCP 连接关闭后,socket 可能会进入 TIME_WAIT 状态,并保留一段时间。这是为什么?本文深入解析 TIME_WAIT 状态的成因、作用和处理策略。

一、TCP 连接关闭流程#

1.1 四次挥手#

sequenceDiagram participant C as 客户端 participant S as 服务端 C->>S: FIN seq=u S->>C: ACK ack=u+1 Note over S: 客户端已关闭发送,<br/>但服务端可能还有数据要发 S->>C: FIN seq=v C->>S: ACK ack=v+1 Note over C: 进入 TIME_WAIT 状态<br/>等待 2MSL

1.2 状态转换#

flowchart LR subgraph 客户端状态 C1[ESTABLISHED] --> C2[FIN_WAIT_1] C2 --> C3[FIN_WAIT_2] C2 --> C4[CLOSING] C3 --> C4 C4 --> C5[TIME_WAIT] C3 --> C5 end subgraph 服务端状态 S1[ESTABLISHED] --> S2[CLOSE_WAIT] S2 --> S3[LAST_ACK] S3 --> S4[CLOSED] end

二、为什么需要 TIME_WAIT?#

2.1 防止旧连接的延迟数据包被新连接接收#

sequenceDiagram participant C as 客户端 participant S as 服务端 participant N as 网络 Note over C,S: 旧连接关闭 C->>S: FIN, ACK S->>C: ACK C->>S: ACK Note over C: 进入 TIME_WAIT S->>C: 数据包 X (延迟到达) Note over C: 如果没有 TIME_WAIT<br/>数据包 X 会被新连接接收!

TIME_WAIT 的作用:确保旧连接的延迟数据包在网络中消失后再关闭。

2.2 保证被动关闭方收到最后的 ACK#

sequenceDiagram participant C as 客户端 participant S as 服务端 C->>S: FIN seq=u S->>C: ACK ack=u+1 S->>C: FIN seq=v Note over C: ACK 丢失! C->>S: ACK Note over S: 等待重传的 FIN Note over C: TIME_WAIT 保护 ACK 不被延迟

如果 ACK 丢失:被动关闭方会重传 FIN,TIME_WAIT 确保这个重传能被正确处理。

2.3 MSL 的概念#

MSL(Maximum Segment Lifetime) 是数据包在网络中的最大存活时间:

# Linux MSL 默认值
cat /proc/sys/net/ipv4/tcp_fin_timeout
# 通常是 60 秒
# MSL 通常是 60 秒(2MSL = 120 秒)

三、TIME_WAIT 的问题#

3.1 端口耗尽#

flowchart LR C[客户端] --> S1[服务端:80] C --> S2[服务端:80] C --> S3[服务端:80] C -->|...| SN[更多连接] Note over C: TIME_WAIT 连接占用端口<br/>端口范围: 32768-60999<br/>约 28000 个端口 style C fill:#f96

在高并发短连接场景下:

# 问题代码
for i in range(100000):
conn = socket.socket()
conn.connect(("server", 80))
conn.close() # 每次都产生 TIME_WAIT
# 可能导致:
# OSError: [Errno 99] Cannot assign requested address

3.2 内存占用#

# 每个 TIME_WAIT 连接占用内存
# - socket 描述符
# - TCP 状态信息
# - 接收/发送缓冲区
# 监控 TIME_WAIT 连接数
ss -ant | grep TIME-WAIT | wc -l

四、优化策略#

4.1 启用 tcp_tw_reuse#

# 启用 TIME_WAIT 重用
echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse
# 允许将 TIME_WAIT socket 用于新的连接
# 适用于客户端

原理:当新连接的序列号在旧连接的序列号范围之外时,可以使用处于 TIME_WAIT 的端口。

4.2 缩短 MSL 时间#

# 降低 MSL(谨慎)
echo 30 > /proc/sys/net/ipv4/tcp_fin_timeout
# 默认是 60 秒

4.3 套接字选项 SO_REUSEADDR#

# 服务器端应该设置 SO_REUSEADDR
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('0.0.0.0', 8080))
# Nginx 配置
server {
listen 80 reuseport;
# reuseport 启用 SO_REUSEPORT
}

4.4 客户端策略#

# 客户端使用连接池
class ConnectionPool:
def __init__(self, max_connections=100):
self.pool = queue.Queue(max_connections)
for _ in range(max_connections):
self.pool.put(self._create_connection())
def get_conn(self):
return self.pool.get(timeout=5)
def return_conn(self, conn):
if conn.is_healthy():
self.pool.put(conn)
else:
self.pool.put(self._create_connection())

4.5 调整端口范围#

# 扩大本地端口范围
echo "1024 65535" > /proc/sys/net/ipv4/ip_local_port_range
# 增加 TIME_WAIT bucket 数量
echo 20000 > /proc/sys/net/ipv4/tcp_max_tw_buckets

五、最佳实践#

5.1 服务器端配置#

/etc/sysctl.conf
# 启用 TIME_WAIT 重用
net.ipv4.tcp_tw_reuse = 1
# 缩短 FIN 超时
net.ipv4.tcp_fin_timeout = 30
# 增加端口范围
net.ipv4.ip_local_port_range = 1024 65535
# 增加 TW bucket
net.ipv4.tcp_max_tw_buckets = 200000

5.2 HTTP 长连接#

# 使用 HTTP Keep-Alive
import urllib3
http = urllib3.PoolManager(maxsize=100, max_retries=3)
# 复用连接而不是频繁创建/关闭
for url in urls:
response = http.request('GET', url)

5.3 连接池#

// Java HttpClient 使用连接池
HttpClient client = HttpClient.newBuilder()
.connectionPool(HttpConnectionPool.new(100)) // 100 个连接
.build();

六、总结#

6.1 TIME_WAIT 的作用#

作用说明
防止延迟包确保旧连接数据包消失
可靠关闭保证最后的 ACK 被收到
清理资源等待网络中的延迟消息

6.2 优化策略#

策略适用场景
tcp_tw_reuse客户端,高并发
SO_REUSEADDR服务器端
缩短 MSL需要快速释放端口
连接池减少短连接
扩大端口范围端口耗尽

核心原则:理解 TIME_WAIT 的必要性,在大多数场景下它是正确的设计,只是在高并发场景下需要合理配置。

参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

为什么 TCP 协议有 TIME_WAIT 状态
https://blog.souloss.com/posts/why-the-design/why-tcp-has-time-wait-state/
作者
Souloss
发布于
2023-07-05
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时