起因
线上运行的公众号模板消息批量推送时会有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,每个服务域名最多给多少个连接,一般少于总连接数
所以解决这个问题有以下几种方式可以解决
- 更改
connectionRequestTimeout
参数,调到更长时间可减少此异常发生几率,或者直接设置为-1让此参数失效 - 多线程HTTP请求时控制线程数不要超过
maxConnPerHost
- 如机器性能较号可调高
maxConnPerHost
参数,增加允许并发量
分析
推送模板消息实际是通过构造参数发送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();//设置请求连接超时时间
- 请求连接超时时间:1000 (1秒)
耗时:9459毫秒,成功:420
- 请求连接超时时间:3000 (3秒)
耗时:11093毫秒,成功:500
说明这个模拟测试脚本里面,请求连接超时时间设置到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