Table of Contents generated with
问题
线上服务大量请求异常,登录到机器发现主要日志如下
I/O exception (java.net.NoRouteToHostException) caught when processing request to {}->http://xxx.com:80: Cannot assign requested address (Address not available)
看日志查询了下,发现可能是服务器连接打满了()
登录到机器查看连接数
发现了大量的连接处于CLOSE WAIT状态
netstat -antp | grep CLOSE_WAIT
比较严重的机器大概有十几万个CLOSE WAIT连接
排查路径
1、初步定位问题
找到这些连接对应的远端ip,以及找到对应服务,定位到对应调用代码
# 根据远端ip, sort并去重排序
netstat -antp | grep CLOSE_WAIT | awk '{print $5}' | sort | uniq -c
发现有问题连接主要集中在某个服务上的ip上,我们服务调用该服务主要通过HTTP方式 HttpClient使用Spring RestTemplate,请求代码如下
HttpHeaders headers = buildGetDefaultHttpHeader();
HttpEntity<String> httpEntity = new HttpEntity<>(headers);
HttpClientBuilder builder =
HttpClientBuilder.create().setRetryHandler(new DefaultHttpRequestRetryHandler(3, false));
HttpClient httpClient = builder.build();
ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
RestTemplate restTemplate = new RestTemplate(requestFactory);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, httpEntity, String.class);
2、发现问题
每次请求都会创建一个RestTemplate,对应着会创建一个HttpClient以及连接池对象,会带来以下三个问题:
性能开销:每次创建新的 RestTemplate 实例时,都需要初始化连接管理器、HTTP客户端等相关资源。这会增加系统的性能开销,导致请求处理速度变慢。
资源浪费:每个 RestTemplate 实例都会维护自己的连接池。如果每次请求都创建新的实例,那么每个实例的连接池都可能只被使用一次,造成资源浪费。
连接过多:由于每个 RestTemplate 实例都有自己的连接池,如果每次请求都创建新的实例,可能会导致大量的连接被创建,从而消耗系统资源,甚至导致系统崩溃。
连接发生泄露情况:由于每次都会新建 RestTemplate 实例,对应的连接池(PoolingHttpClientConnectionManager中的CPool)中的连接在对象回收时可能无法被正确释放, PoolingHttpClientConnectionManager的finalize方法虽然会进行shutdown,但对象回收是finalize并不是可靠执行的,所以可能存在连接没有被正确释放的情况。
为了避免这些问题,需要使用单例模式创建 RestTemplate 实例,该实例是线程安全的,且这样可以复用连接池和HTTP客户端资源,提高性能并减少资源浪费。 在Spring框架中,可以通过将 RestTemplate 定义为一个Bean来实现单例模式:
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate(RestTemplateConfig.newClientHttpRequestFactory());
return restTemplate;
}
// ...
}
抓包分析
使用tcpdump抓包
tcpdump -i eth0 tcp and host 需要抓包的host -w request.cap
经过200s后,被HTTP请求的服务端主动发送FIN标志位数据包请求断开连接,服务端回了ACK,但没有再发送FIN
3、解决问题
结合服务情况,每次请求获取RestTemplate单例
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate(RestTemplateConfig.newClientHttpRequestFactory());
restTemplate.setInterceptors(Lists.newArrayList(new CatHttpProcessor()));
return restTemplate;
}
public static ClientHttpRequestFactory newClientHttpRequestFactory() {
// 长连接保持时长30秒
PoolingHttpClientConnectionManager pollingConnectionManager =
new PoolingHttpClientConnectionManager(30, TimeUnit.SECONDS);
// 最大连接数
pollingConnectionManager.setMaxTotal(3000);
// 单连接的并发数
pollingConnectionManager.setDefaultMaxPerRoute(100);
HttpClientBuilder httpClientBuilder = HttpClients.custom();
httpClientBuilder.setConnectionManager(pollingConnectionManager);
// 如果不执行下面两行,即使PoolingHttpClientConnectionManager设置了闲置连接存活时间,也不会主动回收
// 只有执行下面两行,才会启动 过期和闲置连接释放的线程,具体逻辑可查看HttpClientBuilder#build
httpClientBuilder.evictExpiredConnections();
httpClientBuilder.evictIdleConnections(30, TimeUnit.SECONDS);
// 当失败时重试次数3次
// 当发生InterruptedIOException UnknownHostException ConnectException SSLException,不会进行重试
httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(3, false));
// 保持长连接配置,需要在头添加Keep-Alive
httpClientBuilder.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy());
HttpClient httpClient = httpClientBuilder.build();
// httpClient连接底层配置clientHttpRequestFactory
HttpComponentsClientHttpRequestFactory clientHttpRequestFactory =
new HttpComponentsClientHttpRequestFactory(httpClient);
// 主要分为三种超时时间,1、获取连接超时 connectionRequestTimeout 2、连接target超时 connectTimeout
// 3、读取响应超时,通常抛出SocketTimeoutException
clientHttpRequestFactory.setConnectTimeout(3000);
clientHttpRequestFactory.setReadTimeout(3000);
return clientHttpRequestFactory;
}
}
改造后效果
减少一些机器负载,数据上少了很多毛刺,机器上CLOSE_WAIT也基本没了,存在少量TIME_WAIT,比较符合预期
4、扩展问题
TCP状态扩展
CLOSE_WAIT通常发生在连接被动关闭端,详细可查看TCP四次挥手流程,如下图
用中文来描述下这个过程:
Client: 服务端大哥,我事情都干完了,准备撤了,这里对应的就是客户端发了一个FIN
Server:知道了,但是你等等我,我还要收收尾,这里对应的就是服务端收到 FIN 后回应的 ACK
经过上面两步之后,服务端就会处于 CLOSE_WAIT 状态。过了一段时间 Server 收尾完了
Server:小弟,哥哥我做完了,撤吧,服务端发送了FIN
Client:大哥,再见啊,这里是客户端对服务端的一个 ACK
到此服务端就可以跑路了,但是客户端还不行。为什么呢?客户端还必须等待 2MSL 个时间,这里为什么客户端还不能直接跑路呢?主要是为了防止发送出去的 ACK 服务端没有收到,服务端重发 FIN 再次来询问,如果客户端发完就跑路了,那么服务端重发的时候就没人理他了。这个等待的时间长度也很讲究。
Maximum Segment Lifetime 报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃
这里一定不要被图里的 client/server 和项目里的客户端服务器端混淆,你只要记住:主动关闭的一方发出 FIN 包(Client),被动关闭(Server)的一方响应 ACK 包,此时,被动关闭的一方就进入了 CLOSE_WAIT 状态。如果一切正常,稍后被动关闭的一方也会发出 FIN 包,然后迁移到 LAST_ACK 状态。
出现了大量TIME_WAIT连接,主要是什么原因?