实战干货:我是如何解决 Spring 调用 MCP 服务响应慢这一痛点的

0 阅读4分钟

一、问题现象 我们使用 Spring Boot 2.7 + RestTemplate 调用下游 MCP 服务(HTTP + JSON)。压测时发现:

平均响应时间:320ms(正常应在 100ms 内)

P99 响应时间:2100ms

服务端 CPU 和内存均正常,无慢 SQL 或 GC 问题

调用方(Spring)线程大量处于 TIMED_WAITING 状态

初步判断:瓶颈不在服务端,而在客户端调用链路。

二、定位过程

  1. 确认网络延迟 用 curl 直接调用 MCP 服务,耗时仅 45ms。说明网络和服务端处理没有问题,问题出在 Spring 调用封装层。

  2. 检查 RestTemplate 配置 发现用的是默认的 RestTemplate,没有配置连接池和超时。默认使用 SimpleClientHttpRequestFactory,每个请求都会新建连接,且没有连接复用。

  3. 抓包分析 用 Wireshark 看到大量 TCP 三次握手和四次挥手,每个请求都重新建立连接,TCP 握手耗时 + TLS 握手(如果是 HTTPS) 占了大头。

  4. 日志分析 打印调用耗时,发现 RestTemplate.exchange() 之前有一段莫名其妙的延迟(~150ms),后来定位到是因为在过滤器里做了同步的用户信息解析,阻塞了 IO。

三、解决方案汇总 针对定位到的几个问题,我逐一做了优化:

问题 优化手段 效果 没有连接池,每次新建连接 改用 HttpComponentsClientHttpRequestFactory + 池化连接管理器 TCP 握手次数减少 95% 同步阻塞调用 改用 WebClient + 响应式编程,或 @Async 异步调用 释放调用线程,提升吞吐量 超时设置不合理(默认无限等待) 设置连接超时、读取超时 避免线程长时间阻塞 请求/响应体过大未压缩 开启 GZIP 压缩 传输体积减少 70% 重复的用户信息解析 将解析结果缓存到 RequestContextHolder(线程本地变量) 消除重复计算 日志打印过多 生产环境调低日志级别,或异步日志 减少日志 IO 开销 下面给出核心代码实现。

四、核心优化代码

  1. 配置带连接池的 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);
    

    } }

  2. 改用 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 的非阻塞特性。

  1. 使用本地缓存减少重复调用 对于不经常变化的数据(如用户权限、配置信息),引入 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,优化思路类似:连接池、多路复用、异步、超时、压缩、缓存 六大法宝。

希望这篇文章能帮到你。如果你也有类似的踩坑经验,欢迎在评论区交流!