84

HttpClient连接池管理

 6 years ago
source link: https://mp.weixin.qq.com/s/g98rXHl3i327CVugBN_5Vg
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

HttpClient连接池管理

Original 谢亚华 58无线技术 2017-11-17 10:07 Posted on

随着微服务的流行,服务之间的http调用越来越多。在java里面我们可以使用httpclient这个开源工具类来进行处理,但若使用不当,可能性能会比较差,尤其是连接池是否能正常使用。接下来会详细分析下httpclient的连接池原理。

1. 使用httpclient的好处

1)、降低延迟:如果不采用连接池,每次连接发起Http请求的时候都会重新建立TCP连接(经历3次握手),用完就会关闭连接(4次挥手),如果采用连接池则减少了这部分时间损耗。
2)、支持更大的并发:如果不采用连接池,每次连接都会打开一个端口,在大并发的情况下系统的端口资源很快就会被用完,导致无法建立新的连接。采用连接池来管理长连接,可以复用之前的连接,而我们使用httpclient,有可能是两个集群之间调用,也就是有限的机器之间进行调用。这样复用连接池,能有效的节约资源。

2. 长连接和短连接

因为httpclient连接池都是管理的基于长连接的socket连接。所以介绍下长连接和短连接的区别。首先需要说下http keep-alive与tcp keep-alive和区别,http keep-alive与tcp keep-alive,不是同一回事,意图不一样。http keep-alive是为了让tcp活得更久一点,以便在同一个连接上传送多个http,提高socket的效率。而tcp keep-alive是TCP的一种检测TCP连接状况的保鲜机制。tcp keep-alive保鲜定时器,支持三个系统内核配置参数:

echo 1800 > /proc/sys/net/ipv4/tcp_keepalive_time
echo 15 > /proc/sys/net/ipv4/tcp_keepalive_intvl
echo 5 > /proc/sys/net/ipv4/tcp_keepalive_probes

keepalive是TCP保鲜定时器,当网络两端建立了TCP连接之后,闲置idle(双方没有任何数据流发送往来)了tcp_keepalive_time后,服务器内核就会尝试向客户端发送侦测包,来判断TCP连接状况(有可能客户端崩溃、强制关闭了应用、主机不可达等等)。如果没有收到对方的回答(ack包),则会在 tcp_keepalive_intvl后再次尝试发送侦测包,直到收到对对方的ack,如果一直没有收到对方的ack,一共会尝试 tcp_keepalive_probes次,每次的间隔时间在这里分别是15s, 30s, 45s, 60s, 75s。如果尝试tcp_keepalive_probes,依然没有收到对方的ack包,则会丢弃该TCP连接。TCP连接默认闲置时间是2小时,一般设置为30分钟足够了。
在httpclient中管理http层 keep-alive,创建连接并获得请求结果后,会对该连接设置长连接策略,如在MinimalClientExec.java类中,

 httpProcessor.process(request, context);
            final HttpResponse response = requestExecutor.execute(request, managedConn, context);
            httpProcessor.process(response, context);

            // The connection is in or can be brought to a re-usable state.
            if (reuseStrategy.keepAlive(response, context)) {
                // Set the idle duration of this connection
                final long duration = keepAliveStrategy.getKeepAliveDuration(response, context);
                releaseTrigger.setValidFor(duration, TimeUnit.MILLISECONDS); // 如果是长连接,此处设置过期时间。
                releaseTrigger.markReusable();
            } else {
                releaseTrigger.markNonReusable();
            }

在看看默认的长连接设置策略:

public class DefaultConnectionKeepAliveStrategy implements ConnectionKeepAliveStrategy {

    public static final DefaultConnectionKeepAliveStrategy INSTANCE = new DefaultConnectionKeepAliveStrategy();

