一次线上接口偶发超时后,我是怎么用抓包 + 指标把问题定位到连接复用策略的

2 阅读10分钟

一次线上接口偶发超时后,我是怎么用抓包 + 指标把问题定位到连接复用策略的

凌晨一点,业务方在群里丢来一句话:“接口没挂,但就是一阵一阵地慢,偶尔还超时。”

这类问题最难的地方,不是彻底不可用,而是有波动、能恢复、还不稳定复现。应用日志里只有零星的 upstream timeout,主机 CPU、内存、磁盘也都不高,看起来每一层都“没毛病”。如果只盯着单点监控,十有八九会把时间浪费在无效怀疑上。

这篇文章想回答一个工程上很实际的问题:当线上接口出现偶发超时,但机器资源看起来正常时,应该优先排查什么?抓包和可观测性指标分别该怎么配合?

先给一句话定义:连接复用策略问题,是指客户端、代理层或服务端在 Keep-Alive、连接池、空闲连接回收、端口复用等配置上存在不匹配,导致请求并不是慢在业务逻辑,而是慢在建连、等待可用连接或重试失败。

如果你也遇到过“QPS 不高却延迟抖动”“偶发 502/504”“重启后短暂恢复正常”的情况,这类问题非常值得优先看。

什么是连接复用策略问题

很多团队谈性能优化时,第一反应是调线程池、扩机器、上缓存。但在真实线上环境里,延迟抖动并不总来自代码执行时间,反而常常来自网络路径上的“连接管理失配”。

常见链路通常长这样:

客户端 SDK → 网关/Nginx/Ingress → 应用服务 → 下游 HTTP/RPC 服务

只要其中任意两层对连接生命周期的理解不一致,就会出现这类现象:

  • 客户端以为连接还能复用,实际上对端已经回收
  • 代理层空闲超时太短,导致频繁重建连接
  • 连接池上限偏小,请求在池里排队
  • NAT/防火墙状态回收导致长连接“表面活着,实际已失效”
  • 服务端 accept 正常,但短时间内 SYN/ACK 重传增加,建连耗时飙高

它和传统“代码慢”问题的边界在于:

  • 代码慢:请求进入应用后,业务处理时间长,通常能从 APM span、SQL、锁等待中直接看到
  • 连接复用问题:请求还没真正稳定进入业务执行,时间已经耗在 TCP 建连、连接获取、连接失效重试、代理转发等待上

也就是说,如果应用层监控看起来平平无奇,但接口 RT 在边缘层、网关层或客户端侧抖得厉害,就要怀疑连接层。

典型场景:为什么“机器很闲,接口却时不时超时”

我们那次故障的特征非常典型:

  • API 平均 QPS 只有平时的 60%
  • CPU 不高,GC 没明显异常
  • 数据库没有慢查询峰值
  • 但是 P99 从 180ms 抬到 2.8s
  • Nginx access log 中少量 504
  • 重启应用后,延迟会短暂恢复

看到“重启后恢复”时,很多人会误以为是内存泄漏或者线程池卡死。其实还有另一种很常见的解释:重启把旧连接池清空了,短时间内全部变成健康新连接,所以现象暂时消失。

后面复盘发现,问题出在上游网关到后端服务的 HTTP Keep-Alive 配置不一致:

  • 网关层空闲连接保留 75 秒
  • 应用服务所在侧车/代理 30 秒就回收空闲连接
  • 客户端连接池还倾向优先复用“看起来可用”的旧连接

结果就是:一部分请求命中了已经失效但未及时剔除的连接,先失败一次,再重试新建连接,最终把少量请求拖成秒级。

这也是为什么它呈现为“偶发”:不是所有请求都慢,只有命中坏连接的那部分慢。

和传统方案的区别:为什么不能只看应用日志

很多排障流程还停留在“先看应用日志,再看机器指标,不行就重启”的模式。这种方法对 CPU 打满、SQL 慢、线程阻塞当然有效,但对连接层问题有明显盲区。

传统方案:只看应用日志和资源监控

优点:

  • 成本低
  • 适合发现显性故障

局限:

  • 看不到请求进入业务前的耗时
  • 看不到 TCP 重传、RST、空闲连接失效
  • 看不到代理层和客户端连接池行为

抓包 + 可观测性联动方案

优点:

  • 能区分“业务慢”还是“网络路径慢”
  • 能识别连接建立、复用、关闭、重传的异常模式
  • 能把网关、服务、下游三个视角串起来

成本:

  • 需要明确抓包窗口和目标端口
  • 需要结合延迟指标、错误码、连接池指标一起看
  • 对团队的网络基础要求更高

边界结论很明确:

  • 如果问题稳定复现且 span 已经明确卡在 SQL/缓存/锁,没必要先抓包
  • 如果问题表现为偶发抖动、少量超时、日志不连续、重启暂时恢复,抓包价值极高

实战排查:我是怎么一步步锁定的

下面这套流程,基本适用于大多数 HTTP/RPC 偶发超时问题。

1)先确认慢在链路哪一段

先把指标拆开,不要只看一个总 RT:

  • 网关上游响应时间
  • 应用处理耗时
  • 下游调用耗时
  • TCP 建连时间(如果有)
  • 错误码分布(502/504/499/5xx)

如果你在 Prometheus 体系里,至少要先把这些维度捞出来:

histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service))
sum(rate(nginx_ingress_controller_requests{status=~"5.."}[5m])) by (status, ingress)
sum(rate(envoy_cluster_upstream_rq_timeout[5m])) by (cluster_name)

