你是否遇到过:

  • 服务端端口开了,客户端就是连不上,返回Connection timed out
  • 进程退了,端口却一直占着,报Address already in use
  • 线上系统莫名其妙卡死,netstat看到一堆CLOSE_WAITTIME_WAIT

这些十有八九是TCP状态机出了岔子。我干了十年运维,这种问题少说碰了上百次。今天不扯虚的,直接拿三个真实场景,教你用tcpdump+ss把协议栈的底裤扒干净。

读完本文你将能:

  1. 用一条命令抓出三次握手失败的根本原因
  2. 区分CLOSE_WAITTIME_WAIT的代码层与内核层解法
  3. 快速定位端口耗尽和连接队列溢出

先备知识 & 工具

你需要:

  • 一台Linux机器(我用的Ubuntu 22.04,CentOS 7/8命令也通用)
  • root或sudo权限(抓包和修改内核参数需要)
  • 基础网络知识:知道TCP flag(SYN, ACK, FIN, RST)

工具清单(全系统自带,不用额外装):

工具

作用

常用参数

tcpdump

抓包看细节

-i eth0 host 1.2.3.4 -nn -S

ss

替代netstat,更准更快

-tnop -tan

sysctl

读/改内核TCP参数

net.ipv4.tcp_tw_reuse

nc

快速起服务/客户端

-l -p 8080


场景一:三次握手失败 – 连不上?抓个包就知道了

典型症状
客户端telnet server_ip 8080卡住,直到超时报Connection timed out。服务端ss -lnt明明显示LISTEN

我踩过的坑:有一次新上线一个Go服务,端口8080,本地curl通,外部机器死活连不上。查了三天防火墙,最后发现是云平台的安全组没放开。但更多时候是协议栈内部问题。

操作步骤

第一步:在服务端抓包(同时让客户端重试连接)

sudo tcpdump -i eth0 port 8080 -nn -S -v

参数解释:-i指定网卡,-nn不解析主机名/端口名,-S打印绝对TCP序号,-v给点详细信息。

第二步:在客户端发起连接

telnet 192.168.1.100 8080   # 换成你的服务端IP

预期输出(正常时)

14:32:01.123456 IP 192.168.1.50.54321 > 192.168.1.100.8080: Flags [S], seq 1234567890
14:32:01.123478 IP 192.168.1.100.8080 > 192.168.1.50.54321: Flags [S.], seq 9876543210, ack 1234567891
14:32:01.123489 IP 192.168.1.50.54321 > 192.168.1.100.8080: Flags [.], ack 9876543211

三次握手:SYN → SYN+ACK → ACK。

异常时的抓包结果(你大概率会见到下面某一种):

抓到的包

含义

根本原因

只有客户端发出[S],没有任何回复

包根本没到服务端或被内核丢弃

防火墙/安全组/路由不通;或者服务端net.ipv4.tcp_syn_rcv队列满

服务端回复[S.],但客户端不回应最后一个ACK

客户端的ACK包丢失或服务端没收到

对称路由问题或客户端内核丢弃了SYN+ACK

服务端直接回复[R.]

服务端对应端口没有进程监听,或监听在127.0.0.1而非0.0.0.0

检查ss -lnt看端口绑定的IP

第三,如果服务端完全没收到SYN,检查防火墙:

sudo iptables -L -n -v | grep 8080
sudo nft list ruleset   # 如果你用nftables的话

常见错误1:net.ipv4.tcp_syn_rcv队列溢出
服务端内核为每个LISTEN端口维护一个半连接队列(SYN Queue)。如果瞬间大量SYN涌来(比如被SYN Flood攻击),队列满了就会丢包。你会在/proc/net/netstat看到ListenOverflowsListenDrops涨了。

查看方法:

grep -E "TcpExtListenOverflows|TcpExtListenDrops" /proc/net/netstat

数值持续增长 -> 调大队列:sysctl -w net.ipv4.tcp_max_syn_backlog=4096(默认通常是1024)

彩蛋:如果你在抓包时看到服务端回了[S.],但客户端居然回了[R]而不是[.],那是客户端收到了意料之外的SYN+ACK(比如源端口冲突),直接reset。这种情况通常客户端代码用了SO_REUSEADDR并且残留了TIME_WAIT的旧连接。我上次排查了四小时才发现是客户端的一个库偷偷开了端口复用。


场景二:四次挥手异常 – 为什么有那么多CLOSE_WAIT?

典型症状
ss -tan一看,一堆CLOSE_WAIT状态的连接,进程打开的文件描述符快爆了(lsof -p PID | wc -l飙到几万)。

先搞清楚CLOSE_WAIT怎么来的

正常四次挥手:

  1. 对端发FIN(想关连接)→ 本端内核回复ACK,应用层收到EOF,连接进入CLOSE_WAIT
  2. 本端应用调用close() → 内核发FIN → 进入LAST_ACK
  3. 对端回ACK → 彻底关闭

