Skip to content
Go back

服务端大量TIME_WAIT:原因、危害与解决方案

服务端大量 TIME_WAIT:原因、危害与解决方案

一句话结论(30s)

TIME_WAIT 不是 bug,而是 TCP 可靠关闭的副产物——因为「主动关闭方」要等 2MSL(约 60 秒)确保最后的 ACK 被对方收到、并让旧连接的延迟数据包自然消亡,所以服务端大量 TIME_WAIT 的根因是应用层短连接/频繁关闭,而非内核参数,解决关键是减少连接的创建与销毁频率。

核心原理(2min)

底层深入(5-10min)

TIME_WAIT 是什么

TIME_WAIT 是 TCP 连接关闭过程中的一个状态,位于主动关闭方(先发送 FIN 的一方)。四次挥手的最后一步:

主动关闭方                         被动关闭方
ESTABLISHED                      ESTABLISHED
  │                                  │
  │──FIN────────→                   │  (主动关闭方:我没有数据要发了)
  │            FIN_WAIT1             │
  │←──────ACK─────                  │  (被动关闭方:收到)
  │            FIN_WAIT2             │  CLOSE_WAIT
  │                                  │
  │←──────FIN─────                  │  (被动关闭方:我的数据也发完了)
  │                                  │  LAST_ACK
  │──ACK────────→                   │
  │            TIME_WAIT             │  CLOSED
  │  (等待 2MSL ≈ 60-120 秒)      │
  │            CLOSED                │

进入 TIME_WAIT 后,主动关闭方等待 2MSL(Maximum Segment Lifetime,报文最大生存时间)。Linux 内核硬编码 TCP_TIMEWAIT_LEN = 60 秒(即 MSL = 30 秒,2MSL = 60 秒),不可通过 sysctl 调整。

TIME_WAIT 存在的两个理由

理由一:确保最后的 ACK 被对方收到

如果主动关闭方发送的最后一个 ACK 丢失,被动关闭方会重传 FIN。如果在 TIME_WAIT 期间收到重传的 FIN,主动关闭方可以重发 ACK 并重置 2MSL 定时器。

如果没有 TIME_WAIT(连接立即关闭),当重传的 FIN 到达时,这个连接已经不存在了,内核会回复 RST(Reset),被动关闭方收到 RST 后认为连接异常终止,可能导致数据丢失。

理由二:让旧连接的延迟数据包自然消亡

网络中可能存在属于这个连接的数据包还在”漫游”(延迟到达)。如果在 TIME_WAIT 结束前,相同五元组的新连接被创建(比如客户端用相同源端口又连接了同一个服务端端口),这些延迟数据包可能被误认为属于新连接,导致数据错乱。2MSL 的等待确保这些延迟包有足够时间被网络丢弃(TTL 过期或被路由器丢弃)。

思考:两个理由本质上在防什么?一个防「没说完就散」,一个防「说完了还被人听错」。理由一是兜底:最后的 ACK 丢了对方会重传 FIN,TIME_WAIT 期间还能再回 ACK,否则只能回 RST 把对方「异常终止」。理由二是隔离:让旧连接的迷路包有 2MSL 时间彻底消亡,否则同五元组的新连接可能收到「前世」数据。一个保证优雅收尾,一个保证新旧隔离。

为什么服务端会出现大量 TIME_WAIT

TIME_WAIT 出现在主动关闭方。在 HTTP 场景(服务端 Nginx/Apache,客户端浏览器),谁主动关闭连接?

场景一:HTTP 不使用长连接

HTTP/1.0 默认每个请求-响应后关闭连接(Connection: close)。此时服务端在发送完响应后主动关闭连接(FIN),于是服务端进入 TIME_WAIT。每处理一个请求就产生一个 TIME_WAIT,高并发下很快就积压数万个 TIME_WAIT 状态。

场景二:长连接超时

HTTP/1.1 默认使用持久连接(Connection: keep-alive),连接在请求完成后不关闭,可以复用。但有两个限制:

keepalive_timeout(Nginx 默认 75 秒):如果一个空闲连接在 75 秒内没有任何新请求,Nginx 主动关闭它。Nginx 作为主动关闭方,连接进入 TIME_WAIT。

keepalive_requests(Nginx 默认 100):一个连接上最多处理 100 个请求,之后 Nginx 主动关闭它。这同样产生 TIME_WAIT。

在高流量场景下,即使配置了长连接,这两条限制仍然会持续产生 TIME_WAIT——每处理 100 个请求或每空闲 75 秒,就产生一个 TIME_WAIT。