    public long getKeepAliveDuration(final HttpResponse response, final HttpContext context) {
        Args.notNull(response, "HTTP response");
        final HeaderElementIterator it = new BasicHeaderElementIterator(
                response.headerIterator(HTTP.CONN_KEEP_ALIVE));
        while (it.hasNext()) {
            final HeaderElement he = it.nextElement();
            final String param = he.getName();
            final String value = he.getValue();
            if (value != null && param.equalsIgnoreCase("timeout")) {
                try {
                    return Long.parseLong(value) * 1000; // 默认返回的是1S
                } catch(final NumberFormatException ignore) {
                }
            }
        }
        return -1;
    }

}

当然也可以自己实现ConnectionKeepAliveStrategy类,来实现自己的长连接策略。

ConnectionKeepAliveStrategy myStrategy = new ConnectionKeepAliveStrategy() {
    public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
        // Honor 'keep-alive' header
        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")) {
                try {
                    return Long.parseLong(value) * 1000;
                } catch(NumberFormatException ignore) {
                }
            }
        }
        HttpHost target = (HttpHost) context.getAttribute(
                HttpClientContext.HTTP_TARGET_HOST);
                // 对特定的域名进行长连接设置
        if ("www.baidu.com".equalsIgnoreCase(target.getHostName())) {
            // Keep alive for 5 seconds only
            return 5 * 1000;
        } else {
            // otherwise keep alive for 30 seconds
            return 30 * 1000;
        }
    }
};
CloseableHttpClient client = HttpClients.custom()
        .setKeepAliveStrategy(myStrategy)// 设置为自己定义的长连接策略
        .build();

3. Httpclient连接池原理
连接池的结构如下:

Image

其中CPoolEntry是连接实体,leased代表被占用的连接集合,avaliabled代表可用的连接的集合,pending代表阻塞状态的连接。外层的leased,avaliabled,pending是全局的,用来限制全局的数量,里面有各个小的routeToPool,对应不同的server机器。
从连接池获取可用连接的过程为:
   1). 每个server对应一个routeToPool,从对应的routeToPool中获取可用的连接,有则返回该连接。若没有则转入下一步。
   2). 若routeToPool和外层CPool连接池均还有可用的空间,则新建连接,并将该连接作为可用连接返回;否则进行下一步
   3). 将当前请求放入pending队列,等待执行。
   4). 上述过程中会判断各种条件是否满足,比如不能超过我们设置的总连接数等。

CPool继承的基类:AbstractConnPool,其代码结构如下

    public abstract class AbstractConnPool<T, C, E extends PoolEntry<T, C>>  
                                                   implements ConnPool<T, E>, ConnPoolControl<T> {  

        private final Lock lock;  
        private final ConnFactory<T, C> connFactory;  
        private final Map<T, RouteSpecificPool<T, C, E>> routeToPool;  //路由和连接之间的对应关系
        private final Set<E> leased;  // 被占用的连接
        private final LinkedList<E> available;  // 可用连接
        private final LinkedList<PoolEntryFuture<E>> pending;  
        private final Map<T, Integer> maxPerRoute;  

        private volatile boolean isShutDown;  
        private volatile int defaultMaxPerRoute;  
        private volatile int maxTotal;  
        private volatile int validateAfterInactivity;

从上述代码可以看出CPoolEntry便是线程池里面的一个个元素,CPool里面包含routeToPool这个小的线程池,routeToPool里面都是相对于一个固定的HttpRoute(也可以说到一个固定的机器地址)所建立的所有连接。

4. HttpClient参数配置
HttpClient有多个参数可以配置,比如:
PoolingClientConnectionManager conMgr = new PoolingClientConnectionManager();
conMgr.setMaxTotal(200); //设置整个连接池最大连接数,每个routeToPool的大小加起来不能超过maxTotal的值。
conMgr.setDefaultMaxPerRoute(20);//这个便是设置的routeToPool线程池的大小。
还可以设置请求超时时间,socket等待数据超时时间,从连接池获取不到数据时的等待时间。


通过以上分析可知,通过合理的设置连接池的长连接策略,连接池的大小,以及一些连接相关的参数,能有效的提高系统的并发量。HttpClient的其它一些特性诸如自动管理cookie,支持HTTPS协议,以可扩展的面向对象的结构实现了http的全部方法等,增加了易用性和灵活性,也方便了我们的使用。

Image

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK