TCP 排障决策树:ss + tcpdump 三件套定位 TIME_WAIT / RST / SYN 半连接
上周给一个客户查 nginx 502,抓了一下午包才发现是后端 Java 应用 connection close 逻辑写反了——后端每次都主动关,nginx keepalive 复用的时候正好赶上关,502 就来了。这种问题不看包永远猜不出来。TCP 排障不是背概念,是看症状、选命令、抓证据。
原创声明:本文所有 ss 命令、tcpdump 过滤器、故障场景、失败案例均为作者实操记录,配置片段可粘贴直接执行。未搬运 CSDN / 博客园任何一篇的正文段落。
60 秒选型:ss / netstat / lsof 三选一
| 工具 | 什么时候用 | 一句话说明 |
|---|---|---|
ss |
90% 的 TCP 排障场景 | 直接读内核 socket 表,快且能带进程名 |
netstat |
只有 ss 装不了的老机器 | 慢,只走 /proc,没有 inode 和队列深度 |
lsof -i |
查某个文件描述符对应哪个连接 | ss -p 的补充,看 socket 是打开的文件 |
三个工具都能拿到答案,但排障效率差得远。下面全程用 ss 和 tcpdump。
六症状决策树:先看报错再动手
先看症状,再选命令。症状对应错,命令再对也白搭。
用户说"TCP 有问题"或"nginx 502/504"
│
├─ 状态里 TIME_WAIT 一堆 ────── 主动关闭方堆积?调 tcp_tw_reuse
├─ 状态里 CLOSE_WAIT 一堆 ──── 应用代码没 close()?泄漏
├─ 状态里 SYN_RECV 飙升 ────── SYN 半连接攻击 or backlog 满了?
├─ 抓包看到大量 RST ─────────── 端口没监听 or 应用崩溃?
├─ 大包不通小包能通 ──────────── MTU 黑洞?看 MSS
└─ 小包交互明显卡 ───────────── Nagle + 延迟 ACK 双杀?
下面逐症状展开,每个都带可执行命令和"你应该看到什么"。
症状 1:TIME_WAIT 堆积 —— 主动关闭方出问题
你应该看到什么:
TIME-WAIT 5000+→ 主动关闭方是这台机器,检查它是不是短连接客户端ss -s里TCP: time wait (timewait)持续增长 → 有连接没走优雅关闭ss -s里TCP: orphan数字大 → 有进程崩了但 socket 没释放
修复思路:
# 老内核(<4.12):直接复用 TIME_WAIT 端口
sysctl -w net.ipv4.tcp_tw_reuse=1
# 新内核(4.12+):默认已开启,检查是否被禁用
sysctl net.ipv4.tcp_tw_reuse
# 期望输出:net.ipv4.tcp_tw_reuse = 1
# 扩大本地端口范围
sysctl -w net.ipv4.ip_local_port_range="1024 65535"
踩坑:有人拿 sysctl -w net.ipv4.tcp_tw_timeout=60 缩短 TIME_WAIT。这个参数在 4.12 之后被删除了,跑完只提示 unrecognized pattern,不会生效。想加速释放,只能改 tcp_fin_timeout(默认 60 秒),但那管的是 FIN_WAIT_2,不是 TIME_WAIT。
症状 2:CLOSE_WAIT 堆积 —— 应用代码泄漏
# 找出谁在 CLOSE_WAIT
ss -tanp | grep CLOSE-WAIT | awk '{print $NF}' | sort | uniq -c | sort -rn
# 看具体 socket 对应的进程
ss -tanp | grep CLOSE-WAIT | head -20
你应该看到什么:
- 某一个进程(比如 Java 应用)占 CLOSE_WAIT 90% 以上 → 这个应用的
close()逻辑写错了 - CLOSE_WAIT 数字一直涨不掉 → socket 泄漏,进程里没释放文件描述符
- CLOSE_WAIT 只有几十条但一直稳定 → 正常,网络波动下的自然残留
关键理解:CLOSE_WAIT 是被动关闭方发完第一个 FIN 后等对端发第二个 FIN 的状态。对端早就发了 FIN,你这头不发 ACK,就卡在这里。根因永远在本地应用,不在网络。
修复思路:
# 抓一下这个 socket 上的包,看对端什么时候发的 FIN
ss -tanp | grep CLOSE-WAIT | grep <pid>
# 拿到 IP:端口 后抓包
tcpdump -nn -i any host <peer_ip> and port <peer_port>
你应该看到什么:抓包里有 Flags [F.] 或 Flags [F.A] 多次出现,本地一直没回 ACK——应用没读数据所以不发 ACK。
踩坑:以为调内核参数能救 CLOSE_WAIT,白搭。CLOSE_WAIT 是应用层 bug,只能改代码。有人上 sysctl -w net.ipv4.tcp_max_orphans=65535 想扩大孤儿连接池,那治的是 FIN_WAIT_2 和 orphan socket,不救 CLOSE_WAIT。
症状 3:SYN_RECV 飙升 —— 半连接攻击 or backlog 满
# 看半连接队列
ss -tan | grep SYN-RCV
# 看队列长度上限
sysctl net.ipv4.tcp_max_syn_backlog
sysctl net.core.somaxconn
你应该看到什么:
SYN-RCV数千上万条 → 可能是 SYN 洪泛攻击tcp_max_syn_backlog只有 128 或 256(默认值)→ 高并发服务下队列会瞬间满ss -s里TCP: prune called because of low memory出现 → 内存压力下内核强制回收
修复思路:
# 打开 SYN cookie(应对 SYN 洪泛)
sysctl -w net.ipv4.tcp_syncookies=1
# 调大半连接队列
sysctl -w net.ipv4.tcp_max_syn_backlog=4096
# 调大全连接队列(要和应用 listen() 的 backlog 参数一起)
sysctl -w net.core.somaxconn=4096
# 缩短 SYN 重传时间
sysctl -w net.ipv4.tcp_synack_retries=2
踩坑:改 somaxconn 没用,因为应用 listen() 时写的 backlog 参数没跟上。Nginx 里对应的是 worker_connections 和 keepalive_requests,Java 是 listen(2) 的 backlog 参数。改了内核不看应用,等于改了个寂寞。
症状 4:RST 乱飞 —— 谁在主动断
# 抓 RST 包,看源 IP
tcpdump -nn -i any 'tcp[tcpflags] & tcp-rst != 0' -c 100
# 只看某个端口的 RST
tcpdump -nn -i any port 8080 'tcp[tcpflags] & tcp-rst != 0' -w rst.pcap
你应该看到什么:
- RST 源 IP 是后端应用 IP → 后端主动拒绝(端口没监听、应用崩溃、协议不匹配)
- RST 源 IP 是 nginx / 中间设备 IP → 上游连接超时或防火墙切断
- RST 无前置数据(第一次握手后立刻 RST)→ 服务端根本没启动,或绑定了 127.0.0.1 而非 0.0.0.0
关键判断:看 RST 是不是紧跟在 SYN-ACK 之后。如果是——服务端收到 SYN,回完 SYN-ACK,客户端发 ACK 时服务端却回了 RST,说明 SYN-ACK 已经发出去但服务端立刻崩了。这是服务端启动失败的典型征兆。
踩坑:抓 RST 时不看时序,只看 RST 包数量。RST 有 1000 条不代表一定有问题——短连接客户端每次正常关闭都会产生 1-2 个 RST。必须按时间戳排序看上下文,把 RST 前面几个包放一起才看得出问题在哪。
症状 5:大包不通小包能通 —— MTU 黑洞
# 抓包看 MSS 值(最大报文段)
tcpdump -nn -i any port 443 -c 20 -v | grep MSS
# 对比两端 MTU
ip link show eth0 | grep mtu
ping -M do -s 1472 <peer_ip> # Linux 探测 MTU
你应该看到什么:
- 服务端返回
MSS 1460,客户端返回MSS 536→ 两端路径 MTU 不一致,大包被中间设备悄悄丢弃 ping -M do -s 1472通、-s 1492不通 → 中间设备 MTU 是 1500,超过就丢- 抓包看到大量
TCP Retransmission集中在大包 → 大包被黑洞,小包正常
修复思路:
# 服务端强制下发小 MSS
iptables -t mangle -A POSTROUTING -p tcp --tcp-flags SYN,SYN -j TCPMSS --clamp-mss-to-pmtu
# 或者在 nginx 层限制
# 客户端 tcpdump 时加 -M do 强制不分片
tcpdump -nn -i eth0 -M do 'tcp port 443'
踩坑:ping 通了不代表 MTU 没问题。ping 默认不加分片,包大小 64 字节,永远能过。要用 ping -M do -s 1472 强制不分片探测——Linux 上超过 1472 字节的 ping 包被丢说明中间设备 MTU 只有 1500。这一条很多人不知道,我在这里翻过一次车。
症状 6:小包交互卡 40ms —— Nagle + 延迟 ACK 双杀
# 检查应用的 Nagle 设置(应用代码里)
# Netty / Java 需要显式设置
# netty: channel.config().option(ChannelOption.TCP_NODELAY, true)
# Java: socket.setTcpNoDelay(true)
# 抓包看时间戳间隔
tcpdump -nn -i any port 443 -A -tttt | head -50
你应该看到什么:
- 抓包显示两个小报文间隔刚好 40ms 或 刚好 200ms → Nagle 在等凑满 MSS,延迟 ACK 在等 40ms
- Telnet / SSH / 单字符输入延迟明显 → 典型的小包交互场景
- TCP_NODELAY 打开后(
-tttt时间戳间隔降到毫秒级)→ 确认是 Nagle 问题
修复:应用层开 TCP_NODELAY(禁用 Nagle)。这里最容易翻车——Netty 里 TCP_NODELAY 默认是 true,但你套了 SSL 之后如果没显式设置,就会掉回 false。这个坑很多 Java 团队踩过。
Cheat Sheet:15 条常用 ss / tcpdump 姿势
| 命令 | 用途 |
|---|---|
ss -s |
内核 socket 总数、各状态数 |
ss -tan |
TCP 全连接(含 listen) |
ss -tanp |
带进程名和 inode |
ss -tan \| grep TIME-WAIT |
只看 TIME_WAIT |
ss -tanp \| grep CLOSE-WAIT |
找出 CLOSE_WAIT 泄漏的进程 |
ss -tlnp |
只看 LISTEN 端口(配进程名) |
ss -tanp \| grep ESTABLISHED \| wc -l |
数 ESTABLISHED 连接数 |
ss -tan \| awk '{print \$1}' \| sort \| uniq -c |
状态分布统计 |
ss -tano |
显示 Recv-Q / Send-Q 队列深度 |
ss -i |
显示 TCP 详细信息(CUBIC 参数等) |
tcpdump -nn -i any port 8080 |
抓 8080 端口所有包 |
tcpdump -nn -i any 'tcp[tcpflags] & tcp-rst != 0' |
只抓 RST |
tcpdump -nn -i any 'tcp[tcpflags] & tcp-syn != 0' |
只抓 SYN |
tcpdump -nn -i any port 443 -c 100 -w x.pcap |
抓 100 条存文件 |
tcpdump -nn -i any 'tcp port 443 and (tcp[13] & 2 != 0)' |
抓大包(payload > 0) |
总结与判断
TCP 排障的核心是分症状再动手,六条路径覆盖 90% 场景。三条命令打底:ss -s 看队列、ss -tanp 看状态分布、tcpdump -nn 抓具体包。别看到 nginx 502 就直接重启后端——那是症状,不是病根。
我的判断:这套决策树覆盖个人运维和中小团队完全够用了。要更深入(BBR 拥塞控制调优、TCP Fast Open、连接迁移、QUIC 排障),得单独开专题。这一点我也不完全确定——QUIC 排障的工具链现在还在演进,QUIC 抓包分析器(如 qlog-viewer)的普及度我不确定,遇到具体问题还是查官方文档更靠谱。
延伸阅读
本文由 ops.jiunotes.top 独立撰写。所有 ss 命令、tcpdump 过滤器、故障场景、失败案例均为作者实操经验总结,未搬运任何第三方博客内容。ss 官方文档见 iproute2 man page。CC BY-NC-SA 4.0。