场景三:反向代理到后端

Nginx 作为反向代理连接后端服务(如 Tomcat、Golang 服务),Nginx 与后端的连接通常由 Nginx 端主动关闭(发送完请求、收到响应后)。如果后端处理速度快 + QPS 高,Nginx 端会产生短时大量 TIME_WAIT 连接。

思考:为什么「服务端」反而成了 TIME_WAIT 的重灾区?因为 TIME_WAIT 只落在「先发 FIN 的主动关闭方」身上,而服务端在短连接、keepalive 到期、反向代理这三个场景里恰恰是那个先关连接的人。客户端浏览器通常复用连接、后发 FIN,反而是「被动关闭方」——所以大量 TIME_WAIT 常长在 Nginx 而非浏览器上。

大量 TIME_WAIT 的危害

端口耗尽

客户端而言 TIME_WAIT 的危害更直接——连接的唯一性由五元组保证,而客户端每次连接通常使用一个递增的临时端口。如果作为主动关闭方的客户端产生大量 TIME_WAIT,可用端口(默认 28232 个,net.ipv4.ip_local_port_range 控制)被 TIME_WAIT 状态占满,新的 socket 连接会报 EADDRNOTAVAIL(Cannot assign requested address)。

思考:为什么端口耗尽对「客户端」比对「服务端」更致命?因为服务端用的是固定监听端口,再多的 TIME_WAIT 也不占新端口;客户端却靠递增的临时端口来区分每条连接,端口是稀缺资源。客户端做主动关闭方时,TIME_WAIT 会快速占满 ip_local_port_range,新连接直接报 EADDRNOTAVAIL——这才是端口耗尽最疼的地方。

内存占用

每个 TIME_WAIT 连接在内核中占据一个 tcp_timewait_sock 结构(约 200-300 字节)。如果有 10 万个 TIME_WAIT 连接,内存开销约 20-30 MB——在 64GB/128GB 的服务器上不算显著,但在小型 VPS 上可能是个问题。

连接追踪表压力

每个 TIME_WAIT 都在内核的连接追踪表(conntrack table)中占据一个条目。如果系统配置了 NAT 或 iptables,conntrack 表满会导致后续连接被丢弃,表现为随机性的连接失败。

解决方案

方案一:调整内核参数

net.ipv4.tcp_tw_reuse = 1(推荐)

允许将 TIME_WAIT 状态的 socket 复用于新的客户端连接。内核在分配客户端端口时,如果发现所有端口都被占用但存在 TIME_WAIT 连接,可以直接复用(需满足一系列安全条件:新连接的初始序列号大于旧连接的最后一个序列号等)。

注意:tcp_tw_reuse 只对客户端(主动发起连接的一方)生效,对服务端监听 socket 无效。

不要用 tcp_tw_recycle(Linux 4.12 后已废弃)——它在 NAT 环境下(多个客户端共享同一个公网 IP)会导致严重问题:来自同一 IP 但不同客户端的数据包被认为时间戳非法而被丢弃。

net.ipv4.tcp_fin_timeout

调整从 FIN_WAIT_2 到 TIME_WAIT 的超时时间(实际只影响 FIN_WAIT_2 的等待时间,2MSL 本身在代码中硬编码为 60 秒)。

方案二:让客户端主动关闭连接

在 HTTP 场景中,如果是服务端产生大量 TIME_WAIT,根本的解决思路是让客户端成为主动关闭方

# 客户端在请求中设置 Connection: close
# 并主动关闭连接

但在浏览器场景中无法可靠控制浏览器的关闭行为。在反向代理到后端的场景中,可以让后端主动关闭(后端设置较短的 keep-alive timeout,让后端先发 FIN)。

💭 思考:既然 TIME_WAIT 总要落在某个人头上,那“谁做主动关闭方”有什么讲究?——讲究的是“让承受力强的一方扛”。服务端端口固定、资源充裕,客户端端口稀缺且容易被占满。如果服务端老是当主动关闭方,大量 TIME_WAIT 堆在服务端,虽然端口不愁,但内存和 conntrack 会涨;反过来如果客户端当主动关闭方,客户端那几千个临时端口会被 TIME_WAIT 快速吃光,直接报 EADDRNOTAVAIL。所以把“主动关闭”这个负担,交给“扛得住”的那一方,是从结构上转移代价。

方案三:增大可用端口范围

# 扩大临时端口范围(默认 32768-60999,约 28000 个端口)
sysctl net.ipv4.ip_local_port_range="10240 65535"

这增加了客户端约 55000 个可用端口。但根本解决还是要控制 TIME_WAIT 的产生速度。

