内部系统调用腾讯微信公众号平台与其进行网络通信,通过监控观察发现业务高峰期调用大的时候,网络请求耗时高达数10秒甚至更高,走查代码发现网络请求使用了apache开源组件HttpClient调用微信api,实现方式是最常见的方案。起初怀疑可能是网络原因,咨询运维http抓包、网络带宽调研等,最终排除网络原因。至此便开始了http请求优化探索之路。
Http连接的建立和关闭本质上就是TCP连接的建立和关闭,在建立和关闭时会有三次握手和四次挥手的过程,占用资源多、开销大。httpclient中使用连接池来管理网络连接,同一个tcp链路上请求连接是可以复用的,以减少连接次数,保证一定数量的长连接,且系统能拥有更高的并发性能。通过连接池的方式将连接持久化,其实大多数“池“化技术是一种通用设计,大致思路如下:
PoolingHttpClientConnectionManager全局对象
// 它是线程安全的,所有的线程都可以使用它一起发送http请求
private static CloseableHttpClient closeableHttpClient;
// 池化管理器
private static PoolingHttpClientConnectionManager clientConnectionManager = null;
static{
try {
//SSL认证
SSLContextBuilder builder = new SSLContextBuilder();
builder.loadTrustMaterial(null, new TrustSelfSignedStrategy());
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(builder.build());
// 配置同时支持 HTTP 和 HTPPS
Registry socketFactoryRegistry = RegistryBuilder.create().register("http", PlainConnectionSocketFactory.getSocketFactory()).register("https", sslsf).build();
// 初始化连接管理器
clientConnectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
// 同时最多连接数
clientConnectionManager.setMaxTotal(2000);
// 每个路由最大连接数,路由指IP+PORT或者域名
clientConnectionManager.setDefaultMaxPerRoute(1000);
closeableHttpClient = getHttpClient();
} catch (Exception e) {
log.error("创建httpclient连接失败",e);
throw new RuntimeException(e);
}
}
因为CloseableHttpClient是线程安全的,所有的线程都可以使用它发送http请求,特别注意CloseableHttpClient对象使用完后不可以调用close方法,否则此连接不会归还到连接池中。
public static CloseableHttpClient getHttpClient() {
RequestConfig requestConfig = RequestConfig.custom()
//设置创建连接超时时间
.setConnectTimeout(3000)
//设置从连接池获取连接超时时间
.setConnectionRequestTimeout(3000)
//设置数据传输超时时间
.setSocketTimeout(3000)
//是否测试连接可用
.setStaleConnectionCheckEnabled(true)
.build();
//设置连接存活策略(这里也可以使用默认实现类DefaultConnectionKeepAliveStrategy)
ConnectionKeepAliveStrategy connectionKeepAliveStrategy = (response, 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;
}
}
//如果没有约定,则默认定义时长为60s
return 60 * 1000;
};
//创建CloseableHttpClient对象
CloseableHttpClient httpClient = HttpClients.custom()
// 设置连接池管理
.setConnectionManager(clientConnectionManager)
//设置请求配置
.setDefaultRequestConfig(requestConfig)
//设置连接存活策略
.setKeepAliveStrategy(connectionKeepAliveStrategy)
// 设置重试配置 重试次数
.setRetryHandler(new DefaultHttpRequestRetryHandler(2, false)).build();
//启动线程,清理过期及闲置连接
new IdleConnectionMonitorThread(clientConnectionManager).start();
return httpClient;
}
通过启动异步线程,定时检查连接池的连接是否过期、空闲超时释放。
/**
* 设置空闲连接处理任务
*/
public static class IdleConnectionMonitorThread extends Thread {
private final HttpClientConnectionManager connMgr;
public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) {
super();
this.connMgr = connMgr;
}
@Override
public void run() {
try {
while (true) {
synchronized (this) {
wait(20000);
// 关闭过期连接
connMgr.closeExpiredConnections();
// 关闭空闲连接大于30s的连接
connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
}
}
} catch (InterruptedException ex) {
log.error("InterruptedException",ex);
}
}
}
上面基本的配置,已经完成,接下来单元测试验证效果啦。
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(30);
for(int i=0;i<30;i++){
executorService.execute(() -> request());
}
}
public static void request(){
CloseableHttpClient closeableHttpClient = HttpClientUtil.getHttpClient();
HttpGet httpget = new HttpGet("http://www.weather.com.cn/data/sk/101010100.html");
CloseableHttpResponse response = null;
try {
response = closeableHttpClient.execute(httpget);
int status = response.getStatusLine().getStatusCode();
if(200==status){
log.info("请求成功");
String res = EntityUtils.toString(response.getEntity(),"utf-8");
log.info("响应结果:{}",res);
}else{
log.error("请求失败,code:"+status);
}
}catch (Exception e){
log.error("请求异常",e);
}finally {
if (response != null) {
try {
response.close();
} catch (IOException e) {
log.error("IOException",e);
}
}
}
}
通过日志我们可以很明显的看到连接池的使用情况,到这一步证明我们的连接池已经生效。
Connection [id: 22][route: {}->http://www.weather.com.cn:80] can be kept alive for 60.0 seconds
Connection [id: 23][route: {}->http://www.weather.com.cn:80] can be kept alive for 60.0 seconds
Connection released: [id: 22][route: {}->http://www.weather.com.cn:80] [total kept alive: 3; route allocated: 30 of 1000; total allocated: 30 of 2000]
HttpClient 关于持久连接的处理在下面的代码中可以集中体现,下面从 MainClientExec 摘取了和连接池相关的部分
public CloseableHttpResponse execute(
final HttpRoute route,
final HttpRequestWrapper request,
final HttpClientContext context,
final HttpExecutionAware execAware) throws IOException, HttpException {
//从连接管理器HttpClientConnectionManager中获取一个连接请求ConnectionRequest
final ConnectionRequest connRequest = connManager.requestConnection(route, userToken);final HttpClientConnection managedConn;
final int timeout = config.getConnectionRequestTimeout();
//从连接请求ConnectionRequest中获取一个被管理的连接HttpClientConnection
managedConn = connRequest.get(timeout > 0 ? timeout : 0, TimeUnit.MILLISECONDS);
//将连接管理器HttpClientConnectionManager与被管理的连接HttpClientConnection交给一个ConnectionHolder持有
final ConnectionHolder connHolder = new ConnectionHolder(this.log, this.connManager, managedConn);
try {
HttpResponse response;
if (!managedConn.isOpen()) {
//如果当前被管理的连接不是出于打开状态,需要重新建立连接
establishRoute(proxyAuthState, managedConn, route, request, context);
}
//通过连接HttpClientConnection发送请求
response = requestExecutor.execute(request, managedConn, context);
//通过连接重用策略判断是否连接可重用
if (reuseStrategy.keepAlive(response, context)) {
//获得连接有效期
final long duration = keepAliveStrategy.getKeepAliveDuration(response, context);
//设置连接有效期
connHolder.setValidFor(duration, TimeUnit.MILLISECONDS);
//将当前连接标记为可重用状态
connHolder.markReusable();
} else {
connHolder.markNonReusable();
}
}
final HttpEntity entity = response.getEntity();
if (entity == null || !entity.isStreaming()) {
//将当前连接释放到池中,供下次调用
connHolder.releaseConnection();
return new HttpResponseProxy(response, null);
} else {
return new HttpResponseProxy(response, connHolder);
}
}
这里看到了在 Http 请求过程中对连接的处理是和协议规范是一致的,这里要展开讲一下具体实现
PoolingHttpClientConnectionManager 是 HttpClient 默认的连接管理器,首先通过 requestConnection() 获得一个连接的请求,注意这里不是真正的连接,而是ConnectionRequest 对象实际上是一个持有了 Future,CPoolEntry 是被连接池管理的真正连接实例。从上面的代码我们应该关注的是HttpClientConnection conn = leaseConnection(future, timeout, tunit)如何通过异步连接 Future 获得一个真正的连接 HttpClientConnection;
看一下 CPool 是如何释放一个 Future 的,AbstractConnPool 核心代码如下
private E getPoolEntryBlocking(
final T route, final Object state,
final long timeout, final TimeUnit tunit,
final Future future) throws IOException, InterruptedException, TimeoutException {
//首先对当前连接池加锁,当前锁是可重入锁ReentrantLockthis.lock.lock();
try {
//获得一个当前HttpRoute对应的连接池,对于HttpClient的连接池而言,总池有个大小,每个route对应的连接也是个池,所以是“池中池”
final RouteSpecificPool pool = getPool(route);
E entry;
for (;;) {
Asserts.check(!this.isShutDown, "Connection pool shut down");
//死循环获得连接
for (;;) {
//从route对应的池中拿连接,可能是null,也可能是有效连接
entry = pool.getFree(state);
//如果拿到null,就退出循环
if (entry == null) {
break;
}
//如果拿到过期连接或者已关闭连接,就释放资源,继续循环获取
if (entry.isExpired(System.currentTimeMillis())) {
entry.close();
}
if (entry.isClosed()) {
this.available.remove(entry);
pool.free(entry, false);
} else {
//如果拿到有效连接就退出循环
break;
}
}
//拿到有效连接就退出
if (entry != null) {
this.available.remove(entry);
this.leased.add(entry);
onReuse(entry);
return entry;
}
//到这里证明没有拿到有效连接,需要自己生成一个
final int maxPerRoute = getMax(route);
//每个route对应的连接最大数量是可配置的,如果超过了,就需要通过LRU清理掉一些连接
final int excess = Math.max(0, pool.getAllocatedCount() + 1 - maxPerRoute);
if (excess > 0) {
for (int i = 0; i < excess; i++) {
final E lastUsed = pool.getLastUsed();
if (lastUsed == null) {
break;
}
lastUsed.close();
this.available.remove(lastUsed);
pool.remove(lastUsed);
}
}
//当前route池中的连接数,没有达到上线
if (pool.getAllocatedCount() < maxPerRoute) {
final int totalUsed = this.leased.size();
final int freeCapacity = Math.max(this.maxTotal - totalUsed, 0);
//判断连接池是否超过上线,如果超过了,需要通过LRU清理掉一些连接
if (freeCapacity > 0) {
final int totalAvailable = this.available.size();
//如果空闲连接数已经大于剩余可用空间,则需要清理下空闲连接
if (totalAvailable > freeCapacity - 1) {
if (!this.available.isEmpty()) {
final E lastUsed = this.available.removeLast();
lastUsed.close();
final RouteSpecificPool otherpool = getPool(lastUsed.getRoute());
otherpool.remove(lastUsed);
}
}
//根据route建立一个连接
final C conn = this.connFactory.create(route);
//将这个连接放入route对应的“小池”中
entry = pool.add(conn);
//将这个连接放入“大池”中
this.leased.add(entry);
return entry;
}
}
//到这里证明没有从获得route池中获得有效连接,并且想要自己建立连接时当前route连接池已经到达最大值,即已经有连接在使用,但是对当前线程不可用
boolean success = false;
try {
if (future.isCancelled()) {
throw new InterruptedException("Operation interrupted");
}
//将future放入route池中等待
pool.queue(future);
//将future放入大连接池中等待
this.pending.add(future);
//如果等待到了信号量的通知,success为true
if (deadline != null) {
success = this.condition.awaitUntil(deadline);
} else {
this.condition.await();
success = true;
}
if (future.isCancelled()) {
throw new InterruptedException("Operation interrupted");
}
} finally {
//从等待队列中移除
pool.unqueue(future);
this.pending.remove(future);
}
//如果没有等到信号量通知并且当前时间已经超时,则退出循环
if (!success && (deadline != null && deadline.getTime() <= System.currentTimeMillis())) {
break;
}
}
//最终也没有等到信号量通知,没有拿到可用连接,则抛异常
throw new TimeoutException("Timeout waiting for connection");
} finally {
//释放对大连接池的锁
this.lock.unlock();
}
}
HttpClient 如何判断一个连接在使用完毕后是要关闭,还是要放入池中供程序复用的呢?再看一下 MainClientExec 的代码
//根据重用策略判断当前连接是否要复用
if (reuseStrategy.keepAlive(response, context)) {
//需要复用的连接,获取连接超时时间,以response中的timeout为准
final long duration = keepAliveStrategy.getKeepAliveDuration(response, context);
if (this.log.isDebugEnabled()) {
final String s;
//timeout的是毫秒数,如果没有设置则为-1,即没有超时时间
if (duration > 0) {
s = "for " + duration + " " + TimeUnit.MILLISECONDS;
} else {
s = "indefinitely";
}
this.log.debug("Connection can be kept alive " + s);
}
//设置超时时间,当请求结束时连接管理器会根据超时时间决定是关闭还是放回到池中
connHolder.setValidFor(duration, TimeUnit.MILLISECONDS);
//将连接标记为可重用
connHolder.markReusable();
} else {
//将连接标记为不可重用
connHolder.markNonReusable();
}
虽然有连接池,但并不是所有连接都可以复用,具体复用策略感兴趣的同学可以看源码org.apache.http.impl.DefaultConnectionReuseStrategy#keepAlive
本文首先通过入门级demo,介绍httpclient连接池基本原理和使用方式,再通过源码来分析连接池是如果管理连接的,包含如何获取连接、连接复用策略。但由于篇幅有限,只分析了关键核心代码,如有不正之处,还请多多指正,在此感谢!
[1]https://blog.csdn.net/mawming/article/details/49617829 来源:CSDN
[2]https://juejin.cn/post/7032673516151537694 来源:掘金