跳转至

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 堆积 —— 主动关闭方出问题

# 一步看状态分布
ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn

# 看内核层面的 socket 统计
ss -s

你应该看到什么:

  • 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。