大量TCP连接滞留TIME_WAIT、SYN_SENT、CLOSE_WAIT状态的分析


本文记录在nginx、tomcat服务器上一些处理异常TCP连接的方案

一、统计各类状态的tcp连接数量

ss、netstat两个工具都能统计:

ss -ant | awk '{print $1}' | sort | uniq -c

netstat -ant | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

二、TIME_WAIT

这种状态是正常的完结状态,但是要尽量把完结状态留在发起请求的客户端上,并使用长连接来减少数量

应用服务器上,来自反向代理的连接

原因:从nginx发起的请求,申明的是http 1.0版本的协议(或者请求头的Connection字段指是Close),则tomcat响应完请求后会主动断开tcp连接

方案:nginx http_proxy模块的proxy_http_version配置默认使用http 1.0协议访问upstream实例,需要修改为1.1

proxy_connect_timeout 3s;
proxy_http_version 1.1;
# 通知客户端,连接保持60s;服务端实际在75s后才会主动关闭连接;
# 如果不设置第二个参数来返回空闲长连接的超时建议,
# 有的客户端不会利用http连接池来长期保持空闲连接,
# 有的客户端会使用一个默认的空闲连接断开时间
keepalive_timeout 75s 60s;
# 要与请求端保持http1.1通讯,就不能关闭chunked机制;
# 否则nginx会在完成响应后主动关闭与请求端的tcp连接,相当于退化为http1.0协议
chunked_transfer_encoding on;

upstream myapp {
    # nginx与upstream服务器之间的空闲长连接数量(默认最多保持60s)
    keepalive 20;
    server 10.0.0.1:8080;
    server 10.0.0.2:8080;
}

server {
    listen 80 default;
 
    location / {
        proxy_pass http://myapp;
        # 请求头指定HTTP 1.1协议,并且Connection不为Close时,
        # 对方完成响应后才不会主动断开TCP连接
        proxy_http_version 1.1;
        proxy_set_header Connection '';
        proxy_set_header Cookie $http_cookie;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For ${proxy_add_x_forwarded_for};
    }
}

反向代理上,访问应用服务的连接

原因:nginx使用http 1.1协议访问upstream实例后,如果未开启空闲连接复用机制,就会主动关闭tcp连接

方案:nginx upstream模块的keepalive配置默认未开启,需要主动提供一个数值

反向代理上,来自用户的连接

原因1:在请求端(浏览器、http请求框架)默认开启连接池并使用http 1.1协议的情况下,如果nginx关闭了http 1.1协议的chunked_transfer_encoding机制,那么在完成请求后,nginx会主动断开与请求端的连接

方案:不要关闭chunked_transfer_encoding

原因2:未返回建议客户端保持连接的时长(response header里的Keep-Alive: timeout=time),导致用户的客户端迟迟不断开空闲连接,最终由nginx来主动断开连接,把TIME_WAIT留在了nginx服务器上

方案keepalive_timeout配置最长空闲时间和建议客户端保持连接的时长,让客户端知道应该在什么时间之前关闭空闲连接

三、SYN_SENT

反向代理上,访问位于防火墙另一侧的目标

原因:telnet目标端口时,命令阻塞(未立即得到目标未开通此端口的响应),证明SYN包被防火墙drop了

方案:申请防火墙策略

反向代理上,访问无防火墙阻断的目标

原因1:目标tomcat服务器已接收(springboot应用的server.tomcat.max-connections配置,默认10000)的http连接数量、在服务端口排队等待accept(操作系统的net.core.somaxconn配置,默认128或1024)的tcp socket数量,都达到上限后,后续到达服务端口的SYN包会被丢弃,请求端的连接状态保持为SYN_SENT

方案:在使用webflux、websocket等响应式IO框架时,可调大server.tomcat.max-connections配置

原因2:telnet目标端口时,命令阻塞(未立即得到目标未开通此端口的响应),证明目标服务器上使用iptables对访问服务端口的请求进行了DROP处理

方案:使用iptables规则把请求方IP加入放行名单

原因3:应用服务的进程已处理的文件句柄(包含tcp socket)数量超过限额

# 查看当前用户下单进程的文件句柄限额
ulimit -n

方案:编辑/etc/security/limits.conf文件,重启应用服务进程

四、CLOSE_WAIT

应用服务器上,来自反向代理的连接

