Linux运维13 min read次阅读

抓包不是玄学:把网络问题钉死在 tcpdump 和 tshark 上

有次线上两个服务之间偶尔抽风:A 调 B 的接口,大部分请求几十毫秒回来,但每隔一阵就有几个请求卡到两三秒,甚至直接连不上。ping 是通的,telnet 端口也是通的,应用日志里只看到「调用超时」。这种问题如果只盯着应用层,会一直陷在「是不是代码有 bug」里转圈。

后来在 A 所在机器上抓了一分钟的包,发现卡的那几个请求,TCP 三次握手之后立刻出现了重传(retransmission),而且重传间隔越来越大——典型的链路丢包。再往上追到中间那台交换机,果然有个端口错包在涨。这件事让我养成一个习惯:只要是「时不时慢」「偶发连不上」「握手慢」这类问题,先抓包,别先猜。

抓包前先想清楚三件事

第一,在哪台机器抓。原则是从「两边都抓,对比看」开始。如果只在客户端抓到 SYN 发出去了但没有 SYN-ACK 回来,问题就在客户端到服务端之间的某个环节;如果服务端抓到了 SYN 但应用层没反应,那是服务端本地(backlog 满了、服务没监听、iptables 挡了)。只在一端抓,结论永远是不完整的。

第二,抓哪块网卡。-i any 能抓所有网卡,但有些发行版上 any 抓不到某些隧道流量,且 any 下的包序号排序偶尔会乱。实在不确定就 ip addr 看一眼,指定具体网卡,比如 -i eth0。容器场景后面单独说。

第三,抓多久、存多大。-w 落盘比直接打印到屏幕靠谱得多,因为屏幕滚动太快根本看不过来,而且落盘后才能用 tshark / Wireshark 反复分析。但落盘也有代价:抓全量大包在高 PPS 下会丢包,还会写满磁盘。生产上我一般用 -s 96 只截前 96 字节(TCP 头 + 一点 payload),定位握手、重传、RST 这种问题完全够,磁盘压力小很多。

第一个能用的命令

# 在客户端抓:和目标 IP 的 443 端口有关的全部流量,截断到 96 字节,落盘
sudo tcpdump -i any -nn -s 96 -w /tmp/capture.pcap host 10.0.0.5 and port 443

几个参数拆开说:

  • -nn:不做端口和主机名的反向解析。生产上一定加,不然 tcpdump 为了把 443 解析成 https、把 IP 解析成主机名,会自己发 DNS 请求,既拖慢又污染抓包内容。
  • -s 96:前面说过,只截头部。要完整看应用层 payload(比如明文 HTTP 内容)才用 -s 0(等于不截断)。
  • -w:写文件,后面用 -r 读回来分析。
  • 最后的 host 10.0.0.5 and port 443捕获过滤器(BPF),在内核层就过滤,只把符合条件的包交给 tcpdump,开销最小。

这里有个几乎人人都踩过的坑:捕获过滤器(BPF)和显示过滤器(Wireshark/tshark 用的)语法完全不同。BPF 里写 tcp port 443,到了 tshark 的 -Y 里得写 tcp.port == 443。混着用要么报错要么抓不到东西。我个人的做法是:tcpdump 抓包时 BPF 写得宽松一点(比如只限定 host),把文件留全,后续用 tshark 的显示过滤器做精细筛选——显示过滤器能用的字段多得多。

抓完怎么看,不依赖 Wireshark

很多时候服务器上根本装不了图形界面,得纯命令行分析。

# 直接读 pcap,带过滤条件
tcpdump -nn -r /tmp/capture.pcap 'tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0'

# 只看 RST 包(连接被重置)
tcpdump -nn -r /tmp/capture.pcap 'tcp[tcpflags] & tcp-rst != 0'

# 统计每个 IP 的包数和字节数
tcpdump -nn -r /tmp/capture.pcap -q | awk '{print $3}' | sort | uniq -c | sort -rn | head

第一条里的 tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0 是「只看纯 SYN(没有带 ACK 的那个)」,也就是三次握手的第一个包。如果抓到的 SYN 一堆但对应的 SYN-ACK 一个都没有,那不是服务挂了就是中间被挡了。tcpdump 支持 tcp-syntcp-acktcp-rsttcp-fin 这些符号常量,比直接写十六进制位掩码好记。

但说实话,纯靠 tcpdump 的过滤表达式做深度分析很累。真要定位「哪一跳慢」「是不是重传」,得上 tshark。

tshark 离线分析:把慢的地方量化出来

tshark 是 Wireshark 的命令行版,能用上全部显示过滤器,还能按字段提取成表格。

# 找出所有重传包
tshark -r /tmp/capture.pcap -Y "tcp.analysis.retransmission"

# 找出所有 RST
tshark -r /tmp/capture.pcap -Y "tcp.flags.reset == 1"

# 提取每段的相对时间、源、RTT,找延迟发生在哪
tshark -r /tmp/capture.pcap -T fields \
  -e frame.time_relative \
  -e ip.src -e ip.dst \
  -e tcp.stream \
  -e tcp.analysis.ack_rtt \
  | head -40

tcp.analysis.ack_rtt 这个字段特别有用:它表示 ACK 的往返时间。如果某一段 RTT 突然从 1ms 跳到 200ms,而这段正好对应业务「卡顿」的时间窗,那就坐实了「慢在网络」而不是应用。我曾经用这个字段,把一个「数据库查询慢」的锅,精准甩回了网络组——因为 RTT 在那个时间点飙到了 300ms,而数据库本机查询一直是 2ms。

