RestTemplate 在慢请求场景下的优化

发布于:4/26/2023, 10:36:00 AM @孙博
技术分享 | Java,RestTemplate,高并发,ChatGPT
许可协议:署名-非商业性使用(by-nc)

众所周知,ChatGPT 的能力虽然很强,但是响应速度并没有那么快。我们在产品中接入它的接口时,甚至会存在等待时间超过30秒的情况。

为了防止因为它的接口响应慢而拖垮系统,我们也向它咨询了如何解决这个问题的方案。

最终的核心要素就是解决 RestTemplate 资源占用的问题,以下为对话实录:


来自你的消息:

RestTemplate 在大并发情况下可能会导致阻塞么

来自 ChatGPT 的消息:

在大并发情况下,RestTemplate可能会导致阻塞。造成这种情况的原因可能是:

  1. 连接池耗尽:如果在短时间内发送大量请求,可能会使连接池中的连接全部用尽,导致后续请求阻塞等待连接。

  2. 等待响应结果:RestTemplate是同步调用方式,发送请求后需要等待服务器响应,如果响应时间过长,会导致后续请求阻塞。

  3. 代码实现问题:如果在使用RestTemplate时代码实现有问题,也可能会导致阻塞。例如,在请求响应结果时没有使用超时机制,或者在请求失败时没有进行重试。

针对这些问题,可以采取以下措施来避免RestTemplate的阻塞问题:

  1. 使用连接池:在RestTemplate中可以设置连接池,避免每次请求都新建连接。这样可以避免连接池用尽的问题。

  2. 使用异步调用:RestTemplate也支持异步调用方式,通过使用回调函数等方式,可以避免大量请求阻塞。

  3. 设置超时时间和重试:在使用RestTemplate发送请求时,可以设置超时时间和重试次数,避免长时间等待响应和请求失败不打算重新请求的问题。

综上所述,在大并发情况下,RestTemplate可能会导致阻塞,但可以通过上述措施来避免这种情况的发生。

来自你的消息:

假如我所请求的接口平均耗时要长达30秒,如果使用RestTemplate的话,我最大可以扛多少并发

来自 ChatGPT 的消息:

需要注意的是,并发能力不只是取决于接口平均耗时,还需要考虑服务器的硬件配置、网络带宽等因素。

另外,RestTemplate的并发承载能力还取决于系统内存、系统CPU等因素。一般情况下,RestTemplate的并发本身并没有特别明确的上限,而是由系统资源和配置的限制。

如果考虑接口平均耗时30秒,建议根据系统的资源配置和实际情况进行测试,调整并发量,观察相应的响应时间、吞吐量等指标,并适时进行优化。通常情况下,建议保持并发量在可控范围内,避免对系统造成过大的压力。

来自你的消息:

RestTemplate 的连接池如何使用

来自 ChatGPT 的消息:

RestTemplate的连接池使用需要依赖HttpClient,并且需要添加相应的依赖包。

  1. 添加依赖

在Maven项目的pom.xml文件中添加以下依赖:

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.6</version>
</dependency>
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpasyncclient</artifactId>
    <version>4.1.4</version>
</dependency>
  1. 配置连接池

在配置RestTemplate时,需要配置HttpClientConnectionManager,具体可以通过以下代码进行配置:

ConnectionKeepAliveStrategy keepAliveStrategy = new ConnectionKeepAliveStrategy() {
    @Override
    public long getKeepAliveDuration(HttpResponse httpResponse, HttpContext httpContext) {
        return 30 * 1000;
    }
};

PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(200);//连接池最大连接数
connectionManager.setDefaultMaxPerRoute(50);//每个主机的并发最多数

HttpClient httpClient = HttpClientBuilder.create()
        .setConnectionManager(connectionManager)
        .setKeepAliveStrategy(keepAliveStrategy)
        .build();

HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);

RestTemplate restTemplate = new RestTemplate(factory);

