第二十九章 常见网络故障排查:strace、tcpdump、netstat、lsof 工具使用与案例分析
说实话,搞网络编程这么多年,我见过太多「代码看着没问题,一跑就崩」的场面了。你写的 socket 逻辑再漂亮,到了线上环境,可能一个防火墙规则、一个端口被占用、甚至一个 DNS 解析超时,就能让你抓狂一整天。
所以这一章,我想跟你聊聊我平时排查网络故障最常用的四个工具:strace、tcpdump、netstat、lsof。它们就像我的「四把刀」,各有各的用法,也各有各的脾气。掌握了它们,你基本能搞定 90% 的网络疑难杂症。
核心思路:排查网络故障,本质上是「分层定位」。应用层出问题,先看 strace;传输层出问题,上 tcpdump;连接状态不清楚,用 netstat;文件描述符搞不定,找 lsof。别一上来就抓包,那是浪费生命。
29.1 strace:看你的程序到底在干嘛
strace 这工具,说白了就是「偷窥」你的程序跟操作系统之间的对话。每次你的程序调用 read、write、connect、accept 这些系统函数,strace 都能给你记下来。
我记得有一次,一个同事写的 HTTP 客户端总是超时。代码看了三遍,逻辑没问题。我让他 strace 一下,结果发现程序在 connect 之前先调了一个 DNS 解析,那个 DNS 服务器响应巨慢。嗯,问题一下就找到了。
基本用法
# 追踪一个正在运行的进程
strace -p 12345
# 只追踪网络相关的系统调用
strace -e trace=network -p 12345
# 把输出保存到文件,方便分析
strace -o trace.log -p 12345
# 追踪新启动的程序
strace -f ./my_server
我的小技巧:用 -f 参数可以追踪子进程。如果你的服务器用了 fork,不加这个参数只能看到父进程的调用,子进程的调用全丢了。我曾经因为这个浪费了半天时间。
实战案例:connect 失败
假设你的客户端连不上服务器。用 strace 跑一下:
$ strace -e trace=connect ./my_client
connect(3, {sa_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("192.168.1.100")}, 16) = -1 ETIMEDOUT (Connection timed out)
看到 ETIMEDOUT 了吗?这说明你的 SYN 包发出去了,但对方没回 SYN-ACK。可能是防火墙拦了,也可能是服务器根本没启动。这时候就该上 tcpdump 了。
29.2 tcpdump:抓包才是硬道理
strace 只能看到「你的程序想干嘛」,但网络线上到底发生了什么,还得靠 tcpdump。这工具能把你网卡上经过的所有数据包都抓下来,然后你可以慢慢分析。
我个人习惯是:先用 tcpdump 抓个几秒钟,保存成 pcap 文件,然后用 Wireshark 打开看。命令行看包太费眼睛了。
基本用法
# 抓取 eth0 网卡的所有包
tcpdump -i eth0
# 抓取特定端口
tcpdump -i eth0 port 8080
# 抓取特定主机的流量
tcpdump -i eth0 host 192.168.1.100
# 保存到文件
tcpdump -i eth0 -w capture.pcap
# 读取文件
tcpdump -r capture.pcap
实战案例:TCP 三次握手失败
你想想看,如果客户端连不上服务器,tcpdump 能看到什么?
$ tcpdump -i eth0 port 8080 -nn
13:45:12.123456 IP 192.168.1.10.54321 > 192.168.1.100.8080: Flags [S], seq 1000
13:45:12.123789 IP 192.168.1.100.8080 > 192.168.1.10.54321: Flags [S.], seq 2000, ack 1001
13:45:12.124012 IP 192.168.1.10.54321 > 192.168.1.100.8080: Flags [.], ack 2001
这是正常的三次握手。如果只看到 [S] 没看到 [S.],那就是 SYN 包发出去了但没收到响应。如果看到 [R] 或者 [R.],说明对方主动拒绝了连接——可能是端口没开,或者防火墙发了 RST。
注意:tcpdump 需要 root 权限。如果你用普通用户跑,会报错说「权限不够」。另外,在生产环境抓包要小心,流量大的时候抓包文件会膨胀得很快,别把磁盘撑爆了。
29.3 netstat:看一眼连接状态
netstat 是我用得最频繁的工具。它就像网络连接的「体检报告」,看一眼就知道当前系统上有多少连接、每个连接在什么状态。
我曾经遇到过一个 case:服务器上跑着 Nginx,但客户端总是报「连接被拒绝」。我 netstat 一看,发现 80 端口根本没在监听。原来是 Nginx 启动失败了,但启动脚本没报错。嗯,这种坑踩过一次就记住了。
常用命令
# 查看所有监听端口
netstat -tlnp
# 查看所有连接(包括已建立的)
netstat -tanp
# 查看 Unix socket
netstat -xlnp
# 统计各种状态的连接数
netstat -tan | awk '{print $6}' | sort | uniq -c
输出解读
| 列名 | 含义 | 常见值 |
|---|---|---|
| Proto | 协议类型 | tcp, udp |
| Recv-Q | 接收队列字节数 | 0 表示正常,持续增长说明应用没及时读数据 |
| Send-Q | 发送队列字节数 | 0 表示正常,持续增长说明对端没及时收数据 |
| Local Address | 本地地址和端口 | 0.0.0.0:80 表示监听所有网卡的 80 端口 |
| Foreign Address | 远端地址和端口 | 192.168.1.10:54321 |
| State | 连接状态 | LISTEN, ESTABLISHED, TIME_WAIT, CLOSE_WAIT |
重点关注:如果看到大量 TIME_WAIT 或 CLOSE_WAIT,说明你的程序在连接关闭上可能有问题。TIME_WAIT 太多会耗尽端口资源,CLOSE_WAIT 太多说明对端关了连接但你这边没关——说白了就是代码里忘了 close。
29.4 lsof:谁占了我的端口?
「端口被占用」这个问题,我估计每个做网络编程的人都遇到过。你启动服务器,报错说「Address already in use」。这时候 lsof 就是你的救星。
lsof 的全称是「List Open Files」。在 Unix 世界里,一切皆文件——socket 也是文件。所以 lsof 能告诉你哪个进程打开了哪个端口。
常用命令
# 查看哪个进程占用了 8080 端口
lsof -i :8080
# 查看某个进程打开的所有网络连接
lsof -i -a -p 12345
# 查看所有监听状态的 socket
lsof -i -s TCP:LISTEN
# 查看某个用户的所有网络连接
lsof -i -u username
实战案例:端口被占用
$ lsof -i :8080
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
java 12345 root 30u IPv4 123456 0t0 TCP *:8080 (LISTEN)
看到了吧?PID 12345 的 Java 进程占着 8080 端口。你可以选择 kill 掉它,或者换一个端口。
避坑指南:我曾经用 kill -9 杀掉了一个占端口的进程,结果发现端口还是被占着。为什么?因为进程虽然死了,但 socket 还处于 TIME_WAIT 状态。这时候要么等 2MSL 超时,要么设置 SO_REUSEADDR 选项。嗯,这个坑我踩过两次才记住。
29.5 综合案例:一个完整的排查过程
假设你写了一个简单的 HTTP 服务器,监听 8888 端口。客户端连上去之后,发了一个请求,但服务器没响应。我们来一步步排查。
第一步:检查端口是否在监听
$ netstat -tlnp | grep 8888
tcp 0 0 0.0.0.0:8888 0.0.0.0:* LISTEN 12345/./my_server
嗯,端口在监听,进程也在。那问题可能出在连接上。
第二步:抓包看看
$ tcpdump -i lo port 8888 -nn
13:50:00.123456 IP 127.0.0.1.54321 > 127.0.0.1.8888: Flags [S], seq 1000
13:50:00.123789 IP 127.0.0.1.8888 > 127.0.0.1.54321: Flags [S.], seq 2000, ack 1001
13:50:00.124012 IP 127.0.0.1.54321 > 127.0.0.1.8888: Flags [.], ack 2001
13:50:00.124500 IP 127.0.0.1.54321 > 127.0.0.1.8888: Flags [P.], seq 1001:1100, ack 2001
... 然后就没有然后了
三次握手成功了,客户端也发了数据,但服务器没回任何包。这说明服务器收到了数据,但没处理——或者说处理了但没发响应。
第三步:用 strace 看服务器在干嘛
$ strace -p 12345 -e trace=read,write,accept
accept(3, ...) = 4
read(4, "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n", 1024) = 35
# 注意:这里没有 write 调用!
# 服务器读了请求,但没写响应
问题找到了:服务器读了请求之后,没有调用 write 发送响应。要么是业务逻辑有 bug,要么是代码里忘了写 send() 或 write()。
第四步:用 lsof 确认文件描述符
$ lsof -i -a -p 12345
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
my_serve 12345 root 3u IPv4 123456 0t0 TCP *:8888 (LISTEN)
my_serve 12345 root 4u IPv4 123457 0t0 TCP localhost:8888->localhost:54321 (ESTABLISHED)
FD 4 是已建立的连接,但 strace 显示没有 write 调用。嗯,代码问题实锤了。
总结一下排查思路:
- 先用 netstat 看端口是否在监听
- 用 tcpdump 看网络包是否正常交互
- 用 strace 看程序内部系统调用情况
- 用 lsof 确认文件描述符和进程关系
这四步走下来,90% 的网络问题都能定位到根因。
29.6 一些实用的小技巧
最后分享几个我平时常用的组合拳:
- 快速查看系统连接数:
netstat -tan | grep ESTABLISHED | wc -l - 查看哪个端口被占用最多:
netstat -tan | awk '{print $4}' | cut -d: -f2 | sort | uniq -c | sort -rn - 实时追踪某个进程的网络活动:
strace -e trace=network -p PID -o /dev/stdout | grep -E "connect|accept|read|write" - 抓取 HTTP 请求内容:
tcpdump -A -i any port 80(-A 参数会以 ASCII 格式显示包内容) - 查看某个端口的历史连接:
lsof -i :8080 -r 2(每 2 秒刷新一次)
说实话,这些工具用熟了之后,排查问题就像「庖丁解牛」一样。你看到错误信息,脑子里就能大概猜到是哪一层出了问题,然后直接上对应的工具验证。这种「直觉」是靠一次次踩坑练出来的。
嗯,这一章的内容就到这里。工具是死的,思路是活的。下次你遇到网络问题,别慌,按着这个流程走一遍,大概率能找到答案。