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