一次接口超时排查:从线程池排队到连接复用

米阳 2023-1-19 4 1/19

线上某个列表接口偶尔超时,监控上看到 P99 突然从 80ms 涨到 3s,但数据库慢查询日志里什么都没有。这类问题通常不在 SQL 本身,而在请求排队。记录一下当时的排查路径。

一、先分清时间花在哪

给一次请求打上分段耗时:

网关接入 → 应用排队 → 业务执行 → 外部调用 → 响应序列化

排查时发现 业务执行 只用了 20ms,绝大部分时间落在 应用排队。这说明请求已经进到服务,但迟迟没拿到工作线程。

二、线程池被打满的常见原因

Web 容器(Tomcat / php-fpm / uvicorn worker)的并发数是固定的。当大量请求卡在慢的外部调用上,工作线程就会被占住,后面的请求只能排队。

当时的问题是一个下游 HTTP 调用没设读超时,对方抖动时请求挂在连接读取上,默认超时长达数分钟。于是几十个线程被占满,整个服务的健康接口都开始超时。

修复要点:

connect_timeout = 1s     # 建连
read_timeout    = 2s     # 等响应
retry           = 1 次,且只对幂等请求

任何外部调用都必须显式设置超时,不要依赖库默认值。超时时间要小于上游给你的超时预算,留出重试和降级的余量。

三、连接池复用别忽略

另一个坑是每次请求新建连接。短连接在高并发下会带来三次握手开销,还可能把本地端口耗尽,表现为偶发 Cannot assign requested address。

用连接池时三个参数要配套:

  • maxTotal:上限,按 峰值 QPS × 平均耗时 估,留一点余量
  • maxIdle:空闲保留数,太大浪费、太小会频繁建连
  • maxWait:拿不到连接时最多等多久,超时要快速失败而不是无限阻塞

四、加上隔离和兜底

把不同下游的调用拆到独立线程池或独立信号量,避免一个慢接口拖垮全站。同时对非核心数据做降级:推荐列表取不到就返回空数组,不要阻塞主流程返回。

小结

超时类问题先看排队耗时再优化 SQL,往往能少走弯路。改完之后这个接口的 P99 回到 90ms 左右,而且下游再抖动也不会扩散到其它接口。

- THE END -

米阳

1月19日14:10

最后修改:2023年1月19日
0

非特殊说明,本博所有文章均为博主原创。

共有 0 条评论