☰
Wireshark抓包实战:TCP连接生命周期如何引发接口间歇性超时
2026/9/25 11:35:15 网站建设 项目流程

看到“WWWWWWWWWWWWW”这串 W,我第一反应不是电量耗尽乱敲键盘,而是想起那天抓包软件里刷出来的波形图——一连串尖刺,层层叠叠,像海浪一样拍在时间轴上。那次排查花了我整整一个晚上,最后发现问题的根子不在网络,却藏在一堆 TCP 连接的生命周期里。所以今天这篇也想从一串 W 写起:它既能是网线里的波动,也能是 Wireshark 的 W,更能是很多人反复踩中的“网络真凶”留下的波纹。

这篇文章写给天天和后端、运维、SRE 打交道,或者自己在排查接口超时却迟迟找不到证据的开发者。我会用一次完整的真实排查过程,把 Wireshark 怎么抓包、怎么看包、怎么从一堆看似正常的流量里挖出“间歇性超时”的根因,全部讲清楚。我会尽量保留现场的操作顺序和判断过程,包括我中间走偏的地方。如果你能跟着走一遍,下次再遇到“重试一下就恢复”的诡异故障,至少知道从哪一刀切下去。

1. 那串 W 背后的真实现场:间歇性超时与“查不出来”的网络

1.1 现象特征:固定时间段、特定接口、重试即恢复

先说当时的现场。业务方反馈,线上某个核心查询接口在每小时的固定几分钟内会出现间歇性超时,客户端拿到的要么是 504,要么是连接被重置。每次超时持续大概几十秒到一分钟,过了这个窗口,什么都不用做,接口自己就恢复了。运维最开始的处理方式很简单:加超时时间、调大重试次数。结果确实能掩盖问题——毕竟报错率没涨,业务方也接受“偶尔慢一下”。但这不是长久之计,因为随着流量增长,这个间歇性超时的窗口在变长。

我当时拿到手的日志有三个特征:第一,超时集中在每小时的 07 分、22 分、37 分、52 分左右,非常规律。第二,超时请求的分布不均匀,同一拨请求里有的秒回,有的直接卡死。第三,客户端重试后基本都成功,很少连续失败两次。这几个特征组合在一起,已经能排除掉“网络出口抖动”“运营商链路问题”这类完全随机的故障,更倾向于应用层或连接层有周期性的资源竞争。

1.2 为什么前三板斧失效:ping 通、mtr 正常不等于 TCP 没问题

按常规排查顺序,我先让网络同事测了 ping 和 mtr。结果很有意思:从客户端到服务端的丢包率为 0,延迟也很平稳,带宽占用不高,路由器层面没有任何丢包记录。于是大家一度怀疑是应用代码的问题,开始查慢查询、GC、锁竞争,折腾了大半天也没结果。

这里有个非常容易踩的认知误区:ping用的是 ICMP 协议,路径可能和业务 TCP 流量完全不同,而且很多网络设备对 ICMP 有单独的高优先级处理。mtr只能证明“网络设备的转发面”通,不能证明“TCP 协议的连接建立和数据传输”没问题。TCP 层面的重传、乱序、连接队列溢出,ICMP 和 mtr 全都看不到。所以当应用层排查无果、网络设备又一切正常的时候,唯一的出路就是把真实业务流量抓到包里看。这也是我坚持上 Wireshark 的原因。

2. Wireshark 在这个局里的位置:部署方案与抓包现场

2.1 抓包位置:双端同时抓,而不是只抓一端

很多人第一次抓包会习惯性地在服务器上tcpdump,抓完发现全是正常流量,什么问题都看不出来。这不是没抓到,而是抓漏了。TCP 是端到端协议,服务端看自己的视角和客户端看自己的视角完全不一样:如果服务端发出 SYN-ACK 后一直没等到 ACK,服务端视角看到的是重传;客户端视角看到的可能是自己发的 ACK 早被中间设备丢了。只抓一端,永远只能知道一半真相。

