当前位置: 首页 > news >正文

RestTemplate使用 | RestTemplate设置http连接池参数

场景:接口从httpClient调用改成RestTemplate调用之后频报以下异常,开始排查是否连接泄漏或存在连接池问题。
排查发现配置类没设置线程池参数,导致走了默认的配置:因默认单个路由最大连接数为2加上第三方接口响应时间较长,造成http连接长时间获取不到导致的超时问题。另外,使用RestTemplate不用手动释放或关闭连接,不用httpclient.close(),spring已经托管了、不会存在连接泄漏的问题。
注:后文贴出的是已修复好的、加上httpClient连接池设置的配置,可放心食用
新发现:原来http连接池还可以设置单个路由最大并发数,这是为了防止单一服务抢占所有资源

异常日志:

I/O error on POST request for “https://xxxx”: Timeout waiting for connection from pool; nested exception is org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool org.springframework.web.client.ResourceAccessException: I/O error on POST request for “https://xxxx”: Timeout waiting for connection from pool; nested exception is org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool

问题排查思路:
在这里插入图片描述

文章目录

  • 零、环境说明
  • 一、配置类和maven依赖
    • (一)配置类
    • (二)依赖
  • 二、http连接池参数监控类(非必须,主要是为了后续观察http连接池参数)
  • 三、用Controller测试
    • (一)多线程并发测试,观察线程池参数值
    • (二)用postman模拟并发,观察线程池参数值变化(未观察到并发数变化,可能这种测试方法不适用)
    • (三)注释掉http连接池,观察默认连接设置
  • 四、结论
  • 五、其他问题
    • (一). RestTemplate 和 HttpClient的关系
      • 1.RestTemplate (Spring Framework)
      • 2.HttpClient (Apache)
      • 3.实际调用流程:
      • 4.为什么需要这种分层设计
      • 5.多序列化模板
    • (二)线程池、http连接池、durid数据库连接池、Tomcat连接池,这些池的连接数有没有重叠的地方(AI回答)
    • 1. 各“池”的作用与层级
    • 2. 连接数的重叠场景(串联关系)
    • 3. 配置建议(如何设置连接数)
    • 4.总结

零、环境说明

  • java
  • jdk8
  • maven
  • spring-boot-starter-parent 2.7.5

一、配置类和maven依赖

(一)配置类

双template,能同时应对响应时间较长的接口、响应时间正常的接口。性能方面,设置了http连接池参数,资源可高效复用


@Slf4j
@Configuration
public class RestTemplateConfig {// http连接池@Beanpublic PoolingHttpClientConnectionManager connectionManager() {PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();// 设置整个连接池最大连接数connectionManager.setMaxTotal(200);// 设置每个路由(目标主机)的最大连接数connectionManager.setDefaultMaxPerRoute(50);// 空闲连接验证间隔(毫秒)connectionManager.setValidateAfterInactivity(5000);return connectionManager;}@Beanpublic RequestConfig requestConfig() {return RequestConfig.custom().setSocketTimeout(10000)           // Socket超时 10: 根据接口响应时长评估。不可设置过长。在连接泄漏或服务端卡顿时,会长时间占用线程资源。容易导致客户端线程池被占满,引发级联故障.setConnectTimeout(2000)           // 连接超时 2: 建立TCP连接通常在毫秒级.setConnectionRequestTimeout(2000) // 从连接池获取连接超时 2秒 :从连接池获取连接应该是瞬间的,除非连接池已满.build();}@Beanpublic CloseableHttpClient httpClient() {return HttpClients.custom().setConnectionManager(connectionManager()) // 关键:添加连接池管理器.setDefaultRequestConfig(requestConfig()).evictExpiredConnections() // 驱逐过期连接.evictIdleConnections(30, TimeUnit.SECONDS) // 驱逐空闲30秒以上的连接.build();}@Beanpublic ClientHttpRequestFactory clientHttpRequestFactory() {return new HttpComponentsClientHttpRequestFactory(httpClient());}// 正常版的RestTemplate@Beanpublic RestTemplate restTemplate() {return new RestTemplate(clientHttpRequestFactory());}// 长响应时长版本的RestTemplate@Bean("longTimeoutRestTemplate")public RestTemplate longTimeoutRestTemplate() {RequestConfig longTimeoutConfig = RequestConfig.custom().setSocketTimeout(60000).setConnectTimeout(2000).setConnectionRequestTimeout(2000).build();// http连接池PoolingHttpClientConnectionManager longTimeoutConnectionManager =new PoolingHttpClientConnectionManager();longTimeoutConnectionManager.setMaxTotal(200);longTimeoutConnectionManager.setDefaultMaxPerRoute(50);CloseableHttpClient longTimeoutClient = HttpClients.custom().setConnectionManager(longTimeoutConnectionManager).setDefaultRequestConfig(longTimeoutConfig).evictExpiredConnections().evictIdleConnections(30, TimeUnit.SECONDS).build();return new RestTemplate(new HttpComponentsClientHttpRequestFactory(longTimeoutClient));}}

(二)依赖

