第二十九章 常见网络故障排查:strace、tcpdump、netstat、lsof 工具使用与案例分析

说实话,搞网络编程这么多年,我见过太多「代码看着没问题,一跑就崩」的场面了。你写的 socket 逻辑再漂亮,到了线上环境,可能一个防火墙规则、一个端口被占用、甚至一个 DNS 解析超时,就能让你抓狂一整天。

所以这一章,我想跟你聊聊我平时排查网络故障最常用的四个工具:strace、tcpdump、netstat、lsof。它们就像我的「四把刀」,各有各的用法,也各有各的脾气。掌握了它们,你基本能搞定 90% 的网络疑难杂症。

核心思路:排查网络故障,本质上是「分层定位」。应用层出问题,先看 strace;传输层出问题,上 tcpdump;连接状态不清楚,用 netstat;文件描述符搞不定,找 lsof。别一上来就抓包,那是浪费生命。

网络故障排查工具知识体系 网络故障排查 strace 系统调用追踪 tcpdump 网络包抓取 netstat 连接状态查看 lsof 文件描述符排查 应用层 → 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_WAITCLOSE_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 调用。嗯,代码问题实锤了。

总结一下排查思路:

  1. 先用 netstat 看端口是否在监听
  2. tcpdump 看网络包是否正常交互
  3. strace 看程序内部系统调用情况
  4. 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 秒刷新一次)

说实话,这些工具用熟了之后,排查问题就像「庖丁解牛」一样。你看到错误信息,脑子里就能大概猜到是哪一层出了问题,然后直接上对应的工具验证。这种「直觉」是靠一次次踩坑练出来的。

嗯,这一章的内容就到这里。工具是死的,思路是活的。下次你遇到网络问题,别慌,按着这个流程走一遍,大概率能找到答案。

公众号:蓝海资料掘金营,微信deep3321