Linux 网络故障排查实战:从 TCP 超时到连接池耗尽的全链路诊断(2026)

📝 518 字 · ☕ 2 分钟阅读

凌晨三点,PagerDuty 响了

你打开监控面板,发现核心 API 的 P99 延迟从平时的 45ms 飙升到了 3800ms。没有 CPU 飙升,没有内存泄漏,没有磁盘 IO 瓶颈——所有常规指标都是绿的。这大概率是一个网络层的问题。

网络故障是生产环境里最阴险的一类问题。它不会像 OOM 那样给你留一个 kill log,也不会像死锁那样在堆栈里摆个 trace。它只给你一堆「超时」「连接拒绝」「connection reset」——然后你的任务是逆向还原出到底发生了什么。

这篇文章记录我在过去两年里遇到的三个典型网络故障场景,以及每一层的排查思路和工具链。文末附了一张故障决策流程图,下次凌晨告警可以直接对着走。

网络排查的四层模型

我习惯把网络问题按 OSI 的粒度拆成四层来排查,一层一层往下刨:

典型问题 首选工具
连接层(TCP握手/队列) SYN丢包、backlog满、TIME_WAIT堆积 ss -s, netstat -an
传输层(TCP重传/窗口) 重传率过高、零窗口、乱序 ss -ti, nstat
解析层(DNS/ARP) DNS超时、ARP缓存异常 dig, strace -e trace=network
应用层(连接池/超时配置) 连接池耗尽、idle timeout不匹配 应用日志 + tcpdump

经验法则:从连接层开始,如果能在这里找到答案就不要往下挖。80% 的「网络问题」其实就停在 TCP 连接层面。

Linux 网络故障排查决策树
图:Linux 网络故障排查四层决策树 — 从连接层开始逐层下钻

场景一:TIME_WAIT 把连接池吃干抹净

某次上线后,服务开始间歇性报 Cannot assign requested address。监控显示同一时间 ESTABLISHED 连接数并没那么高,CPU 也只有 20%。

第一刀:看连接状态分布

$ ss -s
Total: 41280
TCP:   40120 (estab 1024, closed 0, orphaned 0, timewait 38896, synrecv 0)
Transport Total     IP        IPv6
RAW       0         0         0
UDP       4         2         2
TCP       40120     40116     4

看到没?38896 个 TIME_WAIT,只有 1024 个 ESTABLISHED。这已经不是在「用连接池」了,这是在用 TIME_WAIT 池。

根因分析

TIME_WAIT 是 TCP 主动关闭方在连接关闭后必须保持 2MSL(约60秒)的状态。如果服务频繁创建短连接——比如每次 HTTP 请求都是一个新的 TCP 连接——TIME_WAIT 会快速堆积。当可用端口被耗尽时,新连接就无法建立了。

这里的关键参数:

# 查看当前系统限制
$ sysctl net.ipv4.ip_local_port_range
net.ipv4.ip_local_port_range = 32768    60999
# 可用端口 = 60999 - 32768 = 28231 个

2.8 万个端口,QPS 超过 470 就会在一个 2MSL 窗口内耗尽。这个服务的 QPS 刚好是 500——正好踩在边界上。

修复(按优先级排)

  1. 上连接池:改 HTTP 客户端为 keep-alive 长连接,QPS 500 只需要十几个连接就能扛住。这个才是治本。
  2. 开 tcp_tw_reusesysctl -w net.ipv4.tcp_tw_reuse=1,允许复用 TIME_WAIT 状态的连接给新的出站请求。
  3. 扩端口范围net.ipv4.ip_local_port_range = 10240 65535,但这是治标的创可贴。

💡 踩坑记录tcp_tw_recycle 在 Linux 4.12 之后已经被移除了。如果你在网上看到有人推荐开 net.ipv4.tcp_tw_recycle=1,那篇文章至少是 2017 年的。这东西在 NAT 环境下会随机丢掉合法连接——我就是那个花了半天排查「为什么只有部分用户连不上」的倒霉蛋。

场景二:TCP 重传率 18%,用户说「卡」

这个场景更隐蔽。服务没有报错,连接也都正常建立,但用户反馈「页面有时候卡一下,刷新就好了」。APM 显示 P50 延迟 60ms,但 P99 偶尔跳到 2 秒以上。

第一刀:看 TCP 统计

