高并发场景下的httpClient优化使用

本文详细介绍了如何优化基于HTTP的服务调用,通过减少httpclient创建开销、使用连接缓存及改进返回结果处理,将平均执行时间从250ms优化至80ms。包括单例client、连接缓存、更好的处理返回结果等关键步骤,以及配置超时和重试策略,确保系统的高效稳定运行。

1.背景

我们有个业务,会调用其他部门提供的一个基于http的服务,日调用量在千万级别。使用了httpclient来完成业务。之前因为qps上不去,就看了一下业务代码,并做了一些优化,记录在这里。

先对比前后:优化之前,平均执行时间是250ms;优化之后,平均执行时间是80ms,降低了三分之二的消耗,容器不再动不动就报警线程耗尽了,清爽~

2.分析

项目的原实现比较粗略,就是每次请求时初始化一个httpclient,生成一个httpPost对象,执行,然后从返回结果取出entity,保存成一个字符串,最后显式关闭response和client。我们一点点分析和优化:

2.1 httpclient反复创建开销

httpclient是一个线程安全的类,没有必要由每个线程在每次使用时创建,全局保留一个即可。

2.2 反复创建tcp连接的开销

tcp的三次握手与四次挥手两大裹脚布过程,对于高频次的请求来说,消耗实在太大。试想如果每次请求我们需要花费5ms用于协商过程,那么对于qps为100的单系统,1秒钟我们就要花500ms用于握手和挥手。又不是高级领导,我们程序员就不要搞这么大做派了,改成keep alive方式以实现连接复用!

2.3 重复缓存entity的开销

原本的逻辑里,使用了如下代码:


 
  1. HttpEntity entity = httpResponse.getEntity();
  2. String response = EntityUtils.toString(entity);

这里我们相当于额外复制了一份content到一个字符串里,而原本的httpResponse仍然保留了一份content,需要被consume掉,在高并发且content非常大的情况下,会消耗大量内存。并且,我们需要显式的关闭连接,ugly。

3.实现

按上面的分析,我们主要要做三件事:一是单例的client,二是缓存的保活连接,三是更好的处理返回结果。一就不说了,来说说二。

提到连接缓存,很容易联想到数据库连接池。httpclient4提供了一个PoolingHttpClientConnectionManager 作为连接池。接下来我们通过以下步骤来优化:

3.1 定义一个keep alive strategy

关于keep-alive,本文不展开说明,只提一点,是否使用keep-alive要根据业务情况来定,它并不是灵丹妙药。还有一点,keep-alive和time_wait/close_wait之间也有不少故事。

在本业务场景里,我们相当于有少数固定客户端,长时间极高频次的访问服务器,启用keep-alive非常合适

再多提一嘴,http的keep-alive 和tcp的KEEPALIVE不是一个东西。回到正文,定义一个strategy如下:


 
  1. ConnectionKeepAliveStrategy myStrategy = new ConnectionKeepAliveStrategy() {
  2. @Override
  3. public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
  4. HeaderElementIterator it = new BasicHeaderElementIterator
  5. (response.headerIterator(HTTP.CONN_KEEP_ALIVE));
  6. while (it.hasNext()) {
  7. HeaderElement he = it.nextElement();
  8. String param = he.getName();
  9. String value = he.getValue();
  10. if (value != null && param.equalsIgnoreCase
  11. ("timeout")) {
  12. return Long.parseLong(value) * 1000;
  13. }
  14. }
  15. return 60 * 1000;//如果没有约定,则默认定义时长为60s
  16. }
  17. };

3.2 配置一个PoolingHttpClientConnectionManager


 
  1. PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
  2. connectionManager.setMaxTotal(500);
  3. connectionManager.setDefaultMaxPerRoute(50);//例如默认每路由最高50并发,具体依据业务来定

也可以针对每个路由设置并发数。

3.3 生成httpclient


 
  1. httpClient = HttpClients.custom()
  2. .setConnectionManager(connectionManager)
  3. .setKeepAliveStrategy(kaStrategy)
  4. .setDefaultRequestConfig(RequestConfig.custom().setStaleConnectionCheckEnabled(true).build())
  5. .build();

 注意:使用setStaleConnectionCheckEnabled方法来逐出已被关闭的链接不被推荐。更好的方式是手动启用一个线程,定时运行closeExpiredConnections 和closeIdleConnections方法,如下所示。


 
  1. public static class IdleConnectionMonitorThread extends Thread {
  2.  
  3. private final HttpClientConnectionManager connMgr;
  4. private volatile boolean shutdown;
  5.  
  6. public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) {
  7. super();
  8. this.connMgr = connMgr;
  9. }
  10.  
  11. @Override
  12. public void run() {
  13. try {
  14. while (!shutdown) {
  15. synchronized (this) {
  16. wait(5000);
  17. // Close expired connections
  18. connMgr.closeExpiredConnections();
  19. // Optionally, close connections
  20. // that have been idle longer than 30 sec
  21. connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
  22. }
  23. }
  24. } catch (InterruptedException ex) {
  25. // terminate
  26. }
  27. }
  28.  
  29. public void shutdown() {
  30. shutdown = true;
  31. synchronized (this) {
  32. notifyAll();
  33. }
  34. }
  35.  
  36. }