方案四:优化 Nginx 连接管理

# nginx.conf
http {
    # 增加 keepalive 的最大请求数
    keepalive_requests 1000;  # 默认 100
    
    # 调整 keepalive 超时
    keepalive_timeout 300;    # 默认 75 秒
    
    # 对 upstream 启用连接池
    upstream backend {
        server 127.0.0.1:8080;
        keepalive 64;  # 保持最多 64 个到后端的空闲连接
    }
    
    # 使用 HTTP/1.1 连接后端(支持真正的连接复用)
    proxy_http_version 1.1;
    proxy_set_header Connection "";
}

这些配置的核心思路:减少连接的产生和销毁频率。更多的请求复用一个连接 → 更少的关闭操作 → 更少的 TIME_WAIT。

方案五:使用连接池

在微服务架构中,服务间的 RPC 调用应始终使用连接池(gRPC 默认连接池,Dubbo 默认长连接)。连接池中的连接保持为 ESTABLISHED 状态,完全避免 TIME_WAIT 的产生。

定量分析

假设一个 Nginx 服务处理 10000 QPS:

未优化(无长连接)

使用长连接 + keepalive_requests=100

使用连接池(微服务场景)

💭 思考:为什么长连接能把 TIME_WAIT 从 120 万降到 1.2 万、连接池甚至能归零?——因为 TIME_WAIT 的数量正比于“连接的关闭次数”。长连接让一个连接扛 100 个请求才关一次,关闭次数直接除以 100;连接池更彻底,连接始终保持在 ESTABLISHED,压根不发生关闭,TIME_WAIT 自然无从产生。这也印证了核心结论:TIME_WAIT 的源头是“频繁创建销毁连接”,治本的方向只能是“少关连接”,而不是跟内核参数较劲。

总结

大量 TIME_WAIT 不是 bug,而是 TCP 协议正确运行的副产物。问题通常不是 TIME_WAIT 本身,而是应用层使用了不适合的连接管理策略(短连接、频繁关闭)。解决的核心思路是三个方向:

  1. 减少连接的创建和销毁频率(长连接、连接池、增大 keepalive_requests)
  2. 允许 TIME_WAIT 状态的端口复用tcp_tw_reuse
  3. 调整谁来做主动关闭方(让承受力更强的一方来承担 TIME_WAIT)

遇到大量 TIME_WAIT 时,不要急着调内核参数——先检查是不是应用层在不必要地频繁创建和关闭连接。在大多数情况下,引入长连接或连接池比调任何内核参数都有效得多。

💭 思考:为什么“遇到大量 TIME_WAIT”时,第一反应不该是调内核参数?——因为内核参数大多只是“复用端口、扩大上限”这类缓解手段,治的是标;真正产生 TIME_WAIT 的是应用层“每处理一个请求就建一条、关一条”的连接策略。不改应用层的短连接模式、只调内核,TIME_WAIT 的产生速度不变,迟早还会积压回来。所以先查连接管理策略、上长连接/连接池,才是对症下药。

章末提问

Q1:TIME_WAIT 是什么?为什么会有这个状态?

结论先行:TIME_WAIT 是主动关闭方发完最后一个 ACK 后、等待 2MSL(约 60 秒)才进入 CLOSED 的状态,是 TCP 可靠关闭的副产物。因为两个原因:一是确保最后的 ACK 被对端收到(丢了还能重发,否则回 RST);二是让旧连接的迷路报文自然消亡,避免污染同五元组的新连接。

Q2:服务端为什么会产生大量 TIME_WAIT?

结论先行:因为 TIME_WAIT 只落在主动关闭方,而服务端在三个场景里主动关闭连接:HTTP 短连接、keepalive 超时/请求数到期、反向代理到后端。因为每关闭一次就产生一个 TIME_WAIT,高并发下短连接频繁创建销毁,稳态会积压成千上万个,根因是应用层连接管理策略而非内核参数。

Q3:大量 TIME_WAIT 怎么解决?

结论先行:核心是减少连接的创建与销毁频率——长连接、连接池、增大 keepalive_requests。因为 TIME_WAIT 的产生源头是「频繁关闭」,复用连接能从根本上减少开关次数(连接池保持 ESTABLISHED 基本不产生 TIME_WAIT);辅助手段是 tcp_tw_reuse 复用端口、让承受力更强的一方做主动关闭方,而非盲目调内核参数。


Share this post on:

Previous Post
服务端正常启动但客户端请求不到:网络可达性排查全链路
Next Post
为什么四次挥手要等2MSL