HttpClient 4.3链接池参数配置及源码解读

    目前所在公司使用HttpClient 4.3.3版本发送Rest请求,调用接口。最近出现了调用查询接口服务慢的生产问题,在排查整个调用链可能存在的问题时(从客户端发起Http请求->ESB->服务端处理请求,查询数据并返回),发现本来的HttpClient链接池中的一些参数配置可能存在问题,如defaultMaxPerRoute、一些timeout时间的设置等,虽不能肯定是因为此链接池致使接口查询慢,但确实存在可优化的地方,故花时间作一些研究。本文主要涉及HttpClient链接池、请求的参数配置,使用及源码解读。html

 

    如下是本文的目录大纲:java

    1、HttpClient链接池、请求参数含义apache

    2、执行原理及源码解读c#

        一、建立HttpClient,执行request服务器

        二、链接池管理网络

            2.一、链接池结构并发

            2.二、分配链接 & 创建链接app

            2.三、回收链接 & 保持链接异步

            2.四、instream.close()、response.close()、httpclient.close()的区别socket

            2.五、过时和空闲链接清理

    3、如何设置合理的参数

 

1、HttpClient链接池、请求参数含义

import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.net.UnknownHostException;
import java.nio.charset.CodingErrorAction;
import javax.net.ssl.SSLException;
import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.client.HttpRequestRetryHandler;
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.client.protocol.HttpClientContext;
import org.apache.http.config.ConnectionConfig;
import org.apache.http.config.MessageConstraints;
import org.apache.http.config.SocketConfig;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.routing.HttpRoute;
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.protocol.HttpContext;