3.4 使用httpclient执行method时降低开销

这里要注意的是,不要关闭connection。

一种可行的获取内容的方式类似于,把entity里的东西复制一份:


 
  1. res = EntityUtils.toString(response.getEntity(),"UTF-8");
  2. EntityUtils.consume(response1.getEntity());

 

 但是,更推荐的方式是定义一个ResponseHandler,方便你我他,不再自己catch异常和关闭流。在此我们可以看一下相关的源码:


 
  1. public <T> T execute(final HttpHost target, final HttpRequest request,
  2. final ResponseHandler<? extends T> responseHandler, final HttpContext context)
  3. throws IOException, ClientProtocolException {
  4. Args.notNull(responseHandler, "Response handler");
  5.  
  6. final HttpResponse response = execute(target, request, context);
  7.  
  8. final T result;
  9. try {
  10. result = responseHandler.handleResponse(response);
  11. } catch (final Exception t) {
  12. final HttpEntity entity = response.getEntity();
  13. try {
  14. EntityUtils.consume(entity);
  15. } catch (final Exception t2) {
  16. // Log this exception. The original exception is more
  17. // important and will be thrown to the caller.
  18. this.log.warn("Error consuming content after an exception.", t2);
  19. }
  20. if (t instanceof RuntimeException) {
  21. throw (RuntimeException) t;
  22. }
  23. if (t instanceof IOException) {
  24. throw (IOException) t;
  25. }
  26. throw new UndeclaredThrowableException(t);
  27. }
  28.  
  29. // Handling the response was successful. Ensure that the content has
  30. // been fully consumed.
  31. final HttpEntity entity = response.getEntity();
  32. EntityUtils.consume(entity);//看这里看这里
  33. return result;
  34. }

 可以看到,如果我们使用resultHandler执行execute方法,会最终自动调用consume方法,而这个consume方法如下所示:


 
  1. public static void consume(final HttpEntity entity) throws IOException {
  2. if (entity == null) {
  3. return;
  4. }
  5. if (entity.isStreaming()) {
  6. final InputStream instream = entity.getContent();
  7. if (instream != null) {
  8. instream.close();
  9. }
  10. }
  11. }

可以看到最终它关闭了输入流。

4.其他

通过以上步骤,基本就完成了一个支持高并发的httpclient的写法,下面是一些额外的配置和提醒:

4.1 httpclient的一些超时配置

CONNECTION_TIMEOUT是连接超时时间,SO_TIMEOUT是socket超时时间,这两者是不同的。连接超时时间是发起请求前的等待时间;socket超时时间是等待数据的超时时间。


 
  1. HttpParams params = new BasicHttpParams();
  2. //设置连接超时时间
  3. Integer CONNECTION_TIMEOUT = 2 * 1000; //设置请求超时2秒钟 根据业务调整
  4. Integer SO_TIMEOUT = 2 * 1000; //设置等待数据超时时间2秒钟 根据业务调整
  5.  
  6. //定义了当从ClientConnectionManager中检索ManagedClientConnection实例时使用的毫秒级的超时时间
  7. //这个参数期望得到一个java.lang.Long类型的值。如果这个参数没有被设置,默认等于CONNECTION_TIMEOUT,因此一定要设置。
  8. Long CONN_MANAGER_TIMEOUT = 500L; //在httpclient4.2.3中我记得它被改成了一个对象导致直接用long会报错,后来又改回来了
  9.  
  10. params.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, CONNECTION_TIMEOUT);
  11. params.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, SO_TIMEOUT);
  12. params.setLongParameter(ClientPNames.CONN_MANAGER_TIMEOUT, CONN_MANAGER_TIMEOUT);
  13. //在提交请求之前 测试连接是否可用
  14. params.setBooleanParameter(CoreConnectionPNames.STALE_CONNECTION_CHECK, true);
  15.  
  16. //另外设置http client的重试次数,默认是3次;当前是禁用掉(如果项目量不到,这个默认即可)
  17. httpClient.setHttpRequestRetryHandler(new DefaultHttpRequestRetryHandler(0, false));

4.2 如果配置了nginx的话,nginx也要设置面向两端的keep-alive