  <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.5</version></parent><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>commons-httpclient</groupId><artifactId>commons-httpclient</artifactId><version>3.1</version></dependency><dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpclient</artifactId><version>4.5.13</version></dependency><dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpmime</artifactId><version>4.5.13</version></dependency>
</dependencies>

二、http连接池参数监控类(非必须,主要是为了后续观察http连接池参数)

通过springboot的定时任务监控,主启动类要加上@EnableScheduling
启动后,服务将定时打印如下信息:
在这里插入图片描述

@Component
@Slf4j
public class HttpClientMonitor {private final RestTemplate restTemplate;private PoolingHttpClientConnectionManager connectionManager;public HttpClientMonitor(@Lazy @Qualifier("longTimeoutRestTemplate") RestTemplate restTemplate) {this.restTemplate = restTemplate;}@PostConstructpublic void init() {initializeConnectionManager();}private void initializeConnectionManager() {try {if (restTemplate.getRequestFactory() instanceof HttpComponentsClientHttpRequestFactory) {HttpComponentsClientHttpRequestFactory factory =(HttpComponentsClientHttpRequestFactory) restTemplate.getRequestFactory();HttpClient httpClient = factory.getHttpClient();// 使用反射获取真实的连接管理器this.connectionManager = getRealConnectionManager(httpClient);if (connectionManager != null) {log.info("HTTP连接池监控器初始化成功");} else {log.warn("无法获取PoolingHttpClientConnectionManager");}}} catch (Exception e) {log.warn("初始化连接管理器失败: {}", e.getMessage());}}/*** 使用反射获取真实的连接管理器*/private PoolingHttpClientConnectionManager getRealConnectionManager(HttpClient httpClient) {try {// 获取connectionManager字段Field connManagerField = httpClient.getClass().getDeclaredField("connManager");connManagerField.setAccessible(true);Object connectionManager = connManagerField.get(httpClient);// 如果是PoolingHttpClientConnectionManager,直接返回if (connectionManager instanceof PoolingHttpClientConnectionManager) {return (PoolingHttpClientConnectionManager) connectionManager;}// 如果不是,尝试查找内部的PoolingManagerreturn findPoolingManagerInFields(connectionManager);} catch (Exception e) {log.warn("通过反射获取连接管理器失败: {}", e.getMessage());return null;}}/*** 递归查找字段中的PoolingHttpClientConnectionManager*/private PoolingHttpClientConnectionManager findPoolingManagerInFields(Object obj) {if (obj == null) {return null;}// 如果已经是目标类型,直接返回if (obj instanceof PoolingHttpClientConnectionManager) {return (PoolingHttpClientConnectionManager) obj;}try {// 遍历所有字段,查找PoolingHttpClientConnectionManagerfor (Field field : obj.getClass().getDeclaredFields()) {field.setAccessible(true);Object fieldValue = field.get(obj);if (fieldValue instanceof PoolingHttpClientConnectionManager) {return (PoolingHttpClientConnectionManager) fieldValue;}// 递归查找if (fieldValue != null && !fieldValue.getClass().isPrimitive()) {PoolingHttpClientConnectionManager result = findPoolingManagerInFields(fieldValue);if (result != null) {return result;}}}} catch (Exception e) {log.warn("http连接池监控,反射异常,忽略: {}", e.getMessage());}return null;}@Scheduled(initialDelay = 10000, fixedRate = 500)public void monitorConnectionPool() {if (connectionManager == null) {initializeConnectionManager(); // 重新尝试初始化if (connectionManager == null) {log.warn("连接池监控器尚未初始化完成");return;}}try {int defaultMaxPerRoute = connectionManager.getDefaultMaxPerRoute();int validateAfterInactivity = connectionManager.getValidateAfterInactivity();PoolStats totalStats = connectionManager.getTotalStats();log.info("HTTP连接池状态 - 最大连接数: {}, 已用: {}, 可用: {}, 等待: {}, 每路由最大并发数: {}, 验证时间间隔: {}ms",totalStats.getMax(),totalStats.getLeased(),totalStats.getAvailable(),totalStats.getPending(),defaultMaxPerRoute, validateAfterInactivity); // 记录详细的连接池信息logDetailedStats();} catch (Exception e) {log.warn("监控连接池时发生异常: {}", e.getMessage());}}/*** 记录更详细的连接池统计信息*/private void logDetailedStats() {try {Set<HttpRoute> routes = connectionManager.getRoutes();if (routes != null && !routes.isEmpty()) {for (HttpRoute route : routes) {PoolStats routeStats = connectionManager.getStats(route);if (routeStats.getLeased() > 0 || routeStats.getAvailable() > 0) {log.debug("路由 {} - 最大: {}, 已用: {}, 可用: {}, 等待: {}",route.getTargetHost(),routeStats.getMax(),routeStats.getLeased(),routeStats.getAvailable(),routeStats.getPending());}}}} catch (Exception e) {log.debug("记录详细统计信息时发生异常: {}", e.getMessage());}}/*** 获取连接池统计信息*/public Map<String, Object> getConnectionPoolStats() {Map<String, Object> stats = new HashMap<>();if (connectionManager != null) {try {PoolStats totalStats = connectionManager.getTotalStats();stats.put("maxTotal", totalStats.getMax());stats.put("leased", totalStats.getLeased());stats.put("available", totalStats.getAvailable());stats.put("pending", totalStats.getPending());} catch (Exception e) {stats.put("error", e.getMessage());}} else {stats.put("error", "Connection manager not initialized");}return stats;}
}

三、用Controller测试

(一)多线程并发测试,观察线程池参数值

@Autowired@Qualifier("longTimeoutRestTemplate")private RestTemplate restTemplate;@GetMapping("/testCall")public void testCall() {ModelScoreResult request = new ModelScoreResult();// 发送POST请求ResponseEntity<ModelScoreResult> response = restTemplate.postForEntity("http://localhost:8080/testCall",request,ModelScoreResult.class);log.info("响应= {}", JSON.toJSONString(response));}@GetMapping("/testCall2")public void testCall2() {testSlowRequests();}public void testSlowRequests() {ExecutorService executor = Executors.newFixedThreadPool(100);CountDownLatch latch = new CountDownLatch(80); log.info("开始慢速接口并发测试...");ModelScoreResult request = new ModelScoreResult();for (int i = 0; i < 80; i++) {final int index = i;executor.submit(() -> {try {long start = System.currentTimeMillis();ResponseEntity<ModelScoreResult> response = restTemplate.postForEntity("http://localhost:8080/testCall",request,ModelScoreResult.class);long end = System.currentTimeMillis();log.info("慢请求 {} - 状态: {}, 耗时: {}ms",index, response.getStatusCode(), end - start);} catch (Exception e) {log.error("慢请求 {} 失败: {}", index, e.getMessage());} finally {latch.countDown();}});}try {latch.await();log.info("所有慢速请求完成");} catch (InterruptedException e) {Thread.currentThread().interrupt();}executor.shutdown();}

(二)用postman模拟并发,观察线程池参数值变化(未观察到并发数变化,可能这种测试方法不适用)

在这里插入图片描述

(三)注释掉http连接池,观察默认连接设置

在这里插入图片描述
不设置http线程池参数时,默认:最大: 20, 路由最大并发连接数: 2

四、结论

1.不设置http线程池参数时,默认:最大: 20, 路由最大并发连接数: 2

  • 本实例最多可以同时建立20个TCP连接;
  • 每个路由最大并发连接数是2,路由并发限制是为了防止单个服务耗尽所有资源,比如A服务要调BCD服务,如果不设置路由并发数限制,B服务有可能会抢CD服务的连接数导致CD服务接口无法被调用;
    路由:由三要素构成:协议(http 或 https)+目标主机(ip或ym)+ 端口

举个例子,服务Z 要分别同时调用 服务ABCDEFGHIJK 的接口(同个字母视作同个路由),假设服务被调用接口都要调三次,如A1 A2 A3;B1 B2 B3
那么此时:A1 A2 B1 B2 …. J1 J2 都可成功被调用,
A3,B3 …J3,K1 K2 K3 则无法立即被调用,需要等待http线程池的资源。
A3,B3 …J3 不能立即被调的原因是:受路由最大并发连接数限制(超过2了);
K1 K2 K3 无法立即被调用的原因是:受最大连接数限制(超过20了)

五、其他问题

(一). RestTemplate 和 HttpClient的关系

1.RestTemplate (Spring Framework)

角色:高级HTTP客户端模板

功能:

  • 提供简洁的API(getForObject, postForEntity等)

  • 处理消息转换(JSON/XML/Protobuf序列化反序列化)

  • 异常转换(将HTTP错误转换为Spring异常)

  • 请求/响应拦截

2.HttpClient (Apache)

角色:底层HTTP协议实现

功能:

  • 实际的网络通信

  • 连接池管理

  • 超时控制

  • 重定向处理

  • SSL/TLS配置

3.实际调用流程:

// 1. RestTemplate 解析URL和参数
// 2. RestTemplate 选择合适的MessageConverter
// 3. RestTemplate 通过ClientHttpRequestFactory创建请求
// 4. HttpComponentsClientHttpRequestFactory 将请求转换为HttpClient格式
// 5. HttpClient 执行实际的HTTP请求
// 6. HttpClient 返回响应给HttpComponentsClientHttpRequestFactory
// 7. RestTemplate 使用MessageConverter解析响应
// 8. RestTemplate 返回最终结果

4.为什么需要这种分层设计

优点:

  • 解耦:应用层不依赖具体的HTTP实现

  • 可替换性:可以轻松切换底层HTTP客户端

  • 功能分离:RestTemplate处理业务逻辑,HttpClient处理网络通信

  • 易于测试:可以mock ClientHttpRequestFactory进行单元测试

5.多序列化模板

@Bean(“jsonHttpClient”)@Bean(“protobufHttpClient”) @Bean(“xmlHttpClient”)
配置多个序列化方式的RestTemplate Bean

@Configuration
public class MultiRestTemplateConfig {// JSON协议的HttpClient (5秒超时)@Bean("jsonHttpClient")public CloseableHttpClient jsonHttpClient() {RequestConfig config = RequestConfig.custom().setSocketTimeout(5000).setConnectTimeout(5000).setConnectionRequestTimeout(5000).build();return HttpClients.custom().setDefaultRequestConfig(config).build();}// Protobuf协议的HttpClient (10秒超时)@Bean("protobufHttpClient")public CloseableHttpClient protobufHttpClient() {RequestConfig config = RequestConfig.custom().setSocketTimeout(10000).setConnectTimeout(10000).setConnectionRequestTimeout(5000).build();return HttpClients.custom().setDefaultRequestConfig(config).build();}// XML协议的HttpClient (30秒超时)@Bean("xmlHttpClient")public CloseableHttpClient xmlHttpClient() {RequestConfig config = RequestConfig.custom().setSocketTimeout(30000).setConnectTimeout(30000).setConnectionRequestTimeout(5000).build();return HttpClients.custom().setDefaultRequestConfig(config).build();}// 对应的RestTemplate配置@Bean("jsonRestTemplate")public RestTemplate jsonRestTemplate(@Qualifier("jsonHttpClient") CloseableHttpClient httpClient) {ClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);RestTemplate restTemplate = new RestTemplate(factory);restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());return restTemplate;}@Bean("protobufRestTemplate")public RestTemplate protobufRestTemplate(@Qualifier("protobufHttpClient") CloseableHttpClient httpClient) {ClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);RestTemplate restTemplate = new RestTemplate(factory);// 假设有Protobuf消息转换器// restTemplate.getMessageConverters().add(new ProtobufHttpMessageConverter());return restTemplate;}@Bean("xmlRestTemplate")public RestTemplate xmlRestTemplate(@Qualifier("xmlHttpClient") CloseableHttpClient httpClient) {ClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);RestTemplate restTemplate = new RestTemplate(factory);restTemplate.getMessageConverters().add(new Jaxb2RootElementHttpMessageConverter());return restTemplate;}
}

使用时按bean名注入

    @Autowired@Qualifier("jsonRestTemplate")private RestTemplate jsonRestTemplate;@Autowired@Qualifier("protobufRestTemplate")private RestTemplate protobufRestTemplate;@Autowired@Qualifier("xmlRestTemplate")private RestTemplate xmlRestTemplate;

(二)线程池、http连接池、durid数据库连接池、Tomcat连接池,这些池的连接数有没有重叠的地方(AI回答)

是的,这些“池”的连接数存在重叠,但它们管理的资源对象和层级完全不同。 它们像一套俄罗斯套娃或者一个生产线上的不同工位,各自负责特定环节的资源复用,最终共同完成一个对外请求。

核心结论是:一个外部HTTP请求,可能会顺序地、一对一地使用到这些池中的每一个连接。因此,它们的连接数不是加算关系,而是串联关系。

下面我们来详细分解每一个“池”的作用和它们之间的关系。

1. 各“池”的作用与层级

我们可以将这些连接池从外到内划分为几个层级:

连接池类型管理的资源所属层级目的
Tomcat连接池Socket连接(通常是TCP)Web服务层复用处理HTTP请求的线程,避免频繁创建销毁线程的开销。
HTTP连接池HTTP连接应用客户端层复用到其他服务的HTTP连接(包括TCP连接和SSL上下文等),避免三次握手等开销。
数据库连接池(Druid等)数据库连接(如MySQL Connector)数据持久层复用到数据库的TCP连接,避免频繁建立数据库连接、认证的开销。
线程池工作者线程(Worker Thread)基础架构层复用线程资源,避免频繁创建和销毁线程的开销,提高系统性能。

2. 连接数的重叠场景(串联关系)

想象一个最常见的业务场景:你的Java Web应用(使用Tomcat)接收一个用户请求,这个请求需要调用另一个第三方API,然后查询自己的数据库返回结果。

一个用户请求的生命周期如下:

Tomcat线程池分配连接(线程):

用户发送HTTP请求到你的服务器。

Tomcat的Acceptor线程接收到这个Socket连接。

Tomcat的线程池(即你问题中的Tomcat连接池)分配一个空闲的工作者线程(Thread-1)来处理这个请求。

此时,占用1个Tomcat线程池的连接(即一个工作线程)。


HTTP连接池分配连接:

在Thread-1的执行过程中,代码需要调用一个外部服务的REST API。

你的应用使用HttpClient(配置了连接池)来发起调用。

HttpClient从它的HTTP连接池中取出一个空闲的、到目标服务的HTTP连接(这个连接背后是一个已经建立好的TCP Socket)。

通过这个连接发送HTTP请求并接收响应。

此时,占用1个HTTP连接池中的连接。


数据库连接池分配连接:

拿到外部API的响应后,Thread-1需要将结果保存到数据库。

你的应用(通过ORM框架如MyBatis)向Druid(或其他数据库连接池)请求一个数据库连接。

Druid从它的数据库连接池中分配一个空闲的、到MySQL数据库的连接(这背后也是一个TCP Socket)。

执行SQL语句,然后释放连接(归还给池)。

此时,占用1个数据库连接池中的连接。


请求完成:

Thread-1处理完毕,生成HTTP响应返回给用户。

Thread-1变为空闲状态,归还给Tomcat线程池。

HttpClient使用的HTTP连接也归还给其连接池。

由此可见,在处理这单个用户请求的某个瞬间,它可能同时占用了:

1个 Tomcat线程池中的线程

1个 HTTP客户端连接池中的连接

1个 数据库连接池中的连接

它们是一种串联的、一对一的依赖关系,而不是重叠的竞争关系。

3. 配置建议(如何设置连接数)

正因为这种串联关系,在配置这些池的大小时,必须考虑它们之间的依赖和系统的瓶颈所在。

Tomcat最大线程数 (maxThreads):这决定了你的应用同时能处理多少个用户请求。这是整个系统的入口并发度。

HTTP客户端最大连接数:这决定了你的应用同时能向外发起多少个HTTP调用。这个数通常应该小于或等于Tomcat的maxThreads。如果Tomcat有200个线程,但HTTP连接池只有20个连接,那么当有50个请求都需要调用外部API时,最多只有20个能同时进行,剩下的30个请求会被阻塞,等待HTTP连接空闲。

数据库连接池最大连接数 (maxActive):这决定了你的应用同时能打开多少个数据库连接。这个数通常应该小于数据库服务器本身允许的最大连接数,并且也要参考Tomcat的线程数。如果数据库连接池设置过大,可能会导致数据库压力过高;如果设置过小,会导致Tomcat线程大量阻塞在等待数据库连接上。

4.总结

这些池协同工作,而不是相互竞争。它们的连接数在处理单个请求的路径上是串联且叠加的,共同决定了系统的整体并发能力和资源利用率。正确配置它们的关键在于理解整个请求链路的瓶颈所在。

http://www.xdnf.cn/news/1486243.html

相关文章:

  • axios的两种异步方式对比
  • K8S-Pod(下)
  • 笔记本、平板如何成为电脑拓展屏?向日葵16成为副屏功能一键实现
  • python---静态方法和类方法
  • Python学习——安装配置python环境+入门
  • Onecode 可视化动作揭秘系列二:组件类型个性化配置技术协议
  • 嵌入式解谜日志之数据结构—基本概念
  • 插入排序与希尔排序
  • Python3使用Flask开发Web项目新手入门开发文档
  • JavaEE 进阶第三期:开启前端入门之旅(三)
  • 数据结构——排序
  • 内网后渗透攻击--linux系统(权限维持)
  • MySQL 8.0+ 内核剖析:架构、事务与数据管理
  • thinkphp和vue基于Workerman搭建Websocket服务实现用户实时聊天,完整前后端源码demo及数据表sql
  • 20250907_梳理异地备份每日自动巡检py脚本逻辑流程+安装Python+PyCharm+配置自动运行
  • 什么叫进件?在第三方支付行业里,这是一个非常专业的词汇。
  • Linux驱动开发(1)环境与代码框架
  • Linux —— 虚拟进程地址空间
  • Java-Spring入门指南(三)深入剖析IoC容器与Bean核心机制
  • 基于 Django+Vue3 的 AI 海报生成平台开发博客(海报模块专项)
  • HTTPS协议——对于HTTP的协议的加密
  • 架构进阶——解读121页IT规划咨询项目规划报告【附全文阅读】
  • HarmonyOS 应用开发深度解析:掌握 ArkTS 声明式 UI 与现代化状态管理
  • 大数据(非结构化数据,Spark,MongoDB)
  • 《沈南鹏传 - 做最擅长的事》(下篇)读书笔记
  • Gitlab 配置自定义 clone 地址
  • 【面试向】边缘计算基础介绍
  • Java全栈开发面试实录:从基础到高阶技术深度解析
  • Oracle到金仓数据库信创改造迁移实施规划方案(下篇)
  • 【mysql】SQL自连接:什么时候需要,什么时候不需要?