【案例解析】一次 TIME_WAIT 导致 TPS 断崖式下降的排查与优化
大家好,我是G探险者!
背景
在某次系统长稳压测过程中,我们遭遇了一个诡异的问题:TPS 周期性地出现“断崖式下跌”,随后又慢慢恢复正常,周而复始。这种现象极大地影响了系统的稳定性预期,因此我们启动了问题排查与优化流程。
最终定位到一个“老朋友”——TCP 的 TIME_WAIT
状态。本文将完整还原这一问题的现象、排查路径、技术原理及优化方案,希望能为你在高并发场景下提供参考。
问题现象
测试同事使用 JMeter 持续压测服务,观察到 TPS 呈周期性波动:
- 每隔一段时间,TPS 会突降至一个较低值;
- 数十秒后,TPS 又恢复到正常水平;
- 如此循环往复,严重影响系统的稳定性指标。
我们首先从操作系统层面入手,进入服务 Pod 执行如下命令:
netstat -anp | grep 9080
结果发现:
- 有大量连接处于
TIME_WAIT
状态; - 每当 TPS 下降,
TIME_WAIT
数量会暴涨。
结合现象和连接状态,我们初步判断:服务在调用下游服务时频繁建立和关闭 TCP 连接,最终导致系统本地临时端口资源耗尽,从而影响 TPS。
初步分析
RestTemplate 用法检查
服务通过 RestTemplate
调用下游服务,其创建方式如下:
public static RestTemplate createRestTemplate() {OkHttp3ClientHttpRequestFactory factory = new OkHttp3ClientHttpRequestFactory();factory.setConnectTimeout(DEFAULT_CONNECT_TIMEOUT);factory.setReadTimeout(DEFAULT_READ_TIMEOUT);return new RestTemplate(factory);
}
关键问题来了:
- 每次创建
RestTemplate
都是全新对象; - 底层
OkHttpClient
未显示传入(隐式新建); - 未启用连接池,导致每次请求都是一次完整的 TCP 短连接。
TIME_WAIT 是什么?
在 TCP 协议中,TIME_WAIT
是主动关闭连接的一方所进入的状态,它存在的目的是:
- 防止旧连接残留数据污染新连接;
- 确保被动关闭方若未收到最后一个 ACK,还可以重发。
默认情况下,Linux 会将连接保留在 TIME_WAIT
状态长达 60 秒(2×MSL)。在高并发场景下:
- 短连接频繁创建 → 每次都进入
TIME_WAIT
; - 数万连接堆积 → 本地端口资源被占满;
- 导致新连接
connect()
超时 → TPS 出现断崖。
解决方案:引入连接池
我们尝试对 OkHttpClient
显式配置连接池并注入 RestTemplate,核心代码如下:
@Configuration
public class RestClientConfig {@Beanpublic OkHttpClient okHttpClient() {ConnectionPool pool = new ConnectionPool(100, 30, TimeUnit.MINUTES);Dispatcher dispatcher = new Dispatcher();dispatcher.setMaxRequests(200);dispatcher.setMaxRequestsPerHost(100);return new OkHttpClient.Builder().dispatcher(dispatcher).connectionPool(pool).connectTimeout(Duration.ofSeconds(5)).readTimeout(Duration.ofSeconds(30)).followRedirects(false).build();}@Beanpublic RestTemplate restTemplate(OkHttpClient okHttpClient) {OkHttp3ClientHttpRequestFactory factory =new OkHttp3ClientHttpRequestFactory(okHttpClient);return new RestTemplate(factory);}
}
效果观察
调整部署后重新压测,结果如下:
TIME_WAIT
数量大幅下降;- TPS 波动消失,长期保持平稳;
- 网络端口资源利用率恢复正常。
技术要点总结
维度 | 建议 | 原因 |
---|---|---|
RestTemplate 构建 | 使用单例 Bean | 避免重复创建连接池 |
OkHttpClient 配置 | 显式配置 ConnectionPool 和 Dispatcher | 控制最大连接数与请求并发度 |
系统内核参数(可选) | 调整 ip_local_port_range 和 tcp_tw_reuse | 提高本地端口复用能力(仅作兜底) |
下游配合 | 下游服务需支持 keep-alive | 否则连接池复用无效 |
监控指标 | TIME_WAIT 数、连接池状态、TPS 曲线 | 实时观测变化,辅助优化 |
附:一些底层原理问答
Q: 为什么 TIME_WAIT
会阻塞新连接?
A: TCP 协议要求 TIME_WAIT
状态的套接字在未过期前,不能被复用为同样四元组的新连接。连接数太多 → 可用本地端口耗尽 → 后续请求卡在 connect()
。
Q: 客户端为什么是 TIME_WAIT
?
A: 主动调用 close()
的一方进入 TIME_WAIT
,在我们的场景中,服务调用下游时请求发起方是客户端,它主动关闭连接。
Q: 为什么“加了连接池”后问题解决?
A: 连接池使得请求可以复用已有连接,避免频繁建链与断链,自然 TIME_WAIT
数量大大减少。
结语
这是一个典型的“连接未复用 → TIME_WAIT 滥发 → 系统资源耗尽 → TPS 周期性下降”的问题。看似是一个 TPS 波动的问题,实则背后隐藏着连接池使用不当、短连接滥用、系统资源上限等多个技术点。
性能测试不仅是服务能力的验证,也是架构合理性的“压力测试”。本次问题的排查,也提醒我们在设计服务间通信时,连接池和复用策略是高并发系统的生命线。