一次 P99 偶发飙升到 5s 的排查实战:从 APM 一路追到 TCP 重传

简介: 监控告警 P99 飙到 5s,但 CPU 不满、DB 没慢查询、APM 看不出异常——所有应用层指标都正常,但用户就是说慢。本文完整复盘一次从 APM 链路追踪、tcpdump 抓包到 TCP 重传定位的真实排障过程,附 ss/tshark/BBR 等命令实操和 5 个网络层慢请求元凶 Checklist。

这是一次让我至今印象深刻的线上排障。监控告警 P99 飙到 5s,但 QPS 不高、CPU 不满、DB 没有慢查询、APM 看不出任何异常。所有应用层指标都告诉你"没事",但用户就是说慢。

最后定位到的元凶藏在 TCP 协议层——一个绝大多数后端开发者从来不去看的地方。这篇把整个排查过程完整复盘,从 APM → tcpdump → 内核 socket 状态一路追下去,附完整命令和经验沉淀。


一、背景与现象

业务背景:一个典型的微服务架构,核心订单查询服务,部署在阿里云 ECS 上,跨多个可用区,前面挂 SLB,下游依赖 RDS、Redis、外部支付服务。日常 QPS 大约 2000,P99 稳定在 80ms 以内。

某个工作日下午,监控告警群开始炸:

[ALERT] order-query-service P99 latency: 4823ms (threshold: 500ms)
[ALERT] order-query-service P99 latency: 5210ms
[ALERT] order-query-service error rate: 0.8% (threshold: 0.1%)

P99 不是一直高,是偶发飙升——每隔几分钟突然出现一批慢请求,然后又恢复正常。错误率虽然只有 0.8%,但绝对数量已经够客服爆炸了。

第一反应是去看监控大盘:

  • CPU:35%,正常
  • 内存:60%,正常
  • GC:YoungGC 频率正常,没有 FullGC
  • 线程池:核心线程在工作,队列没积压
  • DB 慢查询:0 条
  • Redis 延迟:1ms 以内
  • 下游接口:上游 SLA 都在 99.95% 以上

应用层所有指标都告诉你"一切正常"。但慢请求是真实存在的。

这种"所有监控正常但用户说慢"的场景,是最难排查的一类问题。


二、第一轮:APM 链路追踪

我们的链路追踪用的是 SkyWalking。打开 P99 那个时间段的慢请求 trace,找到一条具体的 5200ms 请求:

[Trace ID: a3f5e8c9b2d14761]
└─ /api/order/query (5217ms)
   ├─ checkAuth (3ms)
   ├─ getUserInfo via RPC (2ms)
   ├─ queryOrderFromDB (8ms)
   ├─ enrichOrderDetails (5ms)
   ├─ callPaymentService (5189ms)  ← 元凶?
   └─ buildResponse (2ms)

看起来定位到了:callPaymentService 这个外部调用花了 5.1 秒。下意识就要去拍支付服务的同事。

但作为一个被坑过的人,我没有立刻甩锅。先去支付服务那边查同一条 traceId:

[Trace ID: a3f5e8c9b2d14761]
└─ /payment/check (12ms)
   ├─ validateOrder (3ms)
   ├─ queryPaymentStatus (8ms)
   └─ return (1ms)

支付服务说他们只处理了 12ms。

这就有意思了。订单服务说"我等了支付服务 5.1 秒",支付服务说"我 12ms 就处理完了"。5 秒钟去哪了?

中间这段时间,数据要么卡在网络上,要么卡在某一端的内核 socket 缓冲区里。这是典型的"网络层吃掉了时间"的信号。


三、第二轮:怀疑 GC、连接池、线程

在动手抓包之前,先把另外几个可能的"应用层但 APM 看不出来"的因素排除掉。

3.1 GC?

虽然 GC 监控显示没 FullGC,但保险起见看一下 GC 日志:

# 看慢请求时间点附近的 GC 情况
grep "2024-XX-XX 14:" gc.log | tail -100

# 看 STW 是否有异常
jstat -gc <pid> 1000 10

GC 干净。最长一次 YoungGC 也就 30ms,跟 5 秒完全不在一个量级。排除。

