RestTemplate + ConnectionPoolTimeoutException: 从池中等待连接超时

4

我突然在生产环境中遇到了这个错误,而应用程序并未承受任何负载。

问题发生在我的代码尝试使用Spring RestTemplate发送PUT消息时。

这是我初始化RestTemplate的代码:

private static final RestTemplate restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
{

    List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
    Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
    marshaller.setClassesToBeBound(PaymentSession.class);
    MarshallingHttpMessageConverter marshallingHttpMessageConverter = new MarshallingHttpMessageConverter(marshaller, marshaller);
    marshallingHttpMessageConverter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_XML, MediaType.TEXT_HTML));
    messageConverters.add(marshallingHttpMessageConverter);
    restTemplate.setMessageConverters(messageConverters);
}

PUT调用

try {
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_XML);
    HttpEntity<PaymentSession> httpEntity = new HttpEntity<PaymentSession>(session, headers);

    restTemplate.exchange(baseUrl+"/v1/psps", HttpMethod.PUT, httpEntity, PaymentSession.class);

}catch(HttpClientErrorException e){
        logger.error("Exception..!!",e)
}

异常堆栈跟踪

Caused by: org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool
at org.apache.http.impl.conn.PoolingClientConnectionManager.leaseConnection(PoolingClientConnectionManager.java:232)
at org.apache.http.impl.conn.PoolingClientConnectionManager$1.getConnection(PoolingClientConnectionManager.java:199)
at org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:456)
at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:906)
at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:805)
at org.springframework.http.client.HttpComponentsClientHttpRequest.executeInternal(HttpComponentsClientHttpRequest.java:88)
at org.springframework.http.client.AbstractBufferingClientHttpRequest.executeInternal(AbstractBufferingClientHttpRequest.java:46)
at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:49)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:509)
3个回答

12

我建议配置HttpComponentsClientHttpRequestFactory实例,在RestTemplate的构造函数中传递,并增加defaultMaxPerRoute或特定HTTP路由的maxPerRoute,以解决请求超时的问题。增加池大小是不够的,就像我在评论中提到的那样,即使将PoolingHttpClientConnectionManager.setMaxTotal()设置为200,HttpComponentsClientHttpRequestFactory仍然使用defaultMaxPerRoute为4。我猜想这是为了避免主机路由(协议、主机、端口)占用连接池。

...
public PoolingHttpClientConnectionManager poolingHttpClientConnectionManager() {
        PoolingHttpClientConnectionManager result = new PoolingHttpClientConnectionManager();
        result.setMaxTotal(this.httpHostConfiguration.getMaxTotal());
        // Default max per route is used in case it's not set for a specific route
        result.setDefaultMaxPerRoute(this.httpHostConfiguration.getDefaultMaxPerRoute());
        // and / or
        if (CollectionUtils.isNotEmpty(this.httpHostConfiguration.getMaxPerRoutes())) {
            for (HttpHostConfiguration httpHostConfig : this.httpHostConfiguration.getMaxPerRoutes()) {
                HttpHost host = new HttpHost(httpHostConfig.getHost(), httpHostConfig.getPort(), httpHostConfig.getScheme());
                // Max per route for a specific host route
                result.setMaxPerRoute(new HttpRoute(host), httpHostConfig.getMaxPerRoute());
            }
        }
        return result;
    }
  ...


@Configuration
@ConfigurationProperties(prefix = "httpConnPool")
public class HttpHostsConfiguration {

  private Integer maxTotal;
  private Integer defaultMaxPerRoute;
  private List<HttpHostConfiguration> maxPerRoutes;

  // Getters, Setters
...

application.yml

httpConnPool:
  maxTotal: 20
  defaultMaxPerRoute: 20
  maxPerRoutes:
    -
      scheme: http
      host: localhost
      port: 8800
      maxPerRoute: 20

最近我写了一篇关于 解决Spring RestTemplate请求超时问题 的博客文章,其中使用了 JMeter 和 shell 命令来排查超时的请求,并通过配置设置进行修复。


10

由于:org.apache.http.conn.ConnectionPoolTimeoutException:从池中等待连接超时

这个错误是自我描述的。您需要在生产环境中增加连接池-当前实现 HttpComponentsClientHttpRequestFactory 默认构造函数使用 HttpClientBuilder .useSystemProperties()

我相信默认情况下将会有5个连接。客户端可以工作,但不太适合服务器环境。您需要使用类似以下内容:

new RestTemplate(new HttpComponentsClientHttpRequestFactory(HttpClientBuilder.create()
                    .setMaxConnTotal(200)
                    .setMaxConnPerRoute(50)
                    .build()));

我怀疑连接池大小是问题所在,因为我提到了在非常小的负载下出现了问题,并且当我们进行50个并发用户的负载测试时没有遇到这个问题。但是,我们将把连接池大小从5增加到25(即4个服务器,共100个连接)。 - NullPointerException
@NullPointerException请设置超时值-如果连接失败,则可能需要几秒钟,因此它会从池中退出。 - Boris Treukhov
@NullPointerException 30秒太长了——我能想象的是2-3秒——如果服务器在地球的不同部分。 - Boris Treukhov
6
需要注意的是,如果您不设置.setMaxConnPerRoute()属性,无论连接池有多大都没用。默认情况下,这个值为4,即使您有196个空闲连接,它也只会针对同一主机使用4个连接,我猜测这是为了防止主机劫持连接池。请注意,本文不包含解释。 - ootero
1
@BorisTreukhov 不,这并不太高。有ConnectTimeout(我需要多少时间来建立连接)和ReadTimeout(我应该等待请求完成多长时间)。如果我在拨号链接上读取10MB(只是举个例子),那么30秒的ReadTimeout可能太低了。 - Mejmo
@Mejmo 今天是2017年,我们的任务是提供良好的服务。任何服务响应时间超过1秒的前端服务都是不满意的,后端服务的要求更加严格(有时候甚至精确到纳秒级别,而不是毫秒级别)。如果您期望有长时间的结果,则您的服务应该是异步的(在这种情况下,您应该考虑使用反应式编程)。此外,本问题涉及后端处理,对于最后一英里的GPRS连接可能是可以接受的,但您的服务器不太可能使用GPRS连接到另一个服务器。 - Boris Treukhov

1
问题在于HTTP客户端连接没有被关闭。我曾经遇到过一个服务的同样问题,它每秒只有大约1个请求。
"exception":"org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool"

你需要添加一个 finally 块并关闭连接。


6
如果出现Apache客户端异常,而我们正在使用RestTemplate包装器,该怎么做? - Антон Ткаченко

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接