并发场景org.apache.http.conn.Connect

2020-04-23  本文已影响0人  Nifury

起因

线上运行的公众号模板消息批量推送时会有10%左右的失败,这在测试环境下并未出现,日志定位为如下错误:

org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool
    at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.leaseConnection(PoolingHttpClientConnectionManager.java:316)
    at org.apache.http.impl.conn.PoolingHttpClientConnectionManager$1.get(PoolingHttpClientConnectionManager.java:282)
    at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:190)

先说结果

使用的公众号开发SDK使用HttpClient作为HTTP请求工具,并默认设置connectionRequestTimeout=3000。此问题与请求服务端无关,抛出这个异常时请求还未从客户端发出,它实际是由于大量HTTP请求需处理时连接池没有可用连接,等待超过设置时间抛出异常。虽然我们发送模板消息时使用了线程池,但线程池最大线程数(30)多于HttpClient默认maxConnPerHost连接池数量(10),所以网络请求相对较慢依然会造成请求堆积。
connectionRequestTimeout=3000参数的含义:当一个线程需要发送HTTP请求时,从连接池取一个连接,如果等待3秒还未取到可用连接,那么直接抛出异常不再发送此请求。
maxConnPerHost参数含义:默认是10,每个服务域名最多给多少个连接,一般少于总连接数
所以解决这个问题有以下几种方式可以解决

分析

推送模板消息实际是通过构造参数发送POST请求腾讯微信公众平台,测试环境没有进行大量并发测试(推送模板消息需要真实的粉丝用户openid,测试号没有很多粉丝)。那么是使用配置不当?网络不稳定导致请求发送不成功?还是HttpClient 并发有bug?HttpClient 是apache出品的成熟工具,发送模板消息使用的SDK已做了请求失败重试,还是先检查是不是自己使用配置的问题。

我们知道HttpClient是有配置连接池的,这个错误一看就能大概猜到是从连接池获取连接超时,可是为什么会出现这个错误?超出连接池连接数量的请求不是应该在排队等待?

经过模拟脚本模仿生产环境配置参数,并多次调整配置参数测试,定位问题为connectionRequestTimeout参数配置不当。

模拟测试

模拟环境如下

  • JDK1.8
  • HttpClient 4.5.11

模拟代码如下

import org.apache.http.Consts;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;

import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

public class Test {
    static CloseableHttpClient httpClient = HttpClients.createDefault();
    static ExecutorService excutor = Executors.newFixedThreadPool(30);
    static RequestConfig requestConfig = RequestConfig.custom()
            .setConnectionRequestTimeout(100).build();//设置获取连接超时时间,为重现问题这里故意设置比较小
    static final String TEST_URL = "http://pv.sohu.com/cityjson";//测试链接,这里使用搜狐的开放IP查询接口

    public static void main(String[] args) throws InterruptedException {
        final int testCount = 500;//任务重复次数
        AtomicInteger successCount = new AtomicInteger(0);//请求成功数量
        CountDownLatch latch  = new CountDownLatch(testCount);//用于判断线程池中任务是否全部执行完毕
        Long time1 = System.currentTimeMillis();
        for (int i = 0; i < testCount; i++) {
            excutor.submit(()->{
                String res = httpPost(TEST_URL,null);
                System.out.println(res);
                if(null!=res && !res.isEmpty())successCount.addAndGet(1);
                latch.countDown();
            });
        }
        latch.await();//等待线程池中的线程全部执行完
        Long time2 = System.currentTimeMillis();
        System.out.println("耗时:"+(time2-time1)+"毫秒,成功:"+successCount.get());
        excutor.shutdown();
    }
    public static String httpPost(String uri,String data){
        HttpPost  post = new HttpPost(uri);
        post.setConfig(requestConfig);
        if(data!=null){
            StringEntity entity = new StringEntity(data, Consts.UTF_8);
            post.setEntity(entity);
        }
        CloseableHttpResponse response=null;
        try {
            response = httpClient.execute(post);
            int statusCode = response.getStatusLine().getStatusCode();
            return new BasicResponseHandler().handleResponse(response);
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            post.releaseConnection();
        }
        return null;
    }
}

测试结果,500个请求成功27个😂,大量的ConnectionPoolTimeoutException

耗时:862毫秒,成功:27

调整参数测试

static RequestConfig requestConfig = RequestConfig.custom()
            .setConnectionRequestTimeout(1000).build();//设置请求连接超时时间

说明这个模拟测试脚本里面,请求连接超时时间设置到3S可以达到100%成功,但是生产环境对发送成功率要求很高,设置3S超时合适吗?所以再看看HttpClient源码来找找答案:

//获取连接关键代码,有精简:org.apache.http.impl.conn.PoolingHttpClientConnectionManager#leaseConnection
protected HttpClientConnection leaseConnection(Future<CPoolEntry> future, long timeout, TimeUnit timeUnit) throws InterruptedException, ExecutionException, ConnectionPoolTimeoutException {
        try {
            CPoolEntry entry = (CPoolEntry)future.get(timeout, timeUnit);
            if (entry != null && !future.isCancelled()) {
                Asserts.check(entry.getConnection() != null, "Pool entry with no connection");
                //...
                return CPoolProxy.newProxy(entry);
            } else {
                throw new ExecutionException(new CancellationException("Operation cancelled"));
            }
        } catch (TimeoutException var7) {
            throw new ConnectionPoolTimeoutException("Timeout waiting for connection from pool");
        }
    }

其中连接从Future<CPoolEntry>中获取,其默认connectionRequestTimeout=-1,也就是永不过期,生产环境要求请求一定要成功,所以设置一直等待获取连接即可!

配置参考

connectionRequestTimout:指从连接池获取连接的timeout
connetionTimeout:指客户端和服务器建立连接的timeout,就是http请求的三个阶段,一:建立连接;二:数据传送;三,断开连接。超时后会ConnectionTimeOutException
socketTimeout:指客户端从服务器读取数据的timeout,超出后会抛出SocketTimeOutException
上一篇 下一篇

猜你喜欢

热点阅读