HttpClient连接池的配置

在之前的文章 连接池------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),校验失败会关闭释放当前连接并且从连接池中重新获取连接。

defaultMaxPerRouteMaxTotal

defaultMaxPerRouteMaxTotal这个配置也是在连接池中配置,他的默认值是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时,将不再允许创建新的连接,直到现有连接被释放或超时

定时清除连接

连接池里面还有两个关于定时清除连接配置就是evictExpiredConnectionsevictIdleConnections

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();
}

这个后台线程主要就是调用closeExpiredConnectionscloseIdleConnections方法,也就是我们上面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]

小结

通过设置evictExpiredConnectionsevictIdleConnections配置会开启一个线程定时扫描,清理过期和空闲的连接信息。默认不开启

总结

最后我们来做个总结

  • 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);
        }
    }
}
相关推荐
lwprain9 分钟前
解决tomcat双击startup.bat乱码的几种方法
java·tomcat
小汤猿人类30 分钟前
nacos-gateway动态路由
java·前端·gateway
GraduationDesign35 分钟前
基于SpringBoot的在线文档管理系统的设计与实现
java·spring boot·后端
TANGLONG22241 分钟前
【初阶数据结构与算法】八大排序之非递归系列( 快排(使用栈或队列实现)、归并排序)
java·c语言·数据结构·c++·算法·蓝桥杯·排序算法
言之。1 小时前
【Java】面试题 并发安全 (1)
java·开发语言
m0_748234521 小时前
2025最新版Java面试八股文大全
java·开发语言·面试
van叶~1 小时前
仓颉语言实战——2.名字、作用域、变量、修饰符
android·java·javascript·仓颉
张声录11 小时前
【ETCD】【实操篇(十九)】ETCD基准测试实战
java·数据库·etcd
xiaosannihaiyl241 小时前
Scala语言的函数实现
开发语言·后端·golang
鱼香鱼香rose1 小时前
面经hwl
java·服务器·数据库