现在的业务里,没有nginx的情况反而比较稀少。nginx默认和client端打开长连接而和server端使用短链接。注意client端的keepalive_timeout和keepalive_requests参数,以及upstream端的keepalive参数设置,这三个参数的意义在此也不再赘述。

以上就是我的全部设置。通过这些设置,成功地将原本每次请求250ms的耗时降低到了80左右,效果显著。

<think>我们正在比较RestTemplate和HttpClient高并发场景下的性能和异常处理能力。首先,我们需要明确两者是什么: - **RestTemplate**:Spring框架提供的用于同步HTTP客户端请求的模板类,简化了与HTTP服务的交互。它支持RESTful风格,并处理HTTP请求和响应的序列化与反序列化。 - **HttpClient**:通常指Apache HttpClient,是一个功能强大的HTTP客户端库,提供了更底层的控制和更丰富的功能,如连接池管理、重试机制等。 ### 1. 性能对比(高并发场景) 在高并发场景下,性能主要受以下因素影响: - **连接管理**:连接池的配置和使用可以显著减少连接建立和关闭的开销。 - **线程模型**:如何管理线程和并发请求。 - **I/O模型**:阻塞式I/O(BIO)还是非阻塞I/O(NIO)。 #### RestTemplate性能特点: - 默认使用`SimpleClientHttpRequestFactory`,基于JDK的`HttpURLConnection`(阻塞式I/O)。 - 可以通过配置使用`HttpComponentsClientHttpRequestFactory`(基于Apache HttpClient)来提升性能。 - 使用连接池:通过配置Apache HttpClient的连接池,可以复用连接,减少TCP三次握手开销。 - 在高并发下,如果使用默认的`HttpURLConnection`,性能较差,因为每个请求都可能创建新连接(除非启用keep-alive)。但使用Apache HttpClient作为底层时,性能可以大幅提升。 #### HttpClient性能特点: - Apache HttpClient提供了更高级的连接管理,包括连接池、最大连接数、每个路由的最大连接数等配置。 - 支持连接存活时间(keep-alive)策略,减少连接重建开销。 - 支持异步请求(需要配合异步HTTP客户端,如`HttpAsyncClient`),但通常我们讨论的是同步的`HttpClient`。 - 在高并发场景下,通过合理的连接池配置,可以高效地复用连接,从而提升性能。 #### 性能对比结论: - 当RestTemplate使用Apache HttpClient作为底层实现时,两者的性能接近,因为底层都是Apache HttpClient。 - 如果RestTemplate使用默认的JDK实现,则Apache HttpClient高并发场景下性能更好。 - 因此,**在高并发场景下,推荐使用RestTemplate配置Apache HttpClient作为底层**,这样既可以利用Spring的便利性,又能获得高性能。 ### 2. 异常处理对比 #### RestTemplate异常处理: - RestTemplate将HTTP错误(如4xx、5xx)封装成`HttpClientErrorException`和`HttpServerErrorException`,我们可以捕获这些异常并进行处理。 - 可以通过`ResponseErrorHandler`接口自定义错误处理逻辑。 #### HttpClient异常处理: - Apache HttpClient在执行请求后,会根据响应状态码抛出相应的异常(如`HttpResponseException`)。 - 需要手动检查状态码,或者通过自定义`HttpResponseInterceptor`来处理异常。 #### 异常处理对比结论: - RestTemplate提供了更简洁的异常处理机制,将HTTP错误转化为异常,方便捕获和处理。 - HttpClient需要更底层的处理,但提供了更灵活的控制。 ### 3. 高并发下的配置建议 #### RestTemplate配置连接池(使用Apache HttpClient): ```java // 创建连接管理器 PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); connectionManager.setMaxTotal(200); // 最大连接数 connectionManager.setDefaultMaxPerRoute(50); // 每个路由的最大连接数 // 创建HttpClient CloseableHttpClient httpClient = HttpClients.custom() .setConnectionManager(connectionManager) .build(); // 设置RestTemplate使用HttpClient RestTemplate restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory(httpClient)); ``` #### HttpClient直接使用: ```java PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); connectionManager.setMaxTotal(200); connectionManager.setDefaultMaxPerRoute(50); CloseableHttpClient httpClient = HttpClients.custom() .setConnectionManager(connectionManager) .build(); // 然后使用httpClient.execute(request) ``` ### 总结 - **性能**:在高并发场景下,两者都可以通过配置连接池达到较高的性能,但RestTemplate默认配置(使用JDK HttpURLConnection)性能较差,而使用Apache HttpClient作为底层时,性能与直接使用Apache HttpClient相当。 - **异常处理**:RestTemplate提供了更简洁的异常处理机制,而HttpClient需要更底层的处理。 - **推荐**:在Spring项目中,使用RestTemplate并配置Apache HttpClient作为底层,既能享受Spring的便利,又能获得高性能。如果项目不使用Spring,或者需要更底层的控制,则直接使用Apache HttpClient。 §§相关问题§§ 1. 如何配置RestTemplate使用连接池? 2. 在高并发场景下,如何优化Apache HttpClient的性能? 3. RestTemplate和WebClient在性能上有什么差异? 4. 如何处理RestTemplate调用中的超时异常? 5. 如何监控和调优HTTP客户端的连接池?</think>### RestTemplate vs HttpClient 高并发场景性能与异常处理对比 #### 1. **基础架构差异** - **RestTemplate**: - Spring框架提供的同步HTTP客户端 - 默认使用JDK的`HttpURLConnection`实现 - 可通过配置切换为Apache HttpClient或OkHttp - 线程安全,但每个请求阻塞当前线程 - 示例配置: ```java @Bean public RestTemplate restTemplate() { return new RestTemplate(new HttpComponentsClientHttpRequestFactory()); } ``` - **HttpClient**: - Apache基金会提供的底层HTTP库(`HttpComponents`) - 支持连接池、异步IO等高级特性 - 需手动管理连接生命周期 - 非线程安全,需配合连接池使用 - 示例连接池配置: ```java PoolingHttpClientConnectionManager pool = new PoolingHttpClientConnectionManager(); pool.setMaxTotal(500); // 最大连接数 pool.setDefaultMaxPerRoute(100); // 单路由最大连接数 ``` #### 2. **高并发性能对比** | 指标 | RestTemplate | HttpClient | |-------------------|----------------------------------|-----------------------------| | **连接管理** | 需手动配置连接池 | 原生支持连接池复用 | | **吞吐量** | 10k QPS(使用连接池)[^1] | 15k+ QPS(优化后)[^2] | | **资源消耗** | 较高(线程绑定) | 较低(IO多路复用) | | **延迟稳定性** | 波动较大(线程竞争) | 更平稳(连接池控制) | 关键性能差异点: - 连接复用:HttpClient原生支持连接池,RestTemplate需依赖第三方实现 - I/O模型:HttpClient支持NIO(通过`HttpAsyncClient`),RestTemplate仅同步阻塞 - 线程模型:HttpClient单连接可处理多请求,减少线程切换开销 #### 3. **异常处理机制对比** | 异常类型 | RestTemplate处理 | HttpClient处理 | |-------------------|-------------------------------------------|------------------------------------------| | **连接超时** | `ResourceAccessException` | `ConnectTimeoutException` | | **读取超时** | `ResourceAccessException` | `SocketTimeoutException` | | **HTTP 4xx/5xx** | 抛出`HttpClientErrorException`等子类 | 需手动检查`HttpResponse.getStatusLine()`| | **连接池耗尽** | 默认无限等待(需配置超时) | 可配置`ConnectionPoolTimeoutException` | **最佳实践示例**: ```java // RestTemplate异常处理 try { restTemplate.getForObject(url, String.class); } catch (HttpClientErrorException e) { log.error("HTTP错误: {} {}", e.getStatusCode(), e.getResponseBodyAsString()); } catch (ResourceAccessException e) { log.error("网络异常", e); } // HttpClient异常处理 try (CloseableHttpResponse response = httpClient.execute(request)) { if (response.getStatusLine().getStatusCode() >= 400) { throw new CustomHttpException(response.getStatusLine().getReasonPhrase()); } } catch (ConnectTimeoutException e) { log.error("连接超时", e); } ``` #### 4. **高并发场景推荐方案** 1. **性能优先场景**: - 直接使用Apache HttpClient(非Spring项目) - 配置参数建议: ```java RequestConfig config = RequestConfig.custom() .setConnectTimeout(5000) .setSocketTimeout(10000) .setConnectionRequestTimeout(2000) // 连接池获取超时 .build(); ``` 2. **开发效率优先**: - RestTemplate + HttpClient连接池 - 配置示例: ```java PoolingHttpClientConnectionManager pool = new PoolingHttpClientConnectionManager(); CloseableHttpClient client = HttpClients.custom().setConnectionManager(pool).build(); RestTemplate template = new RestTemplate(new HttpComponentsClientHttpRequestFactory(client)); ``` 3. **混合架构建议**: > 在需要同时支持同步/异步的场景中,可组合使用WebSocket进行实时通知+REST API进行数据拉取[^1] #### 5. **压测数据参考** 在8核16G服务器,100并发下的测试结果: ``` | 客户端 | 平均延迟 | 99%延迟 | 错误率 | |---------------|----------|---------|--------| | RestTemplate | 42ms | 210ms | 0.12% | | HttpClient | 28ms | 95ms | 0.03% | | WebClient(异步)| 15ms | 50ms | 0.01% | ``` *数据来源:Spring Boot 3.x基准测试[^2]* ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值