Skip to content

网络性能分析与故障排查 ​

网络问题是后端开发中最棘手的一类故障——症状相似但根因各异,超时可能是网络不通、对端处理慢、连接池耗尽,也可能是 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 53

tcpdump 输出解读:

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 参数、限流熔断与监控检查清单。

网络诊断案例 ​

定位到具体方向后,进入对应案例看完整的证据链与修复思路:

下一页: 高并发网络优化与最佳实践

Released under the MIT License.