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

结论:
如果使用httpclient 3.1并发量比较大的项目,最好升级到httpclient4.2.3上,保证并发量大时能抗住。httpclient 4.3.3,目前还有一些bug;还是用4.2.x稳定版本吧。

以库存项目为例:
httpclient一天并发量在1500w左右,峰值一秒7万。

在之前使用过程中,一直存在大量的

org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool
at org.apache.http.impl.conn.PoolingClientConnectionManager.leaseConnection(PoolingClientConnectionManager.java:232)
at org.apache.http.impl.conn.PoolingClientConnectionManager$1.getConnection(PoolingClientConnectionManager.java:199)
at org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:456)
另外通过jstack查看线程,会发现:
"pool-21-thread-3" prio=10 tid=0x00007f6b7c002800 nid=0x40ff waiting on condition [0x00007f6b37020000]
java.lang.Thread.State: TIMED_WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000000f97918b8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.parkUntil(LockSupport.java:239)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitUntil(AbstractQueuedSynchronizer.java:2072)
at org.apache.http.pool.PoolEntryFuture.await(PoolEntryFuture.java:129)
at org.apache.http.pool.AbstractConnPool.getPoolEntryBlocking(AbstractConnPool.java:281)
at org.apache.http.pool.AbstractConnPool.access$000(AbstractConnPool.java:62)
at org.apache.http.pool.AbstractConnPool$2.getPoolEntry(AbstractConnPool.java:176)
at org.apache.http.pool.AbstractConnPool$2.getPoolEntry(AbstractConnPool.java:172)
at org.apache.http.pool.PoolEntryFuture.get(PoolEntryFuture.java:100)
at org.apache.http.impl.conn.PoolingClientConnectionManager.leaseConnection(PoolingClientConnectionManager.java:212)


问题:
因为使用了连接池,但连接不够用,造成大量的等待;而且这种等待都有滚雪球的效应(和交易组最近使用的apache common dbcp存在的风险是类似的)。


解决方案
最终我们定了一些合理的参数值,目前来看还没有遇到问题。


思考
其实出问题的原因是我们对一些参数不了解,随意设置其值,不出现问题则好,出现问题很难排查到原因,因此我把使用httpclient必须设置的参数及代码写法及排查方法总结一下,供参考。

参数设置
1、httpclient 4.2.3
HttpParams params = new BasicHttpParams();
//设置连接超时时间
Integer CONNECTION_TIMEOUT = 2 * 1000; //设置请求超时2秒钟 根据业务调整
Integer SO_TIMEOUT = 2 * 1000; //设置等待数据超时时间2秒钟 根据业务调整
//定义了当从ClientConnectionManager中检索ManagedClientConnection实例时使用的毫秒级的超时时间
//这个参数期望得到一个java.lang.Long类型的值。如果这个参数没有被设置,默认等于CONNECTION_TIMEOUT,因此一定要设置
Long CONN_MANAGER_TIMEOUT = 500L; //该值就是连接不够用的时候等待超时时间,一定要设置,而且不能太大 ()

params.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, CONNECTION_TIMEOUT);
params.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, SO_TIMEOUT);
params.setLongParameter(ClientPNames.CONN_MANAGER_TIMEOUT, CONN_MANAGER_TIMEOUT);
//在提交请求之前 测试连接是否可用
params.setBooleanParameter(CoreConnectionPNames.STALE_CONNECTION_CHECK, true);

PoolingClientConnectionManager conMgr = new PoolingClientConnectionManager();
conMgr.setMaxTotal(200); //设置整个连接池最大连接数 根据自己的场景决定
//是路由的默认最大连接(该值默认为2),限制数量实际使用DefaultMaxPerRoute并非MaxTotal。
//设置过小无法支持大并发(ConnectionPoolTimeoutException: Timeout waiting for connection from pool),路由是对maxTotal的细分。
conMgr.setDefaultMaxPerRoute(conMgr.getMaxTotal());//(目前只有一个路由,因此让他等于最大值)

//另外设置http client的重试次数,默认是3次;当前是禁用掉(如果项目量不到,这个默认即可)
httpClient.setHttpRequestRetryHandler(new DefaultHttpRequestRetryHandler(0, false));
此处解释下MaxtTotal和DefaultMaxPerRoute的区别:
1、MaxtTotal是整个池子的大小;
2、DefaultMaxPerRoute是根据连接到的主机对MaxTotal的一个细分;比如:
MaxtTotal=400 DefaultMaxPerRoute=200
而我只连接到http://sishuok.com时,到这个主机的并发最多只有200;而不是400;
而我连接到http://sishuok.com 和 http://qq.com时,到每个主机的并发最多只有200;即加起来是400(但不能超过400);所以起作用的设置是DefaultMaxPerRoute。


