如果你用netstat -an命令查看香港服务器的连接状态,发现TIME_WAIT的数量好几千甚至上万,先别慌——这事儿太常见了。特别是做跨境电商、海外直播、API接口服务的香港服务器,几乎天天都能碰上。但“常见”不代表可以不管。TIME_WAIT太多会耗尽本地端口,导致新连接建立失败,具体表现就是:网站突然打不开、报错“Cannot assign requested address”、数据库连不上。
一、先搞清楚TIME_WAIT是啥玩意儿
简单说,TIME_WAIT是TCP连接主动关闭后必须经历的一个状态。主动关闭连接的那一端发送最后一个ACK之后,不能马上把连接彻底抹掉,得等一段时间——通常是2MSL,Linux默认是60秒。
为什么要等这60秒?两个原因:一是怕最后一个ACK丢了,被动关闭那边没收到会重发FIN,这边得能响应;二是让迷路的老数据包在网络上消失,别干扰新连接。
问题来了:高并发场景下,每秒几千个请求,每个请求处理完就主动关闭连接,TIME_WAIT就像滚雪球一样越积越多。而系统的可用端口范围通常是32768到60999,一共也就两万多个端口。60秒内攒够两万多个TIME_WAIT,新连接就没端口可用了。
香港服务器尤其容易出这个问题,原因后面会讲。先上解决方案。
二、方法一:调整内核参数,开启端口复用(最常用)
这是最直接的解决办法。让系统允许处于TIME_WAIT状态的端口被重新使用,同时缩短TIME_WAIT的超时时间。
编辑/etc/sysctl.conf文件,加上这几行:
# 允许重用TIME_WAIT状态的端口
net.ipv4.tcp_tw_reuse = 1
# 快速回收TIME_WAIT连接(注意:高版本内核已废弃)
# net.ipv4.tcp_tw_recycle = 1 # 这个不要用了,下面会解释
# 调整TIME_WAIT的最大数量,超过这个值直接释放
net.ipv4.tcp_max_tw_buckets = 5000
# 扩大可用端口范围
net.ipv4.ip_local_port_range = 1024 65000
然后执行sysctl -p让配置生效。
特别注意: tcp_tw_recycle这个参数在Linux 4.12之后已经被移除了,如果你用的是CentOS 7/8、Ubuntu 18.04之后的内核,写进去也没用。而且即使在旧内核上,开启tcp_tw_recycle配合NAT网关会导致严重问题——因为NAT后面的多个客户端会被服务器误认为是同一个连接,造成大量丢包。所以现在主流做法是只用tcp_tw_reuse。
tcp_tw_reuse的原理是在发起新连接时,如果对方的时间戳比之前的连接更新,就允许复用TIME_WAIT端口。这个机制在服务器作为客户端向外发起连接时(比如你的PHP连接后端数据库)尤其有效。
改完之后观察一下,TIME_WAIT数量通常会明显下降。如果还是很多,继续往下看。
三、方法二:修改应用层逻辑,把“主动关闭”改成“被动关闭”
这个很多人想不到。TIME_WAIT只会出现在主动关闭连接的那一端。也就是说,谁先调用close(),谁的头上就扛着TIME_WAIT。
举个例子:你的Web服务器(Nginx/Apache)处理完请求后,主动关闭了和客户端的连接——那么TIME_WAIT就落在服务器头上。但如果让客户端主动关闭连接,服务器这边的TIME_WAIT就会大幅减少。
在HTTP/1.1协议里,客户端可以复用同一个TCP连接发多个请求。但很多老旧客户端或某些场景下,客户端每次请求完就主动断开,这时候你改不了客户端的行为。反过来,如果你的服务器是向后端发请求(比如PHP-FPM连Redis、MySQL),那么你完全可以控制谁先关。
实战案例: 一台香港服务器跑了100多个微服务实例,每个服务都要频繁读写Redis。默认配置下,PHP脚本执行完会主动关闭到Redis的连接,结果TIME_WAIT堆到一万多。解决方案是在Redis客户端配置里开启连接池,让连接保持在ESTABLISHED状态而不是用完就关。
用Go语言写的服务可以这样控制:
// 不好的做法:每次请求都新建连接
func badRequest() {
conn, _ := net.Dial("tcp", "redis:6379")
defer conn.Close() // 主动关闭,产生TIME_WAIT
conn.Write(data)
}
// 好的做法:复用连接
var pool = sync.Pool{
New: func() interface{} {
conn, _ := net.Dial("tcp", "redis:6379")
return conn
},
}
func goodRequest() {
conn := pool.Get().(net.Conn)
defer pool.Put(conn) // 不关闭,放回池里复用
conn.Write(data)
}
对于Nginx反向代理的场景,可以开启keepalive让上游连接复用:
upstream backend {
server 10.0.0.1:8080;
server 10.0.0.2:8080;
keepalive 32; # 保持32个空闲连接不断开
}
server {
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
核心思想就一句话:能复用的连接就别关,实在要关也让对方来关。
四、方法三:调整socket选项,开启SO_LINGER
有时候连接里还有未发送完的数据,或者双方通信不正常,导致正常的四次挥手被拖延。这种情况下可以通过设置socket选项来强制关闭连接,跳过TIME_WAIT。
SO_LINGER选项可以让TCP在关闭时直接发送RST包而不是正常的FIN包。收到RST的一方不会进入TIME_WAIT,连接立即释放。
在代码里这样设置:
struct linger so_linger;
so_linger.l_onoff = 1; // 开启linger
so_linger.l_linger = 0; // 超时0秒,立即RST
setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &so_linger, sizeof(so_linger));
close(sockfd);
Python版本:
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置SO_LINGER,超时0秒
sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 0))
sock.close() # 发送RST,不经过TIME_WAIT
注意: 这个方法有副作用。发送RST意味着连接被异常终止,对端会收到ECONNRESET错误。如果你的应用层协议对连接关闭比较敏感(比如要求可靠的四次挥手),不要用这个方法。比较适合的场景是:你明确知道连接上没有需要完整传输的数据,比如健康检查连接、已超时的长连接。
五、方法四:升级到HTTP/2或使用UDP协议
这属于“降维打击”的思路。TIME_WAIT问题的根源是TCP短连接太多,每个连接都要经历三次握手、四次挥手。如果能减少连接数量,问题自然就缓解了。
HTTP/2的多路复用:一个TCP连接可以同时传输成百上千个HTTP请求,没有队头阻塞,也就不用频繁建连断开。从Nginx 1.9.5开始就支持HTTP/2了,配置很简单:
server {
listen 443 ssl http2; # 加上http2就行
server_name your-domain.com;
# ...其他配置
}
客户端也支持HTTP/2的情况下,你的服务器上TIME_WAIT数量会断崖式下降。我在一台香港服务器上做过测试:同样的流量,HTTP/1.1下TIME_WAIT稳定在8000左右,切到HTTP/2后直接降到200以下。
UDP协议:如果你的业务能容忍偶尔丢包(比如实时音视频、DNS查询、游戏位置同步),可以考虑换UDP。UDP是无连接的,没有TIME_WAIT这个概念。国内很多出海游戏公司,香港服务器到东南亚节点的通信都用的KCP协议(基于UDP实现了可靠传输),既绕开了TIME_WAIT问题,延迟还比TCP低。
六、为什么香港服务器尤其容易出现TIME_WAIT问题?
这个问题值得单独拎出来说。香港服务器的TIME_WAIT问题往往比内地服务器更严重,主要有三个原因:
1. 跨境网络延迟高。 TIME_WAIT的默认超时是60秒,但这个60秒是基于网络RTT的。内地机房内网延迟不到1ms,香港到内地动辄30-80ms。这意味着同样的连接关闭速度,香港服务器端口被占用的时间其实更长——因为2MSL时间是按实际RTT计算的。
2. 大量短连接业务。 香港服务器很多是跑API网关、海外电商、爬虫代理的。这类业务的特点是:每个请求很小、处理很快、但建立和关闭连接的开销占比很高。请求量上去了,TIME_WAIT自然爆。
3. 很多香港服务器做了NAT和端口映射。 如果你用docker跑服务,或者做了多层NAT转发,每个连接在内核里要占用的资源更多,对端口耗尽更敏感。
七、验证方法:如何确认问题已解决?
改完配置之后,用下面几个命令观察效果:
# 查看当前TIME_WAIT数量
netstat -an | awk '/tcp/ {print $6}' | sort | uniq -c
# 更精确地看(推荐)
ss -tan state time-wait | wc -l
# 实时监控连接状态变化
watch -n 1 'ss -tan | grep -c TIME-WAIT'
# 查看端口使用情况
cat /proc/sys/net/ipv4/ip_local_port_range
如果TIME_WAIT数量从几千上万降到了几百,而且在业务高峰期也能保持稳定,说明问题解决了。
TIME_WAIT不是洪水猛兽,它是TCP协议为了保证可靠传输而设计的正常机制。一台香港服务器上有一两千个TIME_WAIT完全正常,不用大惊小怪。真正要警惕的是:TIME_WAIT数量持续增长从不下降,或者接近端口范围的上限。
在动手优化之前,先用ss -tan | grep TIME-WAIT | awk '{print $4}' | cut -d: -f2 | sort | uniq -c | sort -rn | head -10看看TIME_WAIT集中在哪些目标端口上。如果是连某个后端服务的端口占了大头,那优先考虑方法二——加连接池或者让服务常驻。如果是连客户端的随机端口占了大头,那优先使用方法一的内核参数调整。
最后提醒一句:香港服务器的网络环境和内地有很大不同,有些在内地跑得好好的优化方案,放到香港反而可能起反作用。建议改完配置先观察24小时,别一次性把所有方法都用上,否则出了问题都不好定位。慢慢调、耐心看,比上来就“四板斧”靠谱得多。
推荐文章
