referer http://blog.csdn.net/shootyou/article/details/6415248
在一次服务器异常的排查过程当中(服务器异常排查的过程我会另起文章),我们决定使用HttpClient4.X替代HttpClient3.X或者HttpConnection。
为什么使用HttpClient4?主要是HttpConnection没有连接池的概念,多少次请求就会建立多少个IO,在访问量巨大的情况下服务器的IO可能会耗尽。
HttpClient3也有连接池的东西在里头,使用MultiThreadedHttpConnectionManager,大致过程如下:
- MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
- HttpClient client = new HttpClient(connectionManager);...// 在某个线程中。
- GetMethod get = new GetMethod("http://jakarta.apache.org/");
- try {
- client.executeMethod(get);// print response to stdout
- System.out.println(get.getResponseBodyAsStream());
- } finally {
- // be sure the connection is released back to the connection
- managerget.releaseConnection();
- }
可以看出来,它的方式与jdbc连接池的使用方式相近,我觉得比较不爽的就是需要手动调用releaseConnection去释放连接。对每一个HttpClient.executeMethod须有一个method.releaseConnection()与之匹配。
HttpClient4在这点上做了改进,使用我们常用的InputStream.close()来确认连接关闭(4.1版本之前使用entity.consumeContent()来确认内容已经被消耗关闭连接)。具体方式如下:
- ...HttpClient client = null;InputStream in = null;
- try{
- client = HttpConnectionManager.getHttpClient();
- HttpGet get = new HttpGet();
- get.setURI(new URI(urlPath));
- HttpResponse response = client.execute(get);
- HttpEntity entity =response.getEntity();
- if( entity != null ){
- in = entity.getContent();
- ....
- }catch (Exception e){
- ....
- }finally{
- if (in != null){
- try{in.close ();}catch (IOException e){
- e.printStackTrace ();
- }
- }
- }
2012-03-06更新:
有网友提出调用in.close()是否会关闭底层socket,事情是这样的:
- 回复kangkang203:感谢你提出的这个问题。
- 首先我文中提出的方法in.close()它会触发一个连接的释放这个连接将重新被连接管理器收回,官网的原文是这么说的:“Closing the input stream will trigger connection release...the underlying connection gets released back to the connection manager”。但是底层的socket是否会被关闭是不一定的,我看了部分源码(EofSensorInputStream)发现,大多数情况socket并不会关闭,而是否关闭socket貌似是由一个Watcher去决定的。所以in.close的调用不会引起socket的关闭。
- 另外,由于http本身我们把它当做“短连接”,所以在一次请求交互完成后仍然打开socket的意义不是很大,毕竟它不像长连接那样在一个连接建立之后会有很多次数据交互。我们试用连接管理器的更多意义在于它对连接的管理。
好说完了连接池的使用流程,现在来说一说连接池在使用时最重要的几个参数。我用4.1的版本实现了一个简单的HttpConnectionManager,代码如下:
- public class HttpConnectionManager {
- private static HttpParams httpParams;
- private static ClientConnectionManager connectionManager;
- /**
- * 最大连接数
- */
- public final static int MAX_TOTAL_CONNECTIONS = 800;
- /**
- * 获取连接的最大等待时间
- */
- public final static int WAIT_TIMEOUT = 60000;
- /**
- * 每个路由最大连接数
- */
- public final static int MAX_ROUTE_CONNECTIONS = 400;
- /**
- * 连接超时时间
- */
- public final static int CONNECT_TIMEOUT = 10000;
- /**
- * 读取超时时间
- */
- public final static int READ_TIMEOUT = 10000;
- static {
- httpParams = new BasicHttpParams();
- // 设置最大连接数
- ConnManagerParams.setMaxTotalConnections(httpParams, MAX_TOTAL_CONNECTIONS);
- // 设置获取连接的最大等待时间
- ConnManagerParams.setTimeout(httpParams, WAIT_TIMEOUT);
- // 设置每个路由最大连接数
- ConnPerRouteBean connPerRoute = new ConnPerRouteBean(MAX_ROUTE_CONNECTIONS);
- ConnManagerParams.setMaxConnectionsPerRoute(httpParams,connPerRoute);
- // 设置连接超时时间
- HttpConnectionParams.setConnectionTimeout(httpParams, CONNECT_TIMEOUT);
- // 设置读取超时时间
- HttpConnectionParams.setSoTimeout(httpParams, READ_TIMEOUT);
- SchemeRegistry registry = new SchemeRegistry();
- registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
- registry.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));
- connectionManager = new ThreadSafeClientConnManager(httpParams, registry);
- }
- public static HttpClient getHttpClient() {
- return new DefaultHttpClient(connectionManager, httpParams);
- }
- }
最大连接数、获取连接的最大等待时间、读取超时时间 这些配置应该比较容易理解,一般的连接池都会有这些配置,比较特别的是 每个路由(route)最大连接数 。
什么是一个route?
这里route的概念可以理解为 运行环境机器 到 目标机器的一条线路。举例来说,我们使用HttpClient的实现来分别请求 www.baidu.com 的资源和 www.bing.com 的资源那么他就会产生两个route。
这里为什么要特别提到route最大连接数这个参数呢,因为这个参数的默认值为2,如果不设置这个参数值默认情况下对于同一个目标机器的最大并发连接只有2个!这意味着如果你正在执行一个针对某一台目标机器的抓取任务的时候,哪怕你设置连接池的最大连接数为200,但是实际上还是只有2个连接在工作,其他剩余的198个连接都在等待,都是为别的目标机器服务的。
怎么样蛋疼吧,我是已经有过血的教训了,在切换到HttpClient4.1的起初没有注意到这个配置,最后使得服务承受的压力反而不如从前了,所以在这里特别提醒大家注意。
HttpClient4.X 教程下载:
关于版本的补充:
网友w2449008821提醒之后我才发现在HttpClient4.1+的版本ConnManagerParams已经被Deprecated了。
我在写这篇日志的时候时候的httpclient 版本是4.0.3,从4.0版本之后ConnManagerParams被Deprecated,没想到一个小版本升级会有这么大变化。
官网教程举例了新的连接池设置:
- SchemeRegistry schemeRegistry = new SchemeRegistry();
- schemeRegistry.register(
- new Scheme("http", 80, PlainSocketFactory.getSocketFactory()));
- schemeRegistry.register(
- new Scheme("https", 443, SSLSocketFactory.getSocketFactory()));
- ThreadSafeClientConnManager cm = new ThreadSafeClientConnManager(schemeRegistry);
- // Increase max total connection to 200
- cm.setMaxTotalConnections(200);
- // Increase default max connection per route to 20
- cm.setDefaultMaxPerRoute(20);
- // Increase max connections for localhost:80 to 50
- HttpHost localhost = new HttpHost("locahost", 80);
- cm.setMaxForRoute(new HttpRoute(localhost), 50);
- HttpClient httpClient = new DefaultHttpClient(cm);
ConnManagerParams的功能被挪到了 ThreadSafeClientConnManager 和 HttpConnectionParams两个类:
static ConnPerRoute getMaxConnectionsPerRoute(HttpParams params)
Deprecated. use ThreadSafeClientConnManager.getMaxForRoute(org.apache.http.conn.routing.HttpRoute)
static int getMaxTotalConnections(HttpParams params)
Deprecated. use ThreadSafeClientConnManager.getMaxTotal()
static long getTimeout(HttpParams params)
Deprecated. use HttpConnectionParams.getConnectionTimeout(HttpParams)
static void setMaxConnectionsPerRoute(HttpParams params, ConnPerRoute connPerRoute)
Deprecated. use ThreadSafeClientConnManager.setMaxForRoute(org.apache.http.conn.routing.HttpRoute, int)
static void setMaxTotalConnections(HttpParams params, int maxTotalConnections)
Deprecated. use ThreadSafeClientConnManager.setMaxTotal(int)
static void setTimeout(HttpParams params, long timeout)
Deprecated. use HttpConnectionParams.setConnectionTimeout(HttpParams, int)
http://hc.apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html#d4e638
问题的表现:
tomcat服务在重启之后,短时间内会承受大量访问,由于这个时候缓存还没建立,每次访问都将消耗一定资源(数据库连接或者文件IO),并发量在2000左右的时候tomcat服务开始抛出大量Too Many Open Files的异常,主要是文件IO一块的异常,数据库连接池配置恰当就不会抛异常。
问题分析:
这是一个典型的文件句柄耗尽的异常,在linux里头“一切皆文件”,所以虽然提示“文件”打开太多,其实也有可能是socket打开太多或者设备打开太多。
文件句柄是用来干嘛的?
- 句柄当做一个人的名字,并"可以通过名字(句柄),实现对句子的引用和访问",感觉这种比喻好象贴近,但问题是可能有许多人叫同一个名字,但句柄所以有意义正因为它具有唯一性,所以这种比喻也有问题,如果换个说法,说句柄是表示你的进程与外界的i/o连接,把它当作是一个连接的名字是不是更恰当些? 文件句柄是一个十六位长度的二进制代码(Windows95后为32位无符号整数),代表一个已被打开文件的通道号,借助于这个句柄,你的应用程序即可使用这个相应的句柄对文件进行随意存取操作,说白了文件句柄其实是一串代表着特殊含义的号码; 当然其实系统是用句柄与一些资源联系起来的,当由系统管理,动态分配给你应用程序的某些资源的代号,你就可以使用句柄访问相应的资源了,尤其在Windows系统中,有很多东东都使用句柄,如窗口,socket。
简单看来程序通过句柄获得资源的引用,来进行资源的打开和关闭的操作。
为什么会出现文件句柄耗尽的情况?
主要是因为linux在文件句柄的数目上有两个级别的限制。一个是系统级别的总数限制,一个是针对用户的限制。默认情况下每个用户所能使用的句柄数是1024。一般情况下1024也够用了,但是在大容量的系统上,特别是会频繁使用网络通信和文件IO的系统上,1024很快就被耗光了。所以首先我们要调整这个值。修改方法如下:
- 1. ulimit -a 查看当前用户的文件句柄限制
- 2. 用户级别的句柄数限制修改。
- 修改 /etc/security/limits.conf 增加下面的代码:
- 用户名(或者用*表示所有用户) soft nofile 65535
- 用户名 hard nofile 65535
- 有两种限制,一种是soft软限制,在数目超过软限制的时候系统会给出warning警告,但是达到hard硬限制的时候系统将拒绝或者异常了。
- 修改之后可能需要重启shell生效。
- 3. 系统级别的句柄数限制修改。
- sysctl -w fs.file-max 65536
- 或者
- echo "65536" > /proc/sys/fs/file-max
- 两者作用是相同的,前者改内核参数,后者直接作用于内核参数在虚拟文件系统(procfs, psuedo file system)上对应的文件而已。
- 可以用下面的命令查看新的限制
- sysctl -a | grep fs.file-max
- 或者
- cat /proc/sys/fs/file-max
- 修改内核参数
- /etc/sysctl.conf
- echo "fs.file-max=65536" >> /etc/sysctl.conf
- sysctl -p
- 查看系统总限制 命令:cat /proc/sys/fs/file-max
- 查看整个系统目前使用的文件句柄数量命令:cat /proc/sys/fs/file-nr
- 查看某个进程开了哪些句柄 :lsof -p pid
- 某个进程开了几个句柄 :lsof -p pid |wc -l
- 也可以看到某个目录 /文件被什么进程占用了,显示已打开该目录或文件的所有进程信息 :lsof path/filename
具体这个值应该设置成多少?
优先级(Open File Descriptors):
soft limit < hard limit < kernel < 实现最大file descriptor数采用的数据结构所导致的限制
其实这个值倒是没有具体限制,但是分配的值如果太大反而会影响系统性能,所以要根据具体应用调配权衡。
问题的解决方案:
首先当然是修改linux句柄数限制到一个合适的值。
然后就是应用本身的一个调整。有这么几种情况:
1.数据库连接池的优化。必须要使用连接池,否则句柄没耗光数据库就崩了。。。
2.抓取资源的时候有可能会用到HttpClient,尽量也应该使用连接池来控制连接数。
关于HttpClient的连接池配置可以查看我另外一文:http://blog.csdn.net/shootyou/archive/2011/05/12/6415248.aspx
3.连接池设置的把握,建立连接超时时间,读取超时时间,连接数目,等待时间,等都需要配置到一个合适的值,否则发挥不出连接池的性能。