2024 年,一个服务隔三差五就报一次调用下游超时。我把两边的日志都翻了个底朝天,看到的却是一幅自相矛盾的画面:调用方这边,清清楚楚记着"请求 A 发出,等待响应,超时";可被调方那边,翻遍它的访问日志,【根本没有请求 A 的任何记录】——它压根不知道有这么个请求来过。一边说"我发了",一边说"我没收到"。日志只能告诉我每一端"自己以为"发生了什么,可这个请求,究竟有没有离开过那台机器、有没有在网络上跑过——日志永远回答不了。要回答这个问题,我必须去看网络上真实流过的、一个一个的数据包。这件事逼着我把 Linux 的 tcpdump 抓包这一整套彻底理清了。本文复盘这次实战。
问题背景
环境:CentOS 7,服务 A 调用下游服务 B(10.0.2.30:8080)
事故现象:
- 服务 A 偶发报"调用 B 超时"
- 服务 B 的访问日志里,找不到这些超时请求的记录
- 一边说发了,一边说没收到 —— 日志对不上
现场排查:
# 1. 在服务 A 这台机上,抓它发往 B 的包
$ tcpdump -i any -nn host 10.0.2.30 and port 8080 -w a.pcap
# 跑一会儿,复现到一次超时,停止
# 2. 读抓下来的包
$ tcpdump -nn -r a.pcap | head -40
10:20:01.100 IP 10.0.1.10.51234 > 10.0.2.30.8080: Flags [S], seq 1
10:20:02.100 IP 10.0.1.10.51234 > 10.0.2.30.8080: Flags [S], seq 1
10:20:04.100 IP 10.0.1.10.51234 > 10.0.2.30.8080: Flags [S], seq 1
10:20:08.100 IP 10.0.1.10.51234 > 10.0.2.30.8080: Flags [S], seq 1
# ★ 真相:服务 A 连发了 4 个 SYN(Flags [S]),
# 间隔 1s、2s、4s —— 这是 TCP 在【重传 SYN】!
# 说明:SYN 发出去了,但【始终没收到对方的 SYN+ACK】。
# 3. 同时在服务 B 那台机上抓
$ tcpdump -i any -nn host 10.0.1.10 -w b.pcap
$ tcpdump -nn -r b.pcap
#(空)—— 服务 B 这台机,【一个来自 A 的包都没抓到】
根因(后来想清楚的):
1. 服务 A 确实把 SYN 发出去了 —— A 机上抓到了。
2. 服务 B 那台机,却连一个 SYN 都没收到 ——
说明这个 SYN 在【A 到 B 的网络路上】丢了。
3. A 的 TCP 协议栈见迟迟收不到 SYN+ACK,
按指数退避(1s/2s/4s/8s)重传 SYN,
重传几次仍无果 -> 连接建立失败 -> 应用层报"超时"。
4. 后来定位到:中间一台防火墙/安全组,在高峰期
对这条链路有偶发的丢包(规则或负载问题)。
5. ★ 关键认知:应用日志只记录"应用层看到的事"。
连 TCP 握手都没成功,请求根本没进到应用层 ——
所以 B 的访问日志当然是空的,它没"撒谎"。
日志在网络层是瞎的,要看真相,只能抓包。
修复 1:tcpdump 基础——抓哪块网卡、抓什么
# === 先看机器上有哪些网卡可以抓 ===
$ tcpdump -D # 列出所有可抓的接口
1.eth0
2.any (Pseudo-device that captures on all interfaces)
3.lo
# === 最基本的抓包:指定网卡 ===
$ tcpdump -i eth0 # 抓 eth0 上的包
$ tcpdump -i any # ★ 抓【所有】网卡 —— 不确定走哪块时用它
$ tcpdump -i lo # 抓本机回环(排查本机进程间通信)
# === ★ 几个必加的参数 ===
$ tcpdump -i any -nn -tttt -v host 10.0.2.30
# -nn ★ 不把 IP 解析成域名、不把端口解析成服务名 ——
# 不加 -nn,tcpdump 会做 DNS 反查,既慢又会污染抓包结果
# -tttt 时间戳显示成可读的完整日期时间
# -v 显示更详细的包信息(-vv 更详细)
# -c 100 只抓 100 个包就停(别让它无限抓)
# -s 0 抓每个包的【完整内容】(老版本默认只抓前一截)
# === ★ 抓包存文件,事后慢慢分析(强烈推荐这么做)===
$ tcpdump -i any -nn host 10.0.2.30 -w capture.pcap
# -w 把【原始包】写进文件,而不是在屏幕上打印。
# 现场只管抓,分析放到事后 —— 这是抓疑难问题的正确姿势。
# === 读取之前抓下来的文件 ===
$ tcpdump -nn -r capture.pcap # 读文件
$ tcpdump -nn -r capture.pcap 'tcp[tcpflags] & tcp-syn != 0'
# ★ 读文件时,一样可以再套过滤条件,反复筛 —— 这是 -w 的最大好处。
# === 抓包时控制文件大小,避免撑爆磁盘 ===
$ tcpdump -i any -nn -w cap.pcap -C 100 -W 10
# -C 100 每个文件 100MB 就切换 -W 10 最多 10 个文件,循环覆盖
修复 2:tcpdump 的过滤表达式(BPF)
# === ★ 抓包一定要加过滤条件,否则会被海量无关包淹没 ===
# tcpdump 末尾跟的就是 BPF 过滤表达式。
# === 按主机过滤 ===
$ tcpdump -nn host 10.0.2.30 # 和这个 IP 相关的(收+发)
$ tcpdump -nn src host 10.0.1.10 # 只看【源】是它的
$ tcpdump -nn dst host 10.0.2.30 # 只看【目标】是它的
# === 按端口过滤 ===
$ tcpdump -nn port 8080 # 源或目标端口是 8080
$ tcpdump -nn dst port 8080 # 只看目标端口 8080
$ tcpdump -nn portrange 8000-9000 # 一个端口范围
# === 按协议过滤 ===
$ tcpdump -nn tcp # 只看 TCP
$ tcpdump -nn udp port 53 # 只看 DNS(UDP 53)
$ tcpdump -nn icmp # 只看 ping 之类
# === ★ 用 and / or / not 组合(排查时最常用)===
$ tcpdump -nn host 10.0.2.30 and port 8080
$ tcpdump -nn 'host 10.0.2.30 and (port 8080 or port 8443)'
$ tcpdump -nn 'tcp and host 10.0.2.30 and not port 22'
# ★ 表达式里有括号、空格时,用单引号整个引起来。
# ★ not port 22 很实用:排掉你自己 ssh 的流量,免得自己干扰自己。
# === ★ 按 TCP 标志位过滤(精确抓握手/挥手)===
$ tcpdump -nn 'tcp[tcpflags] & tcp-syn != 0' # 只抓带 SYN 的包
$ tcpdump -nn 'tcp[tcpflags] & tcp-rst != 0' # ★ 只抓 RST(连接被重置)
$ tcpdump -nn 'tcp[tcpflags] & (tcp-syn|tcp-fin) != 0' # SYN 或 FIN
# 排查"连接建不上",抓 SYN;排查"连接被掐断",抓 RST。
# === 按网段过滤 ===
$ tcpdump -nn net 10.0.2.0/24 # 整个网段
修复 3:读懂抓包结果——这次的根因
# === tcpdump 输出的一行,怎么读 ===
$ tcpdump -nn -r a.pcap
10:20:01.100 IP 10.0.1.10.51234 > 10.0.2.30.8080: Flags [S], seq 1, win 29200
# 时间戳 源IP.源端口 > 目标IP.目标端口 : 标志位, 序列号, 窗口...
# ">" 表示方向:从左边发往右边。
# === ★ Flags 标志位 —— 读包的核心 ===
# [S] SYN —— 请求建立连接
# [S.] SYN-ACK —— 同意建立连接(. 代表 ACK)
# [.] ACK —— 单纯的确认
# [P.] PSH+ACK —— 带着数据载荷的包(实际在传数据)
# [F.] FIN-ACK —— 请求关闭连接
# [R] RST —— ★ 重置连接:"这个连接有问题,立刻断"
# === ★ 一次正常的三次握手,在包里长这样 ===
# A.51234 > B.8080: Flags [S] —— A 发 SYN
# B.8080 > A.51234: Flags [S.] —— B 回 SYN+ACK
# A.51234 > B.8080: Flags [.] —— A 回 ACK,握手完成
# 看到这三行齐全,连接才算建好。
# === ★ 这次抓到的,是一次【失败】的握手 ===
# 10:20:01 A > B: Flags [S] 第 1 个 SYN
# 10:20:02 A > B: Flags [S] 1 秒后重传
# 10:20:04 A > B: Flags [S] 2 秒后再重传
# 10:20:08 A > B: Flags [S] 4 秒后又重传
# ★ 自始至终,没有一行 B > A 的 [S.] ——
# A 在反复喊"建立连接",B 那边【从来没应答】。
# ★ 而 B 机上抓包是空的 -> SYN 在去往 B 的路上就丢了。
# === 几种典型现象,对应不同病因 ===
# 只有 A 发 SYN、无任何回应 -> 包在路上丢了,或 B 没监听/被防火墙吞
# A 发 SYN,B 回 RST -> B 机【在线但那个端口没人监听】
# 握手成功,但中途突然 RST -> 连接被中间设备/对端强制掐断
# 有数据包发出、但迟迟无 ACK -> 网络丢包,看到大量 [P.] 重传
修复 4:把包抓下来,用 Wireshark 深入分析
# === tcpdump 抓、Wireshark 看 —— 黄金搭档 ===
# tcpdump 在服务器上抓包存成 .pcap,
# 把文件拉回本地,用 Wireshark 图形界面分析 ——
# 复杂问题,Wireshark 的过滤和追踪能力远强于命令行。
# === 把 .pcap 从服务器拉到本地 ===
$ scp root@server:/tmp/capture.pcap ./
# 然后本地用 Wireshark 打开 capture.pcap
# === Wireshark 里几个最有用的功能 ===
# 1. 显示过滤器(和 BPF 不同的语法):
# tcp.flags.syn == 1 && tcp.flags.ack == 0 只看纯 SYN
# tcp.flags.reset == 1 只看 RST
# tcp.analysis.retransmission ★ 只看重传包
# http.request 只看 HTTP 请求
# 2. 右键某个包 -> Follow -> TCP Stream:
# 把一条连接的来回数据【拼成完整对话】,排查神器。
# 3. Statistics -> Conversations:看每对连接的包数/字节数。
# 4. Expert Information:Wireshark 自动标出重传、乱序、丢包。
# === 不想装 Wireshark?命令行也能做基础分析 ===
# tshark 是 Wireshark 的命令行版:
$ tshark -r capture.pcap -Y 'tcp.analysis.retransmission'
# -Y 后面就是 Wireshark 的显示过滤器语法。
# === ★ 抓 HTTP 这类明文协议,可以直接看内容 ===
$ tcpdump -i any -nn -A -s 0 'tcp port 8080'
# -A 把包的载荷按 ASCII 打印出来 —— 能直接看到 HTTP 报文。
$ tcpdump -i any -nn -X 'tcp port 8080'
# -X 同时显示十六进制和 ASCII。
# ★ 注意:HTTPS 是加密的,抓下来是密文,看不到明文内容。
修复 5:抓包配合其他工具一起定位
# === 抓包能看到"包",但要配合别的工具看"为什么" ===
# === 1. 先用 ss 确认对端端口到底有没有在监听 ===
# 在服务 B 那台机上:
$ ss -lntp | grep 8080
# 如果这里是空的 —— B 根本没监听 8080,
# 那 A 抓到的就该是 "B 回 RST",而不是"无响应"。
# === 2. 用 ping / mtr 看 A 到 B 的网络通不通、丢不丢包 ===
$ ping -c 20 10.0.2.30 # 看丢包率和延迟
$ mtr 10.0.2.30 # ★ 逐跳显示路由,哪一跳开始丢包
# mtr 能定位到"丢包发生在中间哪一跳" —— 对本次很关键。
# === 3. 看本机防火墙有没有把包拦了 ===
$ iptables -L -n -v # 看规则,以及每条规则的命中计数
$ iptables -L -n -v | grep DROP
# ★ 看 DROP 规则的包计数在不在涨 —— 涨,就是本机防火墙在丢包。
# === 4. 看连接跟踪表(NAT / 状态防火墙环境)===
$ conntrack -L | grep 10.0.2.30 # 看这条连接的跟踪记录
$ cat /proc/sys/net/netfilter/nf_conntrack_count # 当前跟踪的连接数
$ cat /proc/sys/net/netfilter/nf_conntrack_max # 上限
# ★ conntrack 表【满了】也会丢包 —— count 逼近 max 就是信号。
# === 5. 看网卡层面有没有丢包/错误 ===
$ ip -s link show eth0 # RX/TX 的 errors、dropped 计数
$ ethtool -S eth0 | grep -i -E 'drop|error' # 网卡更详细的统计
# === 6. 看内核有没有丢包相关的统计 ===
$ netstat -s | grep -i -E 'retrans|drop|listen'
# "segments retransmitted" 高 = 在大量重传,网络质量差的铁证。
修复 6:抓包排查纪律
# === 这次事故暴露的排查盲区,定几条纪律 ===
# === 1. ★ 日志在网络层是瞎的,该抓包就抓包 ===
# 两边日志对不上("我发了"vs"我没收到"),
# 别再纠结日志 —— 立刻抓包,看网络上真实流过的包。
# === 2. ★ 怀疑链路问题,要【两端同时抓】 ===
# 只在一端抓,只能知道"包发出去了";
# 两端一起抓,才能判断"包到底在哪一段丢的"。
# 本次正是靠"A 抓到、B 没抓到"锁定了丢包位置。
# === 3. 抓包必须加过滤条件 ===
$ tcpdump -i any -nn host 对端IP and port 端口
# 不加过滤,海量无关包会瞬间淹没你,也增加机器负担。
# === 4. 抓包优先 -w 存文件,事后分析 ===
$ tcpdump ... -w cap.pcap
# 现场只管抓,-r 反复读、反复换条件筛 —— 比盯屏幕高效得多。
# === 5. -nn 永远要加 ===
# 不加 -nn,tcpdump 做 DNS 反查,既慢又会自己产生 DNS 流量
# 污染你的抓包结果。
# === 6. 抓包是有成本的,别在高负载机器上长时间裸抓 ===
# 用 -c 限制包数、用 -C/-W 限制文件大小,加精确过滤,
# 抓到复现就停 —— 别让抓包本身成为新的问题。
# === 7. 排查网络问题的工具链 ===
$ ss -lntp # ① 对端端口在监听吗
$ ping / mtr 对端 # ② 链路通不通、哪一跳丢包
$ tcpdump -w 抓包 # ③ 看真实的包,SYN/RST/重传
$ iptables -L -n -v # ④ 防火墙有没有 DROP
$ netstat -s | grep retrans # ⑤ 内核重传统计
# 按这个顺序,网络问题基本能定位。
命令速查
需求 命令
=============================================================
列出可抓的网卡 tcpdump -D
抓某主机某端口的包 tcpdump -i any -nn host IP and port 端口
抓包存文件 tcpdump -i any -nn host IP -w cap.pcap
读取抓包文件 tcpdump -nn -r cap.pcap
只抓 SYN 包 tcpdump -nn 'tcp[tcpflags] & tcp-syn != 0'
只抓 RST 包 tcpdump -nn 'tcp[tcpflags] & tcp-rst != 0'
按 ASCII 看载荷 tcpdump -nn -A -s 0 'tcp port 8080'
逐跳看路由丢包 mtr 对端IP
看端口是否被监听 ss -lntp | grep 端口
看内核重传统计 netstat -s | grep -i retrans
口诀:日志对不上就抓包 -> 两端同时抓定位丢包段
抓包必加 -nn 和过滤 -> 优先 -w 存文件事后分析
避坑清单
- 应用日志只记录应用层看到的事,握手没成功的请求它根本不会有记录
- 两端日志对不上("我发了"vs"我没收到"),该抓包看网络真相
- 怀疑链路丢包要两端同时抓,只抓一端无法判断包丢在哪一段
- tcpdump 必须加 -nn,否则 DNS 反查既慢又会污染抓包结果
- 抓包必须加过滤条件,否则海量无关包会淹没你并拖累机器
- 抓包优先用 -w 存文件,-r 反复读反复换条件筛比盯屏幕高效
- 读懂 Flags:[S]是SYN [S.]是SYN-ACK [R]是RST [P.]是带数据的包
- 只见 SYN 重传不见 SYN-ACK,说明握手包丢了或对端没响应
- 对端回 RST 说明它在线但那个端口没有进程监听
- 抓包有成本,高负载机器上要用 -c 限包数、精确过滤,抓到就停
总结
这次"两端日志对不上"的事故,纠正了我一个排查时的思维定式:遇到问题,我习惯性地、几乎是唯一地依赖日志——把调用方和被调方的日志都拉出来,逐行比对,试图从字里行间拼出真相。可这一次,日志把我带进了死胡同:服务 A 的日志斩钉截铁地说"我发出了请求 A、等待、然后超时",服务 B 的访问日志却干干净净,找不到请求 A 的半点痕迹。我一度怀疑是不是哪一方的日志漏打了,反复地查日志配置——直到我终于意识到,问题根本不在日志,而在于我对"日志能告诉我什么"这件事的认知,从一开始就是错的。复盘到根上我才真正想明白:应用程序的日志,它记录的,永远只是"应用层这个高度上所能看见的事情"。一个 HTTP 请求要想被服务 B 的应用程序看见、并记进访问日志,有一个雷打不动的前提——承载它的那条 TCP 连接,必须先成功建立。而 TCP 连接的建立,要经过三次握手:A 发出 SYN,B 回应 SYN+ACK,A 再回一个 ACK。只有这趟握手完整地走完,连接才算建好,请求的数据才谈得上被送进 B 的应用层。这次的真相恰恰是:三次握手,在第一步就卡死了。我在服务 A 那台机器上抓包,清清楚楚地看到 A 的 TCP 协议栈发出了一个 SYN,然后在 1 秒、2 秒、4 秒的间隔上,一次又一次地重传这个 SYN——这是 TCP 在收不到对方 SYN+ACK 时,标准的指数退避重传行为;而从头到尾,抓包里没有出现哪怕一个来自 B 的 SYN+ACK。与此同时,我在服务 B 那台机器上同步抓包,结果是空的——B 那台机器,连一个来自 A 的 SYN 都没有收到。这两份抓包结果拼在一起,真相就再无悬念:A 发出的 SYN,在 A 到 B 这段网络路途中,丢了。它从来没能抵达 B。既然连 TCP 握手都没成功,这个请求就根本没有机会进入 B 的应用层,B 的访问日志里没有它的记录,这不是 B 在"撒谎",而是 B 确确实实、从它的视角看,从未感知到这个请求的存在。后来顺着这条线索,用 mtr 逐跳定位,我们找到了中间那台在高峰期偶发丢包的防火墙。这次事故让我刻进脑子里的,是一个关于排查工具分层的清醒认识:日志是应用层的眼睛,它能告诉你应用程序做了什么决定、走了哪条代码路径,但它对网络层发生的一切,是彻底的瞎子——一个在握手阶段就丢失的包,一次被中间设备静默重置的连接,日志永远不会、也不可能告诉你。要看清网络层真实发生了什么,唯一的办法,就是放下日志,拿起 tcpdump,去抓那些在网线上真实流动过的、一个一个的数据包。而抓包还有一条我这次用血换来的纪律:怀疑是链路问题时,一定要在通信的两端同时抓。只在一端抓,你顶多能确认"包从我这儿发出去了";唯有两端的抓包结果摆在一起对比,你才能斩钉截铁地说出那句最关键的话——这个包,是在从 A 到 B 的这一段路上丢的。这次从一组对不上的日志出发,我最大的收获,是终于给自己的排查工具箱做了清晰的分层:应用层的问题去看日志,而网络层的问题,必须去抓包——用错了层次的工具,再努力也只是缘木求鱼。
—— 别看了 · 2026