我们的现场是:应用内部处理时间变化不大,但网关到上游的等待时间明显拉长。 这说明问题更可能在网络路径或连接获取阶段,而不是业务代码本身。

2)看连接池是否在“表面充足,实际失效”

很多语言运行时或 HTTP 客户端都有连接池指标,例如:

  • active connections
  • idle connections
  • pending acquire
  • connection create rate
  • connection reset / timeout

如果你发现:

  • idle 很多,但 timeout 也在涨
  • create rate 周期性突刺
  • pending acquire 偶发升高

那基本就可以怀疑:池里有一批连接并不真正健康。

例如 Java / Reactor Netty 或 Go HTTP Client 都可能出现类似现象。排查时要特别关注连接最大空闲时间、最大生命周期、健康检查策略。

3)抓包确认有没有重传、RST 和失效复用

这是最关键一步。抓包不是为了“把所有包都看一遍”,而是为了验证几个假设:

  • 慢请求前是否有 TCP 重传
  • 是否出现 client 发送请求后收到 RST
  • 是否频繁重新三次握手
  • 是否存在旧连接空闲后首次复用即失败

常用命令:

tcpdump -i any host 10.0.12.34 and port 8080 -nn -tttt -w /tmp/upstream_issue.pcap

如果只想快速看异常摘要,也可以:

tcpdump -i any host 10.0.12.34 and port 8080 -nn | grep -E 'Flags \[R\]|Flags \[S\]|retransmission'

我们抓到的关键模式是:

  1. 某些连接空闲几十秒后再次被复用
  2. 客户端发出 HTTP 请求数据
  3. 很快收到对端 RST 或长时间无响应
  4. 客户端重试并新建连接
  5. 第二次请求成功

这几乎就是“失效长连接复用”的标准画像。

4)核对超时配置,找谁先回收连接

接下来不要急着调大超时,而是先把整条链路的配置摆平:

  • 客户端 keepalive timeout
  • 连接池 max idle time
  • 代理层 upstream keepalive timeout
  • L4/L7 负载均衡空闲超时
  • 云厂商 NAT / 防火墙会话保持时间

原则是谁更短谁说了算。

如果代理 30 秒就回收,而客户端 90 秒还想复用,那客户端就会持续拿到“逻辑上存在、物理上已死”的连接。

5)小流量验证,而不是全量豪赌

定位后,我们没有直接在全站放量,而是先做了两件事:

  • 把客户端空闲连接时间下调到比代理层更短
  • 给连接池增加失效探测和更积极的回收策略

变更后对照指标:

  • P99 延迟是否回落
  • 新建连接数是否异常飙升
  • 5xx/timeout 是否下降
  • CPU 是否因频繁建连反而上升

只有这些指标同时健康,才能说明你不是“用更多建连成本掩盖旧问题”。

选型判断标准:遇到类似问题时,优先看这 5 条

如果你想快速判断这是不是连接复用策略问题,可以直接用下面这份清单。

判断标准 1:延迟抖动是否集中在 P95/P99,而不是平均值

平均延迟正常,尾延迟异常,通常说明是部分请求命中异常路径,这和坏连接复用的分布特征一致。

判断标准 2:重启服务后是否短暂恢复

如果重启有效,但没有代码发布、没有负载变化、没有数据库侧改善,就要优先怀疑连接池或缓存状态,而不只是应用逻辑。

判断标准 3:应用耗时稳定,但网关/客户端侧超时增加

这说明问题可能发生在请求进入应用前,或者发生在代理转发阶段。

判断标准 4:抓包是否看到 RST、重传、重复建连

这类证据一旦出现,优先级非常高,比“感觉像网络问题”靠谱得多。

判断标准 5:链路各层 idle timeout 是否不一致

这是最容易被忽略、但最常见的根因之一。只要多层配置不对齐,就会为偶发超时埋雷。

什么情况下不该优先走这套方法

不是所有慢请求都值得抓包。下面这些场景,应该先用传统方法:

  • APM 已明确显示 SQL 或外部 API 耗时占满全程
  • CPU 打满、Run Queue 飙升、GC Stop-The-World 明显
  • 故障和某次代码发布强相关,且回滚后立即恢复
  • 单机本地压测也稳定复现慢逻辑

换句话说,抓包 + 可观测性联动更适合处理“应用看起来没问题,但线上体验就是抖”的灰度型故障,不适合所有性能问题一把梭。

直接结论

最后把结论说透:线上偶发超时、资源正常、重启短暂恢复,这三件事同时出现时,优先怀疑连接复用策略失配,而不是盲目扩容。

更具体一点:

  1. 先拆分总耗时,确认慢在链路还是慢在业务
  2. 再看连接池指标,识别空闲连接失效、排队、重建连接突刺
  3. 用抓包验证是否存在 RST、重传、坏连接复用
  4. 对齐各层 idle timeout、keepalive、连接池回收策略
  5. 小流量验证尾延迟和错误率,再全量推广

这套方法比“看日志 + 重启 + 祈祷”靠谱得多。毕竟线上系统不是玄学,很多所谓偶发问题,本质上只是连接管理细节在高并发和复杂链路下被放大了

如果你正在做 API 可观测性、链路监控或流量分析,类似这类“不是彻底挂,但持续吞噬尾延迟”的问题,往往比单纯的宕机更影响真实业务体验。像 AnaTraf 这类流量分析与可观测性产品,更适合用来持续发现这类隐蔽的异常模式,而不是等用户投诉之后再临时抓包。更多信息可见:www.anatraf.com