一、问题现象 我们使用 Spring Boot 2.7 + RestTemplate 调用下游 MCP 服务(HTTP + JSON)。压测时发现:
平均响应时间:320ms(正常应在 100ms 内)
P99 响应时间:2100ms
服务端 CPU 和内存均正常,无慢 SQL 或 GC 问题
调用方(Spring)线程大量处于 TIMED_WAITING 状态
初步判断:瓶颈不在服务端,而在客户端调用链路。
二、定位过程
-
确认网络延迟 用 curl 直接调用 MCP 服务,耗时仅 45ms。说明网络和服务端处理没有问题,问题出在 Spring 调用封装层。
-
检查 RestTemplate 配置 发现用的是默认的 RestTemplate,没有配置连接池和超时。默认使用 SimpleClientHttpRequestFactory,每个请求都会新建连接,且没有连接复用。
-
抓包分析 用 Wireshark 看到大量 TCP 三次握手和四次挥手,每个请求都重新建立连接,TCP 握手耗时 + TLS 握手(如果是 HTTPS) 占了大头。
-
日志分析 打印调用耗时,发现 RestTemplate.exchange() 之前有一段莫名其妙的延迟(~150ms),后来定位到是因为在过滤器里做了同步的用户信息解析,阻塞了 IO。
三、解决方案汇总 针对定位到的几个问题,我逐一做了优化:
问题 优化手段 效果 没有连接池,每次新建连接 改用 HttpComponentsClientHttpRequestFactory + 池化连接管理器 TCP 握手次数减少 95% 同步阻塞调用 改用 WebClient + 响应式编程,或 @Async 异步调用 释放调用线程,提升吞吐量 超时设置不合理(默认无限等待) 设置连接超时、读取超时 避免线程长时间阻塞 请求/响应体过大未压缩 开启 GZIP 压缩 传输体积减少 70% 重复的用户信息解析 将解析结果缓存到 RequestContextHolder(线程本地变量) 消除重复计算 日志打印过多 生产环境调低日志级别,或异步日志 减少日志 IO 开销 下面给出核心代码实现。
四、核心优化代码
-
配置带连接池的 RestTemplate java @Configuration public class RestTemplateConfig {
@Bean public RestTemplate restTemplate() { // 使用 HttpClient 连接池 PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); connectionManager.setMaxTotal(200); // 最大连接数 connectionManager.setDefaultMaxPerRoute(50); // 每个路由最大并发
RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(3000) // 连接超时 3s .setSocketTimeout(5000) // 读取超时 5s .setConnectionRequestTimeout(2000) // 从连接池获取连接超时 .build(); CloseableHttpClient httpClient = HttpClients.custom() .setConnectionManager(connectionManager) .setDefaultRequestConfig(requestConfig) .setKeepAliveStrategy(DefaultConnectionKeepAliveStrategy.INSTANCE) // 保持长连接 .build(); HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient); return new RestTemplate(factory);} }
-
改用 WebClient 实现异步非阻塞调用 java @Configuration public class WebClientConfig {
@Bean public WebClient webClient(WebClient.Builder builder) { return builder .baseUrl("http://mcp-service") .defaultHeader("Content-Type", "application/json") .clientConnector(new ReactorClientHttpConnector( HttpClient.create() .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000) .responseTimeout(Duration.ofSeconds(5)) .doOnConnected(conn -> conn.addHandlerLast(new ReadTimeoutHandler(5)) .addHandlerLast(new WriteTimeoutHandler(3))) .compress(true) // 开启压缩 )) .build(); } } 调用示例:
java @Service public class McpService {
private final WebClient webClient;
public McpService(WebClient webClient) {
this.webClient = webClient;
}
public Mono<McpResponse> callMcp(McpRequest request) {
return webClient.post()
.uri("/api/endpoint")
.bodyValue(request)
.retrieve()
.bodyToMono(McpResponse.class)
.timeout(Duration.ofSeconds(5))
.retryWhen(Retry.backoff(2, Duration.ofMillis(100))); // 重试策略
}
} 在 Controller 中返回 Mono,充分利用 Spring WebFlux 的非阻塞特性。
- 使用本地缓存减少重复调用 对于不经常变化的数据(如用户权限、配置信息),引入 Caffeine 缓存:
java @Configuration public class CacheConfig {
@Bean
public Cache<String, UserAuth> userAuthCache() {
return Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.recordStats()
.build();
}
} 4. 开启 HTTP 压缩(服务端和客户端) 在 MCP 服务端(如 Tomcat)配置压缩:
properties server.compression.enabled=true server.compression.mime-types=application/json,application/xml server.compression.min-response-size=1024 客户端自动处理 Accept-Encoding: gzip,RestTemplate 和 WebClient 默认支持,无需额外配置。
五、优化效果 压测结果(100 并发,持续 5 分钟):
指标 优化前 优化后 提升 平均响应时间 320 ms 62 ms 81% ↓ P99 响应时间 2100 ms 145 ms 93% ↓ 吞吐量 (TPS) 280 1520 5.4 倍 ↑ 调用线程阻塞数 经常满 (200) < 10 极大改善 同时,服务端 CPU 使用率也从 45% 下降到 22%,因为减少了大量无用的连接建立和销毁开销。
六、经验总结 永远不要用默认的 RestTemplate 去做生产环境的高频调用,连接池是标配。
超时必须设置,否则一个慢服务会拖垮整个调用链。
异步化是提升吞吐量的利器,Spring WebFlux + WebClient 非常适合 IO 密集型场景。
性能问题先从客户端找原因,不要一上来就怀疑服务端。
善用工具:Wireshark(网络)、Arthas(线程栈)、JMeter(压测)、SkyWalking(链路追踪)。
最后,如果你的 MCP 服务不是 HTTP 协议,而是基于 TCP 或 gRPC,优化思路类似:连接池、多路复用、异步、超时、压缩、缓存 六大法宝。
希望这篇文章能帮到你。如果你也有类似的踩坑经验,欢迎在评论区交流!