接口偶发超时,最后停在连接池上

有一个查询接口,平时一百多毫秒能返回,每隔几天会在晚高峰出现一批 5 秒超时。重试一次往往又好了。监控上错误率不高,所以一直被当成「网络抖动」。直到同一个小时里连续出现三次,我才把这条链路从头对了一遍。

先分开看时间花在哪

网关日志里,超时请求的上游耗时接近 5 秒,应用自己的业务日志却几乎没有慢查询。这说明时间不是耗在 SQL 执行上,而是耗在拿到连接之前。应用侧把数据库调用包在一个统一的计时里,连接等待和查询执行被算成了一段,所以慢查询面板是干净的。

我把计时拆成两段:从池里取连接的等待时间,以及拿到连接之后的查询时间。再次出现超时时,等待时间顶满,查询时间仍然是几十毫秒。

池的大小和空闲策略对不上

这个服务的连接池上限是 10。晚高峰并发大概在三十上下,大部分请求只做一次很短的查询,池子理论上够用。问题出在空闲回收。

数据库侧把空闲连接在 60 秒后断开。应用侧的池仍把这些连接当作可用,下一次借出时才发现对方已经关了,于是重连。重连本身不慢,但失败的借出会占着池的名额重试,高峰时新请求就排在后面,直到网关的 5 秒超时。

池的空闲超时需要短于数据库的断开时间。我把应用侧的空闲回收改成 30 秒,并打开借出前的探活。探活多一次往返,但避免把已经断开的连接交给业务。

还有一个隐藏的占用

修完池之后,等待时间下来了,却发现有一个定时任务会在每小时的第 50 分钟连续做二十多次查询,而且没有把连接还回去。任务的代码用了手动获取连接,异常分支里漏了释放。偶发超时的节奏和这个定时任务对得上。

改成用完即还,并在测试里断言:任务结束后池的借出数回到 0。

这次之后留下的习惯

超时不要只看查询耗时。连接等待、序列化、写响应,至少分开打点。池的上限、空闲时间、对端的断开时间写在同一段配置注释里,避免以后只改一边。

错误率不高不代表可以先放着。能自己恢复的超时,往往是容量或生命周期的问题,流量再高一点就会从偶发变成持续。