A 说没收到 B 说发了:一次 Linux tcpdump 抓包定位丢包复盘

服务 A 调 B 偶发 read timeout,A 说请求发了没回应、B 说收到了处理只用 50ms,两边应用日志都无辜。排查梳理:应用日志只是主观陈述、网卡之外那段链路是盲区、tcpdump 给网卡装录像机、-nn 与 host/port 过滤、读懂一行包记录的 Flags 和 seq、同一 seq 反复出现就是丢包重传、三次握手与 RST 在抓包里的样子,以及一套抓包排查纪律。

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 = 连接被重置

避坑清单

  1. 两边应用日志各执一词时,下到网络层用 tcpdump 抓包让数据包说话
  2. tcpdump 必加 -nn,否则 IP/端口反解析又慢又乱,还可能引入 DNS 延迟
  3. 必须用 host/port/and/or 过滤收窄范围,否则成千上万个包没法看
  4. 抓现场用 -w 存成 pcap 文件,分析慢慢来;长抓加 -C/-W 限大小
  5. Flags 标志位:S 握手 S. 同意 . 确认 P. 带数据 F. 关闭 R 重置
  6. 同一个 seq 在抓包里反复出现就是重传,意味着链路在丢包
  7. 重传间隔指数退避,几次累加很容易超过应用层的超时阈值
  8. 对 SYN 回 RST 是连接拒绝,只发 SYN 无任何回应是连接超时
  9. TLS,在 HTTP 之上加一层 TLS 加密,防止中间人窃听和篡改。">HTTPS 流量加密,-A/-X 抓到也是密文,看不到明文内容
  10. 抓包确诊是丢包后,还要用 mtr、ip -s link 定位丢在哪一跳

总结

这次"A 说没收到、B 说发了,请求在路上人间蒸发"的事故,纠正了我一个长期存在的、关于"我到底能看见什么"的认知局限。在这次之前,我排查一切网络调用问题的全部手段,就是看两端的应用日志——调用方的日志,加上被调用方的日志。在我朴素的世界观里,一次远程调用,无非就是"A 把请求发出去""B 收到、处理、把响应发回来"这么几个环节,而这几个环节,理应都被两端的日志忠实地记录下来。所以当 A 的日志清清楚楚写着"请求发出,等待 3 秒,超时",B 的日志同样清清楚楚写着"请求收到,处理 50 毫秒,响应已返回"时,我彻底卡住了。这两份日志,单独看每一份都天衣无缝、都理直气壮,可把它们拼在一起,中间却裂开了一道无法解释的鸿沟:B 明明说自己把响应发出去了,A 却斩钉截铁说自己什么都没等到。我一遍遍地读这两份日志,试图从字里行间找出哪一边在撒谎,可读到最后我才慢慢明白——没有人撒谎,是我能看见的范围,根本不够。复盘到根上,我才真正意识到,应用日志,本质上是一份"主观陈述"。A 的日志,记录的只是"A 这个进程,自己以为发生了什么";B 的日志,记录的也只是"B 这个进程,自己以为发生了什么"。A 的进程,在它调用了发送函数、把数据交给操作系统的那一刻,它"主观上"就认为"我已经发送了";B 的进程,也只能记录"我从操作系统这里收到了什么、又把什么交还给了操作系统"。而数据包真正离开 B 这台机器的网卡、踏上那段连接 A 和 B 的物理链路、再抵达 A 的网卡——这整整一段旅程,完完全全发生在两个进程的视野之外。它们俩,谁也没有亲眼看见过网线上真正传输的东西。我那个失踪的请求,恰恰就是在这段"应用日志的盲区"里出的事。要看见这段盲区,我需要的不是另一份主观陈述,而是一台架在网卡上的、客观的"录像机"——这就是 tcpdumptcpdump 抓的不是任何进程"以为"发生了什么,而是流经这块网卡的、一个一个实实在在的数据包。当我在 B 上架起 tcpdump、收窄到只盯它和 A 的通信、复现出一次超时之后,真相在抓包文件里一目了然:B 那个携带响应数据的包,序列号 seq 1:5000,在抓包记录里反反复复地出现了一遍又一遍,间隔从 0.2 秒、0.4 秒一路退避到 0.8 秒。这一串重复,就是 TCP 的"重传"——B 把响应发出去了(所以 B 的日志没撒谎),但这个包在中途的链路上丢了,A 自然没收到(所以 A 的日志也没撒谎),而 TCP 作为可靠协议,发现包没被确认就会一次次重传,这一次次重传的等待累加起来,轻松就突破了 A 那 3 秒的超时红线。两份都"无辜"的日志之间那道鸿沟,填进去的真相就是两个字:丢包。这次从一个"请求人间蒸发"的谜题出发,我最大的收获,是清醒地认识到了应用日志的边界——它是宝贵的,但它只是一份从进程视角出发的、主观的陈述,它对网卡之外那段网络旅程一无所知。当两端的应用日志各执一词、却又都看似无懈可击时,真相往往不在任何一份日志里,而在它们共同的盲区——那段网络链路——之中。这时候唯一的办法,就是停止在主观陈述里找凶手,转而下到网络层,用 tcpdump 架起那台不会撒谎的录像机,让数据包自己,把它在路上的遭遇,一帧一帧地讲给你听。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
Linux教程

服务半夜凭空消失日志却很干净:一次 Linux 内存与 OOM Killer 排查复盘

2026-5-20 19:35:07

Linux教程

手动能跑交给 systemd 就启动失败:一次 unit 文件与 journalctl 排查复盘

2026-5-20 19:41:23

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索