故事
前段时间进行招聘笔试,有这么一个问题,请描述实践过程中解决httpclient并发性能问题的案例。然后自己之前是有遇到过,但是一直没有总结,趁此机会总结一波。
问题
请描述实践过程中解决httpclient并发性能问题的案例。并描述应用场景,问题所在,解决方案。请编写示例代码。
思路
1、使用连接池进行优化
2、并发情况改NIO非阻塞异步调用
未优化直接代码
package ordinary;import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.apache.log4j.Logger;import java.io.IOException;/*** @author sirwsl* @createTime 2024/4/27 17:05* @version 1.0* @description httpClient正常没有并发情况下使用例子* <p>* 该样例仅作为程序在进行使用HttpClient进行普通调用返回,不会出现并发情况下的简单调用。* </p>*/public class HttpCommonUse {private static final Logger logger = Logger.getLogger(HttpCommonUse.class);public static void main(String[] args) throws IOException {get("https://uapis.cn/api/weather?name=昆明市");post("https://uapis.cn/api/weather?name=昆明市",new StringEntity(""));}public static void get(String url) throws IOException {// 1. 创建HttpClient实例CloseableHttpClient httpClient = HttpClients.createDefault();// 2. 创建GET请求方法实例HttpGet httpGet = new HttpGet(url);// 3. 调用HttpClient实例来执行GET请求方法,得到responseCloseableHttpResponse response = httpClient.execute(httpGet);// 4对得到后的实例可以进行处理,例如读取回复体,读取htmlHttpEntity entity = response.getEntity();//5 执行业务logger.info(EntityUtils.toString(entity));// 6. 释放连接response.close();httpClient.close();}public static void post(String url,HttpEntity entityString) throws IOException {// 1. 创建HttpClient实例CloseableHttpClient httpclient = HttpClients.createDefault();// 2. 创建HttpPost实例HttpPost httpPost = new HttpPost(url);httpPost.setEntity(entityString);// 3. 调用HttpClient实例来执行HttpPost实例CloseableHttpResponse response = httpclient.execute(httpPost);// 4. 读 responseString html = EntityUtils.toString(response.getEntity());logger.info(html);// 5. 释放连接response.close();httpclient.close();}
}
优化后代码
本次优化代码分为1-4个优化样例。请选择适合自己的方式。
httpClient 异步优化
package demo;import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.concurrent.FutureCallback;
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
import org.apache.http.impl.nio.client.HttpAsyncClients;
import org.apache.log4j.Logger;import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import static java.lang.Thread.sleep;/*** @author: sirwsl* @createTime: 2024/4/27 18:12* @version: 1.0* @description: httpClient 异步优化* <p>* 该例子模拟httpClient进行异步调用,本地测试耗时:2500-2900。* 场景:适用于在项目中对同时对第三方服务进行大批量调用,如果不进行异步处理,高并发场景下会出现程序消耗大量资源,* 且等待时间较长。* 程序主要采用CloseableHttpAsyncClient 对httpclient进行整体异步调用,实现NIO实现非阻塞情况。* 通过CloseableHttpAsyncClient 异步策略对原httpClient同步进行整体改善提升效率* </p>*/public class HttpClientAsync {private static final Logger logger = Logger.getLogger(HttpClientAsync.class);public static void main(String[] args) throws IOException, InterruptedException{long start = System.currentTimeMillis();logger.info("请求开始,"+start);CloseableHttpAsyncClient httpclient = HttpAsyncClients.custom().build();int sendTimes = 10;CountDownLatch latch = new CountDownLatch(sendTimes);// 3.发起调用try {// 3.0启动httpclient.start();// 3.1请求参数String url = "https://uapis.cn/api/weather?name=昆明市";for (int i = 0; i < sendTimes; i++) {HttpGet httpget = new HttpGet(url);httpclient.execute(httpget, new FutureCallback<HttpResponse>() {public void failed(final Exception ex) {latch.countDown();logger.error(ex.getLocalizedMessage());}public void completed(final HttpResponse response) {latch.countDown();try {sleep(100);} catch (Exception e) {}}public void cancelled() {latch.countDown();logger.error("取消");}});}} finally {latch.await();httpclient.close();}long end = System.currentTimeMillis();logger.error("请求结束:"+end);logger.error("耗时:"+(end- start));}
}
HttpClient 并发调用
package demo;import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.apache.log4j.Logger;import static java.lang.Thread.sleep;/*** @author: sirwsl* @createTime: 2024/4/27 17:56* @version: 1.0* @description: HttpClient 并发调用* <p>* 该例子中 以10个请求并发,本地测试耗时约在10000-13000左右** 未进行任何优化,以普通方式进行调用,常见于普通项目中,不需要并发前提下可使用该方式* 以该方式调用会出现程序执行效率低,等待时间长等等问题。主要造成该问题是由于HttpClient在进行执行时,重复创建导致,* 一般的每次请求时会初创建一个httpclient,执行httpPost对象或者httpGet对象,然后从返回结果取出entity,最后关闭response释放链接。* 高并发场景下会消耗大量服务器资源。**** </p>*/public class HttpClientDemo {private static final Logger logger = Logger.getLogger(HttpClientDemo.class);public static void main(String[] args) throws InterruptedException, ExecutionException, IOException {long start = System.currentTimeMillis();logger.info("请求开始,"+start);HttpClient httpClient = HttpClients.createDefault();String url = "https://uapis.cn/api/weather?name=昆明市";int sendTimes = 10;ExecutorService executorService = Executors.newFixedThreadPool(sendTimes);List <Future<String>> futures = new ArrayList<>();// 模拟10个httpClient秦秋for (int j = 0; j< sendTimes; j++) {Future<String> future = executorService.submit(() -> {HttpGet httpGet = new HttpGet(url);HttpResponse response = httpClient.execute(httpGet);String responseBody = EntityUtils.toString(response.getEntity());EntityUtils.consume(response.getEntity());return responseBody;});futures.add(future);}for (Future<String> future : futures) {String responseBody = future.get();// 处理业务逻辑sleep(100);}// Shutdown the executor serviceexecutorService.shutdown();long end = System.currentTimeMillis();logger.error("请求结束:"+end);logger.error("耗时:"+(end- start));}}
使用HttpClientBuilder 异步池化方式进行整体并发优化
package demo;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.apache.log4j.Logger;import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;import static java.lang.Thread.sleep;/*** @author: sirwsl* @createTime: 2024/4/27 18:54* @version:* @description: 使用HttpClientBuilder 异步池化方式进行整体并发优化。* <p>* 该例子中 以10个请求并发,本地测试耗时约在110-150左右。* 场景:在进行某些并发程序处理时,由于第三方服务调用并为影响程序主体逻辑执行,可采用CloseableHttpClient+线程池方式进行处理。* 例如某一并发请求调用第三方服务,但需对第三方服务数据进行落库处理,并不会对程序主体逻辑进行改动,此时可通过该方式缩短程序整体响应时间* </p>*/
public class HttpClientPool {private static final Logger logger = Logger.getLogger(HttpClientPool.class);public static void main(String[] args) throws IOException {long start = System.currentTimeMillis();logger.info("请求开始,"+start);// 创建线程池ExecutorService executorService = Executors.newFixedThreadPool(10);// 创建 HttpClient 对象CloseableHttpClient httpClient = HttpClients.createDefault();// 发送 HTTP 请求String url = "https://uapis.cn/api/weather?name=昆明市";for (int i = 0; i < 100; i++) {executorService.execute(() -> {try {HttpClientBuilder httpClientBuilder = HttpClients.custom();httpClientBuilder.setMaxConnTotal(10);httpClientBuilder.setMaxConnPerRoute(10);CloseableHttpClient optimizedHttpClient = httpClientBuilder.build();HttpGet httpGet = new HttpGet(url);CloseableHttpResponse response = httpClient.execute(httpGet);HttpEntity entity = response.getEntity();// 执行业务sleep(100);logger.info(EntityUtils.toString(entity));// 关闭 HttpClientoptimizedHttpClient.close();} catch (Exception e) {e.printStackTrace();}});}// 关闭线程池executorService.shutdown();long end = System.currentTimeMillis();logger.error("请求结束:"+end);logger.error("耗时:"+(end- start));}
}
使用http连接池避免重复开销
package demo;import org.apache.http.HttpEntity;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;import java.io.IOException;import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.log4j.Logger;import static java.lang.Thread.sleep;/*** @author: sirwsl* @createTime: 2024/4/27 19:21* @version: 1.0* @description: 使用http连接池避免重复开销* <p>* 该例子中 以10个请求并发,本地测试耗时约在6000-6400左右。* 场景:在并发情况下调用第三方服务,由于HttpClient不主动发起close,链接会维持一段时间* 而该链接又没有进行复用,在维持的时间内,其他并发一进来,服务器会开启大量句炳,当超过上限时,无法建立新的连接,* 此时查看netstart会出现大量的TCP链接处于ESTABLISHED状态。** 在该例子中通过采用CloseableHttpResponse 对httpClient进行池化处理,* 避免httpClient每次new\close的流程对JVM的内存消耗很大,有效避免请求过多句柄不够用情况* </p>*/
public class HttpClientPool2 {private static final Logger logger = Logger.getLogger(demo.HttpClientPool2.class);public static void main(String[] args) throws IOException, InterruptedException {long start = System.currentTimeMillis();logger.info("请求开始,"+start);String url = "https://uapis.cn/api/weather?name=昆明市";int sendTimes = 10;HttpGet httpGet = new HttpGet(url);for (int i = 0; i < sendTimes; i++){CloseableHttpResponse response= HttpClientPool.getHttpClient().execute(httpGet);HttpEntity entity = response.getEntity();//logger.info(EntityUtils.toString(entity));sleep(100);}long end = System.currentTimeMillis();logger.error("请求结束:"+end);logger.error("耗时:"+(end- start));}public static class HttpClientPool {private static PoolingHttpClientConnectionManager cm = null;static {cm = new PoolingHttpClientConnectionManager();cm.setMaxTotal(100);cm.setDefaultMaxPerRoute(10);}public static CloseableHttpClient getHttpClient() {RequestConfig globalConfig = RequestConfig.custom().setCookieSpec(CookieSpecs.IGNORE_COOKIES).build();CloseableHttpClient client = HttpClients.custom().setConnectionManager(cm).setDefaultRequestConfig(globalConfig).build();return client;}}}
其他准备
所使用的pom文件
<dependencies><dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpclient</artifactId><version>4.5.2</version></dependency><dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpcore</artifactId><version>4.4.5</version></dependency><dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpcore-nio</artifactId><version>4.4.5</version></dependency><dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpasyncclient</artifactId><version>4.1.2</version></dependency><dependency><groupId>log4j</groupId><artifactId>log4j</artifactId><version>1.2.17</version></dependency></dependencies>