2、httpclient 3.1
HttpConnectionManagerParams params = new HttpConnectionManagerParams();
params.setConnectionTimeout(2000);
params.setSoTimeout(2000);
// 最大连接数
params.setMaxTotalConnections(500);
params.setDefaultMaxConnectionsPerHost(500);
params.setStaleCheckingEnabled(true);
connectionManager.setParams(params);

HttpClientParams httpClientParams = new HttpClientParams();
// 设置httpClient的连接超时,对连接管理器设置的连接超时是无用的
httpClientParams.setConnectionManagerTimeout(5000); //等价于4.2.3中的CONN_MANAGER_TIMEOUT
httpClient = new HttpClient(connectionManager);
httpClient.setParams(httpClientParams);

//另外设置http client的重试次数,默认是3次;当前是禁用掉(如果项目量不到,这个默认即可)
httpClientParams.setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler(0, false));

参数类似 就不多解释了;

代码写法
1、httpclient 4.2.3
HttpResponse response = null;
HttpEntity entity = null;
try {
HttpGet get = new HttpGet();
String url = "http://hc.apache.org/";
get.setURI(new URI(url));
response = getHttpClient().execute(get);
/ /处理响应
} catch (Exception e) {
//处理异常
} finally {
if(response != null) {
EntityUtils.consume(response.getEntity()); //会自动释放连接
}
//如下方法也是可以的,但是存在一些风险;不要用
//InputStream is = response.getEntity().getContent();
//is.close();
}

2、httpclient 3.1
PostMethod postMethod = new PostMethod(yxUrl);
try {
httpClient.executeMethod(postMethod);
} catch (Exception e) {
//处理异常
} finally {
if(postMethod != null) { //不要忘记释放,尽量通过该方法实现,
postMethod.releaseConnection();
//存在风险,不要用
//postMethod.setParameter("Connection", "close");
//InputStream is = postMethod.getResponseBodyAsStream();
//is.clsoe();也会关闭并释放连接的
}
}

存在的风险
1、httpclient 4.2.3 在释放连接时
if (managedConn.isOpen() && !managedConn.isMarkedReusable()) { //如果连接打开的且不可重用(not keepalive) close socket
try {
managedConn.shutdown();
} catch (IOException iox) {
if (this.log.isDebugEnabled()) {
this.log.debug("I/O exception shutting down released connection", iox);
}
}
}
// Only reusable connections can be kept alive
if (managedConn.isMarkedReusable()) {
entry.updateExpiry(keepalive, tunit != null ? tunit : TimeUnit.MILLISECONDS);
if (this.log.isDebugEnabled()) {
String s;
if (keepalive > 0) {
s = "for " + keepalive + " " + tunit;
} else {
s = "indefinitely";
}
this.log.debug("Connection " + format(entry) + " can be kept alive " + s);
}
}
无风险

2、httpclient 3.1
1、如果走http1.1协议:如果proxy-connection/connection请求头设置为close;那么会关闭socket; 或者这两个头不等于close 也会自动关;
2、如果是keep-alive ,不会关闭;
3、如果协议小于等于http1.0协议没有问题;调用releaseConnection时会close socket;
4、其他情况不会close;

也就是说如果走http1.1且没有设置相关参数;那么socket其实是没有关闭的;可能造成很多TIME_WAIT;因此如果是走短连接建议设置postMethod.setParameter("Connection", "close")。

其他注意事项:
1、使用keep-alive一定要设置Content-Length头(否则也不是长连接)。

2、在使用httpclient3.1时(4.2.3没问题);尽量不要调用 byte[] getResponseBody() :因为如果Content-Length没设置或者传输的数据大于1M,会有大量如下日志
LOG.warn("Going to buffer response body of large or unknown size. "
+"Using getResponseBodyAsStream instead is recommended.");

如果大于1M可以设置该参数;但是-1的话就没办法了,就不要调用 byte[] getResponseBody()
httpClientParams.setLongParameter(HttpMethodParams.BUFFER_WARN_TRIGGER_LIMIT, 2L * 1024 * 1024);


3、锁
httpclient 3.1 使用synchronized+wait+notifyAll,存在两个问题,量大synchronized慢和notifyAll可能造成线程饥饿;httpclient 4.2.3 使用 ReentrantLock(默认非公平) + Condition(每个线程一个)。

这里有个测试:http://java.dzone.com/articles/synchronized-vs-lock ,在我本机(jdk1.6.0_43 )测试结果明细锁的优势比较大
1x synchronized {} with 32 threads took 2.621 seconds
1x Lock.lock()/unlock() with 32 threads took 1.951 seconds
1x AtomicInteger with 32 threads took 4.113 seconds
1x synchronized {} with 64 threads took 2.621 seconds
1x Lock.lock()/unlock() with 64 threads took 1.983 seconds

这也是为什么在库存项目中使用httpclient 3.1 依然有大量的wait,而httpclient4.2.3 一个没有的问题所在。

如有问题,请赐教。


7
顶2
踩 分享到:
《跟我学Shiro》PDF完结版下载
13 小时前浏览 1185评论(9)收藏分类:企业架构相关推荐
评论
9 楼 jinnianshilongnian 3 小时前 引用
malie0 写道
好像很久没看到LZ了,本来在问答区还是很活跃的

谢谢,上班忙啊
8 楼 jinnianshilongnian 3 小时前 引用
yixiandave 写道
卧槽。。。今天看了你的文章才发现commons-httpclient原来是07年的老版本。。。我还一直在用
话说4.0接口变化真大,之前一个项目改了我一上午

最后问下4.3.4版本有什么已知bug吗

https://issues.apache.org/jira/browse/HTTPCLIENT-1478 现在说是修复了,没看最新的源代码。 用之前可以再看下源代码或者跑下量测试下。
7 楼 yixiandave 5 小时前 引用
卧槽。。。今天看了你的文章才发现commons-httpclient原来是07年的老版本。。。我还一直在用
话说4.0接口变化真大,之前一个项目改了我一上午

最后问下4.3.4版本有什么已知bug吗
6 楼 malie0 5 小时前 引用
好像很久没看到LZ了,本来在问答区还是很活跃的
5 楼 jinnianshilongnian 5 小时前 引用
kidding87 写道


so_timeout这个必须设置,否则pool就可能呆死在这里了


这个是控制并发量的,两个都要设置
cm.setDefaultMaxPerRoute(100);
cm.setMaxTotal(200);

不知道楼主那么大的并发不是针对相同的机器么,相同的话,keepalive能提升不少性能,减少了连接次数
time_wait 需要设置reuseaddr

下面是官方默认给的
httpClient.setHttpRequestRetryHandler(new DefaultHttpRequestRetryHandler());
httpClient.setReuseStrategy(new DefaultConnectionReuseStrategy());
httpClient.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy());

官方的例子还给了一个定期清理pool中connection的例子

Java代码 收藏代码
class IdleConnectionMonitorThread extends Thread {
private final ClientConnectionManager connMgr;
private volatile boolean shutdown;

public IdleConnectionMonitorThread(ClientConnectionManager connMgr) {
super();
this.setName("idle-connection-monitor");
this.setDaemon(true);
this.connMgr = connMgr;
this.start();
}

@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(60, TimeUnit.SECONDS);
}
}
} catch (InterruptedException ex) {
// terminate
}
}

public void shutdown() {
synchronized (this) {
shutdown = true;
notifyAll();
}
}

恩,是相同机器 keepalive方式 我没有试,我是帮同事调试的,只是给他建议。
4 楼 kidding87 7 小时前 引用


so_timeout这个必须设置,否则pool就可能呆死在这里了


这个是控制并发量的,两个都要设置
cm.setDefaultMaxPerRoute(100);
cm.setMaxTotal(200);

不知道楼主那么大的并发不是针对相同的机器么,相同的话,keepalive能提升不少性能,减少了连接次数
time_wait 需要设置reuseaddr

下面是官方默认给的
httpClient.setHttpRequestRetryHandler(new DefaultHttpRequestRetryHandler());
httpClient.setReuseStrategy(new DefaultConnectionReuseStrategy());
httpClient.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy());

官方的例子还给了一个定期清理pool中connection的例子

Java代码 收藏代码
class IdleConnectionMonitorThread extends Thread {
private final ClientConnectionManager connMgr;
private volatile boolean shutdown;

public IdleConnectionMonitorThread(ClientConnectionManager connMgr) {
super();
this.setName("idle-connection-monitor");
this.setDaemon(true);
this.connMgr = connMgr;
this.start();
}

@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(60, TimeUnit.SECONDS);
}
}
} catch (InterruptedException ex) {
// terminate
}
}

public void shutdown() {
synchronized (this) {
shutdown = true;
notifyAll();
}
}
3 楼 qiaoshaohua 9 小时前 引用
精典文章,保留了
2 楼 e241138 12 小时前 引用
这货的接口老是变
1 楼 ie7shop 12 小时前 引用
涛-哥出品,必属精口。必须MARK
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值