$ nstat -az | grep -E 'TcpRetrans|TcpExtTCPTimeouts'
TcpRetransSegs              3847291          # TCP重传段数
TcpExtTCPTimeouts            128403          # TCP超时次数

重传率怎么算?拿 TcpRetransSegs 除以 TcpOutSegs

$ nstat -az | grep TcpOutSegs
TcpOutSegs                  21038412
# 重传率 = 3847291 / 21038412 ≈ 18.3%

18% 的重传率。正常的应该是 < 0.1%。 这已经不是偶发丢包了,是链路出问题了。

第二刀:用 ss -ti 定位问题连接

$ ss -ti 'dst 10.0.1.50'
State  Recv-Q Send-Q  Local Address:Port   Peer Address:Port
ESTAB  0      287456   10.0.2.10:34126     10.0.1.50:3306
     cubic wscale:7,7 rto:264 rtt:87.5/16.2 ato:40 
     retrans:0/7 lost:0 sacked:12 reordering:9
     pmtu:1500 rcvmss:1448 advmss:1448 cwnd:10
     bytes_acked:4127841 bytes_received:139211
     segs_out:3847 segs_in:2278
     lastsnd:352 lastrcv:352 lastack:348
     pacing_rate 8.8Mbps delivery_rate 194.7Kbps
     busy:80ms rcv_space:14480 rcv_ssthresh:64088
     minrtt:83.2

几个关键指标:

  • cwnd:10 — 拥塞窗口只有 10 个 MSS,正常应该是几十上百。说明 TCP 自己检测到了拥塞在主动降速。
  • rtt:87.5ms — 对于同机房来说高得不正常(应该是 <1ms)。跨机房或跨地域延迟。
  • delivery_rate:194.7Kbps — 实际吞吐只有 24KB/s,基本废了。

第三刀:tcpdump 抓包确认

$ tcpdump -i eth0 host 10.0.1.50 and port 3306 -w /tmp/mysql.pcap -c 5000

然后用 Wireshark 打开(或者在服务器上用 tcpdump -r 直接看),Statistics → TCP Stream Graphs → Time-Sequence (Stevens)。如果看到锯齿状的序列号曲线——确认是丢包导致的重传。

根因最后发现是负载均衡器上的一个健康检查配置问题:health check 间隔设成了 100ms,每台后端机器每秒被打了 10 个 SYN,但代理没开 TCP fast open,大量 SYN 在 LB 的 conntrack 表里排队——间接导致正常流量的数据包被丢。

🔥 这个 bug 我排查了整整一天。问题出在 conntrack 表满和 LB health check 的相互作用上,单独看哪个环节都正常,组合在一起就互相伤害。网络层的排障就是这样——你永远在跟「间接原因」打交道。

场景三:DNS 解析 5 秒超时,拖垮整个服务

有一次,一个 Python 微服务突然开始大量超时。日志全是 socket.timeout: timed out,但下游服务明明健康。CPU/内存/磁盘全绿灯。

第一刀:strace 看系统调用在干嘛

$ strace -e trace=network -p 28471 -T
connect(7, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.0.1.100")}, 16) = -1 ETIMEDOUT <5.001242>

看到了吗?connect 到 DNS 服务器 10.0.1.100:53 花了 5 秒然后超时。但 getaddrinfo 调用在任何日志里都没有出现——因为 Python 的 socket.create_connection() 在底层做了 DNS 解析,业务代码看不到。

第二刀:dig 验证 DNS 服务器状态

$ dig @10.0.1.100 google.com +time=2
;; connection timed out; no servers could be reached

DNS 服务器挂了。但为什么监控没发现?因为健康检查用的是 ping,DNS 进程挂了但主机还活着。

修复

改 /etc/resolv.conf 加备 DNS:

nameserver 10.0.1.100
nameserver 10.0.1.101     # 新增备DNS
options timeout:1 attempts:2 rotate

timeout:1 把 DNS 超时从默认的 5 秒降到 1 秒,rotate 让请求在主备间轮询而不是全压在主 DNS 上。

📌 教训:监控 DNS 服务器不要只 ping。用 dig +short @dns_server your_domain 做应用层健康检查,模拟真实解析请求。

实战复盘:MySQL 连接超时 → 全链路排查

最后分享一个完整的排查链路。某天下午,订单服务开始报 MySQL server has gone away,频率从每小时几次涨到每分钟几十次。

Step 1:应用日志

pymysql.err.OperationalError: (2006, "MySQL server has gone away 
(BrokenPipeError(32, 'Broken pipe'))")

Broken pipe = 连接在应用层被 RST 了。

Step 2:MySQL 端 slow log

查 MySQL slow query log,没有异常查询——所有请求都在 50ms 内返回。说明不是查询慢。

Step 3:网络层抓包

$ tcpdump -i eth0 port 3306 -w /tmp/mysql.pcap -c 2000

Wireshark 打开,Filter: tcp.flags.reset 1,看到了大量 RST 包。RST 的源 IP 不是 MySQL 服务器,也不是应用服务器——是中间的一台 F5 负载均衡器。

Step 4:F5 配置

F5 的 TCP profile 里 idle timeout 设的是 300 秒。应用端的连接池配置:

# Python DB pool 配置
pool_size = 20
max_overflow = 10
pool_recycle = 600   # 600秒后回收连接

找到了。连接池 600 秒回收连接,但 F5 300 秒就断开空闲连接。300-600 秒之间的连接,应用以为还活着,F5 已经把它杀了。应用发请求→ F5 回 RST → Broken pipe

修复

pool_recycle = 240(小于 F5 的 300 秒 idle timeout),确保应用在 F5 断连接之前主动回收。

这个问题的诊断链:业务报错 → MySQL slow log(排除查询慢)→ tcpdump(定位 RST 来源)→ 中间件配置(找到根因)。单独看任何一个环节都找不到答案。

快速排查速查表

症状 第一刀命令 看什么
连接拒绝 ss -tlnp | grep PORT 端口有没有在监听?backlog 满没?
间歇超时 ss -ti retrans、cwnd、rtt 是否异常
连接池耗尽 ss -s TIME_WAIT / ESTABLISHED 比例
响应慢但无报错 nstat -az | grep TcpRetrans 重传率是否 > 1%
偶尔 connection reset tcpdump -i any -w /tmp/dump.pcap 谁发的 RST?
DNS 相关超时 strace -e trace=network -p PID connect 到 53 端口的耗时

常见问题

Q: TIME_WAIT 多少算多?

没有固定阈值,取决于可用端口数。简单计算:可用端口数 / 60秒(2MSL) = 安全 QPS 上限。如果你的服务 QPS 接近这个值,就该上连接池或调内核参数了。生产环境 TIME_WAIT 数上万很正常,不用恐慌,先看比例。

Q: tcpdump 在生产环境跑安全吗?

-c 5000 限制抓包数量,不用 -w /tmp/(避免塞满磁盘),不要跑 -vvv(verbose 输出本身就是 CPU 开销)。高流量场景用 host IP and port PORT 精确过滤。另外别忘了跑完 kill 掉。

Q: conntrack 表满怎么看?

cat /proc/sys/net/netfilter/nf_conntrack_count 对比 /proc/sys/net/netfilter/nf_conntrack_max。接近上限时内核日志会出现 nf_conntrack: table full, dropping packet。生产环境建议把 conntrack_max 调到 262144 以上。

相关阅读: strace生产环境调试完全指南:从原理到实战,不重启不改代码就能定位疑难问题(2026)  |  eBPF + bpftrace 生产环境调试实战:不用改代码、不用重启,一行命令定位线上问题(2026)  |  Linux生产环境CPU 100%问题排查实战:从发现到定位的完整复盘(2026)

总结

网络排障的核心能力不是背命令,是建立分层排查的心智模型

  • 先看连接状态(ss -s)—— 连接都建立不了就往下查是浪费时间
  • 再看传输质量(ss -ti, nstat)—— 重传率和拥塞窗口比你想象的更能说明问题
  • 必要时抓包(tcpdump)—— 这是终极武器,但也是最后手段
  • 别忘了中间设备(LB、F5、防火墙、conntrack)—— 很多时候「网络问题」根本不在两台通信的主机上

下次凌晨告警,先 ss -s,再 ss -ti,然后决定要不要掏 tcpdump。你会发现大部分问题停在第一步就找到了。


本文涉及的生产环境案例均来自个人工作经历,已脱敏处理。工具版本基于 Linux 5.15+ / tcpdump 4.99+。

📤 分享这篇文章