public class HttpClientParamTest {
	public static void main(String[] args) {
		/**
		 * 建立链接管理器,并设置相关参数
		 */
		//链接管理器,使用无惨构造
		PoolingHttpClientConnectionManager connManager 
		                            = new PoolingHttpClientConnectionManager();
		
		/**
		 * 链接数相关设置
		 */
		//最大链接数
		connManager.setMaxTotal(200); 
		//默认的每一个路由的最大链接数
		connManager.setDefaultMaxPerRoute(100); 
		//设置到某个路由的最大链接数,会覆盖defaultMaxPerRoute
		connManager.setMaxPerRoute(new HttpRoute(new HttpHost("somehost", 80)), 150); 
		
		/**
		 * socket配置(默认配置 和 某个host的配置)
		 */
		SocketConfig socketConfig = SocketConfig.custom()
				.setTcpNoDelay(true)     //是否当即发送数据,设置为true会关闭Socket缓冲,默认为false
				.setSoReuseAddress(true) //是否能够在一个进程关闭Socket后,即便它尚未释放端口,其它进程还能够当即重用端口
				.setSoTimeout(500)       //接收数据的等待超时时间,单位ms
				.setSoLinger(60)         //关闭Socket时,要么发送完全部数据,要么等待60s后,就关闭链接,此时socket.close()是阻塞的
	            .setSoKeepAlive(true)    //开启监视TCP链接是否有效
	            .build();
		connManager.setDefaultSocketConfig(socketConfig);
		connManager.setSocketConfig(new HttpHost("somehost", 80), socketConfig);
		
		/**
		 * HTTP connection相关配置(默认配置 和 某个host的配置)
		 * 通常不修改HTTP connection相关配置,故不设置
		 */
		//消息约束
		MessageConstraints messageConstraints = MessageConstraints.custom()
	            .setMaxHeaderCount(200)
	            .setMaxLineLength(2000)
	            .build();
		//Http connection相关配置
		ConnectionConfig connectionConfig = ConnectionConfig.custom()
	            .setMalformedInputAction(CodingErrorAction.IGNORE)
	            .setUnmappableInputAction(CodingErrorAction.IGNORE)
	            .setCharset(Consts.UTF_8)
	            .setMessageConstraints(messageConstraints)
	            .build();
		//通常不修改HTTP connection相关配置,故不设置
		//connManager.setDefaultConnectionConfig(connectionConfig);
        //connManager.setConnectionConfig(new HttpHost("somehost", 80), ConnectionConfig.DEFAULT);
		
        /**
         * request请求相关配置
         */
		RequestConfig defaultRequestConfig = RequestConfig.custom()
				.setConnectTimeout(2 * 1000)         //链接超时时间
                .setSocketTimeout(2 * 1000)          //读超时时间(等待数据超时时间)
                .setConnectionRequestTimeout(500)    //从池中获取链接超时时间
                .setStaleConnectionCheckEnabled(true)//检查是否为陈旧的链接,默认为true,相似testOnBorrow
                .build();
		
		/**
		 * 重试处理
		 * 默认是重试3次
		 */
		//禁用重试(参数:retryCount、requestSentRetryEnabled)
		HttpRequestRetryHandler requestRetryHandler = new DefaultHttpRequestRetryHandler(0, false);
		//自定义重试策略
		HttpRequestRetryHandler myRetryHandler = new HttpRequestRetryHandler() {

		    public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
		    	//Do not retry if over max retry count
		        if (executionCount >= 3) {
		            return false;
		        }
		        //Timeout
		        if (exception instanceof InterruptedIOException) {
		            return false;
		        }
		        //Unknown host
		        if (exception instanceof UnknownHostException) {
		            return false;
		        }
		        //Connection refused
		        if (exception instanceof ConnectTimeoutException) {
		            return false;
		        }
		        //SSL handshake exception
		        if (exception instanceof SSLException) {
		            return false;
		        }
		        
		        HttpClientContext clientContext = HttpClientContext.adapt(context);
		        HttpRequest request = clientContext.getRequest();
		        boolean idempotent = !(request instanceof HttpEntityEnclosingRequest);
		        //Retry if the request is considered idempotent
		        //若是请求类型不是HttpEntityEnclosingRequest,被认为是幂等的,那么就重试
		        //HttpEntityEnclosingRequest指的是有请求体的request,比HttpRequest多一个Entity属性
		        //而经常使用的GET请求是没有请求体的,POST、PUT都是有请求体的
		        //Rest通常用GET请求获取数据,故幂等,POST用于新增数据,故不幂等
		        if (idempotent) {
		            return true;
		        }
		        
		        return false;
		    }
		};
		
		/**
		 * 建立httpClient
		 */
		CloseableHttpClient httpclient = HttpClients.custom()
	            .setConnectionManager(connManager)             //链接管理器
	            .setProxy(new HttpHost("myproxy", 8080))       //设置代理
	            .setDefaultRequestConfig(defaultRequestConfig) //默认请求配置
	            .setRetryHandler(myRetryHandler)               //重试策略
	            .build();
		
		//建立一个Get请求,并从新设置请求参数,覆盖默认
		HttpGet httpget = new HttpGet("http://www.somehost.com/");
        RequestConfig requestConfig = RequestConfig.copy(defaultRequestConfig)
            .setSocketTimeout(5000)
            .setConnectTimeout(5000)
            .setConnectionRequestTimeout(5000)
            .setProxy(new HttpHost("myotherproxy", 8080))
            .build();
        httpget.setConfig(requestConfig);
        