3.2 HTTP 连接池?

订单服务调支付服务用的是 OkHttp,连接池配置:

maxIdleConnections: 100
keepAliveDuration: 5 minutes
connectTimeout: 1s
readTimeout: 3s

诡异的事来了——配的 readTimeout 是 3s,但慢请求耗时 5s。说明请求并没有触发超时,是真的等到了响应。

但连接池里的连接复用是不是有问题?比如复用了一个"半死不活"的 TCP 连接?

抓一下连接池状态:

# 通过 JMX 或 Actuator 看 OkHttp 连接池状态
curl http://localhost:8080/actuator/metrics/okhttp.pool.connection.count

连接池数量正常,没有打满。但连接复用导致的 stale connection 是个高度可疑点,记下来后面验证。

3.3 线程?

# 在告警时间点附近 dump 线程栈
jstack <pid> > stack.txt

看了一下没发现明显的线程阻塞。线程池也都在工作,没有死锁。

应用层、JVM 层、框架层基本排除完,问题大概率在 TCP 协议层或者网络链路层


四、第三轮:tcpdump 抓包

到这一步,没有什么比直接抓包看真相更直接的了。

4.1 在订单服务这一侧开抓

# 抓订单服务到支付服务的所有 TCP 包,过滤端口
sudo tcpdump -i eth0 -w order-to-payment.pcap \
  host <payment-svc-ip> and tcp port <payment-port> \
  -s 0 -G 60 -W 30
# -G 60: 每 60s 切一个文件
# -W 30: 保留最多 30 个文件

这里有个小技巧:生产环境抓包不要一直抓,用 -G + -W 做循环写盘,避免磁盘打满。

抓了大约 10 分钟,等到下一次告警,立刻停掉抓包。

4.2 用 tshark 提取慢连接的特征

直接用 wireshark 打开太花眼,先用 tshark 命令行做粗筛——找出耗时超过 1s 的 HTTP 请求:

# 提取所有 HTTP 请求和响应的时间差
tshark -r order-to-payment.pcap -Y "http" \
  -T fields -e tcp.stream -e frame.time_relative -e http.request.uri -e http.response.code

很快找到一条异常的 stream,HTTP 请求发出后,过了 5.1 秒才收到响应。

4.3 看那条 stream 的完整握手过程

# 用 stream id 过滤,看这条 TCP 流的所有包
tshark -r order-to-payment.pcap -Y "tcp.stream eq 47" \
  -T fields -e frame.time_relative -e tcp.flags -e tcp.seq -e tcp.ack -e tcp.len -e tcp.analysis.retransmission

输出的关键片段(简化):

0.000   [SYN]        seq=0
0.002   [SYN, ACK]   seq=0 ack=1
0.002   [ACK]        seq=1 ack=1
0.003   [PSH, ACK]   seq=1 len=487    ← HTTP 请求发出
0.005   [ACK]        seq=1 ack=488
3.012   [PSH, ACK]   seq=1 len=487    ← 重传!
3.014   [ACK]        seq=488 ack=488  ← 重复 ACK
3.014   [PSH, ACK]   seq=488 len=215  ← 服务器开始发响应
5.123   [ACK]        seq=488 ack=703

看到关键证据了——TCP retransmission。在 3 秒处出现了重传。

这意味着:

  1. 服务器其实早就收到了请求(看到了 ACK)
  2. 但客户端因为某种原因没有收到响应包
  3. 触发了 TCP 重传定时器(RTO)
  4. 重传后才正常拿到响应

5 秒延迟里,3 秒是在等 RTO 超时,2 秒是后续重传 + 恢复期。


五、第四轮:定位丢包根因

知道是 TCP 重传以后,下一个问题是:包丢在哪里?

可能的位置:

  1. 客户端发出的请求包丢失
  2. 服务器发出的响应包丢失
  3. 中间某个网络节点(LB、网关、交换机)丢包
  4. 客户端网卡接收时丢弃(队列溢出、CPU 软中断打满)
  5. 服务端发送时丢弃

5.1 看本机的 TCP 重传统计

