在之前的文章 连接池------HTTP连接池) ,HttpClient连接池的和具体实现过程,今天我们看下连接池相关配置内容
连接池的配置
在前面介绍HttpClient
的时候说过HttpClient
是通过PoolingHttpClientConnectionManager
来创建并配置连接池。那么我们接下来直接先看下他的构造函数,看下里面的关键参数。
通过代码发现里面关键的参数主要是:defaultMaxPerRoute,maxTotal,timeToLive,setValidateAfterInactivity
这几个,接下来我们一个个讲解下这几个参数的具体使用与配置
timeToLive
我们先来看下timeToLive
(连接存活时间)的配置,它是通过 PoolingHttpClientConnectionManager
类参数设置,默认值为 -1,表示永不过期
java
public static final PoolingHttpClientConnectionManager cm
= new PoolingHttpClientConnectionManager(2,TimeUnit.SECONDS);
设置完成后,在创建连接池的时候通过构造函数传给了连接池CPool
。
java
//PoolingHttpClientConnectionManager
public PoolingHttpClientConnectionManager(
final HttpClientConnectionOperator httpClientConnectionOperator,
final HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory,
final long timeToLive, final TimeUnit timeUnit) {
super();
this.configData = new ConfigData();
this.pool = new CPool(new InternalConnectionFactory(
this.configData, connFactory), 2, 20, timeToLive, timeUnit);
this.pool.setValidateAfterInactivity(2000);
this.connectionOperator = Args.notNull(httpClientConnectionOperator, "HttpClientConnectionOperator");
this.isShutDown = new AtomicBoolean(false);
}
接着发现timeToLive
最终是设置到Cpool
里,在创建createEntry
时传入。
java
//CPool #CPool()
public CPool(
final ConnFactory<HttpRoute, ManagedHttpClientConnection> connFactory,
final int defaultMaxPerRoute, final int maxTotal,
final long timeToLive, final TimeUnit timeUnit) {
super(connFactory, defaultMaxPerRoute, maxTotal);
this.timeToLive = timeToLive;
this.timeUnit = timeUnit;
}
@Override
protected CPoolEntry createEntry(final HttpRoute route, final ManagedHttpClientConnection conn) {
final String id = Long.toString(COUNTER.getAndIncrement());
return new CPoolEntry(this.log, id, route, conn, this.timeToLive, this.timeUnit);
}
最终这个时间会在PoolEntry
里转为validityDeadline
,最后设置为了expiry
java
//PoolEntry #PoolEntry()
public PoolEntry(final String id, final T route, final C conn,
final long timeToLive, final TimeUnit timeUnit) {
super();
Args.notNull(route, "Route");
Args.notNull(conn, "Connection");
Args.notNull(timeUnit, "Time unit");
this.id = id;
this.route = route;
this.conn = conn;
this.created = System.currentTimeMillis();
this.updated = this.created;
if (timeToLive > 0) {
//当前时间 + timeToLive 存活时间
final long deadline = this.created + timeUnit.toMillis(timeToLive);
// If the above overflows then default to Long.MAX_VALUE
this.validityDeadline = deadline > 0 ? deadline : Long.MAX_VALUE;
} else {
this.validityDeadline = Long.MAX_VALUE;
}
this.expiry = this.validityDeadline;
}
PoolEntry
这个类里面还有一个updateExpiry()
方法,在每次请求完成释放连接releaseConnection
的时候会调用,会把过期时间再延后,但最大不超过设置的timeToLive
,里面的参数time
是服务器返回的keepalive
时间,也就是如果服务器返回的keepalive
小于当前最大存活时间,那么会直接使用keepalive
时间作为连接存活时间。
java
//PoolEntry#updateExpiry
public synchronized void updateExpiry(final long time, final TimeUnit timeUnit) {
Args.notNull(timeUnit, "Time unit");
this.updated = System.currentTimeMillis();
final long newExpiry;
if (time > 0) {
newExpiry = this.updated + timeUnit.toMillis(time);
} else {
newExpiry = Long.MAX_VALUE;
}
this.expiry = Math.min(newExpiry, this.validityDeadline);
}
接着我们看下这个配置在什么时候会用上呢?在AbstractConnPooll
类里getPoolEntryBlocking()
方法中,这个方法是连接池获取连接的方法代码,如果从池中获取到的已经是过期的(当前时间 >= expiry
),那么会直接关闭,重新获取
java
//AbstractConnPooll#getPoolEntryBlocking
private E getPoolEntryBlocking(
final T route, final Object state,
final long timeout, final TimeUnit timeUnit,
final Future<E> future) throws IOException, InterruptedException, ExecutionException, TimeoutException {
.........
this.lock.lock();
try {
E entry;
for (;;) {
Asserts.check(!this.isShutDown, "Connection pool shut down");
if (future.isCancelled()) {
throw new ExecutionException(operationAborted());
}
final RouteSpecificPool<T, C, E> pool = getPool(route);
for (;;) {
entry = pool.getFree(state);
if (entry == null) {
break;
}
//如果连接过期,直接关闭当前连接,重新获取
if (entry.isExpired(System.currentTimeMillis())) {
entry.close();
}
........
还有一个是在调用的地方是在连接池中关闭过期连接时closeExpiredConnections
方法,也是通过expiry
判断是否过期。
java
//PoolingHttpClientConnectionManager#closeExpiredConnections
@Override
public void closeExpiredConnections() {
this.log.debug("Closing expired connections");
this.pool.closeExpired();
}
java
//AbstractConnPool#closeExpired
public void closeExpired() {
final long now = System.currentTimeMillis();
enumAvailable(new PoolEntryCallback<T, C>() {
@Override
public void process(final PoolEntry<T, C> entry) {
if (entry.isExpired(now)) {
entry.close();
}
}
});
}
小结
配置timeToLive
,相当于是一个连接存活的最大时间。默认永不过期。每次通过连接池获取连接的时候,会去判断当前连接的存活时间,过期了会直接关闭当前连接。
还可以通过连接池手动调用closeExpiredConnections
方法关闭过期连接,但是需要注意的是如果没有设置 timeToLive
,关闭过期连接方法是没有任何用处的。
setValidateAfterInactivity
这个配置是属于连接池的配置,默认单位是毫秒,默认值是2000ms
ini
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setValidateAfterInactivity(10);
这个配置使用场景是在获取连接后进行判断验证,也就是上面的getPoolEntryBlocking
方法的后面
java
//AbstractConnPool#lease
public E get(final long timeout, final TimeUnit timeUnit) throws InterruptedException, ExecutionException, TimeoutException {
for (;;) {
synchronized (this) {
......
final E leasedEntry = getPoolEntryBlocking(route, state, timeout, timeUnit, this);
//校验连接
if (validateAfterInactivity > 0) {
if (leasedEntry.getUpdated() + validateAfterInactivity <= System.currentTimeMillis()) {
if (!validate(leasedEntry)) {
leasedEntry.close();
release(leasedEntry, false);
continue;
}
}
}
......
}
当配置的连接空闲时间validateAfterInactivity
超过当前时间后,进入到validate
方法,最终调用的就是 BHttpConnectionBase
类中的isStale
方法了。
java
//CPool#validate
protected boolean validate(final CPoolEntry entry) {
return !entry.getConnection().isStale();
}
该方法会从socket
进行读取数据,如果读取失败或者出现SocketTimeoutException
,则判断服务端已断开连接
java
//BHttpConnectionBase#isStale
public boolean isStale() {
if (!isOpen()) {
return true;
}
try {
final int bytesRead = fillInputBuffer(1);
return bytesRead < 0;
} catch (final SocketTimeoutException ex) {
return false;
} catch (final IOException ex) {
return true;
}
}
小结
在连接池PoolingHttpClientConnectionManager
中通过设置setValidateAfterInactivity
配置,也就是连接的校验时间,超过该时间的连接,每次从池中获取,会进行校验(读取socket
),校验失败会关闭释放当前连接并且从连接池中重新获取连接。
defaultMaxPerRoute
和 MaxTotal
defaultMaxPerRoute
和 MaxTotal
这个配置也是在连接池中配置,他的默认值是2 与 20。
我们直接先通过下面的案例看下这两个参数的作用,这个代码很简单就是10个线程分别请求,百度,掘金,知乎,b站4个网站,相当于每个网站一共请求了10次,一起产生了40次的连接,然后打印下连接池的状态信息,我们的配置使用的是默认值2 与 20。
java
public class HttpUtil {
public static final CloseableHttpClient httpClient;
public static final PoolingHttpClientConnectionManager cm;
static {
cm = new PoolingHttpClientConnectionManager();
cm.setDefaultMaxPerRoute(2);
cm.setMaxTotal(20);
httpClient = HttpClients.custom()
// 设置连接池管理
.setConnectionManager(cm)
.build();
}
public static void main(String[] args){
HttpUtil.execute("http://www.baidu.com");
HttpUtil.execute("https://juejin.cn/");
HttpUtil.execute("https://www.zhihu.com/hot");
HttpUtil.execute("https://www.bilibili.com/?utm_source=gold_browser_extension");
// 创建一个定时线程池,包含单个线程
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
// 安排任务在初始延迟后执行,然后每隔一定时间打印状态
executor.scheduleAtFixedRate(() -> {
HttpUtil.httpPoolStats();
}, 0, 1, TimeUnit.SECONDS);
}
public static void execute(String url){
for (int i = 0; i <10 ; i++) {
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
HttpGet get = new HttpGet(url);
CloseableHttpResponse response = null;
try {
Thread.sleep(1000);
response = HttpUtil.httpClient.execute(get);
if (response.getStatusLine().getStatusCode() == 200) {
HttpEntity resEntity = response.getEntity();
String message = null;
message = EntityUtils.toString(resEntity, "UTF-8");
}
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
}
public static void httpPoolStats() {
// 获取所有路由的连接池状态
PoolStats totalStats = cm.getTotalStats();
System.out.println("Total status:" + totalStats.toString());
}
接下来我们直接看下结果,其中available
连接池中可用的连接数 ,pending
代表着连接池满了,正在等待的任务列表,leased
连接池中正在使用的连接数,创建的总连接数 = leased + available
css
Total status:[leased: 0; pending: 0; available: 0; max: 20]
Total status:[leased: 6; pending: 14; available: 2; max: 20]
Total status:[leased: 2; pending: 2; available: 6; max: 20]
Total status:[leased: 1; pending: 0; available: 7; max: 20]
Total status:[leased: 0; pending: 0; available: 8; max: 20]
可用很容易发现,我们一共发起了40次请求,但是其中连接池的最大可用连接并没有达到配置的20,一直是8,这其实就是受限于defaultMaxPerRoute
参数。我们再试下设置defaultMaxPerRoute
=5
css
Total status:[leased: 0; pending: 0; available: 0; max: 20]
Total status:[leased: 16; pending: 2; available: 4; max: 20]
Total status:[leased: 1; pending: 0; available: 19; max: 20]
Total status:[leased: 1; pending: 0; available: 19; max: 20]
Total status:[leased: 0; pending: 0; available: 20; max: 20]
基于上面的案例其实很容易得出结论,也就是连接池的最大连接数其实是受限defaultMaxPerRoute
参数的配置,以及调用的服务器的个数。
小结
defaultMaxPerRoute
参数表示每个路由(即每个服务器)的最大连接数。当客户端与特定路由进行通信时,连接池中将保持这些最大连接数。这意味着,对于每个特定的服务器,客户端最多将创建defaultMaxPerRoute
个并发连接。
MaxTotal
参数表示整个连接池的最大连接数。这是整个HttpClient
实例可以使用的最大并发连接数,包括所有路由的连接。当连接池中的当前连接数达到MaxTotal
时,将不再允许创建新的连接,直到现有连接被释放或超时
定时清除连接
连接池里面还有两个关于定时清除连接配置就是evictExpiredConnections
和evictIdleConnections
java
HttpClients.custom()
.setConnectionManager(cm)
.evictExpiredConnections()
.evictIdleConnections(3, TimeUnit.SECONDS)
.build();
在我们配置这两个参数之后,在创建 HttpClients
实例的时候也就是build
方法里面,会启动一个后台的线程
java
//HttpClientBuilder#build
if (evictExpiredConnections || evictIdleConnections) {
//新增后台线程
final IdleConnectionEvictor connectionEvictor = new IdleConnectionEvictor(cm,
maxIdleTime > 0 ? maxIdleTime : 10, maxIdleTimeUnit != null ? maxIdleTimeUnit : TimeUnit.SECONDS,
maxIdleTime, maxIdleTimeUnit);
closeablesCopy.add(new Closeable() {
@Override
public void close() throws IOException {
connectionEvictor.shutdown();
try {
connectionEvictor.awaitTermination(1L, TimeUnit.SECONDS);
} catch (final InterruptedException interrupted) {
Thread.currentThread().interrupt();
}
}
});
connectionEvictor.start();
}
这个后台线程主要就是调用closeExpiredConnections
与closeIdleConnections
方法,也就是我们上面timeToLive
配置中讲的手动关闭连接的方法
java
//IdleConnectionEvictor#IdleConnectionEvictor
public IdleConnectionEvictor(
final HttpClientConnectionManager connectionManager,
final ThreadFactory threadFactory,
final long sleepTime, final TimeUnit sleepTimeUnit,
final long maxIdleTime, final TimeUnit maxIdleTimeUnit) {
this.connectionManager = Args.notNull(connectionManager, "Connection manager");
this.threadFactory = threadFactory != null ? threadFactory : new DefaultThreadFactory();
this.sleepTimeMs = sleepTimeUnit != null ? sleepTimeUnit.toMillis(sleepTime) : sleepTime;
this.maxIdleTimeMs = maxIdleTimeUnit != null ? maxIdleTimeUnit.toMillis(maxIdleTime) : maxIdleTime;
this.thread = this.threadFactory.newThread(new Runnable() {
@Override
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
Thread.sleep(sleepTimeMs);
connectionManager.closeExpiredConnections();
if (maxIdleTimeMs > 0) {
connectionManager.closeIdleConnections(maxIdleTimeMs, TimeUnit.MILLISECONDS);
}
}
} catch (final Exception ex) {
exception = ex;
}
}
});
}
我们设置3秒试下,还是上面的案例,我们看下连接池的返回结果,在连接空闲3秒后,会清除掉可用连接available
列表中的所有连接。
yaml
time 2023-10-21 14:03:16 Total status:[leased: 0; pending: 0; available: 0; max: 20]
time 2023-10-21 14:03:18 Total status:[leased: 7; pending: 0; available: 13; max: 20]
time 2023-10-21 14:03:19 Total status:[leased: 0; pending: 0; available: 20; max: 20]
time 2023-10-21 14:03:20 Total status:[leased: 0; pending: 0; available: 20; max: 20]
time 2023-10-21 14:03:21 Total status:[leased: 0; pending: 0; available: 20; max: 20]
time 2023-10-21 14:03:22 Total status:[leased: 0; pending: 0; available: 0; max: 20]
小结
通过设置evictExpiredConnections
与evictIdleConnections
配置会开启一个线程定时扫描,清理过期和空闲的连接信息。默认不开启
总结
最后我们来做个总结
-
timeToLive
: 连接存活时间,默认永不过期,建议使用默认值即可不需要配置 ,这样会使用服务器返回的keepalive
,只要在keepalive
时间范围内,连接就不会关闭。 -
setValidateAfterInactivity
:连接校验时间,默认2000ms,超过当前设置时间每次从连接池获取的连接,都会进行socket
连接校验,看连接是否有效,时间越短越不容易拿到无效的连接,建议使用默认值即可不需要配置 -
defaultMaxPerRoute
:每个服务器最大连接数,默认是2,这个配置就要看具体的调用的域名的数量,比如最大连接数50,调用了10个域名,那么这个defaultMaxPerRoute
设置为5即可
<math xmlns="http://www.w3.org/1998/Math/MathML"> d e f a u l t M a x P e r R o u t e = m a x T o t a l / 调用服务器个数 defaultMaxPerRoute = maxTotal/ 调用服务器个数 </math>defaultMaxPerRoute=maxTotal/调用服务器个数
-
maxTotal
: 整个连接池的最大连接数。包括所有路由的连接。当连接池中的当前连接数达到maxTotal
时,将不再允许创建新的连接,直到现有连接被释放或超时,这个就要根据自身具体的业务,包括服务器的负载,通过连接池的监控调整对应的大小,建议不要设置过大。 -
evictExpiredConnections
: 清除过期时间的连接,后台启动定时任务,关闭过期的连接,需要与timeToLive
配合使用 -
evictIdleConnections
: 清除空闲的连接,与上面配置一样后台启动定时任务,关闭空闲连接,推荐配置,在没有请求的时候,节约服务器的资源
HttpUtil
代码
最后HttpUtil
完整代码案例,大家可用copy
直接使用,具体的配置大家还是要根据自身的业务场景不同去设置
java
import org.apache.http.HttpEntity;
import org.apache.http.client.config.RequestConfig;
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.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.pool.PoolStats;
import org.apache.http.util.EntityUtils;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class HttpUtil {
/**
* 从连接管理器请求连接时使用的超时时间(单位/毫秒)
* 默认值: -1,为无限超时。
*/
private final static int connectionRequestTimeout = 5000;
/**
* 确定建立连接之前的超时时间(单位/毫秒)
* 默认值: -1,为无限超时。
*/
private final static int connectTimeout = 5000;
/**
* 待数据的超时(单位/毫秒)
* 默认值: -1,为无限超时。
*/
private final static int socketTimeout = 5000;
/**
* 连接池最大连接数
* 默认值:20
*/
private final static int maxTotal = 50;
/**
* 每个路由最大连接数
* 默认值:2
*/
private final static int maxPreRoute = 4;
/**
* 连接存活时长:秒
*/
private final static long connectionTimeToLive = 60;
/**
* 重试尝试最大次数
* 默认为3
*/
private final static int retryCount = 3;
/**
* 非幂等请求是否可以重试
* 默认不开启
*/
private final static boolean requestSentRetryEnabled = false;
/**
* Http客户端
*/
public static final CloseableHttpClient httpClient;
public static final PoolingHttpClientConnectionManager connectionManager;
static {
// 配置请求参数
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(connectTimeout)
.setConnectionRequestTimeout(connectionRequestTimeout)
.setSocketTimeout(socketTimeout)
.build();
connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(maxTotal);
connectionManager.setDefaultMaxPerRoute(maxPreRoute);
// 初始化客户端
httpClient = HttpClients.custom()
.setConnectionManager(connectionManager)
.setDefaultRequestConfig(requestConfig)
// 重试机制
.setRetryHandler(new DefaultHttpRequestRetryHandler(retryCount, requestSentRetryEnabled))
// 开启后台线程清除闲置的连接
.evictIdleConnections(connectionTimeToLive, TimeUnit.SECONDS)
.build();
// 添加监控线程,创建一个定时线程池
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
// 每隔1s打印连接池信息
executor.scheduleAtFixedRate(HttpUtil::httpPoolStats, 0, 1, TimeUnit.SECONDS);
}
public static void httpPoolStats() {
// 获取所有路由的连接池状态
PoolStats totalStats = connectionManager.getTotalStats();
System.out.println("time "+DateUtils.format(new Date(),DateUtils.YMD_HMS)+" Total status:" + totalStats.toString());
}
public static void main(String[] args) {
//使用方法
HttpGet get = new HttpGet("https://juejin.cn/");
CloseableHttpResponse response = null;
try {
response = HttpUtil.httpClient.execute(get);
if (response.getStatusLine().getStatusCode() == 200) {
HttpEntity resEntity = response.getEntity();
String message = null;
message = EntityUtils.toString(resEntity, "UTF-8");
System.out.println(message);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}