在上述代码中,PoolingHttpClientConnectionManager是连接池的核心组件,setMaxTotal方法可以设置连接池的最大连接数,setDefaultMaxPerRoute方法可以设置每个主机的并发最多数。

建议通过连接池的方式避免RestTemplate每次请求都新建连接,提高服务性能和吞吐量。

来自你的消息:

是不是每次请求从开始到获得响应的时间段内,都会占用连接池的一个连接?

来自 ChatGPT 的消息:

是的,对于使用连接池的RestTemplate,在请求从开始到获得响应的时间段内都会占用连接池的一条连接。

当RestTemplate发起请求时,首先从连接池中获取可用的连接。连接池的连接数量是有限的,如果连接池中的所有连接已经被占用,则后续请求需要等待已经被占用的连接释放,才能继续使用连接池中的连接发送请求。

因此,在高并发场景下,为了避免连接池的连接被占满导致后续请求阻塞等问题,需要配置连接池的大小,适当调整连接池中可用连接的数量。同时,在每次请求结束后,需要显式地释放连接,将连接还回连接池中。这样可以保证连接池中的连接一直处于可用状态,并且能够更好地支持高并发场景。

来自你的消息:

连接池可以设置的很大吗?它会占用很多系统资源吗?

来自 ChatGPT 的消息:

连接池大小的设置需要根据具体的场景和硬件资源情况作出权衡。设置连接池过大可能会占用较多的系统资源,从而导致系统性能和资源利用率下降,同时吞吐量也不一定能够得到明显提升。

一般来说,建议依据硬件资源情况逐步扩大连接池的大小,逐步测试并观察响应时间和系统负载等指标变化,找到一个最合适的连接池大小。

另外,在设置连接池大小时,还需要结合实际业务情况和系统负载等指标。如果在业务高峰期连接数需要达到峰值,需要适当增大连接池的大小;如果系统负载高并且连接数不够,需要适当扩大连接池的大小,以满足业务需求。最好的做法是结合实际情况进行测试和调整。

总之,连接池的大小需要根据具体情况进行权衡和调整,不能盲目设置过大或过小。

来自你的消息:

请给我一个设置连接池的示例

来自 ChatGPT 的消息:

以下是一个简单的设置连接池大小的示例:

PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(200);//设置连接池大小
connectionManager.setDefaultMaxPerRoute(50);//设置每个路由最大连接数

在上述代码中,连接池最大连接数被设置为200,每个路由(即每个主机)的最大连接数被设置为50。这种设置方式适合于单台服务器攻击并发比较小的场景。

如果需要将连接池大小进行动态调整或者根据业务场景的变化动态调整,则需要结合线程池等技术进行动态调整。例如,可以监控当前的连接数和系统负载等指标,以决定是否需要动态调整连接池大小,从而提高系统性能和吞吐量。

来自你的消息:

我应该怎么使用

来自 ChatGPT 的消息:

你可以按照以下步骤使用RestTemplate进行请求:

  1. 创建RestTemplate实例

在代码中创建RestTemplate实例,并设置对应的配置信息。例如,可以为RestTemplate设置连接池、超时时间、拦截器等信息:

RestTemplate restTemplate = new RestTemplate();
  1. 发送请求

使用RestTemplate发送请求,可以通过getForObject、postForObject等方法发送常见的HTTP请求。例如,发送GET请求并接收响应:

String result = restTemplate.getForObject("http://example.com/resource", String.class);
  1. 处理响应

RestTemplate发送请求后,返回的是响应结果。根据实际情况可以对这些结果进行处理。例如,可以将响应结果转换成Java对象。

ResponseEntity<User> response = restTemplate.getForEntity("http://example.com/user/{id}", User.class, "42");
User user = response.getBody();

在上述代码中,restTemplate.getForEntity方法返回的结果是ResponseEntity对象,可以通过getBody方法获取转换后的Java对象。

  1. 销毁RestTemplate

当RestTemplate不再需要使用时,建议调用destroy方法将其销毁,释放资源。