# 查看 TCP 重传次数
cat /proc/net/snmp | grep Tcp:
# 关注 RetransSegs 字段

# 用 nstat 看实时变化
nstat -a | grep -i retrans

# 看更细的 TCP 统计
ss -s

# 看具体 socket 的拥塞窗口、RTT、重传
ss -tin

ss -tin 的输出特别值得细看,能看到每个 TCP 连接的:

  • rtt: 平滑的 RTT
  • rttvar: RTT 抖动
  • cwnd: 拥塞窗口
  • retrans: 重传次数

抓到的几条慢连接,retrans 字段都不是 0,确实是 TCP 层在重传。

5.2 检查网卡丢包

# 看网卡硬件层丢包
ethtool -S eth0 | grep -i drop
ethtool -S eth0 | grep -i discard

# 看接收队列溢出
netstat -s | grep -i "listen overflows\|dropped\|prune"

# 看软中断在哪个 CPU 上跑(是否打满某个 CPU)
cat /proc/softirqs | grep NET_RX

# 用 sar 看历史网卡丢包
sar -n EDEV 1

订单服务这边网卡没看到异常丢包,软中断分布均匀。

5.3 关键证据:多地 Ping 看链路质量

到这一步基本可以判断丢包不在本机,而在链路上。但需要更直接的证据。

我们的订单服务和支付服务部署在阿里云不同 VPC,通过私网 SLB 互通。让 SRE 同事从外部多个节点持续 Ping 两边的服务 IP,重点看抖动和丢包率

可以用聚合多地节点的工具同时对目标地址发起持续 Ping,比单点判断更可靠。我们用的是 BiuPing 多地 Ping ,5 分钟跑下来发现:

  • 大部分地区 Ping 都很稳定
  • 但访问支付服务那个 IP 的某条路径,从某个时间点开始出现 2-3% 的丢包
  • 配合 路由追踪 工具,定位到中间某一跳的物理设备在抖动

这跟我们看到的 TCP 重传时间点完全吻合。

5.4 提工单 + 临时方案

把抓包数据 + 多地 Ping 截图 + 路由追踪结果打包发工单。同时立刻做几件临时止血的事:

  1. 缩短 readTimeout:从 3s 改成 800ms,避免单个慢请求把线程阻塞太久
  2. 加重试机制:失败请求快速重试一次(要确认接口幂等)
  3. 降低连接 keepAlive 时间:让"半死不活"的连接尽快被回收

重新发布后,P99 立刻降到 1s 以内(虽然偶发还是有丢包,但不会再卡 5s)。

云厂商工单两小时后回复,确认是某个底层网络设备的链路抖动,他们已经做了路由切换。

后续 24 小时 P99 完全恢复到 80ms 的正常水平。


六、深入:TCP 重传为什么这么慢?

排查完很多人会问:为什么一次 TCP 重传就要等 3 秒?

这跟 Linux 内核的 RTO(Retransmission Timeout)机制有关。

6.1 初始 RTO 是多少?

Linux 默认 tcp_rto_min 是 200ms,但第一次重传不是按这个值,而是按 RFC 6298 的初始 RTO 计算的,通常是 1 秒

如果重传后还没收到 ACK,RTO 会指数退避

1st retransmit: 1s
2nd retransmit: 2s
3rd retransmit: 4s
4th retransmit: 8s
...

所以一次重传卡 1-3 秒是非常常见的,5 秒+ 也不少见。

6.2 怎么"优化"TCP 重传?

重要:你不能阻止重传发生(除非链路本身好),但可以减少重传带来的伤害。

6.2.1 调整内核参数(谨慎)

# 看当前值
sysctl net.ipv4.tcp_retries2
sysctl net.ipv4.tcp_syn_retries
sysctl net.ipv4.tcp_synack_retries

# 减少最大重试次数(默认 15 次,约 924 秒才放弃;可以调小到 8 次约 100 秒)
# 编辑 /etc/sysctl.conf
net.ipv4.tcp_retries2 = 8

# 启用 RACK(Recent ACKnowledgment)替代传统 RTO 机制,更快感知丢包
net.ipv4.tcp_recovery = 1

# 应用配置
sysctl -p