        CloseableHttpResponse response = null;
        try {
        	//执行请求
			response = httpclient.execute(httpget);
			
			HttpEntity entity = response.getEntity();
			
			// If the response does not enclose an entity, there is no need
            // to bother about connection release
            if (entity != null) {
                InputStream instream = entity.getContent();
                try {
                    instream.read();
                    // do something useful with the response
                } 
                catch (IOException ex) {
                    // In case of an IOException the connection will be released
                    // back to the connection manager automatically
                    throw ex;
                } 
                finally {
                    // Closing the input stream will trigger connection release
                	// 释放链接回到链接池
                    instream.close();
                }
            }
		} 
        catch (Exception e) {
			e.printStackTrace();
		} 
        finally{
        	if(response != null){
        		try {
        			//关闭链接(若是已经释放链接回链接池,则什么也不作)
    				response.close();
    			} catch (IOException e) {
    				e.printStackTrace();
    			}
        	}
        	
        	if(httpclient != null){
        		try {
        			//关闭链接管理器,并会关闭其管理的链接
        			httpclient.close();
        		} catch (IOException e) {
        			e.printStackTrace();
        		}
        	}
        }
	}
}
    上面的代码参考 httpClient 4.3.x的官方样例,其实官方样例中可配置的更多,我只将一些以为平时经常使用的摘了出来,其实咱们在实际使用中也是使用默认的 socketConfig 和 connectionConfig。具体参数含义请看注释。

    我的感受在实际应用中链接数相关配置(如maxTotal、maxPerRoute),还有请求相关的超时时间设置(如connectionTimeout、socketTimeout、connectionRequestTimeout)是比较重要的。

    链接数配置有问题就可能产生总的 链接数不够 或者 到某个路由的链接数过小 的问题,咱们公司一些项目总链接数800,而defaultMaxPerRoute仅为20,这样致使真正须要比较多链接数,访问量比较大的路由也仅能从链接池中获取最大20个链接,应该在默认的基础上,针对访问量大的路由单独设置。

    链接超时时间,读超时时间,从池中获取链接的超时时间若是不设置或者设置的太大,可能致使当业务高峰时,服务端响应较慢 或 链接池中确实没有空闲链接时,不可以及时将timeout异常抛出来,致使等待读取数据的,或者等待从池中获取链接的越积越多,像滚雪球同样,致使相关业务都开始变得缓慢,而若是配置合理的超时时间就能够及时抛出异常,发现问题。

    后面会尽可能去阐述这些重要参数的原理以及如何配置一个合适的值。

 

2、执行原理及源码解读

一、建立HttpClient,执行request

/**
 * 建立httpClient
 */
CloseableHttpClient httpclient = HttpClients.custom()
                                 .setConnectionManager(connManager)             //链接管理器
                                 .setDefaultRequestConfig(defaultRequestConfig) //默认请求配置
                                 .setRetryHandler(myRetryHandler)               //重试策略
                                 .build();

    建立HttpClient的过程就是在设置了“链接管理器”、“请求相关配置”、“重试策略”后,调用 HttpClientBuilder.build()。

    build()方法会根据设置的属性不一样,建立不一样的Executor执行器,如设置了retryHandler就会 new RetryExec(execChain, retryHandler),至关于retry Executor。固然有些Executor是必须建立的,如MainClientExec、ProtocolExec。最后new InternalHttpClient(execChain, connManager, routePlanner …)并返回。

 

CloseableHttpResponse httpResponse = httpClient.execute(httpUriRequest);

    HttpClient使用了责任链模式,全部Executor都实现了ClientExecChain接口的execute()方法,每一个Executor都持有下一个要执行的Executor的引用,这样就会造成一个Executor的执行链条,请求在这个链条上传递。按照上面的方式构造的httpClient造成的执行链条为:

HttpRequestExecutor                              //发送请求报文,并接收响应信息
MainClientExec(requestExec, connManager, ...)    //main Executor,负责链接管理相关
ProtocolExec(execChain, httpprocessor)           //HTTP协议封装
RetryExec(execChain, retryHandler)               //重试策略
RedirectExec(execChain, routePlanner, redirectStrategy)   //重定向

    请求执行是按照从下到上的顺序(即每一个下面的Executor都持有上面一个Executor的引用),每个执行器都会负责请求过程当中的一部分工做,最终返回response。

 

二、链接池管理

2.一、链接池结构

链接池结构图以下:

6f3717d34737_thumb2

