Commons-Httpclient3.x请求阻塞问题排查

Apache commons-httpclient3.x包中的HttpClient类使用问题排查

1.问题现象

系统刚上线,由于调用了别人的http接口,所以使用了Apache的commons-httpclient包中的HttpClient工具类,但是过了一小会儿,就会出现大量的请求无法获取到连接,提示连接不够用的问题,接口调用量相对比较大。

2.Http工具类使用方法

@Component
public class HttpUtil {
   private static int maxConnection;

   /**
   * 通过配置文件中的属性注入maxConnection
   */
   @Value("${httpclient.max.conn:60}")
   public void setMaxConnection(int maxConnection) {
      HttpClientUtil.maxConnection = maxConnection;
   }
   /**
   * 连接池
   */
   private final static HttpConnectionManager HTTP_CONNECTION_MANAGER;
   static {
     HTTP_CONNECTION_MANAGER = new MultiThreadedHttpConnectionManager();
     HttpConnectionManagerParams params = HTTP_CONNECTION_MANAGER.getParams();
     params.setConnectionTimeout(30000);
     params.setSoTimeout(1000);
     params.setDefaultMaxConnectionsPerHost(20);
     params.setMaxTotalConnections(maxConnection);
   }
   public static String doGet(String url) {
     HttpClient httpClient = new HttpClient(HTTP_CONNECTION_MANAGER);
     GetMethod getMethod = new GetMethod(url);
     getMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler());
     getMethod.getParams().setParameter(HttpMethodParams.HTTP_CONTENT_CHARSET, "GBK");
     getMethod.getParams().setSoTimeout(3000);
     getMethod.getParams().setParameter("Connection:keep-alive", "Keep-Alive: timeout=30");
     return executeMethod(httpClient, getMethod);
   }
   private static String executeMethod(HttpClient httpClient, HttpMethod httpMethod) {
     try {
        httpClient.executeMethod(httpMethod);
        return httpMethod.getResponseBodyAsString();
     } catch (Exception e) {
        log.error("executeMethod error", e);
     } finally {
        httpMethod.releaseConnection();
     }
     return null;
   }
}

3.问题出现原因

(1)上述工具类中使用@Component注解,想通过配置文件中的配置值动态修改maxConnection,但是maxConnection的值是在static代码块中使用的,导致在Spring启动初始化HttpUtil这个Bean的时候,先执行staitc代码块,此时使用@Value标注的变量maxConnection还未执行注入,所以它的值为0,因此客户端的HTTP_CONNECTION_MANAGER中的maxTotalConnections值被设置为0;
(2)客户端请求之后,调用doGet方法,发送Http请求,此时maxTotalConnections的值还是初始化时的0,因为static只会执行一次。尽管第一次调用的时候这个maxConnection值已经有值了。但是由于static代码款的特性,这个值没法被重新设置,仍然是0;
(3)查看commons-httpclient3.x的MultiThreadHttpConnectionManager类中的doGetConnection(HostConfiguration hostConfiguration)方法中的逻辑。发现如果这个maxTotalConnections的值为0,而且未设置等待超时时间,将会执行while中的connectionPool.waitingThreads.addLast(waitingThread),而且会一直等待不释放连接,导致服务器的TCP链接被耗尽。
(4)MultiThreadHttpConnectionManager中的doGetConnection代码如下

