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

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的开销

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

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

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

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如下:

ConnectionKeepAliveStrategy myStrategy = new ConnectionKeepAliveStrategy() {@Overridepublic long getKeepAliveDuration(HttpResponse response, HttpContext context) {HeaderElementIterator it = new BasicHeaderElementIterator(response.headerIterator(HTTP.CONN_KEEP_ALIVE));while (it.hasNext()) {HeaderElement he = it.nextElement();String param = he.getName();String value = he.getValue();if (value != null && param.equalsIgnoreCase("timeout")) {return Long.parseLong(value) * 1000;}}return 60 * 1000;//如果没有约定,则默认定义时长为60s}
};

3.2 配置一个PoolingHttpClientConnectionManager

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

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

3.3 生成httpclient

httpClient = HttpClients.custom().setConnectionManager(connectionManager).setKeepAliveStrategy(kaStrategy).setDefaultRequestConfig(RequestConfig.custom().setStaleConnectionCheckEnabled(true).build()).build();

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

这里要注意的是,不要关闭connection。
一种可行的获取内容的方式类似于,把entity里的东西复制一份:

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

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

public <T> T execute(final HttpHost target, final HttpRequest request,final ResponseHandler<? extends T> responseHandler, final HttpContext context)throws IOException, ClientProtocolException {Args.notNull(responseHandler, "Response handler");final HttpResponse response = execute(target, request, context);final T result;try {result = responseHandler.handleResponse(response);} catch (final Exception t) {final HttpEntity entity = response.getEntity();try {EntityUtils.consume(entity);} catch (final Exception t2) {// Log this exception. The original exception is more// important and will be thrown to the caller.this.log.warn("Error consuming content after an exception.", t2);}if (t instanceof RuntimeException) {throw (RuntimeException) t;}if (t instanceof IOException) {throw (IOException) t;}throw new UndeclaredThrowableException(t);}// Handling the response was successful. Ensure that the content has// been fully consumed.final HttpEntity entity = response.getEntity();EntityUtils.consume(entity);//看这里看这里return result;}

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

public static void consume(final HttpEntity entity) throws IOException {if (entity == null) {return;}if (entity.isStreaming()) {final InputStream instream = entity.getContent();if (instream != null) {instream.close();}}}

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

4. 其他

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

4.1 httpclient的一些超时配置

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

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

JAR包如下:

<!-- httpclient -->
<dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpclient</artifactId><version>4.5.6</version>
</dependency>

代码如下:

//Basic认证
private static final CredentialsProvider credsProvider = new BasicCredentialsProvider();
//httpClient
private static final CloseableHttpClient httpclient;
//httpGet方法
private static final HttpGet httpget;
//
private static final RequestConfig reqestConfig;
//响应处理器
private static final ResponseHandler<String> responseHandler;
//jackson解析工具
private static final ObjectMapper mapper = new ObjectMapper();
static {System.setProperty("http.maxConnections","50");System.setProperty("http.keepAlive", "true");//设置basic校验credsProvider.setCredentials(new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT, AuthScope.ANY_REALM),new UsernamePasswordCredentials("", ""));//创建http客户端httpclient = HttpClients.custom().useSystemProperties().setRetryHandler(new DefaultHttpRequestRetryHandler(3,true)).setDefaultCredentialsProvider(credsProvider).build();//初始化httpGethttpget = new HttpGet();//初始化HTTP请求配置reqestConfig = RequestConfig.custom().setContentCompressionEnabled(true).setSocketTimeout(100).setAuthenticationEnabled(true).setConnectionRequestTimeout(100).setConnectTimeout(100).build();httpget.setConfig(reqestConfig);//初始化response解析器responseHandler = new BasicResponseHandler();
}
/** 功能:返回响应*/
public static String getResponse(String url) throws IOException {HttpGet get = new HttpGet(url);String response = httpclient.execute(get,responseHandler);return response;
}/** 功能:发送http请求,并用net.sf.json工具解析*/
public static JSONObject getUrl(String url) throws Exception{try {httpget.setURI(URI.create(url));String response = httpclient.execute(httpget,responseHandler);JSONObject json = JSONObject.fromObject(response);return json;} catch (IOException e) {e.printStackTrace();}return null;
}
/** 功能:发送http请求,并用jackson工具解析*/
public static JsonNode getUrl2(String url){try {httpget.setURI(URI.create(url));String response = httpclient.execute(httpget,responseHandler);JsonNode node = mapper.readTree(response);return node;} catch (IOException e) {e.printStackTrace();}return null;
}
/** 功能:发送http请求,并用fastjson工具解析*/
public static com.alibaba.fastjson.JSONObject getUrl3(String url){try {httpget.setURI(URI.create(url));String response = httpclient.execute(httpget,responseHandler);com.alibaba.fastjson.JSONObject jsonObject = com.alibaba.fastjson.JSONObject.parseObject(response);return jsonObject;} catch (IOException e) {e.printStackTrace();}return null;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/207887.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何做好口译服务,同传和交传哪个服务好

随着中国经济的蓬勃发展和综合实力的不断增强&#xff0c;中国与世界各国的交流也日益频繁。口译作为对外交流的桥梁与纽带&#xff0c;需求量与日俱增&#xff0c;其重要性不言而喻。那么&#xff0c;如何做好口译服务呢&#xff1f;是同传还是交传更好呢&#xff1f; 业内专家…

渗透测试工具AWVS的全面解析

在当今的数字化时代&#xff0c;网络安全已经成为了企业和个人必须关注的重要问题。为了确保网络的安全&#xff0c;我们需要使用各种工具和技术进行检测和防护。其中&#xff0c;渗透测试是一种非常重要的方法&#xff0c;它可以帮助我们发现网络中的安全漏洞&#xff0c;并采…

机器人纯阻抗控制接触刚性环境

问题描述 在机器人学中&#xff0c;阻抗控制是一种常用的控制策略&#xff0c;用于管理机器人在与环境交互时的运动和力。阻抗控制背后的关键概念是将环境视为导纳&#xff0c;而将机器人视为阻抗。 纯阻抗控制接触刚性环境时&#xff0c;机器人的行为方式主要受其阻抗参数的…

表单小程序作用体现在哪

表单的用途非常广泛&#xff0c;它是线上收集信息或用户预约/需求服务的重要方式&#xff0c;对商家来说如今线上平台非常多&#xff0c;生意开展的形式也越来越多&#xff0c;比如常见的预约、报名、收款、产品支付等都可以通过表单实现。 接下来啊让我们看看通过【雨科】平台…

家用智能门锁——智能指纹锁方案

智能指纹锁产品功能&#xff1a; 1&#xff1a;指纹识别技术&#xff1a;光学传感器、半导体传感器或超声波传感器等。 2&#xff1a;指纹容量&#xff1a;智能指纹锁可以存储的指纹数量&#xff0c;通常在几十到几百个指纹之间。 3&#xff1a;解锁时间&#xff1a;指纹识别和…

【力扣100】8.找到字符串中所有字母异位词

添加链接描述 class Solution:def findAnagrams(self, s: str, p: str) -> List[int]:sildingstrresult[]p.join(sorted(p))for i in range(len(s)):if len(sildingstr)<len(p):sildingstrsildingstrs[i]# print(sildingstr)if len(sildingstr)len(p):sort_sildingstr.j…

Android开发常用工具类集合

目录 DownloadGradleAPIsActivity 相关 -> ActivityUtils.java -> DemoAdaptScreen 相关 -> AdaptScreenUtils.java -> DemoApp 相关 -> AppUtils.java -> Demo栏相关 -> BarUtils.java -> Demo亮度相关 -> BrightnessUtils.java -> DemoBus 相关…

身份认证技术

身份认证是对系统的用户进行有效性、真实性验证。 1&#xff0e;口令认证方式 使用口令认证方式&#xff0c;用户必须具有一个唯一的系统标识&#xff0c;并且保证口令在系统的使用和存储过程中是安全的&#xff0c;同时口令在传输过程中不能被窃取、替换。另外特别要注意的是在…

解决:During handling of the above exception, another exception occurred

解决&#xff1a;During handling of the above exception, another exception occurred 文章目录 解决&#xff1a;During handling of the above exception, another exception occurred背景报错问题报错翻译报错位置代码报错原因解决方法参考内容&#xff1a;今天的分享就到…

numpy数据读取保存及速度测试

目录 数据保存及读取 速度比对测试 数据保存及读取 代码示例&#xff1a; # 导入必要的库 import numpy as np # 生成测试数据 arr_disk np.arange(8) # 打印生成能的数据 print(arr_disk) # numpy保存数据到本地 np.save("arr_disk", arr_disk) # 加载本地数据…

排序算法-插入/希尔排序

1 插入排序 1.1基本思想&#xff1a; 直接插入排序是一种简单的插入排序法&#xff0c;其基本思想是&#xff1a;把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中&#xff0c;直到所有的记录插入完为止&#xff0c;得到一个新的有序序列 。 1.2直…

CentOS7.0 下rpm安装MySQL5.5.60

下载 下载路径: MySQL :: Download MySQL Community Server -->looking for the latest GA version-->5.5.60 此压缩包中有多个rpm包 有四个不是必须的,只需安装这三个 MySQL-server-5.5.60-1.el6.x86_64 MySQL-devel-5.5.60-1.el6.x86_64 MySQL-client-5.5.60-1.el6.x8…

pymysql insert dataframe 遇到 nan 值

def get_db_conn():"""MYSQL连接"""host Config.MYSQL["host"]pwd Config.MYSQL["pwd"]user Config.MYSQL["user"]port Config.MYSQL["port"]database Config.MYSQL["database"]conn p…

智能淘客查券返利机器人与导购app:差异与优势

智能淘客查券返利机器人与导购app&#xff1a;差异与优势 在数字化购物的时代&#xff0c;我们发现越来越多的工具帮助我们省钱和赚钱。其中&#xff0c;智能淘客查券返利机器人和导购app是两种非常受欢迎的工具。然而&#xff0c;这两种工具在运作方式、功能以及效果上存在明…

代码随想Day 31 | 455.分发饼干、376. 摆动序列 、53. 最大子序和

455.分发饼干 这道题目我的思路就是&#xff0c;先满足小胃口的小朋友&#xff0c;这样能够满足更多人&#xff0c;首先把两个数组排序&#xff0c;然后依次对比&#xff0c;不满足某个胃口小的小朋友就一直给更大的饼干&#xff0c;详细代码如下&#xff1a; class Solution…

【js】js实现多个视频连续播放:

文章目录 一、效果&#xff1a;二、实现&#xff1a; 一、效果&#xff1a; 二、实现&#xff1a; <!DOCTYPE html> <html> <head><title>Video Player</title><style>#progressBar { width: 800px;height: 20px;background-color: #dd…

【Vulnhub 靶场】【Momentum: 2】【简单】【20210628】

1、环境介绍 靶场介绍&#xff1a;https://www.vulnhub.com/entry/momentum-2,702/ 靶场下载&#xff1a;https://download.vulnhub.com/momentum/Momentum2.ova 靶场难度&#xff1a;简单 发布日期&#xff1a;2021年06月28日 文件大小&#xff1a;698 MB 靶场作者&#xff1…

uni-app实现安卓原生态调用身份证阅读器读卡库读身份证和社保卡、银行卡、IC卡等功能

DONSEE系列多功能读写器Android Uniapp API接口规范V1.0.0 本项目Uniapp调用了身份证读卡器的库文件&#xff1a;DonseeDeviceLib-debug.aar&#xff0c;该库放到nativeplugins\donsee-card\android&#xff0c;然后会自动加载。SDK会自动检查是否拥有USB设备权限&#xff0c;…

同旺科技 USB TO RS-485 定制款适配器--- 拆解(三)

内附链接 1、USB TO RS-485 定制款适配器 ● 支持USB 2.0/3.0接口&#xff0c;并兼容USB 1.1接口&#xff1b; ● 支持USB总线供电&#xff1b; ● 支持Windows系统驱动&#xff0c;包含WIN10 / WIN11系统32 / 64位&#xff1b; ● 支持Windows RT、Linux、Mac OS X、Windo…

《洛谷深入浅出进阶篇》p2568 GCD

P2568 GCD - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)https://www.luogu.com.cn/problem/P2568 大致题意&#xff1a;给定正整数n&#xff0c;求1< x,y<n 且 gcd&#xff08;x&#xff0c;y&#xff09;为素数的数对&#xff08;x&#xff0c;y&#xff09;有多少对。…