PoolEntry<HttpRoute, ManagedHttpClientConnection>  --  链接池中的实体

包含ManagedHttpClientConnection链接;

链接的route路由信息;

以及链接存活时间相隔信息,如created(建立时间),updated(更新时间,释放链接回链接池时会更新),validUnit(用于初始化expiry过时时间,规则是若是timeToLive>0,则为created+timeToLive,不然为Long.MAX_VALUE),expiry(过时时间,人为规定的链接池能够保有链接的时间,除了初始化时等于validUnit,每次释放链接时也会更新,可是从newExpiry和validUnit取最小值)。timeToLive是在构造链接池时指定的链接存活时间,默认构造的timeToLive=-1。

ManagedHttpClientConnection是httpClient链接,真正创建链接后,其会bind绑定一个socket,用于传输HTTP报文。

LinkedList<PoolEntry>  available  --  存放可用链接

使用完后全部可重用的链接回被放到available链表头部,以后再获取链接时优先从available链表头部迭代可用的链接。

之因此使用LinkedList是利用了其队列的特性,便可以在队首和队尾分别插入、删除。入available链表时都是addFirst()放入头部,获取时都是从头部依次迭代可用的链接,这样能够获取到最新放入链表的链接,其离过时时间更远(这种策略能够尽可能保证获取到的链接没有过时,而从队尾获取链接是能够作到在链接过时前尽可能使用,但获取到过时链接的风险就大了),删除available链表中链接时是从队尾开始,即先删除最可能快要过时的链接。

HashSet<PoolEntry>  leased  --  存放被租用的链接

全部正在被使用的链接存放的集合,只涉及 add() 和 remove() 操做。

maxTotal限制的是外层httpConnPool中leased集合和available队列的总和的大小,leased和available的大小没有单独限制。

LinkedList<PoolEntryFuture>  pending  --  存放等待获取链接的线程的Future

当从池中获取链接时,若是available链表没有现成可用的链接,且当前路由或链接池已经达到了最大数量的限制,也不能建立链接了,此时不会阻塞整个链接池,而是将当前线程用于获取链接的Future放入pending链表的末尾,以后当前线程调用await(),释放持有的锁,并等待被唤醒。

当有链接被release()释放回链接池时,会从pending链表头获取future,并唤醒其线程继续获取链接,作到了先进先出。

routeToPool  --  每一个路由对应的pool

也有针对当前路由的available、leased、pending集合,与整个池的隔离。

maxPerRoute限制的是routeToPool中leased集合和available队列的总和的大小。

 

2.二、分配链接 & 创建链接

分配链接

分配链接指的是从链接池获取可用的PoolEntry,大体过程为:

一、获取route对应链接池routeToPool中可用的链接,有则返回该链接,若没有则转入下一步;

二、若routeToPool和外层HttpConnPool链接池均还有可用的空间,则新建链接,并将该链接做为可用链接返回,不然进行下一步;

三、挂起当前线程,将当前线程的Future放入pending队列,等待后续唤醒执行;

整个分配链接的过程采用了异步操做,只在前两步时锁住链接池,一旦发现没法获取链接则释放锁,等待后续继续获取链接。

创建链接

当分配到PoolEntry链接实体后,会调用establishRoute(),创建socket链接并与conn绑定。

 

2.三、回收链接 & 保持链接

回收链接