所以我在这次排查里做了双端抓包:在客户端所在的接入机和服务端实例上同时起 tcpdump,然后对齐两边的包做对比。两边抓到同一笔请求的握手、传输、挥手全过程,才能判断异常到底发生在哪个环节。这里有个现实问题:生产环境不是实验室,不能随便把网卡设成混杂模式抓所有包。比较稳妥的做法是精准过滤,只抓目标服务的 IP 和端口。

2.2 tcpdump 参数:一次抓到点子上,别把磁盘写爆

我在客户端和服务端分别用了下面这套命令:

tcpdump -i eth0 -s 96 -w /data/capture-$(date +%s).pcap \ 'host 10.0.0.12 and tcp port 8080' -B 4096 -G 300 -W 4

逐项说一下每个参数的理由。-s 96是最关键的取舍:snaplen 只抓每个报文前 96 字节,对 TCP 头分析完全够用,因为我们要看的是序号、确认号、标志位、窗口大小,这些都在 TCP 头里;如果后面需要看 HTTP 明文内容,再考虑抓完整包。-G 300 -W 4是做文件轮转,每 5 分钟写一个新文件,最多保留 4 个,避免长时间抓包把磁盘写满。-B 4096把内核缓冲区调到 4MB,高并发下防止 tcpdump 自己丢包——抓包工具丢包是最隐蔽的陷阱,它会让你误以为网络在丢包。

抓了大约 30 分钟,两个端各产生了约 800MB 的 pcap 文件。文件不小,但因为有 IP 和端口过滤,里面的流量基本都和目标服务相关,后续分析不算太累。

2.3 Wireshark 打开 pcap 后的第一轮阅读:先看宏观再抠细节

打开 pcap 文件之后,我没有立刻翻包,而是先做了三件事。

第一,把时间显示格式改成相对时间。View -> Time Display Format -> Seconds Since Previous Captured Packet。这样能看到包与包之间的间隔,比看绝对时间直观得多。

第二,看 Expert Info。Wireshark 会自己标注异常包,比如重传、重复 ACK、乱序、校验和错误。这一眼扫过去能建立全局印象:异常集中在哪个阶段,数量级是多少。

第三,画一张 IO Graph。Statistics -> IO Graph,把 Y 轴设为 Packets/Tick,过滤器分别填tcp.analysis.retransmission、tcp.analysis.duplicate_ack、tcp.flags.syn==1。图形出来之后我整个人都精神了:每隔 15 分钟就有一波纯尖峰,和业务日志里的超时窗口完全重合。这意味着问题一定和周期性触发的流量模式有关,而不是随机网络事件。

3. 第一波涟漪:SYN 反复重传与 accept 队列打满

3.1 时间轴上为什么出现了一串 S

打开异常时间窗口里的 TCP 流,我发现一个非常典型的画面:客户端发了一个 SYN,等了一会儿没回包,又发了一个 SYN,再等,又发。其中一个连接在 1 秒、2 秒、4 秒的间隔里连续重传了三次,才最终完成握手。在 Wireshark 的包列表里,这几条 SYN 被标成黑色或者浅红色,说明触发了tcp.analysis.retransmission和tcp.analysis.fast_retransmission。

这里要解释一个机制:TCP 握手阶段的 SYN 重传和后续数据段的重传不太一样。SYN 发送后,如果客户端在初始 RTO(重传超时时间,通常 1 秒左右)内没有收到 SYN-ACK,就会重传,并且每次重传的超时时间指数退避:1 秒、2 秒、4 秒、8 秒。所以当你在包里看到间隔翻倍的多条 SYN,基本可以断定服务端的 SYN-ACK 没有按预期返回。要么是 SYN 被中间设备丢了,要么是服务端根本没能力处理这个 SYN。

3.2 服务端视角:SYN-ACK 发出去了,但 ACK 等不到

再看服务端抓到的包,我发现更膈应的情况:服务端确实发出了 SYN-ACK,但紧接着客户端就重传 SYN 了。也就是说,SYN-ACK 可能在网络上绕了一圈,客户端没收到,或者在客户端收到之前,客户端已经进入了重传流程。

顺着这个现象查服务端状态,ss -lnt的输出让我倒吸一口凉气:

State Recv-Q Send-Q Local Address:Port Peer Address:Port SYN-RECV 0 1 10.0.0.12:8080 10.0.0.13:52340 ESTAB 0 512 10.0.0.12:8080 10.0.0.13:52342

Recv-Q 和 Send-Q 都异常,ESTAB 状态的连接背了一大堆数据。更关键的是 SYN-RECV 状态的半连接在堆积,说明全连接队列已经满了,应用程序来不及 accept,内核直接把新的 SYN 丢弃。这就是客户端不断重传 SYN 的根因:不是线路丢包,是服务端没地方放新连接了。

3.3 修复半连接堆积:somaxconn 与 backlog 的一连串调整

当时我做了两个调整。第一,把net.core.somaxconn从 128 调到了 4096;第二,把应用监听 socket 的 backlog 从默认值调大,比如 Nginx 的listen 8080 backlog=1024。这两个参数的关系必须搞清楚:somaxconn是内核允许的全连接队列上限,应用层的 backlog 是应用向内核申请的队列长度,实际生效值取两者中较小的那个。如果只调应用不调内核,等于白调。

调整之后,SYN 重传的数量明显下降,但还没有完全消失。这个阶段告诉我,队列溢出只是表层问题,更重要的是为什么短时间内会突然涌进来这么多新连接。这就引出了下一层问题。

4. 被 Wireshark “冤枉”的 Checksum 与 offload 假乱序

4.1 连续 Dup ACK:看起来像丢包,但两端都说自己没丢

继续往下看传输阶段,我又发现一批标记为tcp.analysis.duplicate_ack的包。客户端在很短的时间里对同一个数据段连续发出重复 ACK,Wireshark 甚至标了 Fast Retransmission(快速重传)。正常情况下,重复 ACK 意味着接收方看到了序号断层,也就是丢包。但诡异的是,两端网卡统计里的丢包计数都是 0,交换机的错误计数也是 0。

这时候我犯过一个错误:差点把锅甩给中间链路,要求网络同事去查光模块和交换机端口。还好在准备提工单之前,我做了一件正确的事——点开几个被 Wireshark 标记为错误校验和的包,仔细看了看。

4.2 Checksum 红色报错的真相:网卡把校验和计算“卸载”了

Wireshark 里大量 TCP 段被标成红色 Checksum 错误,很多人看到这里会直接说“包都坏了,网络有问题”。但这其实是抓包的一个经典误报源:现代网卡普遍支持checksum offload,也就是说,数据包在传给网卡时,校验和字段可能是空的或者不对的,由网卡硬件在发送时计算并填充。tcpdump 抓包发生在协议栈和驱动层之间,它抓到的是网卡“还没来得及计算校验和”之前的包,所以 Wireshark 才会显示校验和错误。

怎么确认这是 offload 的假象而不是网络真的坏了?方法很简单:看两端网卡的统计计数。ethtool -S eth0里rx_crc_errors、rx_error这些字段如果全是 0,说明物理链路上根本没有坏包。再配合一条命令验证:

ethtool -k eth0 | grep checksum

看到tx-checksum-ipv4: on,就可以放心忽略 Wireshark 的 Checksum 报错。这个经验我后面又用到过很多次:抓包工具看到的现象和物理链路实际发生的现象是两回事。

4.3 GRO/GSO 与“超大帧”的误导

另一件让我差点误判的事,是 Wireshark 里出现了不少 len 超过 1518 的“巨型帧”。第一反应是 MTU 不一致导致分片,查了一圈发现 MTU 配置没问题。最后定位到是 GRO(Generic Receive Offload)在接收方向把多个小包合并成一个逻辑大包,tcpdump 抓到的这个“大包”是合并后的形态,不代表线路上真的有超大帧。GSO 是发送方向的对称机制。

当这些 offload 机制和前面的重传、Dup ACK 叠加在一起时,画面变得特别混乱:看起来像丢包加乱序加超大帧,实际物理链路干净得很。这里最关键的动作不是直接关闭 offload——生产环境关掉 offload 通常只会增加 CPU 开销,效果未必好——而是在测试环境做一轮对照:关掉 GRO/GSO 后重试同样的流量,如果抓包里的“异常”消失但业务行为没有任何变化,就证明这些异常是卸载机制导致的观测假象。我做了这个对照,果然如此。