private HttpConnection doGetConnection(HostConfiguration hostConfiguration, 
  long timeout) throws ConnectionPoolTimeoutException {
  HttpConnection connection = null;
  int maxHostConnections = this.params.getMaxConnectionsPerHost(hostConfiguration);
  int maxTotalConnections = this.params.getMaxTotalConnections();
  synchronized (connectionPool) {
    hostConfiguration = new HostConfiguration(hostConfiguration);
    HostConnectionPool hostPool = connectionPool.getHostPool(hostConfiguration, true);
    WaitingThread waitingThread = null;
    boolean useTimeout = (timeout > 0);
    long timeToWait = timeout;
    long startWait = 0;
    long endWait = 0;
    while (connection == null) {
      if (shutdown) {
       throw new IllegalStateException("Connection factory has been shutdown.");
      }
      if (hostPool.freeConnections.size() > 0) {
       connection = connectionPool.getFreeConnection(hostConfiguration);
      } else if ((hostPool.numConnections < maxHostConnections) 
&& (connectionPool.numConnections < maxTotalConnections)) {
        connection = connectionPool.createConnection(hostConfiguration);
      } else if ((hostPool.numConnections < maxHostConnections) 
&& (connectionPool.freeConnections.size() > 0)) {

        connectionPool.deleteLeastUsedConnection();
        connection = connectionPool.createConnection(hostConfiguration);
      } else {
        try {
          if (useTimeout && timeToWait <= 0) {
            throw new ConnectionPoolTimeoutException("Timeout waiting for connection");
          }
          if (LOG.isDebugEnabled()) {
            LOG.debug("Unable to get a connection, waiting..., hostConfig=" + hostConfiguration);
          }
          if (waitingThread == null) {
             waitingThread = new WaitingThread();
             waitingThread.hostConnectionPool = hostPool;
             waitingThread.thread = Thread.currentThread();
          } else {
             waitingThread.interruptedByConnectionPool = false;
          } 
          if (useTimeout) {
            startWait = System.currentTimeMillis();
          }
          // 如果未设置等待超时时间,而且maxTotalConnections的值小于connectionPool.numConnections,会将连接加入到等待队列中,而且永远等待不超时
          hostPool.waitingThreads.addLast(waitingThread);
          connectionPool.waitingThreads.addLast(waitingThread);
          connectionPool.wait(timeToWait);
        } catch (InterruptedException e) {
          if (!waitingThread.interruptedByConnectionPool) {
            LOG.debug("Interrupted while waiting for connection", e);
            throw new IllegalThreadStateException(
              "Interrupted while waiting in MultiThreadedHttpConnectionManager");
          }
        } finally {
          if (!waitingThread.interruptedByConnectionPool) {
            hostPool.waitingThreads.remove(waitingThread);
            connectionPool.waitingThreads.remove(waitingThread);
          }
          if (useTimeout) {
            endWait = System.currentTimeMillis();
            timeToWait -= (endWait - startWait);
          }
        }
      }
    }
  }
  return connection;
}

4.解决方法

(1)不能使用@Value给static代码块中对象赋值
去掉@Value标注的变量,因为使用@Value标注的变量无法在初始化的时候给static代码块中的对象赋值。因为Spring初始化Bean的时候,是先创建,再设置属性,而static代码块是在创建bean的时候执行的,而且只会执行一次,此时执行赋值操作的时候,@Value标注的变量还未初始化,值为0.
(2)设置合理的等待超时时间
在doGet工具方法中发送Http请求之前,设置等待超时时间。这样,请求不会永久阻塞,会等待超时释放,也就是快速失败。设置方法如下:

public static String doGet(String url) {
  String responseMsg = "";
  HttpClient httpClient = new HttpClient(HTTP_CONNECTION_MANAGER);
  // 给httpClient中设置等待超时时间为3000ms,如果连接放入到等待队列中,超过3000ms之后,直接返回,不会一直阻塞.
  HttpClientParams params = new HttpClientParams();
  params.setConnectionManagerTimeout(3000);
  httpClient.setParams(params);
  //使用 GET 方法
  GetMethod getMethod = new GetMethod(url);
  getMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler());
  getMethod.getParams().setParameter(HttpMethodParams.HTTP_CONTENT_CHARSET, "GBK");
  getMethod.getParams().setSoTimeout(3000);
  getMethod.getParams().setParameter("Connection:keep-alive", "Keep-Alive: timeout=30");
  return executeMethod(httpClient, getMethod);
}

本文章属于原创,如果转发请标注文章来源:个人小站【www.jinnianshizhunian.vip

另外提供一些优秀的Java架构师及IT开发视频,书籍资料。无需注册,无需登录即可下载,免费下载地址:https://www.592xuexi.com