"接口慢"是最容易吵起来的一句话:网络说应用写得烂,应用说网络在丢包,两边都有"证据"。抓包的价值不是"看到有包在飞",而是把抱怨切成三个可判定的问题:发出去没有、到没到、时间花在哪一段。抓错位置或读错标志位,只会把吵架升级成甩锅。
1. 抓包要回答的三类问题
| 问题 | 判据 | 该看什么 |
|---|---|---|
| 有没有发 | 本机出口有没该流的 SYN | 抓本机网卡,看目标四元组 |
| 有没有到 | 对端是否回 SYN-ACK / 响应 | 抓对端或中间点,比同一序号 |
| 为什么慢 | 握手、首字节、重传、窗口、TLS 各占多少 | 时间差分解,不看总量看分段 |
先定问题再开抓。没问题驱动的抓包,只会产出一个几 GB 的文件和一句"看起来挺正常"。
2. 抓包点怎么选
| 位置 | 能看到 | 看不到 / 风险 |
|---|---|---|
本机网卡 eth0 |
本机发出的与收到的 | 回环流量、已被转发的流量 |
本机回环(lo) |
同机进程间、代理前后 | 物理链路问题 |
容器内 eth0 |
容器视角收发 | 是否经过 NAT、qdisc 丢包 |
宿主机网桥 cni0 |
容器与外部之间的包 | 已被 DNAT 改写的地址 |
| 代理层 | 应用层请求与上游耗时 | 代理自身排队时间 |
抓错位置的典型错误结论:在宿主机 cni0 上只看到 ClusterIP,就判定"应用连错地址";实际是 kube-proxy 的 DNAT 在后面才改写。只在应用侧抓包,看到"SYN 已发出、无 SYN-ACK"就断言"对端没开端口"——也可能是防火墙静默丢弃(DROP 而非 REJECT,本机连 RST 都收不到)。判据:每个抓包点要能说清"之后包还经过谁",说不清就再抓一个点。
3. tcpdump 的最小命令集
# 全量抓、落盘、不做解析(生产首选:抓完再分析)
tcpdump -i eth0 -s 0 -w /tmp/cap-$(date +%H%M).pcap 'host 10.0.0.5 and port 5432'
# 只看握手与挥手,不落盘
tcpdump -i eth0 -nn 'tcp[tcpflags] & (tcp-syn|tcp-ack) != 0'
# 环形缓冲:单文件 100MB,保留 20 个(约 2GB 上限)
tcpdump -i any -s 0 -C 100 -W 20 -w /tmp/ring.pcap 'port 8080'
# 读文件并加时间差,直接看间隔
tcpdump -nn -tttt -r /tmp/cap.pcap 'tcp and host 10.0.0.5'
| 参数 | 作用 | 不写会怎样 |
|---|---|---|
-s 0 |
抓完整帧 | 老默认 68 字节,应用层被切掉 |
-w |
写 pcap 不解析 | 高流量下解析不过来,丢包 |
-nn |
不解析名字 | 逐包查 DNS,抓包自己变慢 |
-i any |
所有接口 | cooked 模式,无以太头 |
-C / -W |
按 MB 轮转、保留个数 | 单文件写满磁盘 |
-Z tcpdump |
抓完降权 | 长期以 root 跑 |
-i any 的副作用:链路层类型变成 SLL,ether host、vlan 这类二层表达式失效;高流量下它聚合所有接口,一个 pcap 里可能有两条路径的同一条流,读时要靠 tcp.stream 区分。
4. BPF 写错导致的漏包
漏包比抓不到更危险:你以为看到了全部,其实只看到一部分。
| 写法 | 实际含义 | 问题 |
|---|---|---|
tcp port 80 or 443 |
(tcp port 80) or (port 443) |
443 也匹配 UDP/QUIC |
not port 22 and host 10.0.0.5 |
组合多了易错 | 复杂表达式必须加括号 |
host 10.0.0.5 and port 443 |
只匹配该地址 | 走代理时四元组是代理的 |
vlan and host 10.0.0.5 |
带标签才匹配 | 内核多已自动处理,加了反而漏 |
-s 100 |
截断到 100 字节 | 应用层解析失败,误判"无请求体" |
# 只抓 SYN(不含 SYN-ACK)
tcpdump -i eth0 -nn 'tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0'
# 只抓 RST
tcpdump -i eth0 -nn 'tcp[tcpflags] & tcp-rst != 0'
# 抓两台主机之间的双向流量(括号不能省)
tcpdump -i eth0 -nn 'host 10.0.0.5 and (port 3306 or port 6379)'
5. Wireshark 的五个判读点
| 现象 | 过滤器 | 含义 |
|---|---|---|
| 三次握手 | tcp.flags.syn==1 |
SYN/SYN-ACK/ACK,看 RTT 与重试 |
| 重传 | tcp.analysis.retransmission |
同 seq 再发,发送方认为丢了 |
| 快速重传 | tcp.analysis.fast_retransmission |
3 个 Dup ACK 后立即重发 |
| Dup ACK | tcp.analysis.duplicate_ack |
接收方有缺口,重复确认同一 seq |
| 复位 | tcp.flags.reset==1 |
被拒或中断,看是谁发的 |
tcp.analysis.flags 是"任意异常"的合集过滤器,先扫一遍再逐项展开,比手工翻页快得多。
# 非 GUI 环境:直接统计异常与 RTT
tshark -r cap.pcap -q -z io,stat,1,"COUNT(tcp.analysis.retransmission)tcp.analysis.retransmission"
tshark -r cap.pcap -Y "tcp.analysis.flags" -T fields -e frame.number -e ip.src -e tcp.len | head
tshark -r cap.pcap -Y "tcp.flags.reset==1" -T fields -e ip.src -e ip.dst -e tcp.srcport
RST 的方向最容易被误读:服务端发 RST,多半是"端口没监听"或"连接关闭后收到数据";客户端发 RST,常见于主动关闭或有状态防火墙超时。见 RST 就断定网络设备重置,是跳步。
6. 重传与乱序:长得像,指向完全不同
| 维度 | 重传 | 乱序 |
|---|---|---|
| 序号 | 与之前某帧同 seq 同长度 | 新 seq,但小于期望值 |
| 触发方 | 发送方超时或收 Dup ACK | 多队列/多链路 |
| 指向 | 丢包、拥塞、零窗口 | 链路聚合、ECMP 不均 |
| 抓包点 | 接收端"重复到达"也可能是设备复制 | 发送端看不到乱序 |
顺序:先看抓包点(发送侧还是接收侧),再看 seq 是否重复,最后归因。发送侧的重传 = 对方没确认(丢包或对端慢);接收侧重复到达 = 上游复制或抓包点重复。
7. TIME_WAIT 与端口耗尽在抓包里长什么样
| 现象 | 抓包里的样子 | 旁证 |
|---|---|---|
| 大量短连接 | 同一对地址间高频 SYN/FIN,tcp.flags.fin 密集 |
ss -tan state time-wait | wc -l |
| 主动关闭方堆积 | 本端先发 FIN,进入 TIME_WAIT 60 秒 | ss -tan state time-wait '( sport = :80 )' |
| 端口耗尽 | 抓不到任何 SYN,应用报 Cannot assign requested address |
cat /proc/sys/net/ipv4/ip_local_port_range |
| 服务端端口耗尽 | SYN 到达但无 SYN-ACK,或直接 RST | ss -lnt 的 Recv-Q 持续非零 |
端口耗尽的陷阱:连接压根没建起来,pcap 里自然什么都没有。"抓不到包"此时是正确观测,不是抓包失败——去应用日志与 ss 里找 EADDRNOTAVAIL。
8. 延迟分解:把"慢"拆成四段
| 段 | 度量 | 典型量级 |
|---|---|---|
| TCP 握手 | SYN→SYN-ACK 的 tcp.time_delta |
同机房亚毫秒,跨地域数十毫秒 |
| TLS 握手 | ClientHello→Finished | TLS1.3 一个 RTT,1.2 两个 |
| 请求到首字节 | 请求末包→响应首包 | 取决于服务端处理 |
| 数据传输 | 首字节→末字节 | 受带宽与窗口限制 |
# 按流导出关键时间点:每个流的首包、末包、帧数
tshark -r cap.pcap -T fields -e tcp.stream -e frame.time_relative \
-e tcp.time_delta -e ip.src -e tcp.len -Y "tcp" | head -20
tcp.time_delta 是"相对同一流上一帧"的差,不是全局相邻帧的差。看握手与 ACK 间隔很直观;混流 pcap 里按 frame 排序求差,会得出完全错误的结论。
9. HTTPS:能看到什么,看不到什么
| 层 | 可见 | 说明 |
|---|---|---|
| TCP | 完整 | 握手、重传、窗口 |
| TLS 握手 | 明文的 ClientHello/ServerHello | SNI(未用 ECH 时)、版本、套件、证书链 |
| TLS 记录层 | 类型、长度、时间 | 可算握手耗时与记录节奏 |
| 应用数据 | 不可见 | 只能看长度与方向 |
所以 HTTPS 上的结论止于"握手多久、记录多大、有无重传"。要判断"接口 500 还是 200",抓包给不了——看应用日志或代理 access log。解密要会话密钥(SSLKEYLOGFILE),是另一套前提。
10. 文件太大怎么办
# 按包数切分,便于分机器并行分析
editcap -c 200000 big.pcap part.pcap
# 只留某条 TCP 流
tshark -r big.pcap -Y "tcp.stream==7" -w stream7.pcap
# 合并多点抓包(同一时钟源时才有意义)
mergecap -w merged.pcap a.pcap b.pcap
# 抓的时候就用环形缓冲,别等抓完再切
tcpdump -i eth0 -s 0 -C 200 -W 10 -w /tmp/ring.pcap
时间戳精度也要交代:默认微秒,--time-stamp-precision=nano 可到纳秒,需网卡与内核支持;它记的是内核收到包的时刻,不等于链路真实到达时刻,微秒以下的差异别当证据。
11. 容器与 K8s 里在哪抓
| 场景 | 做法 | 注意 |
|---|---|---|
| 单容器 | docker exec -it <id> tcpdump -i eth0 |
镜像常没装 tcpdump |
| 镜像无 tcpdump | nsenter -t $(pidof app) -n tcpdump -i eth0 |
需宿主机 root |
| 共享 netns | docker run --net=container:<id> nicolaka/netshoot |
同一份流量 |
| K8s 单 Pod | kubectl debug -it <pod> --image=nicolaka/netshoot |
临时容器共享 netns |
| Service 层 | 抓 cni0 或 veth 宿主侧 |
是 DNAT 前还是后,要确认 |
veth 是一对管子:容器 eth0 与宿主机 vethXXX 各一端。两边同抓各得一份,合并前先确认是哪侧,否则会把单向流量误判成"双向重复发送"。
12. 排错
| 现象 | 原因 | 处理 |
|---|---|---|
| 回环服务抓不到 | 抓 eth0,流量走 lo |
改 -i lo 或 -i any |
| 校验和全错 | TSO/GSO 卸载,抓到的是未算校验和的帧 | 忽略校验和,或禁用卸载 |
| 过滤后一条没有 | BPF 优先级写错、VLAN、走了代理 | 先全抓,再逐步加条件 |
| 见重传就断定丢包 | 抓点在发送侧,也可能对端未确认 | 到接收侧再抓,比对 seq |
| 应用慢归因网络 | 只看到"响应晚到" | 分解三段,配合服务端日志 |
| 时间差为 0 或乱序 | 多接口同抓,时钟交错 | 单接口抓,或按 tcp.stream 分组 |
13. 边界
| 问题 | 该用什么 | 理由 |
|---|---|---|
| 应用返回 500 | 应用日志 / access log | 抓包看不到 HTTP 语义 |
| 是否丢包 | 两端抓包 + 重传统计 | 单点无法区分丢在哪段 |
| 内核丢包 | netstat -s、ethtool -S、ss -s |
pcap 看不到内核/驱动丢弃 |
| 谁占带宽 | sFlow/NetFlow | 抓包只适合短时窗口 |
| 长期趋势 | 指标系统 | pcap 留不住 |
14. 自测
- "接口慢"要先拆成哪三个可判定的事实?
- 在宿主机
cni0上只看到 ClusterIP,能得出"应用连错地址"的结论吗? tcpdump -i any抓到的帧为什么没有以太网头?tcp.analysis.retransmission与tcp.analysis.out_of_order分别指向什么问题?- 端口耗尽时 pcap 里为什么可能什么都没有?该去哪里找证据?
- HTTPS 流量里,哪一部分是可读的,哪一部分注定不可读?