要提取 HTTP 层面的信息(明文场景)也行:

tshark -r /tmp/capture.pcap -Y http -T fields \
  -e http.host -e http.request.uri -e http.response.code -e http.time

http.time 是请求到响应的耗时,定位「哪个接口慢」比看应用日志还直接,因为包里记录的是真实网络耗时,不含应用内部处理。

三种最常见的「包说了算」信号

SYN 出去了,SYN-ACK 没回来。 意味着握手根本没建立。可能原因:目标端口没监听(服务挂了或没起)、iptables/nftables 把包丢了、中间防火墙拦截、或者目标机器的 net.core.somaxconn / listen 的 backlog 满了导致内核直接丢 SYN(这种情况 ss -lntRecv-Q 会很高,dmesg 可能有 TCP: request_sock_TCP: Possible SYN flooding)。

有重传(retransmission)。 包发出去没收到 ACK,发送方隔一会儿重发。偶发一两个重传在长链路里正常,但如果某个 TCP 流里重传密集,基本就是丢包或者链路质量差。配合 tcp.analysis.ack_rtt 看是不是 RTT 也在涨,能区分「丢包」和「对端处理慢」。

RST 频繁出现。 连接被强行中断。常见几种:服务进程重启(旧连接被内核发 RST)、防火墙会话超时后收到包(比如长连接超过防火墙的 timeout 被清掉会话表,后续包被判非法直接 RST)、应用层主动 close 但没走完四次挥手、还有 keepalive 探测失败后发 RST。RST 最阴险的地方在于它「看起来像网络问题,其实常常是配置问题」——比如很多团队遇到过 MySQL 长连接被中间设备 300 秒超时干掉,表现就是偶发 Lost connection,抓包一看全是 RST。

TLS 能看什么、不能看什么

现在生产基本全是 HTTPS,抓到的满是密文。但别以为抓了没用:

  • 能看到 TLS 握手全过程:Client Hello、Server Hello、证书、密钥交换。握手耗时可以算出来——如果 Client HelloServer Hello Done 之间隔了好几百毫秒,那是握手阶段的延迟,跟后面传数据无关。
  • 能看到 SNI(Client Hello 里带的域名),方便确认连的到底是不是预期的服务。tls.handshake.extensions_server_name 这个字段能直接筛。
  • 能看到 应用数据量(每个方向的字节数),但看不到内容。
  • 有一点要提:TLS 1.3 开始支持 ECH(Encrypted Client Hello),SNI 也能加密了。遇到这种,抓包连域名都看不到,只能看到对端 IP。别硬杠,该去服务端日志看就去服务端。

容器和权限的坑

默认 tcpdump 需要 CAP_NET_RAW 权限,普通用户直接跑会报 socket: Operation not permitted。两个办法:要么 sudo,要么给二进制加 capability 避免长期给 root:

sudo setcap cap_net_raw,cap_net_admin=eip $(readlink -f $(which tcpdump))

容器里抓包更麻烦,因为容器有自己独立的网络命名空间。在宿主机上 docker exec 进容器再抓通常是可行的(只要容器里装了 tcpdump);但如果要在宿主机侧抓某个容器的流量,得先找到它的进程 PID,进到它的 netns:

# 找到容器里 1 号进程的 PID
PID=$(docker inspect -f '{{.State.Pid}}' <container_id>)
# 进它的网络命名空间再抓
sudo nsenter -t $PID -n tcpdump -i eth0 -nn -s 96 -w /tmp/ct.pcap

nsenter -t <pid> -n 是把当前进程的 network namespace 切到目标进程的那个。这一招在 Kubernetes 节点上排查 Pod 网络也通用,把 docker inspect 换成 crictl inspect 就行。

生产抓包的几条保命经验

  • 一定要滚动切文件,别让一个 pcap 写满磁盘。 -G 60 -W 10 表示每 60 秒一轮、最多保留 10 个文件,旧的自动覆盖;-C 100 是每 100MB 切一个。我见过有人 -w 一个文件抓了一晚上,把 /tmp 写爆,连带把线上服务搞挂——因为日志也往 /tmp 写。
  • 抓完及时清理或收紧权限。 pcap 里可能包含 token、cookie、session 这些敏感信息,落在服务器上就是裸奔。分析完立刻 rm,或者抓的时候直接管道给 tshark 不落盘(适合只想要统计的场景)。
  • 高 PPS 全量抓会丢包。 千兆以上网卡全包抓取,tcpdump 自己处理不过来会丢,表现就是抓到的包「不连续」。这种时候 -s 96 截断 + 收窄 BPF 是必须的。要更专业的零丢包抓包,得上 AF_PACKET/DPDK 那套专业工具,那又是另一个话题了。
  • 抓包本身有开销。 别在已经 100% CPU 的机器上猛抓全量包,先收窄范围,或者用 perf 之类的看是不是 CPU 瓶颈。

把这几样串起来,大部分「偶发慢」「连不上」「握手卡」的问题都能在十分钟里定性。抓包最迷人的地方在于它不讲情面——应用层日志可以甩锅、监控系统可以误报,但包里的时间戳和标志位是改不了的。下次再有人说「网络没问题吧」,别争,抓给他看。

分享:

评论区