4.4 那真正导致超时的重传来自哪里

既然物理链路没问题,为什么还会出现触发快速重传的重复 ACK?答案是:这些重传的一部分是被服务端全连接队列溢出带出来的叠加效应。前面握手阶段 SYNs 积压,建立起来的连接被迫排队,应用来不及读数据,接收窗口收缩,发送端被迫减速,加上定时批量请求一拥而上,形成了短时间内的拥塞窗口塌陷。也就是说,传输层的“假乱序、假重传”是症状,真正的病灶还是“连接突发太多太集中”。

这一层分析让我彻底放弃在“是不是网络设备丢包”上继续纠缠,把注意力收回到连接生命周期本身。于是,最后一个关键疑点浮出水面:为什么这些连接都活得这么短,然后大量进入 TIME_WAIT?

5. 连接生命周期太短:TIME_WAIT 池与丢失的连接复用

5.1 抓包里的挥手泛滥:每个请求都新建连接

在异常窗口里,我注意到一个刺眼的统计数字:TCP 流的平均生命周期只有 3 秒左右,而且几乎每个请求结束后都是客户端主动发起四次挥手,也就是客户端先发 FIN。这意味着什么?意味着应用没有复用连接,发完一个 HTTP 请求就断开,下一个请求再重新建立连接。请求量一大,握手风暴和挥手风暴同时出现,服务器所有的精力都花在“创建连接”和“销毁连接”上,反而没空处理真正的业务数据。

这不只是性能浪费,而是直接和前面的 accept 队列溢出挂钩:同样 QPS 下,如果连接能复用,每秒新增连接数可以降到原来的几十分之一;如果每个请求都新建连接,那么新增连接速率就等于 QPS,队列当然会被瞬间打爆。

5.2 TIME_WAIT 堆积:客户端源端口快不够用了

我再看了一眼客户端侧的统计,TIME_WAIT 状态连接数已经逼近三万。TIME_WAIT 是主动关闭连接的一方在发送完最后一个 ACK 之后进入的状态,目的是保证对端重传的 FIN 还能被正确处理。这个状态默认持续 60 秒,由net.ipv4.tcp_fin_timeout控制。

然后做一个简单的算术:如果客户端源端口大约有 28,000 个可用(按常见 ephemeral 端口范围 32768-60999 算),而每个连接要在这个端口上停留 60 秒才释放,那么每秒新建连接数只要超过大约 470 个,就会开始出现端口不够用的情况。业务高峰期每秒新增连接数远超这个值,于是大量新连接找不到源端口,只能排队等待或者直接失败。日志里的“重试即恢复”,本质就是高峰过去、TIME_WAIT 释放了端口,新连接才得以建立。

5.3 内核参数救急:tcp_tw_reuse 为什么不能乱开

网上关于 TIME_WAIT 太多,最常见的救法就是开net.ipv4.tcp_tw_reuse。这个参数的意思是允许内核在新建连接时复用还在 TIME_WAIT 状态的连接,前提是双方都开启了 TCP 时间戳(tcp_timestamps)。我当时没有立刻开,因为有两个顾虑。

第一,tcp_tw_reuse只对发起连接的一方有效,也就是说它是客户端的解法,服务端堆 TIME_WAIT 多开了它也没用。第二,复用连接存在旧包串扰风险:如果老连接里有一个迟到很久的报文,正好新连接复用了同一组四元组并且序号区间重叠,接收方可能把旧包误认为新连接的数据。虽然时间戳机制能降低这种概率,但在高强度流量下不值得赌。至于tcp_tw_recycle,我直接不建议在生产环境碰,它在 NAT 环境里会引发更隐蔽的丢包问题。

5.4 真正的根治方向:连接池与 HTTP keep-alive

所以,TIME_WAIT 堆积只是结果,不是原因。根本问题是应用把 TCP 当成一次性的玩具,用完了就扔。修复方案也顺理成章:在应用层开启 HTTP keep-alive,让客户端和服务端之间的 TCP 连接尽量复用;对内部服务调用引入连接池,把连接数控制在固定水位;同时把周期性触发的批量请求打散,避免整点并发猛冲。