restTemplate.destroy();

总之,使用RestTemplate可以方便地发送HTTP请求,同时也可以快速处理HTTP响应。在使用RestTemplate时,需要注意设置合适的配置,避免出现性能问题。

来自你的消息:

RestTemplate 如何使用连接池

来自 ChatGPT 的消息:

RestTemplate使用连接池需要先配置HttpClient连接池。具体步骤如下:

  1. 在pom.xml中加入以下依赖:
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.12</version>
</dependency>
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpasyncclient</artifactId>
    <version>4.1.4</version>
</dependency>
  1. 配置HttpClient连接池

创建一个PoolingHttpClientConnectionManager,并设置连接池最大连接数、每个路由最大连接数。例如,可以设置连接池大小为100,每个路由的最大连接数为50:

PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(100);
connectionManager.setDefaultMaxPerRoute(50);
  1. 将HttpClient设置到RestTemplate中

使用HttpComponentsClientHttpRequestFactory将HttpClient设置到RestTemplate中,以便RestTemplate使用HttpClient连接池发送请求。例如:

HttpClient httpClient = HttpClientBuilder.create()
        .setConnectionManager(connectionManager)
        .build();
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
RestTemplate restTemplate = new RestTemplate(requestFactory);

使用上述代码配置RestTemplate,可以使其使用Apache HttpClient连接池发送请求,并且在高并发场景下能够更好地发挥性能。同时,连接池大小的设置需要根据实际情况进行权衡和调整,避免过大或者过小导致性能问题。

来自你的消息:

对同一个地址的请求会占用一个新的连接?还是会对现有连接做复用?

来自 ChatGPT 的消息:

使用连接池的RestTemplate会对现有连接做复用,以提高连接的利用率,避免大量的连接浪费,并避免因为连接不足导致请求阻塞。

连接池维护一组可被复用的连接,RestTemplate每次请求时,会从连接池中获取可用的连接,从而避免多次建立TCP连接的开销,并能够优化TCP连接的利用率。如果连接池中没有可用连接,则会等待其它连接释放后重用。

也就是说,对于同一个地址的请求,RestTemplate会尽量复用之前建立的TCP连接,而不是每次新建连接,从而避免了每次建立连接所需的握手和断开连接的耗时开销,提高了请求的响应速度和系统的吞吐量。

当然,连接池需要根据实际情况进行设置,避免连接被过度占用而导致连接池的连接不足,从而影响请求的响应速度。建议根据具体场景进行优化和调整,以确保系统能够更好的发挥性能。

来自你的消息:

你提到复用连接,为了复用连接我还需要销毁 RestTemplate 的实例吗?

来自 ChatGPT 的消息:

没有必要销毁RestTemplate实例来实现连接复用。

RestTemplate使用的连接池是基于Apache HttpClient实现的,它会在创建客户端实例时创建池子,并管理内部连接和回收。RestTemplate和HttpClient实例是可以被重复使用的,因此无需在每次请求后关闭RestTemplate实例。

如果频繁地创建和释放RestTemplate、HttpClient等客户端实例,反而会增加资源使用和销毁的开销,减慢请求的响应时间。

因此,为了实现连接复用,只需要在程序启动时创建一个RestTemplate实例,并在请求结束后将整个应用程序中的RestTemplate实例作为单例重复使用,就可以实现连接池复用。而不需要频繁创建和消耗实例。

来自你的消息:

默认情况下 RestTemplate 实例会自带连接池吗?这个连接池会有多少个可用连接?

来自 ChatGPT 的消息:

默认情况下,RestTemplate实例会自动创建一个SimpleClientHttpRequestFactory,它不带连接池,每次请求都会新建一个连接。

如果要使用连接池,需要手动配置HttpClient连接池,并将HttpClient连接池设置到RestTemplate中。

如果不手动配置连接池,则每个请求都会创建一个新连接。这种方式适合连接数很小的场景,但是对于大并发的场景,建议使用连接池来重用连接。