链接用完以后链接池须要进行回收(AbstractConnPool#release()),具体流程以下:
一、若当前链接标记为重用,则将该链接从routeToPool中的leased集合删除,并添加至available队首,一样的将该请求从外层httpConnPool的leased集合删除,并添加至其available队首。同时唤醒该routeToPool的pending队列的第一个PoolEntryFuture,将其从pending队列删除,并将其从外层httpConnPool的pending队列中删除。
二、若链接没有标记为重用,则分别从routeToPool和外层httpConnPool中删除该链接,并关闭该链接。

保持链接

MainClientExec#execute()是负责链接管理的,在执行完后续调用链,并获得response后,会调用保持链接的逻辑,以下:

// The connection is in or can be brought to a re-usable state.
// 根据response头中的信息判断是否保持链接
if (reuseStrategy.keepAlive(response, context)) {
    // Set the idle duration of this connection
	// 根据response头中的keep-alive中的timeout属性,获得链接能够保持的时间(ms)
    final long duration = keepAliveStrategy.getKeepAliveDuration(response, context);
    if (this.log.isDebugEnabled()) {
        final String s;
        if (duration > 0) {
            s = "for " + duration + " " + TimeUnit.MILLISECONDS;
        } else {
            s = "indefinitely";
        }
        this.log.debug("Connection can be kept alive " + s);
    }
    //设置链接保持时间,最终是调用 PoolEntry#updateExpiry
    connHolder.setValidFor(duration, TimeUnit.MILLISECONDS);
    connHolder.markReusable(); //设置链接reuse=true
} 
else {
    connHolder.markNonReusable();
}

链接是否保持

客户端若是但愿保持长链接,应该在发起请求时告诉服务器但愿服务器保持长链接(http 1.0设置connection字段为keep-alive,http 1.1字段默认保持)。根据服务器的响应来肯定是否保持长链接,判断原则以下:

一、检查返回response报文头的Transfer-Encoding字段,若该字段值存在且不为chunked,则链接不保持,直接关闭。其余状况进入下一步;
二、检查返回的response报文头的Content-Length字段,若该字段值为空或者格式不正确(多个长度,值不是整数)或者小于0,则链接不保持,直接关闭。其余状况进入下一步
三、检查返回的response报文头的connection字段(若该字段不存在,则为Proxy-Connection字段)值,若是字段存在,若字段值为close 则链接不保持,直接关闭,若字段值为keep-alive则链接标记为保持。若是这俩字段都不存在,则http 1.1版本默认为保持,将链接标记为保持, 1.0版本默认为链接不保持,直接关闭。

链接保持时间

链接交还至链接池时,若链接标记为保持reuse=true,则将由链接管理器保持一段时间;若链接没有标记为保持,则直接从链接池中删除并关闭entry。
链接保持时,会更新PoolEntry的expiry到期时间,计算逻辑为:
一、若是response头中的keep-alive字段中timeout属性值存在且为正值:newExpiry = 链接归还至链接池时间System.currentTimeMillis() + timeout;
二、如timeout属性值不存在或为负值:newExpiry = Long.MAX_VALUE(无穷)
三、最后会和PoolEntry本来的expiry到期时间比较,选出一个最小值做为新的到期时间。

 

2.四、instream.close()、response.close()、httpclient.close()的区别

/**
 * This example demonstrates the recommended way of using API to make sure
 * the underlying connection gets released back to the connection manager.
 */
public class ClientConnectionRelease {

    public final static void main(String[] args) throws Exception {
        CloseableHttpClient httpclient = HttpClients.createDefault();
        try {
            HttpGet httpget = new HttpGet("http://localhost/");

            System.out.println("Executing request " + httpget.getRequestLine());
            CloseableHttpResponse response = httpclient.execute(httpget);
            try {
                System.out.println("----------------------------------------");
                System.out.println(response.getStatusLine());

                // Get hold of the response entity
                HttpEntity entity = response.getEntity();

                // If the response does not enclose an entity, there is no need
                // to bother about connection release
                if (entity != null) {
                    InputStream instream = entity.getContent();
                    try {
                        instream.read();
                        // do something useful with the response
                    } catch (IOException ex) {
                        // In case of an IOException the connection will be released
                        // back to the connection manager automatically
                        throw ex;
                    } finally {
                        // Closing the input stream will trigger connection release
                        instream.close();
                    }
                }
            } finally {
                response.close();
            }
        } finally {
            httpclient.close();
        }
    }
}

HttpClient Manual connection release的例子中能够看到,从内层依次调用的是instream.close()、response.close()、httpClient.close(),那么它们有什么区别呢?

 

instream.close()

在主动操做输入流,或者调用EntityUtils.toString(httpResponse.getEntity())时会调用instream.read()、instream.close()等方法。instream的实现类为org.apache.http.conn.EofSensorInputStream。

在每次经过instream.read()读取数据流后,都会判断流是否读取结束

@Override
public int read(final byte[] b, final int off, final int len) throws IOException {
    int l = -1;
    if (isReadAllowed()) {
        try {
            l = wrappedStream.read(b,  off,  len);
            checkEOF(l);
        } catch (final IOException ex) {
            checkAbort();
            throw ex;
        }
    }
    return l;
}

在EofSensorInputStream#checkEOF()方法中若是eof=-1,流已经读完,若是链接可重用,就会尝试释放链接,不然关闭链接。

protected void checkEOF(final int eof) throws IOException {
    if ((wrappedStream != null) && (eof < 0)) {
        try {
            boolean scws = true; // should close wrapped stream?
            if (eofWatcher != null) {
                scws = eofWatcher.eofDetected(wrappedStream);
            }
            if (scws) {
                wrappedStream.close();
            }
        } finally {
            wrappedStream = null;
        }
    }
}

ResponseEntityWrapper#eofDetected

public boolean eofDetected(final InputStream wrapped) throws IOException {
    try {
        // there may be some cleanup required, such as
        // reading trailers after the response body:
        wrapped.close();
        releaseConnection(); //释放链接 或 关闭链接
    } finally {
        cleanup();
    }
    return false;
}

ConnectionHolder#releaseConnection

public void releaseConnection() {
    synchronized (this.managedConn) {
    	//若是链接已经释放,直接返回
        if (this.released) {
            return;
        }
        
        this.released = true;
        //链接可重用,释放回链接池
        if (this.reusable) {
            this.manager.releaseConnection(this.managedConn,
                    this.state, this.validDuration, this.tunit);
        } 
        //不可重用,关闭链接
        else {
            try {
                this.managedConn.close();
                log.debug("Connection discarded");
            } catch (final IOException ex) {
                if (this.log.isDebugEnabled()) {
                    this.log.debug(ex.getMessage(), ex);
                }
            } finally {
                this.manager.releaseConnection(
                        this.managedConn, null, 0, TimeUnit.MILLISECONDS);
            }
        }
    }
}

 

若是没有instream.read()读取数据,在instream.close()时会调用EofSensorInputStream#checkClose(),也会有相似上面的逻辑。

因此就如官方例子注释的同样,在正常操做输入流后,会释放链接。

 

response.close()

最终是调用ConnectionHolder#abortConnection()

public void abortConnection() {
    synchronized (this.managedConn) {
    	//若是链接已经释放,直接返回
        if (this.released) {
            return;
        }
        this.released = true;
        try {
        	//关闭链接
            this.managedConn.shutdown();
            log.debug("Connection discarded");
        } catch (final IOException ex) {
            if (this.log.isDebugEnabled()) {
                this.log.debug(ex.getMessage(), ex);
            }
        } finally {
            this.manager.releaseConnection(
                    this.managedConn, null, 0, TimeUnit.MILLISECONDS);
        }
    }
}

因此,若是在调用response.close()以前,没有读取过输入流,也没有关闭输入流,那么链接没有被释放,released=false,就会关闭链接。

 

httpClient.close()

最终调用的是InternalHttpClient#close(),会关闭整个链接管理器,并关闭链接池中全部链接。

public void close() {
    this.connManager.shutdown();
    if (this.closeables != null) {
        for (final Closeable closeable: this.closeables) {
            try {
                closeable.close();
            } catch (final IOException ex) {
                this.log.error(ex.getMessage(), ex);
            }
        }
    }
}

 

总结:

一、使用链接池时,要正确释放链接须要经过读取输入流 或者 instream.close()方式;

二、若是已经释放链接,response.close()直接返回,不然会关闭链接;

三、httpClient.close()会关闭链接管理器,并关闭其中全部链接,谨慎使用。

 

2.五、过时和空闲链接清理

在链接池保持链接的这段时间,可能出现两种致使链接过时或失效的状况:

一、链接保持时间到期

每一个链接对象PoolEntry都有expiry到期时间,在建立和释放归还链接是都会为expiry到期时间赋值,在链接池保持链接的这段时间,链接已经到了过时时间(注意,这个过时时间是为了管理链接所设定的,并非指的TCP链接真的不能使用了)。

对于这种状况,在每次从链接池获取链接时,都会从routeToPool的available队列获取Entry并检测此时Entry是否已关闭或者已过时,如果则关闭并分别从routeToPool、httpConnPool的available队列移除该Entry,以后再次尝试获取链接。代码以下

/**AbstractConnPool#getPoolEntryBlocking()*/
for (;;) {
	//从availabe链表头迭代查找符合state的entry
    entry = pool.getFree(state);
    //找不到entry,跳出
    if (entry == null) {
        break;
    }
    //若是entry已关闭或已过时,关闭entry,并从routeToPool、httpConnPool的available队列移除
    if (entry.isClosed() || entry.isExpired(System.currentTimeMillis())) {
        entry.close();
        this.available.remove(entry);
        pool.free(entry, false);
    } 
    else {  //找到可用链接
        break;
    }
}

二、底层链接已被关闭

在链接池保持链接的时候,可能会出现链接已经被服务端关闭的状况,而此时链接的客户端并无阻塞着去接收服务端的数据,因此客户端不知道链接已关闭,没法关闭自身的socket。

对于这种状况,在从链接池获取可用链接时没法知晓,在获取到可用链接后,若是链接是打开的,会有判断链接是否陈旧的逻辑,以下

/**MainClientExec#execute()*/
if (config.isStaleConnectionCheckEnabled()) {
    // validate connection
    if (managedConn.isOpen()) {
        this.log.debug("Stale connection check");
        if (managedConn.isStale()) {
            this.log.debug("Stale connection detected");
            managedConn.close();
        }
    }
}

isOpen()会经过链接的状态判断链接是不是open状态;

isStale()会经过socket输入流尝试读取数据,在读取前暂时将soTimeout设置为1ms,若是读取到的字节数小于0,即已经读到了输入流的末尾,或者发生了IOException,可能链接已经关闭,那么isStale()返回true,须要关闭链接;若是读到的字节数大于0,或者发生了SocketTimeoutException,多是读超时,isStale()返回false,链接还可用。

/**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;
    }
}

若是在整个判断过程当中发现链接是陈旧的,就会关闭链接,那么这个从链接池获取的链接就是不可用的,后面的代码逻辑里会重建当前PoolEntry的socket链接,继续后续请求逻辑。

后台监控线程检查链接

上述过程是在从链接池获取链接后,检查链接是否可用,如不可用需从新创建socket链接,创建链接的过程是比较耗时的,可能致使性能问题,也失去了链接池的意义,针对这种状况,HttpClient采起一个策略,经过一个后台的监控线程定时的去检查链接池中链接是否还“新鲜”,若是过时了,或者空闲了必定时间则就将其从链接池里删除掉。

ClientConnectionManager提供了 closeExpiredConnections()和closeIdleConnections()两个方法,关闭过时或空闲了一段时间的链接,并从链接池删除。

closeExpiredConnections()
该方法关闭超过链接保持时间的链接,并从池中移除。

closeIdleConnections(timeout,tunit)

该方法关闭空闲时间超过timeout的链接,空闲时间从交还给链接池时开始,无论是否已过时,超过空闲时间则关闭。

下面是httpClient官方给出的清理过时、空闲链接的例子

public static class IdleConnectionMonitorThread extends Thread {
    
    private final ClientConnectionManager connMgr;
    private volatile boolean shutdown;
    
    public IdleConnectionMonitorThread(ClientConnectionManager connMgr) {
        super();
        this.connMgr = connMgr;
    }

    @Override
    public void run() {
        try {
            while (!shutdown) {
                synchronized (this) {
                    wait(5000);
                    // Close expired connections
                    connMgr.closeExpiredConnections();
                    // Optionally, close connections
                    // that have been idle longer than 30 sec
                    connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
                }
            }
        } catch (InterruptedException ex) {
            // terminate
        }
    }
    
    public void shutdown() {
        shutdown = true;
        synchronized (this) {
            notifyAll();
        }
    }
}

 

3、如何设置合理的参数

关于设置合理的参数,这个提及来真的不是一个简单的话题,须要考虑的方面也听到,是须要必定经验的,这里先简单的说一下本身的理解,欢迎各位批评指教。

这里主要涉及两部分参数:链接数相关参数、超时时间相关参数

一、链接数相关参数

根据“利尔特法则”能够获得简单的公式:

bb1dddfc6ee63

简单地说,利特尔法则解释了这三种变量的关系:L—系统里的请求数量、λ—请求到达的速率、W—每一个请求的处理时间。例如,若是每秒10个请求到达,处理一个请求须要1秒,那么系统在每一个时刻都有10个请求在处理。若是处理每一个请求的时间翻倍,那么系统每时刻须要处理的请求数也翻倍为20,所以须要20个线程。链接池的大小能够参考 L。

qps指标能够做为“λ—请求到达的速率”,因为httpClient是做为http客户端,故须要经过一些监控手段获得服务端集群访问量较高时的qps,如客户端集群为4台,服务端集群为2台,监控到每台服务端机器的qps为100,若是每一个请求处理时间为1秒,那么2台服务端每一个时刻总共有 100 * 2 * 1s = 200 个请求访问,平均到4台客户端机器,每台要负责50,即每台客户端的链接池大小能够设置为50。

固然实际的状况是更复杂的,上面的请求平均处理时间1秒只是一种业务的,实际状况的业务状况更多,评估请求平均处理时间更复杂。因此在设置链接数后,最好经过比较充分性能测试验证是否能够知足要求。

还有一些Linux系统级的配置须要考虑,如单个进程可以打开的最大文件描述符数量open files默认为1024,每一个与服务端创建的链接都须要占用一个文件描述符,若是open files值过小会影响创建链接。

还要注意,链接数主要包含maxTotal-链接总数maxPerRoute-路由最大链接数,尤为是maxPerRoute默认值为2,很小,设置很差的话即便maxTotal再大也没法充分利用链接池。

二、超时时间相关参数

connectTimeout  --  链接超时时间

根据网络状况,内网、外网等,可设置链接超时时间为2秒,具体根据业务调整

socketTimeout  --  读超时时间(等待数据超时时间)

须要根据具体请求的业务而定,如请求的API接口从接到请求到返回数据的平均处理时间为1秒,那么读超时时间能够设置为2秒,考虑并发量较大的状况,也能够经过性能测试获得一个相对靠谱的值。

socketTimeout有默认值,也能够针对每一个请求单独设置。

connectionRequestTimeout  --  从池中获取链接超时时间

建议设置500ms便可,不要设置太大,这样可使链接池链接不够时不用等待过久去获取链接,不要让大量请求堆积在获取链接处,尽快抛出异常,发现问题。

 

参考资料:

httpClient 4.3.x configuration 官方样例

使用httpclient必须知道的参数设置及代码写法、存在的风险

HttpClient链接池的链接保持、超时和失效机制

HttpClient链接池原理及一次链接时序图

相关文章
相关标签/搜索