坑爹的是:很多开发只调了read()发现返回0,忘了close()套接字。结果连接就一直挂在CLOSE_WAIT,直到进程退出。这就是典型的代码bug。

如何定位是哪个文件描述符没关?

ss -tnop | grep CLOSE_WAIT

输出示例:

CLOSE-WAIT 1  0   192.168.1.100:8080   192.168.1.50:54321   users:(("nginx",pid=1234,fd=17))

看到了吗?users:那一列直接告诉你进程名、PID、文件描述符号。然后你可以去查代码里这个fd为什么没关。

快速修复(临时)
如果是小业务,直接重启进程就释放了。但线上不能随便重启?那得改代码。我一般建议开发在read()返回0或errno == ECONNRESET时,务必close()shutdown()

顺便提一嘴CLOSE_WAIT本身不是内核参数能调的,它完全由用户态代码控制。别去改什么tcp_keepalive_time,没用的。


场景三:TIME_WAIT太多导致端口耗尽

典型症状
高并发短连接服务(比如压测工具、代理),跑一会儿就报Cannot assign requested addressss -tan看到大量TIME_WAIT

原因
主动关闭连接的那一端(通常是客户端)会进入TIME_WAIT,持续2MSL(默认60秒)。期间这个四元组(源IP, 源端口, 目标IP, 目标端口)不能被复用。如果你每秒新建几万连接,源端口范围只有约3万个(net.ipv4.ip_local_port_range),一分钟内就会耗尽。

先看看你当前端口范围

sysctl net.ipv4.ip_local_port_range

通常输出:32768 60999(只有28232个端口)。

解决方法(按推荐顺序)

1. 启用tcp_tw_reuse(安全且有效)
tcp_tw_reuse允许内核在TIME_WAIT状态下复用连接,前提是新连接的时间戳比旧的大(依赖TCP timestamps)。不会导致数据错乱。

sysctl -w net.ipv4.tcp_tw_reuse=1
# 同时必须打开timestamps(默认就是开的)
sysctl net.ipv4.tcp_timestamps   # 应该输出1

限制:这个参数只在客户端(发起connect的那端)生效,服务端没用。并且不能和tcp_tw_recycle混用(tcp_tw_recycle在Linux 4.12后已移除,别问了)。

2. 调大本地端口范围

sysctl -w net.ipv4.ip_local_port_range="1024 65535"

范围拉大,端口数变6万多。但注意1024以下端口是系统保留的,别从1开始。

3. 改代码,让服务端主动关闭
谁主动关闭谁进入TIME_WAIT。如果能让服务端先关,客户端就不背这个锅了。比如HTTP/1.1的keepalive,服务端空闲超时后主动发FIN。

实测对比(我用wrk压测本地nginx):

  • 默认配置:压10秒后开始报address already in use,TIME_WAIT数量积压到2.8万
  • 开启tcp_tw_reuse后:同一压测脚本跑1分钟,无报错,TIME_WAIT依然很多但都被复用

警告:不要同时开启tcp_tw_reusetcp_tw_recycle。后者在NAT环境会灾难性丢包(因为它依赖每包的时间戳,NAT后多个客户端的时间戳会乱)。幸好新内核已经删了这个参数。


附:快速排查命令清单(存个备忘)

# 看当前所有TCP状态计数
ss -tan | awk '{print $1}' | sort | uniq -c

# 看半连接队列满没满
ss -lnt | grep -E 'Listen|Recv-Q'
# 如果Recv-Q > 0 且 Send-Q 很大(比如128),说明全连接队列满了

# 修改全连接队列大小(backlog)
# 应用代码里listen(fd, backlog) 那个backlog,同时内核限制 net.core.somaxconn
sysctl net.core.somaxconn   # 默认128,调大到4096
sysctl -w net.core.somaxconn=4096

# 抓指定IP和端口的完整TCP流(存文件后用wireshark看)
sudo tcpdump -i eth0 host 10.0.0.1 and port 3306 -w mysql_trace.pcap

总结一下(实在话)

  1. 连接超时先抓包,看有没有SYN+ACK。没回复查防火墙/队列;回了RST查监听IP和端口。
  2. CLOSE_WAIT是应用层忘了close,ss -tnop直接定位到进程和fd,改代码是唯一解。
  3. TIME_WAIT太多在客户端开tcp_tw_reuse+调大端口范围,瞬间舒服了。
  4. 别迷信netstat,ss更快更准,tcpdump才是终极真相。

你还有没有遇到过更诡异的TCP问题?比如SYN_RECV状态堆积、或者FIN_WAIT2一直不消失?评论区甩出来,我看看能不能帮你排。

如果觉得这篇对你有用,欢迎分享给身边被网络问题折磨的朋友。下篇我准备写《TCP拥塞控制从入门到改参数》,想看的点个赞告诉我。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