网络性能分析与故障排查
网络问题是后端开发中最棘手的一类故障——症状相似但根因各异,超时可能是网络不通、对端处理慢、连接池耗尽,也可能是 GC 停顿。本章要回答的核心问题:常见网络异常分别意味着什么?如何用抓包和诊断工具定位根因?Java 应用有哪些特有的网络陷阱?高并发场景下如何系统性地优化网络性能?
1. 常见网络问题速查
1.1 异常类型与根因对照表
| 错误现象 | 含义 | 常见根因 | 排查方向 |
|---|---|---|---|
ConnectException: Connection refused | 连接被主动拒绝(目标端口无进程监听,或对端/策略返回 RST) | 服务未启动 / 端口配错 / 主动 reject 的网络策略 | 检查服务状态、端口、iptables;注意防火墙静默丢包更常表现为超时而非拒绝 |
SocketTimeoutException: connect timed out | 连接建立超时 | 网络不可达 / 防火墙静默丢包 / SYN 队列满 | ping / telnet / ss -s |
SocketTimeoutException: read timed out | 读数据超时 | 对端处理慢 / 网络拥塞 / 对端 GC | 抓包分析 RTT / 查对端日志 |
Connection reset by peer | 对端强制关闭连接(RST) | 对端进程崩溃 / 负载均衡器超时 / 半开连接 | 查对端状态 / 抓包看 RST |
Broken pipe | 写入已关闭的 socket | 对端已关闭但本端不知道 | 检查连接复用逻辑 |
No route to host | 路由不可达 | 路由配置错误 / 网络断开 | traceroute / ip route |
Address already in use | 本地地址或端口已被占用 | 其他进程占用、端口复用策略或 TIME-WAIT 状态 | 检查监听进程、地址复用配置和连接关闭模式 |
1.2 TCP 连接状态问题
TCP 连接状态是网络排查的第一手信息。两个最容易出问题的状态:
TIME-WAIT 过多:
txt
主动关闭方在发送最后一个 ACK 后进入 TIME-WAIT,等待 2MSL;Linux 常见默认值约为 60 秒,但应以运行内核和配置为准。
高并发短连接场景下,大量 TIME-WAIT 会耗尽端口。
$ ss -s
TCP: 28469 (estab 1204, closed 25000, orphaned 0, timewait 24800)
按状态精确统计 TIME-WAIT(不依赖 ss -s 的汇总字段):
$ ss -Htan state time-wait | wc -l
$ ss -tan state time-wait # 列出具体连接与本地/远端端点
注意:TIME-WAIT 数量多只说明存在大量该状态连接,不能仅凭数量断言端口已耗尽;还需确认本地端口范围、目标地址分布,以及是否出现 EADDRNOTAVAIL 等实际故障。
根因:HTTP 短连接、RPC 短连接频繁创建关闭conf
# /etc/sysctl.conf 片段(需 sysctl -p 生效;或运行时用 sysctl -w 临时设置,不代表建议直接在生产执行)
# 先确认 TIME-WAIT 是否真的造成端口或连接跟踪表耗尽(见上一步的状态统计与 EADDRNOTAVAIL 迹象)
# 仅当本地端口分配确为瓶颈时,再评估以下内核参数;取值与默认值随 Linux 内核版本变化,1 并非所有环境都适用
net.ipv4.tcp_tw_reuse = 1 # 满足协议安全条件时复用 TIME-WAIT 地址;不等于缩短 TIME-WAIT 定时器
net.ipv4.tcp_fin_timeout = 30 # 控制孤儿连接的 FIN-WAIT-2 超时;不影响 TIME-WAIT 保留时间
# 连接复用与连接池:若高频短连接确为 TIME-WAIT 增长主因,可评估减少不必要的建连/断连;并非所有场景的通用解CLOSE-WAIT 过多:
txt
被动关闭方收到 FIN 后进入 CLOSE-WAIT,等待应用层调用 close()。
如果 CLOSE-WAIT 持续堆积,通常说明应用没有及时调用关闭,或关闭路径被异常阻塞;需要结合连接所有者和关闭时机进一步定位。
$ ss -s
TCP: 500 (estab 200, closed 50, timewait 10, closewait 250)
↑ 异常!
根因:代码中 read() 返回 -1/EOF 后没有 close() 连接java
// 错误示例 —— 导致 CLOSE-WAIT
public void handleRequest(Socket socket) {
try {
InputStream in = socket.getInputStream();
byte[] buf = new byte[1024];
int n = in.read(buf);
// 处理数据...
// ❌ 忘记 close,连接进入 CLOSE-WAIT
} catch (IOException e) {
// ❌ 异常时也没 close
}
}
// 正确示例 —— try-with-resources
public void handleRequest(Socket socket) {
try (socket; // Java 9+ 支持
InputStream in = socket.getInputStream()) {
byte[] buf = new byte[1024];
int n = in.read(buf);
if (n == -1) return; // 输入流到达 EOF;对端可能已关闭发送方向(TCP 允许半关闭)
// 处理数据...
} catch (IOException e) {
// 连接自动关闭
}
}1.3 连接状态全景图
txt
主动打开
│
↓
┌───────────┐
┌────────→│ CLOSED │←────────┐
│ └─────┬─────┘ │
│ │ │
四次挥手完成 主动打开/发送SYN 被动打开
│ ↓ │
│ ┌───────────┐ │
└─────────│ SYN_SENT │ │
└─────┬─────┘ │
收到SYN+ACK│ │
↓ │
┌───────────┐ │
│ESTABLISHED│←────────┘
└─────┬─────┘ 收到SYN,发送SYN+ACK
│
┌─────────────┼─────────────┐
主动关闭│ │ 被动关闭│
发送FIN │ │ 收到FIN │
↓ │ ↓
┌───────────┐ │ ┌───────────┐
│ FIN_WAIT_1│ │ │CLOSE_WAIT │ ← 等待应用close()
└─────┬─────┘ │ └─────┬─────┘
收到ACK │ │ 发送FIN│
↓ │ ↓
┌───────────┐ │ ┌───────────┐
│ FIN_WAIT_2│ │ │ LAST_ACK │
└─────┬─────┘ │ └─────┬─────┘
收到FIN │ │ 收到ACK│
↓ │ ↓
┌───────────┐ │ (CLOSED)
│ TIME_WAIT │ │
└─────┬─────┘ │
2MSL后 │ │
↓ │
(CLOSED)─────────┘2. 网络抓包分析
抓包能直接观察请求和响应在网络路径上的时序与状态。当应用日志和监控不足以区分客户端、代理和服务端问题时,可在授权范围内抓取关键连接的包进行分析。
2.1 tcpdump 常用命令
bash
# 1. 抓取特定端口的 TCP 流量
tcpdump -i eth0 port 8080 -w /tmp/capture.pcap
# 2. 抓取特定源/目标 IP 的流量
tcpdump -i eth0 src 10.0.1.100 and dst port 3306
# 3. 只抓 SYN/FIN/RST 包(看连接建立/关闭)
tcpdump -i eth0 'tcp[tcpflags] & (tcp-syn|tcp-fin|tcp-rst) != 0'
# 4. 抓取 HTTP GET 请求
tcpdump -i eth0 -A 'tcp port 80 and tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x47455420'
# 5. 限制抓包数量
tcpdump -i eth0 port 8080 -c 1000
# 6. 实时查看(不写文件)
tcpdump -i eth0 port 8080 -nn -X
# 7. 抓取 DNS 查询
tcpdump -i eth0 port 53tcpdump 输出解读:
txt
# 正常的 TCP 三次握手
22:01:01.100 IP 10.0.1.1.54321 > 10.0.1.2.8080: Flags [S], seq 1000
22:01:01.101 IP 10.0.1.2.8080 > 10.0.1.1.54321: Flags [S.], seq 2000, ack 1001
22:01:01.101 IP 10.0.1.1.54321 > 10.0.1.2.8080: Flags [.], ack 2001
# Flags 含义:
# [S] = SYN(发起连接)
# [S.] = SYN+ACK(接受连接)
# [.] = ACK(确认)
# [P.] = PSH+ACK(推送数据)
# [F.] = FIN+ACK(关闭连接)
# [R] = RST(强制重置)2.2 Wireshark 分析技巧
当 tcpdump 保存了 pcap 文件后,可以用 Wireshark 做深度分析:
常用过滤器:
txt
# 按 IP 过滤
ip.addr == 10.0.1.100
ip.src == 10.0.1.100 && ip.dst == 10.0.1.200
# 按端口过滤
tcp.port == 8080
tcp.dstport == 3306
# 按 TCP 标志过滤
tcp.flags.syn == 1 # 只看 SYN
tcp.flags.rst == 1 # 只看 RST(异常重置)
tcp.flags.fin == 1 # 只看 FIN(关闭)
# 按 HTTP 过滤
http.request.method == "GET"
http.response.code >= 400
# 按序列号追踪特定流
tcp.stream eq 42
# 查找重传
tcp.analysis.retransmission
# 查找窗口为零(流控)
tcp.analysis.zero_window关键分析场景:
| 场景 | Wireshark 过滤器/操作 | 说明 |
|---|---|---|
| 连接慢 | 统计 → Conversations → 看 TCP 建连时间 | 对比 SYN→SYN+ACK 的时间差 |
| 数据丢失 | tcp.analysis.retransmission | 重传多说明网络丢包 |
| 服务端处理慢 | 追踪流,看 Request→Response 的时间差 | 排除网络因素后查服务端 |
| 连接被重置 | tcp.flags.rst == 1 | 查看谁发的 RST |
| 流控停顿 | tcp.analysis.zero_window | 接收方缓冲区满 |
2.3 实战:排查超时问题
bash
# 场景:Java 应用调用外部 API 超时
# Step 1: 在应用服务器上抓包
tcpdump -i eth0 host api.example.com and port 443 -w /tmp/api_timeout.pcap
# Step 2: 触发一次超时请求
curl -v --connect-timeout 5 --max-time 10 https://api.example.com/health
# Step 3: 用 Wireshark 打开分析
# 如果看到(以下仅为“观察点看到了什么”,不能直接等同于根因):
# - 有 SYN 但无 SYN+ACK:可能是路径丢包、策略丢弃或回程异常,也可能是对端未响应;先确认 SYN 是否重传并比对两端抓包
# - SYN+ACK 正常但后续数据慢:可能是对端处理慢、接收窗口限制或后续网络延迟,需分析请求发出/响应开始的时间差
# - 大量重传:优先核对重传、重复 ACK 与序列号,并区分真实丢包与单侧抓包缺失(Wireshark 的 retransmission 是疑似标签)
# - 出现 RST:只说明观察点看到复位报文,需核对源地址/端口/时序,区分主机内核、中间设备与应用行为
# 注意:本例抓的是 443 端口 HTTPS 流量;未做 TLS 解密时不能用 http.request.method / http.response.code 等明文过滤器,
# 应改用 TCP 握手、重传、RST 与 TLS 握手时序分析。3. Java 网络诊断
3.1 netstat / ss 命令
bash
# 查看所有 TCP 连接状态统计
ss -s
# 查看特定端口的连接
ss -tnp | grep 8080
# 查看连接状态分布
ss -tn | awk '{print $1}' | sort | uniq -c | sort -rn
# 查看 ESTABLISHED 连接数(按远程 IP 统计;仅适用于常见 IPv4 端点,IPv6 地址含冒号需另行解析)
ss -tn state established | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn
# 查看哪些进程在监听端口
ss -tlnp
# 查看 socket 缓冲区使用情况
ss -tmp
# 查看连接的拥塞窗口和 RTT
ss -tni输出解读:
txt
$ ss -tni
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 0 10.0.1.1:8080 10.0.1.2:54321
cubic wscale:7,7 rto:204 rtt:1.5/0.75 ato:40
mss:1448 pmtu:1500 rcvmss:1448 advmss:1448
cwnd:10 ssthresh:7 bytes_sent:1234 bytes_acked:1235
# 关键字段:
# rto:204 — 重传超时 204ms
# rtt:1.5/0.75 — RTT 1.5ms,抖动 0.75ms
# cwnd:10 — 拥塞窗口 10 个 MSS
# ssthresh:7 — 慢启动阈值 7
# Recv-Q — 已接收但未被应用读取的数据量(持续增大可能说明应用消费不及时,但只是某时刻的队列状态)
# Send-Q — 尚未被对端确认的数据量(持续偏高可能说明数据未得到确认;监听 socket 的队列字段语义不同)3.2 jstack 分析线程状态
当 Java 应用网络请求卡住时,jstack 能看到线程在做什么:
bash
# 找到 Java 进程 PID
jps -l
# 生成线程 dump
jstack -l <pid> > /tmp/thread_dump.txt
# 查找 BLOCKED / WAITING 的网络相关线程
grep -A 5 "BLOCKED\|WAITING\|TIMED_WAITING" /tmp/thread_dump.txt | grep -i "socket\|nio\|channel\|http"典型的网络相关线程栈:
以下类名、行号与线程名为特定 JDK 实现下的示意栈,随 JDK 版本与实现变化;判断时关注线程状态与调用路径,而非具体帧名。
txt
# 1. 正常的 NIO 线程(Selector 等待事件)
"NIO-Selector-1" #15 daemon prio=5
java.lang.Thread.State: RUNNABLE
at sun.nio.ch.EPollArrayWrapper.epollWait(Native Method)
at sun.nio.ch.EPollArrayWrapper.poll(EPollArrayWrapper.java:269)
at sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:93)
# 2. 阻塞在 socket read(可能是超时设置过长或对端无响应)
"http-nio-8080-exec-1" #20 daemon prio=5
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
# 3. 等待连接池(连接耗尽!)
"http-nio-8080-exec-5" #24 daemon prio=5
java.lang.Thread.State: TIMED_WAITING
at java.lang.Object.wait(Native Method)
at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:449)
# 4. TLS 数据处理路径(SSLEngine.wrap 属正常加密处理,不能单凭方法名判定握手阻塞)
"https-jsse-nio-8443-exec-1" #30 daemon prio=5
java.lang.Thread.State: RUNNABLE
at sun.security.ssl.SSLEngineImpl.wrap(SSLEngineImpl.java:122)3.3 Arthas 在线诊断
Arthas 是阿里巴巴开源的 Java 诊断工具,无需重启应用即可在线排查问题:
bash
# 启动 Arthas
java -jar arthas-boot.jar
# 1. 查看网络相关的线程
thread -n 5 # CPU 占用最高的 5 个线程
thread --state RUNNABLE # 只看 RUNNABLE 状态的线程
# 2. 监控方法调用耗时(排查慢调用)
# watch 观察 HttpClient.execute 的返回值和耗时
watch org.apache.http.impl.client.CloseableHttpClient execute '{params, returnObj, #cost}' -x 3
# trace 追踪方法内部调用链耗时
trace org.apache.http.impl.client.CloseableHttpClient execute
# 3. 监控连接池状态
# 查看 Druid 连接池的活跃连接数
ognl '@com.alibaba.druid.stat.DruidStatManagerFacade@getInstance().getDataSourceStatDataList().{#this["ActiveCount"]}'
# 注意:ActiveCount 字段名与返回结构随 Druid 版本变化,使用前先核对实际返回对象,勿当作通用命令
# 4. 查看 JVM 网络相关的系统属性
sysprop | grep -i "socket\|nio\|ssl\|timeout"
# 5. 反编译线上代码(确认是否有 bug)
jad com.example.MyHttpClient
# 6. 查看堆中的连接对象(按需使用的高级手段,有额外开销)
vmtool --action getInstances \
--className java.net.Socket \
--express 'instances.{? #this.isConnected()}.size()'
# 注意:isConnected() 表示曾成功连接,不等于当前仍打开;该枚举不能据此直接得出活跃连接数Arthas 诊断流程:
txt
┌─────────────────────────────────────────────────────────┐
│ Arthas 网络问题诊断流程 │
│ │
│ Step 1: thread -n 5 │
│ → 找到 CPU 高的线程,看是否在网络 I/O │
│ │
│ Step 2: thread --state TIMED_WAITING │
│ → 找到等待中的线程,看是否在等连接/响应 │
│ │
│ Step 3: watch / trace 慢方法 │
│ → 定位具体哪个外部调用慢 │
│ │
│ Step 4: ognl 查看连接池/线程池状态 │
│ → 确认是否资源耗尽 │
│ │
│ Step 5: jad 反编译核对线上实际类 │
│ → 确认关键调用路径与源码一致(仍需结合栈/日志验证) │
└─────────────────────────────────────────────────────────┘前面的内容侧重定位问题发生在客户端、连接、应用还是外部依赖。确认瓶颈后,下一页转向处理方案:连接复用、I/O 模型、TCP 参数、限流熔断与监控检查清单。
网络诊断案例
定位到具体方向后,进入对应案例看完整的证据链与修复思路:
- 连接失败与建立缓慢案例:区分
connection refused与connect timed out、SYN 队列满、防火墙静默丢包。 - 请求超时与延迟案例:read 超时指向对端慢、重传与 RTT、应用消费不及时。
- 连接泄漏与状态异常案例:CLOSE-WAIT 堆积、TIME-WAIT 数量高是否真故障、连接池泄漏。
下一页: 高并发网络优化与最佳实践