注意:调整这些参数会影响整机所有 TCP 连接,生产环境务必先在压测环境验证

6.2.2 应用层做超时和重试

更安全的做法是不动内核,在应用层兜底:

  • 业务超时设得比 RTO 短:比如 800ms 超时,宁可应用层报错重试,也不要傻等 TCP 重传
  • 重试要幂等:用 traceId 或业务唯一键做去重
  • 连接池配置 validateOnBorrow:每次从池子拿连接时验证一下是否还活着

6.2.3 启用 BBR 拥塞控制(重要)

Linux 4.9+ 支持的 Google BBR 算法,对丢包链路友好得多:

# 看当前拥塞控制算法
sysctl net.ipv4.tcp_congestion_control

# 启用 BBR
echo "net.core.default_qdisc = fq" >> /etc/sysctl.conf
echo "net.ipv4.tcp_congestion_control = bbr" >> /etc/sysctl.conf
sysctl -p

# 验证
lsmod | grep bbr
sysctl net.ipv4.tcp_congestion_control

跟传统的 Cubic 相比,BBR 在有丢包的链路上吞吐和延迟表现都好得多——传统拥塞控制把丢包当成"拥塞"立刻降速,但很多时候丢包是链路抖动而不是拥塞。


七、5 个最容易被忽略的网络层慢请求元凶

复盘完这次排障,整理出 5 个生产环境中常见但容易漏掉的网络层因素,沉淀为 Checklist:

元凶 1:TCP 重传(本次)

  • 现象:偶发的秒级延迟,APM 看不出来
  • 排查ss -tinretrans 字段;tcpdump 抓包看 retransmission
  • 解决:缩短应用超时、加重试、启用 BBR、提工单

元凶 2:连接池中的 stale connection

  • 现象:偶发的"第一次请求"特别慢,连接被复用了一个已经被中间设备静默关闭的旧 TCP 连接
  • 排查:观察是否在低峰期之后第一波请求变慢;连接池配置 validateOnBorrow
  • 解决:缩短 keepAlive、加连接验证、用 HTTP/2 多路复用

元凶 3:DNS 解析偶发慢

  • 现象:偶发的请求加上 100-500ms 不等的延迟
  • 排查:APM 是否记录了 DNS 解析时间;本地 DNS 缓存配置;权威 DNS 性能
  • 解决:本地接入 nscd 或 dnsmasq、应用层缓存 DNS 结果、使用 HTTPDNS

元凶 4:跨可用区调用

  • 现象:同地域不同 AZ 之间,调用延迟比同 AZ 高 1-5ms(看起来不多,但累积起来很可怕)
  • 排查:检查依赖服务的部署 AZ,看是否跟自己同 AZ
  • 解决:开启就近路由(阿里云、腾讯云的 SLB 都支持),或做服务亲和性部署

元凶 5:TIME_WAIT / CLOSE_WAIT 堆积

  • 现象:高并发场景下,建连失败、端口耗尽
  • 排查ss -tan | awk '{print $1}' | sort | uniq -c 看各种状态的连接数
  • 解决:开启 tcp_tw_reuse(不要开 tcp_tw_recycle,已被废弃且有问题)、增加端口范围、用长连接代替短连接

八、写在最后:超越应用层的视角

这次排障让我深刻意识到一件事:作为后端,如果你只懂应用层和 SQL,你的排障能力是有天花板的。

应用层指标可以告诉你"系统正常",但用户依然在受罪。要真正定位到根因,你必须能往下钻——钻到 TCP 协议层、内核网络栈、甚至物理网络。这些"看不见的层",恰恰是生产环境最容易出问题、又最难排查的地方。

总结这次排障用到的工具栈:

层级 工具
应用层 APM(SkyWalking/Jaeger/阿里 ARMS)、应用日志、jstack、jstat
协议层 tcpdump、tshark、wireshark、ss -tin/proc/net/snmp
内核层 nstat、ethtool、/proc/softirqs、sar
链路层 多地 Ping、路由追踪、MTR

把这套工具栈用熟,线上 90% 的"诡异慢"问题都能定位到根因

