TIME_WAIT 48
CLOSE_WAIT 2228
ESTABLISHED 86
常用的三个状态是:ESTABLISHED 表示正在通信,TIMEWAIT 表示主动关闭,CLOSEWAIT 表示被动关闭。关于closewait和timewait,tcp中的交互图:
http交互图:
MSL是Maximum Segment Lifetime英文的缩写,中文可以译为“报文最大生存时间”,他是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。
因为tcp报文(segment)是ip数据报(datagram)的数据部分,具体称谓请参见《数据在网络各层中的称呼》一文,而ip头中有一个TTL域,TTL是time to live的缩写,
中文可以译为“生存时间”,这个生存时间是由源主机设置初始值但不是存的具体时间,而是存储了一个ip数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减1,
当此值为0则数据报将被丢弃,同时发送ICMP报文通知源主机。RFC 793中规定MSL为2分钟,实际应用中常用的是30秒,1分钟和2分钟等。
2MSL即两倍的MSL,TCP的TIME_WAIT状态也称为2MSL等待状态,当TCP的一端发起主动关闭,在发出最后一个ACK包后,即第3次握手完成后发送了第四次握手的ACK包后就进入了TIME_WAIT状态,
必须在此状态上停留两倍的MSL时间,等待2MSL时间主要目的是怕最后一个ACK包对方没收到,那么对方在超时后将重发第三次握手的FIN包,主动关闭端接到重发的FIN包后可以再发一个ACK应答包。
在TIME_WAIT状态时两端的端口不能使用,要等到2MSL时间结束才可继续使用。当连接处于2MSL等待阶段时任何迟到的报文段都将被丢弃。不过在实际应用中可以通过设置SO_REUSEADDR选项达到
不必等待2MSL时间结束再使用此端口。
需要注意的是,对于基于TCP的HTTP协议,关闭TCP连接的是Server端,这样,Server端会进入TIMEWAIT状态,可想而知,对于访问量大的Web Server,会存在大量的TIMEWAIT状态,假如server一秒钟接收1000个请求,那么就会积压240*1000=240,000个 TIMEWAIT的记录,维护这些状态给Server带来负担。当然现代操作系统都会用快速的查找算法来管理这些TIMEWAIT,所以对于新的 TCP连接请求,判断是否hit中一个TIME_WAIT不会太费时间,但是有这么多状态要维护总是不好。HTTP协议1.1版规定default行为是Keep-Alive,也就是会重用TCP连接传输多个 request/response,一个主要原因就是发现了这个问题。
也就是说当服务器上出现大量TIMEWAIT时,可能是该服务器作为别的服务器的客户端rpc访问时别的服务器,在关闭连接时进入了TIMEWAIT状态,这种情况是对方的连接出现了异常。另一种可能是该服务器是一台http服务器,对于大量访问时,会出现大量的TIMEWAIT。这种情况下需要让服务器能够快速回收和重用那些TIMEWAIT的资源,可以通过修改/etc/sysctl.conf中的参数来进行,具体可以参考:https://blog.csdn.net/shootyou/article/details/6622226
服务器A是一台爬虫服务器,它使用简单的HttpClient去请求资源服务器B上面的资源,正常情况下,如果请求成功,需要关闭时服务器A会主动发出关闭连接的请求,这个时候就是主动关闭连接,服务器A的连接状态我们可以看到是TIME_WAIT。
如果一旦发生异常呢?假设请求的资源服务器B上并不存在,那么这个时候就会由服务器B发出关闭连接的请求,服务器A就是被动的关闭了连接,如果服务器A被动关闭连接之后程序员忘了让HttpClient释放连接,那就会造成CLOSE_WAIT的状态了。
这里我理解的CLOSE_WAIT就是服务端被动关闭时没有及时释放连接或客户端连接池在连接被动关闭时没有及时释放连接。出现这种问题最大的可能就是代码的问题。
TIME_WAIT 56
CLOSE_WAIT 2780
ESTABLISHED 86
可以看到CLOSE_WAIT的数量非常多,可能是因为程序中在凌晨五点时会有一个定时任务,通过httpclient去爬取多个不同网站的资讯信息,有些响应信息被异常关闭,而客户端没有及时释放连接导到的C
windows中的命令:
需要注意几点(主要针对httpclient 4.0以上版本):
// 创建Httpclient对象
CloseableHttpClient httpclient = HttpClients.createDefault();
String resultString = "";
CloseableHttpResponse response = null;
HttpGet httpGet = null;
try {
// 创建uri
URIBuilder builder = new URIBuilder(url);
if (param != null) {
for (String key : param.keySet()) {
builder.addParameter(key, param.get(key));
}
}
URI uri = builder.build();
// 创建http GET请求
httpGet = new HttpGet(uri);
if (headers != null) {
httpGet.setHeaders(headers);
}
// 执行请求
response = httpclient.execute(httpGet);
// 判断返回状态是否为200
if (response.getStatusLine().getStatusCode() == 200) {
resultString = EntityUtils.toString(response.getEntity(), encoding);
}
} catch (Exception e) {
LOG.error("连接异常!",e);
} finally {
try {
if (response != null) {
response.close();
}
httpclient.close();
} catch (IOException e) {
LOG.error("IO异常!",e);
}
}
return resultString;
可以看出,主要有几个一眼看出的缺点:(1)直接调用HttpClients.createDefault(),而没有使用连接池; (2)在出现连接异常时,并没有关闭连接,会导致很多的CLOSE_WAIT;
先将上面代码异常处理部分修改成如下:
...
} catch (Exception e) {
if (httpGet != null){
//关闭连接
httpGet.abort();
}
LOG.error("连接异常!",e);
} finally {
try {
if (response != null) {
response.close();
}
httpclient.close();
} catch (IOException e) {
LOG.error("IO异常!",e);
}
}
运行一段时间后,让定时任务执行时间提前,运行一段时间后,执行netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}',结果如下:
TIME_WAIT 48
CLOSE_WAIT 50
ESTABLISHED 86
可见,应该是在去爬取数据时,有些服务器拒绝了请求,导致httpclient中抛出了异常,而没有及时关闭这些异常,引起了大量的CLOSE_WAIT出现。
针对上面的代码,是每个连接只使用一次的,还可以设置一些超时时间:
直接上代码:
连接池代码:
public class ApacheHttpClientManager {
private static final Logger LOGGER = LoggerFactory.getLogger(ApacheHttpClientManager.class);
// 连接池管理器
private volatile PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = null;
private static final ApacheHttpClientManager httpclientManager = new ApacheHttpClientManager();
// 请求处理器
private HttpRequestRetryHandler httpRequestRetryHandler = new HttpRequestRetryHandler() {
@Override
public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
if (executionCount >= 3) {// 如果已经重试了3次,就放弃
return false;
}
if (exception instanceof NoHttpResponseException) {// 如果服务器丢掉了连接,那么就重试
return true;
}
if (exception instanceof SSLHandshakeException) {// 不要重试SSL握手异常
return false;
}
if (exception instanceof InterruptedIOException) {// 超时
return false;
}
if (exception instanceof UnknownHostException) {// 目标服务器不可达
return false;
}
if (exception instanceof ConnectTimeoutException) {// 连接被拒绝
return false;
}
if (exception instanceof SSLException) {// SSL握手异常
return false;
}
HttpClientContext clientContext = HttpClientContext.adapt(context);
HttpRequest request = clientContext.getRequest();
// 如果请求是幂等的,就再次尝试
if (!(request instanceof HttpEntityEnclosingRequest)) {
return true;
}
return false;
}
};
// 连接存活策略
ConnectionKeepAliveStrategy myStrategy = new ConnectionKeepAliveStrategy() {
@Override
public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
// Honor 'keep-alive' header
HeaderElementIterator it = new BasicHeaderElementIterator(response.headerIterator(HTTP.CONN_KEEP_ALIVE));
while (it.hasNext()) {
HeaderElement he = it.nextElement();
String param = he.getName();
String value = he.getValue();
if (value != null && param.equalsIgnoreCase("timeout")) {
try {
return Long.parseLong(value) * 1000;
} catch (NumberFormatException ignore) {
}
}
}
HttpHost target = (HttpHost) context.getAttribute(HttpClientContext.HTTP_TARGET_HOST);
if ("www.naughty-server.com".equalsIgnoreCase(target.getHostName())) {
// Keep alive for 5 seconds only
return 5 * 1000;
} else {
// otherwise keep alive for 30 seconds
//return 30 * 1000;
return 0;
}
}
};
// 锁
private static final Object LOCK = new Object();
private ApacheHttpClientManager() {
}
public static ApacheHttpClientManager getInstance() {
return httpclientManager;
}
/**
*
* @param maxTotal
* 最大连接数
* @param maxPerRoute
* 每个路由的最大连接数
* @param maxRoute
* 主机的最大路由数
* @param hostname
* 主机名或ip地址
* @param port
* 端口
* @return 连接池管理器
*/
private PoolingHttpClientConnectionManager getHttpClientPoolManager(int maxTotal, int maxPerRoute, int maxRoute,
String hostname, int port) {
ConnectionSocketFactory plainsf = PlainConnectionSocketFactory.getSocketFactory();
LayeredConnectionSocketFactory sslsf = SSLConnectionSocketFactory.getSocketFactory();
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory> create()
.register("http", plainsf).register("https", sslsf).build();
PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(
registry);
// 将最大连接数增加
poolingHttpClientConnectionManager.setMaxTotal(maxTotal);
// 将每个路由基础的连接增加
poolingHttpClientConnectionManager.setDefaultMaxPerRoute(maxPerRoute);
HttpHost httpHost = new HttpHost(hostname, port);
// 将目标主机的最大连接数增加
poolingHttpClientConnectionManager.setMaxPerRoute(new HttpRoute(httpHost), maxRoute);
return poolingHttpClientConnectionManager;
}
/**
* 初始化连接池管理器
*
* 此处解释下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。
*
* @see http://jinnianshilongnian.iteye.com/blog/2089792#comments
* @param maxTotal
* @param maxPerRoute
* @return PoolingHttpClientConnectionManager
*/
public PoolingHttpClientConnectionManager initClientPoolManager(int maxTotal, int maxPerRoute) {
if (poolingHttpClientConnectionManager == null) {
synchronized (LOCK) {
if (poolingHttpClientConnectionManager == null) {
ConnectionSocketFactory plainsf = PlainConnectionSocketFactory.getSocketFactory();
LayeredConnectionSocketFactory sslsf = SSLConnectionSocketFactory.getSocketFactory();
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory> create()
.register("http", plainsf).register("https", sslsf).build();
poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(registry);
// 将最大连接数增加
poolingHttpClientConnectionManager.setMaxTotal(maxTotal);
// 将每个路由基础的连接增加
poolingHttpClientConnectionManager.setDefaultMaxPerRoute(maxPerRoute);
// HttpHost httpHost = new HttpHost("localhost", 10012);
// 将目标主机的最大连接数增加
// poolingHttpClientConnectionManager.setMaxPerRoute(new
// HttpRoute(httpHost), 50);
}
//剔除无用连接
IdleConnectionMonitorThread idleConnectionMonitorThread = new IdleConnectionMonitorThread(poolingHttpClientConnectionManager);
idleConnectionMonitorThread.setDaemon(true);
idleConnectionMonitorThread.start();
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
idleConnectionMonitorThread.shutdown();
poolingHttpClientConnectionManager.close();
//poolingHttpClientConnectionManager.closeExpiredConnections();
}
}));
}
}
return poolingHttpClientConnectionManager;
}
/**
* 获取httpclient
*
* @see RequestConfig#getConnectionRequestTimeout()
* @see RequestConfig#getConnectTimeout()
* @see RequestConfig#getSocketTimeout()
* @return httpClient
*/
public CloseableHttpClient getHttpClient() {
int CONNECTION_TIMEOUT = 5 * 1000; // 设置请求超时5秒钟 根据业务调整
int SO_TIMEOUT = 5 * 1000; // 设置等待数据超时时间5秒钟 根据业务调整
// 定义了当从ClientConnectionManager中检索ManagedClientConnection实例时使用的毫秒级的超时时间
int CONN_MANAGER_TIMEOUT = 500; // 该值就是连接不够用的时候等待超时时间,一定要设置,而且不能太大 ()
// @see RequestConfig#getConnectionRequestTimeout()
RequestConfig requestConfig = RequestConfig.custom().setConnectionRequestTimeout(CONN_MANAGER_TIMEOUT)
.setConnectTimeout(CONNECTION_TIMEOUT).setSocketTimeout(SO_TIMEOUT).build();
// 初始化连接池管理器
initClientPoolManager(200, 6);
CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(poolingHttpClientConnectionManager)
.setRetryHandler(httpRequestRetryHandler)// 共用一个请求重试处理器
.setDefaultRequestConfig(requestConfig).setKeepAliveStrategy(myStrategy) // 相同的连接存活策略
.build();
if (poolingHttpClientConnectionManager != null && poolingHttpClientConnectionManager.getTotalStats() != null) {
LOGGER.info("now client pool " + poolingHttpClientConnectionManager.getTotalStats().toString());
}
return httpClient;
}
}
最好设置connectTimeout、socketTimeout,可以防止阻塞,在上面的getHttpClient()方法中已经对这两个参数设置了默认值,使用时可以根据实际情况进行修改。关于这两个参数,可以参考:https://blog.csdn.net/wangjun5159/article/details/78140648