2024 年,我们有两个服务,A 调 B,平时一次调用 50 毫秒搞定,稳得很。可总有那么几次,A 那边会报 read timeout,而且毫无规律,一天偶尔冒出几次。我去查 A 的日志:A 说"我把请求发出去了,等了 3 秒没等到回应,我超时了"。我又去查 B 的日志:B 说"那个请求我收到了啊,处理只花了 50 毫秒,结果我也返回了"。两份日志摆在一起,透着一股荒诞——A 说没收到,B 说我发了,两边都言之凿凿、都觉得自己一点问题没有。请求像是在 A 和 B 之间的某段路上人间蒸发了,可应用日志这个层面,我看到的永远只是"我这头"发生了什么,中间那段网络里到底出了什么事,应用日志一个字都不会告诉我。要查清楚,我必须想办法亲眼看看,数据包在网线上到底经历了什么。这件事逼着我把 Linux 的 tcpdump 抓包、TCP 报文、网络排查这一整套彻底理清了。本文复盘这次实战。
问题背景
环境:CentOS 7,服务 A 调用服务 B 的 HTTP 接口
事故现象:
- 平时一次调用约 50ms,稳定
- 偶发(一天几次)A 报 read timeout,无规律
- 查 A 日志:请求发出去了,等 3 秒没响应,超时
- 查 B 日志:请求收到了,处理 50ms,响应也返回了
- ★ 两边应用日志都"无辜",问题像在网络里
现场排查:
# 1. 两边应用日志各执一词,应用层查不下去了
# A: "sent request, waited 3000ms, read timeout"
# B: "handled request in 51ms, response sent"
# 2. ★ 必须下到网络层:在 B 上抓包,盯住和 A 的通信
$ tcpdump -i eth0 -nn host 10.0.0.21 and port 8080 -w b.pcap
# 3. 复现一次超时后,停止抓包,分析 b.pcap
$ tcpdump -nn -r b.pcap | head -40
... IP 10.0.0.21.51234 > 10.0.0.31.8080: Flags [P.], seq 1:200 ...
... IP 10.0.0.31.8080 > 10.0.0.21.51234: Flags [P.], seq 1:5000 ...
... IP 10.0.0.31.8080 > 10.0.0.21.51234: Flags [P.], seq 1:5000 ... # ★ 重传!
... IP 10.0.0.31.8080 > 10.0.0.21.51234: Flags [P.], seq 1:5000 ... # ★ 又重传!
# ★ B 的响应包确实发了,但发出去【没被确认】,
# 一次次重传 —— 说明这些包在路上【丢了】。
# 4. 看重传统计,实锤
$ netstat -s | grep -i retrans
1832 segments retransmitted # ★ 大量重传
根因(后来想清楚的):
1. ★ B 确实在 50ms 内处理完、把响应发出去了 ——
B 的日志没撒谎。
2. ★ A 也确实没收到响应 —— A 的日志也没撒谎。
3. 真相在两者【之间】:B 发出的响应包,在网络链路上
【丢包】了。TCP 是可靠协议,丢了的包会【重传】,
但重传要先等一个超时(RTO),从几百毫秒到数秒。
4. 偶发的链路丢包 -> 偶发的重传等待 -> A 偶发地等到
超过 3 秒,触发它的 read timeout。
5. ★ 这个真相,应用日志永远看不到 —— 只有抓包,
亲眼看到那一串重复的包,才能确诊。
应用日志各执一词时,下到网络层抓包,让数据包自己说话。
修复 1:tcpdump 是什么——给网卡装一个"录像机"
# === ★ tcpdump:抓取【流经网卡的数据包】并展示出来的工具 ===
# 应用日志是"我这个程序怎么想的";
# tcpdump 抓的是"网线上【实际传输了什么】"—— 客观、不撒谎。
# === 最简单的一次抓包 ===
$ tcpdump -i eth0
# -i eth0 指定在哪块网卡上抓。不加 -i 默认抓第一块。
$ tcpdump -i any # ★ any = 抓所有网卡
# === ★ 三个几乎必加的选项 ===
$ tcpdump -i eth0 -nn -v
# -n 不把 IP 反解成域名(★ 不加会因 DNS 反解又慢又乱)
# -nn 连端口也不解析成服务名(80 就显示 80,不显示 http)
# -v 稍详细;-vv / -vvv 更详细
# === 控制抓多少,别让它无限刷屏 ===
$ tcpdump -i eth0 -nn -c 20 # -c 20 = 抓够 20 个包就停
$ timeout 30 tcpdump -i eth0 -nn # 配合 timeout,抓 30 秒
# === ★ 最重要的用法:抓到文件,事后慢慢分析 ===
$ tcpdump -i eth0 -nn host 10.0.0.21 -w cap.pcap
# -w cap.pcap 把【原始包】写进文件,不在屏幕打印。
# ★ 抓包要"抓现场",分析可以慢慢来 —— 先 -w 存下来最稳妥。
$ tcpdump -nn -r cap.pcap # -r 把文件【读出来】分析
$ tcpdump -nn -r cap.pcap and port 8080 # 读的时候还能再过滤
# === ★ 抓包文件别贪大:加大小/数量限制 ===
$ tcpdump -i eth0 -w cap.pcap -C 100 -W 10
# -C 100 单个文件到 100MB 就切下一个
# -W 10 最多保留 10 个,循环覆盖 —— 长时间抓不撑爆磁盘。
# === 看本机有哪些网卡可抓 ===
$ tcpdump -D # 列出可抓的网卡
$ ip addr # 或用 ip 看网卡名
修复 2:抓包过滤表达式——抓得准,才看得清
# === ★ 直接 tcpdump,几秒就刷出成千上万个包,根本没法看 ===
# 关键是【过滤】—— 只抓你关心的那部分流量。
# === 按主机过滤 ===
$ tcpdump -nn host 10.0.0.21 # 抓和这个 IP 相关的包
$ tcpdump -nn src host 10.0.0.21 # 只抓【源】是它的
$ tcpdump -nn dst host 10.0.0.21 # 只抓【目的】是它的
# === 按端口过滤 ===
$ tcpdump -nn port 8080 # 抓 8080 端口的包
$ tcpdump -nn src port 8080 # 只抓源端口 8080
$ tcpdump -nn portrange 8000-8100 # 一个端口范围
# === 按协议过滤 ===
$ tcpdump -nn tcp # 只抓 TCP
$ tcpdump -nn udp port 53 # 只抓 DNS(UDP 53)
$ tcpdump -nn icmp # 只抓 ping 之类
# === ★ 用 and / or / not 组合 —— 这才是过滤的精髓 ===
$ tcpdump -nn host 10.0.0.21 and port 8080
# 抓"和 10.0.0.21 通信""且"是 8080 端口"的包
$ tcpdump -nn host 10.0.0.21 and not port 22
# 抓和它的通信,但【排除】SSH(22),免得自己的 ssh 刷屏
$ tcpdump -nn 'port 8080 or port 8081'
# 抓 8080 "或" 8081(★ 有特殊字符时用引号括起来)
# === ★ 这次实战用的过滤表达式 ===
$ tcpdump -i eth0 -nn host 10.0.0.21 and port 8080 -w b.pcap
# 只抓"和服务 A(10.0.0.21)在 8080 上的通信"——
# 范围一收窄,b.pcap 里全是有用的包,分析起来清清楚楚。
# === 还能按 TCP 标志位过滤(进阶,排查握手/RST 很有用)===
$ tcpdump -nn 'tcp[tcpflags] & tcp-syn != 0' # 只抓带 SYN 的包
$ tcpdump -nn 'tcp[tcpflags] & tcp-rst != 0' # 只抓带 RST 的包
# ★ 排查"连接被拒/被重置",直接抓 RST 包,一抓一个准。
修复 3:读懂 tcpdump 的输出——一行包记录怎么看
# === ★ 抓到了包,还得看懂。一行输出长这样: ===
# 15:04:05.123456 IP 10.0.0.21.51234 > 10.0.0.31.8080: \
# Flags [S], seq 1001, win 29200, length 0
# ①时间戳 ②源IP.源端口 ③ > ④目的IP.目的端口
# ⑤Flags[...] ⑥seq ⑦win ⑧length
# === 字段①时间戳:精确到微秒 ===
# ★ 看两个包的时间差,就知道"等了多久"—— 排查慢,全靠它。
# === 字段②③④:谁发给谁 ===
# A.端口 > B.端口 —— 箭头方向就是数据流向。
# === ★ 字段⑤ Flags —— 最该看懂的一栏,是 TCP 标志位 ===
# [S] SYN —— 发起连接(三次握手第 1 步)
# [S.] SYN+ACK —— 同意连接(第 2 步;"." 代表 ACK)
# [.] ACK —— 纯确认,没带数据
# [P.] PSH+ACK —— ★ 带着【实际数据】的包,平时最常见
# [F.] FIN+ACK —— 一方要关闭连接(四次挥手)
# [R] RST —— ★ 连接被【重置】,通常意味着出问题了
# ★ 看一眼 Flags,就知道这个包在 TCP 的哪个阶段、干什么。
# === 字段⑥ seq / ack:序列号 —— 重传就靠它认出来 ===
# seq = 这个包数据的起始序号;ack = "我已收到对方到哪个序号"。
# ★ 如果你看到【同样的 seq】反复出现 —— 那就是【重传】!
# 这正是这次抓包里的关键线索。
# === 字段⑦ win:对方还能接收多少(滑动窗口)===
# ★ win 一路掉到很小甚至 0 -> 接收方处理不过来,流控了。
# === 字段⑧ length:这个包带了多少字节的应用数据 ===
# length 0 = 纯控制包(握手/纯 ACK);length > 0 = 带数据。
# === ★ 想直接看到包里的内容(HTTP 明文等)===
$ tcpdump -nn -A host 10.0.0.21 and port 8080
# -A 以 ASCII 打印包内容 —— 能直接看到 HTTP 请求行、响应头
$ tcpdump -nn -X ... # -X = 十六进制 + ASCII 并排
# ★ HTTPS 是加密的,抓到也是密文,这两个选项对它没用。
修复 4:在抓包里认出"重传"——这次的决定性证据
# === ★ 这次的真相,藏在"重传"里。怎么在抓包里认出它 ===
# === 正常的一来一回长什么样 ===
$ tcpdump -nn -r normal.pcap
10.0.0.21.51234 > 10.0.0.31.8080: Flags [P.], seq 1:200, length 199
10.0.0.31.8080 > 10.0.0.21.51234: Flags [.], ack 200 # B 确认收到
10.0.0.31.8080 > 10.0.0.21.51234: Flags [P.], seq 1:5000, len 4999 # B 回数据
10.0.0.21.51234 > 10.0.0.31.8080: Flags [.], ack 5000 # A 确认收到
# ★ 一发一确认,seq/ack 对得上,节奏明快 —— 这是健康的样子。
# === ★ 出问题时的样子:同一个 seq 反复出现 ===
$ tcpdump -nn -r b.pcap
03:14:01.100 10.0.0.31.8080 > 10.0.0.21...: Flags [P.], seq 1:5000
03:14:01.300 10.0.0.31.8080 > 10.0.0.21...: Flags [P.], seq 1:5000 # ★ 0.2s 后重发
03:14:01.700 10.0.0.31.8080 > 10.0.0.21...: Flags [P.], seq 1:5000 # ★ 0.4s 后又发
03:14:02.500 10.0.0.31.8080 > 10.0.0.21...: Flags [P.], seq 1:5000 # ★ 0.8s 后再发
# ★ 同一个 seq 1:5000,B 发了一遍又一遍 ——
# 因为 B 一直没收到 A 的 ack,认定包丢了,只能重传。
# ★ 重传间隔越来越大(指数退避):0.2 -> 0.4 -> 0.8 ...
# 几次重传累加,轻松就超过 A 那 3 秒的 read timeout。
# === 用内核统计交叉验证 ===
$ netstat -s | grep -iE 'retrans|retransmit'
1832 segments retransmitted # ★ 重传段数,在涨
$ nstat -az | grep -i retrans
# ★ 这个数字持续上涨 = 链路确实在丢包、在重传,实锤。
# === ★ 用 tshark / Wireshark 看更直观 ===
$ tshark -r b.pcap -Y tcp.analysis.retransmission
# tshark(Wireshark 的命令行版)能直接【标出】哪些是重传包。
# 把 b.pcap 下载到本地用 Wireshark 打开,
# 重传、乱序、RTT 它都用颜色标得明明白白 —— 比肉眼看强太多。
# === ★ 定位到"链路丢包"之后,往哪查 ===
# - 两台机器之间的网络设备(交换机/路由器/防火墙)
# - 用 mtr 跑一段,看丢包丢在哪一跳:
$ mtr -rwc 100 10.0.0.31
# - 网卡本身的错误计数:
$ ip -s link show eth0 # 看 RX/TX 的 errors、dropped
# 抓包负责"确诊是丢包重传",定位丢在哪还要靠这些。
修复 5:三次握手与连接重置在抓包里的样子
# === ★ 学会在抓包里认出"连接的生老病死",排查事半功倍 ===
# === 三次握手:一次正常连接的开头 ===
$ tcpdump -nn 'tcp[tcpflags] & (tcp-syn|tcp-ack) != 0'
A > B: Flags [S], seq 1000 # ① A 发 SYN
B > A: Flags [S.], seq 8000, ack 1001 # ② B 回 SYN+ACK
A > B: Flags [.], ack 8001 # ③ A 回 ACK,握手完成
# ★ 这三行齐了,连接才算建好。缺哪一步,就是卡在哪一步:
# 只有 ①,没有 ② -> SYN 没到 B,或 B 没回(防火墙/B 没监听)
# 有 ①②,没有 ③ -> B 的 SYN+ACK 没回到 A(回程链路问题)
# === 连接被拒绝:抓到 RST ===
A > B: Flags [S], seq 1000
B > A: Flags [R.], ... # ★ B 直接回 RST
# ★ 对 SYN 回 RST = "这个端口没人监听" -> connection refused。
# 想确诊"连接被拒",直接抓 RST 包最快:
$ tcpdump -nn 'tcp[tcpflags] & tcp-rst != 0'
# === 连接超时:SYN 发出去,石沉大海 ===
A > B: Flags [S], seq 1000
A > B: Flags [S], seq 1000 # ★ 1 秒后重发 SYN
A > B: Flags [S], seq 1000 # ★ 又重发
# ★ 只见 A 反复发 SYN、B 一点回应都没有 = connection timeout。
# 包被中间设备/防火墙【默默丢弃】(refused 会回 RST,
# timeout 是什么都不回 —— 抓包能一眼分清这两者)。
# === 四次挥手:连接正常关闭 ===
A > B: Flags [F.], ... # A 说"我没数据要发了"
B > A: Flags [.], ... # B 确认
B > A: Flags [F.], ... # B 也说"我也发完了"
A > B: Flags [.], ... # A 确认,连接关闭
# === ★ 抓包看连接问题的口诀 ===
# 只发 SYN 没回应 -> 超时(包被丢)
# SYN 换来 RST -> 拒绝(端口没监听)
# 握手成功后 RST -> 连接中途被某一方/设备掐断
# 同一 seq 反复发 -> 丢包重传(就是这次的病根)
修复 6:tcpdump 抓包排查纪律
# === 这次事故暴露的认知盲区,定几条纪律 ===
# === 1. ★ 应用日志各执一词,就下到网络层抓包 ===
# 应用日志只说"我这头",中间网络发生了什么,只有抓包知道。
# === 2. ★ 抓包必加 -nn,并用过滤表达式收窄范围 ===
$ tcpdump -i eth0 -nn host 对端IP and port 端口
# 不过滤 = 几千个包刷屏,根本没法看。
# === 3. ★ 抓现场用 -w 存文件,分析慢慢来 ===
$ tcpdump ... -w cap.pcap # 抓
$ tcpdump -nn -r cap.pcap # 事后读
# 长时间抓加 -C / -W 限制大小,别撑爆磁盘。
# === 4. 看 Flags 认阶段:S/S././/P./F./R 各是什么 ===
# 看到 R(RST)、看到同一 seq 反复出现,都是问题信号。
# === 5. 同一 seq 反复出现 = 重传 = 链路丢包 ===
$ netstat -s | grep retrans # 用重传计数交叉验证
# === 6. 复杂分析交给 Wireshark/tshark ===
$ tshark -r cap.pcap -Y tcp.analysis.retransmission
# 重传/乱序/RTT,图形化工具标得清清楚楚。
# === 7. 排查网络问题的命令链 ===
$ tcpdump -D # ① 有哪些网卡可抓
$ tcpdump -i eth0 -nn host X -w cap.pcap # ② 收窄过滤抓到文件
$ tcpdump -nn -r cap.pcap # ③ 读出来看 Flags/seq
$ netstat -s | grep retrans # ④ 重传计数交叉验证
$ mtr -rwc 100 对端IP # ⑤ 丢包丢在哪一跳
$ ip -s link show eth0 # ⑥ 网卡自身错误/丢包计数
# 按这个顺序,网络层问题基本能定位。
命令速查
需求 命令
=============================================================
在某网卡抓包(不解析名字) tcpdump -i eth0 -nn
列出可抓的网卡 tcpdump -D
按主机和端口过滤 tcpdump -nn host IP and port 端口
只抓带 SYN 的包 tcpdump -nn 'tcp[tcpflags] & tcp-syn != 0'
只抓带 RST 的包 tcpdump -nn 'tcp[tcpflags] & tcp-rst != 0'
抓包写入文件 tcpdump ... -w cap.pcap
读取抓包文件分析 tcpdump -nn -r cap.pcap
抓够 N 个包就停 tcpdump -nn -c 50
以 ASCII 看包内容 tcpdump -nn -A host IP
看重传统计 netstat -s | grep retrans
标出重传包(tshark) tshark -r cap.pcap -Y tcp.analysis.retransmission
口诀:应用日志各执一词 -> 下到网络层抓包让数据包说话
同一 seq 反复出现 = 丢包重传;抓到 RST = 连接被重置
避坑清单
- 两边应用日志各执一词时,下到网络层用 tcpdump 抓包让数据包说话
- tcpdump 必加 -nn,否则 IP/端口反解析又慢又乱,还可能引入 DNS 延迟
- 必须用 host/port/and/or 过滤收窄范围,否则成千上万个包没法看
- 抓现场用 -w 存成 pcap 文件,分析慢慢来;长抓加 -C/-W 限大小
- Flags 标志位:S 握手 S. 同意 . 确认 P. 带数据 F. 关闭 R 重置
- 同一个 seq 在抓包里反复出现就是重传,意味着链路在丢包
- 重传间隔指数退避,几次累加很容易超过应用层的超时阈值
- 对 SYN 回 RST 是连接拒绝,只发 SYN 无任何回应是连接超时
- TLS,在 HTTP 之上加一层 TLS 加密,防止中间人窃听和篡改。">HTTPS 流量加密,-A/-X 抓到也是密文,看不到明文内容
- 抓包确诊是丢包后,还要用 mtr、ip -s link 定位丢在哪一跳
总结
这次"A 说没收到、B 说发了,请求在路上人间蒸发"的事故,纠正了我一个长期存在的、关于"我到底能看见什么"的认知局限。在这次之前,我排查一切网络调用问题的全部手段,就是看两端的应用日志——调用方的日志,加上被调用方的日志。在我朴素的世界观里,一次远程调用,无非就是"A 把请求发出去""B 收到、处理、把响应发回来"这么几个环节,而这几个环节,理应都被两端的日志忠实地记录下来。所以当 A 的日志清清楚楚写着"请求发出,等待 3 秒,超时",B 的日志同样清清楚楚写着"请求收到,处理 50 毫秒,响应已返回"时,我彻底卡住了。这两份日志,单独看每一份都天衣无缝、都理直气壮,可把它们拼在一起,中间却裂开了一道无法解释的鸿沟:B 明明说自己把响应发出去了,A 却斩钉截铁说自己什么都没等到。我一遍遍地读这两份日志,试图从字里行间找出哪一边在撒谎,可读到最后我才慢慢明白——没有人撒谎,是我能看见的范围,根本不够。复盘到根上,我才真正意识到,应用日志,本质上是一份"主观陈述"。A 的日志,记录的只是"A 这个进程,自己以为发生了什么";B 的日志,记录的也只是"B 这个进程,自己以为发生了什么"。A 的进程,在它调用了发送函数、把数据交给操作系统的那一刻,它"主观上"就认为"我已经发送了";B 的进程,也只能记录"我从操作系统这里收到了什么、又把什么交还给了操作系统"。而数据包真正离开 B 这台机器的网卡、踏上那段连接 A 和 B 的物理链路、再抵达 A 的网卡——这整整一段旅程,完完全全发生在两个进程的视野之外。它们俩,谁也没有亲眼看见过网线上真正传输的东西。我那个失踪的请求,恰恰就是在这段"应用日志的盲区"里出的事。要看见这段盲区,我需要的不是另一份主观陈述,而是一台架在网卡上的、客观的"录像机"——这就是 tcpdump。tcpdump 抓的不是任何进程"以为"发生了什么,而是流经这块网卡的、一个一个实实在在的数据包。当我在 B 上架起 tcpdump、收窄到只盯它和 A 的通信、复现出一次超时之后,真相在抓包文件里一目了然:B 那个携带响应数据的包,序列号 seq 1:5000,在抓包记录里反反复复地出现了一遍又一遍,间隔从 0.2 秒、0.4 秒一路退避到 0.8 秒。这一串重复,就是 TCP 的"重传"——B 把响应发出去了(所以 B 的日志没撒谎),但这个包在中途的链路上丢了,A 自然没收到(所以 A 的日志也没撒谎),而 TCP 作为可靠协议,发现包没被确认就会一次次重传,这一次次重传的等待累加起来,轻松就突破了 A 那 3 秒的超时红线。两份都"无辜"的日志之间那道鸿沟,填进去的真相就是两个字:丢包。这次从一个"请求人间蒸发"的谜题出发,我最大的收获,是清醒地认识到了应用日志的边界——它是宝贵的,但它只是一份从进程视角出发的、主观的陈述,它对网卡之外那段网络旅程一无所知。当两端的应用日志各执一词、却又都看似无懈可击时,真相往往不在任何一份日志里,而在它们共同的盲区——那段网络链路——之中。这时候唯一的办法,就是停止在主观陈述里找凶手,转而下到网络层,用 tcpdump 架起那台不会撒谎的录像机,让数据包自己,把它在路上的遭遇,一帧一帧地讲给你听。
—— 别看了 · 2026