如果你也在做高可用服务,建议把上面提到的 5 个元凶整理成自己团队的网络层排障 Runbook,下次告警的时候照着走一遍,能省非常多时间。

文中用到的多地 Ping 和路由追踪工具来自 BiuPing,支持全国电信/联通/移动 + 海外节点同时测,免费且不用注册,对快速验证"是不是网络抖了"特别方便。


希望这篇复盘对你有帮助。也欢迎在评论区分享你踩过的网络层坑——这种事,听一个真实 case 胜过看十本书。

目录
相关文章
|
12天前
|
人工智能 JSON 供应链
畅用7个月无影 JVS Claw |手把手教你把JVS改造成「科研与产业地理情报可视化大师」
LucianaiB分享零成本畅用JVS Claw教程(学生认证享7个月使用权),并开源GeoMind项目——将JVS改造为科研与产业地理情报可视化AI助手,支持飞书文档解析、地理编码与腾讯地图可视化,助力产业关系图谱构建。
23469 10
畅用7个月无影 JVS Claw |手把手教你把JVS改造成「科研与产业地理情报可视化大师」
|
15天前
|
人工智能 缓存 BI
Claude Code + DeepSeek V4-Pro 真实评测:除了贵,没别的毛病
JeecgBoot AI专题研究 把 Claude Code 接入 DeepSeek V4Pro,跑完 Skills —— OA 审批、大屏、报表、部署 5 大实战场景后的真实体验 ![](https://oscimg.oschina.net/oscnet/up608d34aeb6bafc47f
5118 18
Claude Code + DeepSeek V4-Pro 真实评测:除了贵,没别的毛病
|
17天前
|
人工智能 JSON BI
DeepSeek V4 来了!超越 Claude Sonnet 4.5,赶紧对接 Claude Code 体验一把
JeecgBoot AI专题研究 把 Claude Code 接入 DeepSeek V4Pro 的真实体验与避坑记录 本文记录我将 Claude Code 对接 DeepSeek 最新模型(V4Pro)后的真实体验,测试了 Skills 自动化查询和积木报表 AI 建表两个场景——有惊喜,也踩
6141 14
|
5天前
|
人工智能 缓存 Shell
Claude Code 全攻略:命令大全 + 实战工作流(完整版)
Claude Code 是一款运行在终端环境下的 AI 编码助手,能够直接在项目目录中理解代码结构、编辑文件、执行命令、执行开发计划,并支持持久化记忆、上下文压缩、后台任务、多模型切换等专业能力。对于日常开发、项目维护、快速重构、代码审查等场景,它可以大幅减少手动操作、提升编码效率。本文从常用命令、界面模式、核心指令、记忆机制、图片处理、进阶工作流等维度完整说明,帮助开发者快速上手并稳定使用。
1161 2
|
5天前
|
前端开发 API 内存技术
对比claude code等编程cli工具与deepseek v4的适配情况
DeepSeek V4发布后,多家编程工具因未适配其强制要求的`reasoning_content`字段而报错。本文对比Claude Code、GitHub Copilot、Langcli、OpenCode及DeepSeek-TUI等主流工具的兼容性:Claude Code需按官方方式配置;Langcli表现最佳,开箱即用且无报错;Copilot与OpenCode暂未修复问题;DeepSeek-TUI尚处早期阶段。
917 2
对比claude code等编程cli工具与deepseek v4的适配情况
|
1月前
|
人工智能 自然语言处理 安全
Claude Code 全攻略:命令大全 + 实战工作流(建议收藏)
本文介绍了Claude Code终端AI助手的使用指南,主要内容包括:1)常用命令如版本查看、项目启动和更新;2)三种工作模式切换及界面说明;3)核心功能指令速查表,包含初始化、压缩对话、清除历史等操作;4)详细解析了/init、/help、/clear、/compact、/memory等关键命令的使用场景和语法。文章通过丰富的界面截图和场景示例,帮助开发者快速掌握如何通过命令行和交互界面高效使用Claude Code进行项目开发,特别强调了CLAUDE.md文件作为项目知识库的核心作用。
25795 65
Claude Code 全攻略:命令大全 + 实战工作流(建议收藏)