在使用连接池时,连接池的大小可以根据实际情况进行调整。连接池的连接数越多,可以同时处理的并发连接就越多,但是连接数过多可能会占用过多的系统资源。建议根据实际情况评估和设置连接池参数。

在使用Apache HttpClient连接池时,如果没有手动设置连接池大小,则默认情况下连接池的大小为2个连接。可以通过设置MaxConnTotalMaxConnPerRoute来调整连接池大小。例如:

PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(200);
connectionManager.setDefaultMaxPerRoute(50);

在上述代码中,setMaxTotal设置了连接池的最大连接数为200,setDefaultMaxPerRoute设置了每个路由(也就是每个主机)的最大连接数为50。这意味着,如果有10个主机,那么每个主机最多会有50个连接,最多会有200个连接可供使用。

来自你的消息:

PoolingHttpClientConnectionManager 对象的 MaxTotal 和 DefaultMaxPerRoute 支持运行时动态更改吗?当增加或减少时会发生什么?

来自 ChatGPT 的消息:

PoolingHttpClientConnectionManagerMaxTotalDefaultMaxPerRoute可以在运行时动态修改,并且对连接池中连接的数量也会相应地进行调整。连接池会根据新的参数重新计算相关连接数量并对连接池进行动态调整,以符合新的配置值。

当修改MaxTotal时,如果新的值比原来的值大,则连接池会创建更多的连接以满足新的配置。如果新的值比原来的值小,则连接池会关闭多余的连接并调整连接池大小。

当修改DefaultMaxPerRoute时,连接池会重新计算每个主机能够持有连接的最大数量并对连接池进行动态调整。

在修改连接池的参数时,需要特别注意已有的连接以及系统资源的占用情况,避免改变连接池参数导致过多的资源占用,进而影响系统稳定性和性能。

来自你的消息:

如果我减小 MaxTotal 时,当前所有的连接都在被使用中,那么系统是会强制关闭并立即释放一部分连接?还是会等待当前请求处理完毕再进行回收?

来自 ChatGPT 的消息:

当减小MaxTotal时,如果当前所有连接都正在使用中,连接池会等待所有连接都处理结束后再进行回收以便修改连接池大小。

连接池中的每个连接都有一个超时设置,如果连接在超时时间内未被使用,连接池会将该连接加入到空闲连接列表中以便被复用或关闭。因此,当减小MaxTotal时,连接池会等待超时连接可以被关闭或已使用连接可以被释放时再进行回收,连接池大小就会相应地减小。

在实际开发中,减小连接池大小需要结合实际应用场景进行评估和调整,避免在并发高的情况下,尤其是在长时间的高并发场景下可能导致请求阻塞、超时等问题。在调整连接池大小时,应该结合系统的实际情况,根据不同的业务负载特点进行优化和调整。

来自你的消息:

如果连接池中所有的连接都在使用中,这时又发起一次请求,这次请求是会立即失败,还是会等待一段时间?

来自 ChatGPT 的消息:

当连接池中的所有连接都在使用中时,新的请求会进入等待队列,等待使用中的连接被释放后再被处理。等待队列是一个有界队列,队列大小可以通过org.apache.http.impl.pool.AbstractConnPool(连接池抽象类)中的maxPendingAcquires参数进行配置。如果等待队列已满,则新的请求会失败并抛出连接池超时异常org.apache.http.pool.ConnPoolTimeoutException

当等待队列中有空余空间时,新的请求会尽快地去连接池中申请连接,如果连接池中没有连接可用,则阻塞等待,直到连接池中有空闲连接时才返回。

在这种情况下,如果连接池中的连接处于高并发的状态,那么等待队列中注定会有大量请求排队等待获取连接,如果等待队列中排队请求数过多,则会导致请求超时、响应延迟等问题,因此在使用连接池时需要合理控制连接池大小和等待队列大小,并根据实际业务场景来调整连接池参数,在高并发环境下保证请求的可靠性和稳定性。