TCP/IP协议栈排坑指南:3个高频连接故障与tcpdump精准定位

你是否遇到过:

  • 服务端端口开了,客户端就是连不上,返回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拥塞控制从入门到改参数》,想看的点个赞告诉我。

相关推荐
被摘下的星星19 小时前
网际协议(IP协议)
网络·tcp/ip
发光小北1 天前
IEC104 转 Modbus TCP 网关如何应用?
网络·网络协议·tcp/ip
小宏运维有点菜1 天前
服务器 BMC 管理 IP
服务器·tcp/ip·centos
SPC的存折1 天前
Cisco Packet Tracer 静态路由全网互通实验及详细教学文档,包括基础常识、实验信息、IP 地址规划和分步操作流程
网络·tcp/ip·智能路由器
treesforest1 天前
IP 反欺诈查询怎么落地更稳?Ipdatacloud 适用场景与实战决策闭环
网络·数据库·网络协议·tcp/ip·网络安全
lularible1 天前
PTP协议精讲(2.18):遵循规则的艺术——Profile与一致性要求深度解析
网络·网络协议·开源·嵌入式·ptp
皮卡蛋炒饭.1 天前
网络基础概念
服务器·网络协议
郝学胜-神的一滴1 天前
深入理解 epoll_wait:高性能 IO 多路复用核心解密
linux·服务器·开发语言·c++·网络协议
mmWave&THz1 天前
传统微波IDU与数字IP微波ODU扩展单元(数字微波IDU)技术对比分析
大数据·运维·网络·tcp/ip·系统架构·信息与通信·智能硬件
PinTrust SSL证书1 天前
Sectigo(Comodo)域名型DV通配符SSL
网络·网络协议·http·网络安全·https·软件工程·ssl