原因:应用程序开了端口,但是后续初始化失败(比如没有成功连接配置中心、服务注册中心、数据库等原因),accept socket的逻辑没运行起来;
已建立的请求放在服务端口待accept的backlog(操作系统的net.core.somaxconn配置)里,收到的请求内容放在操作系统tcp buffer里;
迟迟得不到应用程序处理并响应后,客户端发出FIN指令,服务端响应ACK后,服务端连接进入CLOSE_WAIT状态,由于tcp buffer里的数据没有被处理,所以服务端没有继续回复FIN,连接以CLOSE_WAIT状态滞留在服务端口待accept的backlog里;
在backlog塞满之前,应用服务端口实际处于可以连接但是不能响应的假死状态

方案:对部署的应用进行readyness定时探测,及时发现未成功初始化的应用

应用服务器上,访问外部服务的连接

原因:使用Apache HttpClient提供的连接池管理http长连接时,如果是服务端先断开连接,则应用服务器上的连接进入CLOSE_WAIT状态,由于没有默认定时任务处理这种socket来完成后续挥手,就会把这个中间状态的连接一直保留在应用服务器上;如果连接池配置不当,HttpClient甚至有可能使用这种服务端注定不会有响应的socket来发起请求(主要是HttpClient不是每次请求前都检查连接状态),得到org.apache.http.NoHttpResponseException

方案
httpclient的执行机制如下:

HttpClient.doExecute 初始化连接失败、请求超时、post请求,默认不重试;否则重试3次
    AbstractConnPool.lease  反复获取连接,除非达到了连接数上限
        getPoolEntryBlocking
            entry.isExpired  先用旧连接,只有这个连接上次收到的response包含Keep-Alive:timeout=?,才进行过期判断
            connFactory.create  如果没有可用旧连接,就建立一个新的TCP连接
        validate(leasedEntry)  这个连接上次使用时间距离现在超过ValidateAfterInactivity时长,才检查一下连接状态是否正常
    HttpRequestExecutor.execute

只有isExpired、validate两个判断都跳过,才会使用CLOSE_WAIT的旧连接;
避免isExpired判断被跳过,需要服务端返回的response包含Keep-Alive:timeout=?,或者在HttpClient配置自定义的ConnectionKeepAliveStrategy来提供默认timeout;
想减少validate判断被跳过,就不要把连接池的ValidateAfterInactivity值设置太大;
并使用IdleConnectionEvictor来定时扫描并主动关闭超时的空闲连接(HttpClientConnectionManager并没有自动启动后台线程进行空闲连接的定期关闭):

	@Bean
	@Primary
	public HttpClientConnectionManager connectionManager() {
		PoolingHttpClientConnectionManager poolingConnManager = new PoolingHttpClientConnectionManager();
		poolingConnManager.setMaxTotal(400);
		poolingConnManager.setDefaultMaxPerRoute(200);
		poolingConnManager.setValidateAfterInactivity(1000);
		return poolingConnManager;
	}
	
	@Bean(initMethod = "start", destroyMethod = "shutdown")
	@Primary
	public IdleConnectionEvictor idleConnectionEvictor(HttpClientConnectionManager connectionManager) {
		// 2秒关闭一轮timeout达标的空闲连接
		return new IdleConnectionEvictor(connectionManager, 2, TimeUnit.SECONDS, 60, TimeUnit.SECONDS);
	}

	@Bean("httpClient")
	@Primary
	public CloseableHttpClient httpClientRequestConfig(HttpClientConnectionManager connectionManager) {
		// 5秒内未获取空闲连接则直接失败;
		// 建立连接时,3秒超时;
		// 发起请求后,30秒未获取响应头则超时;
		RequestConfig defaultRequestConfig = RequestConfig.custom().setConnectionRequestTimeout(5000)
				.setConnectTimeout(3000).setSocketTimeout(30000).build();
		HttpClientBuilder builder = HttpClientBuilder.create().disableAutomaticRetries()
				.setConnectionManager(connectionManager)
				.setDefaultRequestConfig(defaultRequestConfig)
				.setKeepAliveStrategy(new ConnectionKeepAliveStrategy() {
					@Override
					public long getKeepAliveDuration(final HttpResponse response, final HttpContext context) {
						long timeoutMillis = DefaultConnectionKeepAliveStrategy.INSTANCE.getKeepAliveDuration(response,
								context);
						// 空闲连接,10秒关闭(响应头Keep-Alive未提供timeout值时)
						if (timeoutMillis < 0) {
							return 10 * 1000;
						}
						return timeoutMillis;
					}
				});
		return builder.build();
	}

使用时HttpClient时要及时关闭CloseableHttpResponse,以归还连接到连接池:

	@Autowired
	@Qualifier("httpClient")
	private CloseableHttpClient httpClient;

	public String test() throws ClientProtocolException, IOException {
		try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
			return EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
		}
	}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值