我当时的改动清单里,最有效的一行其实是把 HTTP 客户端从“每次请求新建连接”改成“连接池复用 + 空闲保活”。改完之后,同一套业务量下每秒新增连接数降了将近 90%,TIME_WAIT 数量从三万降到两千多,SYN 重传和 accept 队列溢出的现象也随之消失。至此,间歇性超时的问题才算真正闭环。

6. 修复清单与复盘:让问题不再“重试即恢复”

6.1 最终改动清单

这次排查最终落到纸面上的改动,我整理成了表格,方便以后直接对照:

调整项调整前调整后说明
应用连接复用每个请求新建连接连接池 + keep-alive最关键的一步,降低新增连接速率
定时批量任务整点瞬时全量发起分片、限流、错峰消除周期性尖峰
net.core.somaxconn1284096扩大全连接队列上限
Nginx backlog默认值1024和应用层配合,取两者较小值
抓包观测只看一端双端同时抓包还原完整 TCP 视角
内核 TIME_WAIT 参数保持默认保持默认用底层修复替代参数救急

我特意把最后一行写进去,是想提示一点:不要为了显示自己有本事,硬开一堆不明所以的内核参数。这次问题如果只靠开tcp_tw_reuse和调小tcp_fin_timeout去救,大概率能顶一阵子,但很快会在别的地方重新冒出来。连接池是治本,参数只是掩盖。

6.2 用“周期性”反推根因的方法

复盘时我又验证了最早那个“每隔 15 分钟”的特征。客户端业务侧确实存在一个定时任务,每小时的第 07、22、37、52 分钟会批量拉取一批数据,命中同一个服务实例,而且这个客户端代码库里的 HTTP 调用没有开启连接复用。两条线索一拼,一切都对上了:周期性任务触发 -> 瞬时大量新连接 -> accept 队列溢出 -> SYN 重传 -> 部分请求超时 -> 客户端重试时高峰已过 -> “重试即恢复”。

以后遇到任何“重试就好了”的故障,我都会先问三个问题:故障时间点之间有没有固定间隔?故障期间的 TCP 新建连接速率是不是异常高?请求在故障后重试为什么就一定成功?如果这三个问题都有明确答案,大概率不是网络问题,而是连接生命周期或队列容量问题。

6.3 沉淀下来的 Wireshark 操作习惯

经过这次,我把 Wireshark 的使用方式固定成了一组自己的套路。显示过滤器只保留这几条:

tcp.analysis.flags tcp.analysis.retransmission tcp.analysis.duplicate_ack tcp.flags.syn==1 tcp.flags.fin==1

打开 pcap 后先看 Expert Info,再看 IO Graph,最后才跟进单个 TCP 流。右键 -> Follow -> TCP Stream可以还原一次完整会话,Statistics -> Flow Graph能看连接生命周期时序。如果包特别多,用命令行工具tshark做聚合统计比图形界面更快,比如统计每个连接的首包时间、结束时间、重传次数,直接定位异常流。

另外还有一个容易被忽略的点:双端抓包时注意两边的时钟偏差。如果两边系统时钟差太多,按绝对时间对齐会得出完全错误的结论。我一般先在包里找一个两边都看得到的“同时事件”,比如同一笔请求的握手,用相对间隔来对齐时间线。

6.4 一些不再想踩第二次的坑

最后分享几条当时花代价换来的教训。

第一,抓包文件别贪大。线上 tcpdump 记得加-s 96和文件轮转,否则分析时 Wireshark 会卡到你怀疑人生。第二,Wireshark 显示“校验和错误/乱序/重传”时,先怀疑观测手段再做网络论断。第三,改参数一次只改一个,改完立刻回到包里验证效果。我中间犯过的错就是一次性改了 somaxconn、keepalive 和应用超时,结果哪个起作用了完全分不清,只好回滚重来。

现在让我再遇到类似问题,我不会再去死磕网络设备了。我会先问一句:这些连接是复用的,还是用完就丢的?这一句话常常比抓包更早指出方向。回到开头那串 W,它在我眼里早就不是乱敲的字符,而是一条提醒——网络问题从来不会凭空出现,它只是用波浪线一样的方式,把藏在上层代码里的问题画给了你看。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询