请求链路各节点上,如何支持http1.1长连接(java技术栈)

文章目录

  • 一、请求端
    • (一)java内置的HttpURLConnection
    • (二)Apache HttpClient
  • 二、反向代理
    • (一)nginx与请求端保持长连接
    • (二)nginx与服务端(upstream)保持长连接
    • (三)F5 big-ip与前后端保持长连接
  • 三、防火墙
  • 四、服务端
  • 五、使用短连接导致的网络阻断
    • (一)超出防火墙SYN Rate Limit
    • (二)请求端的大量TIME_WAIT记录
    • (三)中间转发节点的大量NAT记录
    • (四)服务端的大量TIME_WAIT记录

一、请求端

请求端都会使用http连接池来维护与服务端的连接,类似的还有jedis连接池、jdbc datasource。

(一)java内置的HttpURLConnection

配置并使用连接池
java系统参数http.maxConnections控制HttpURLConnection为每个地址可维护的空闲tcp连接个数, 建议设置为处理线程池的大小;
但是业务代码要读取并清理完tcp通道里的response数据,HttpURLConnection才会把tcp连接供下个请求复用。

注意事项:

  1. response header里需要返回Keep-Alive,以指示请求端把空闲tcp连接保留多长时间;否则会等待服务端来关闭tcp连接,让服务端遗留TIME_WAIT记录
  2. 对同一个地址并发的请求数超过http.maxConnections设置,则请求还是正常建立新连接,但是完成请求后会立即关闭tcp连接
  3. 状态码400以下的response的body,使用getInputStream读取,否则使用getErrorStream读取;tcp通讯异常或服务端响应超时,则调用HttpURLConnection对象的disconnect方法,弃用当前tcp连接

(二)Apache HttpClient

Apache HttpClient除了像HttpURLConnection一样可以管理连接池,还能通过设置DefaultMaxPerRoute以限制对单个地址的最大并发连接数。

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

    @Bean
    @Primary
    public RestTemplate restTemplate(@Qualifier("httpClient") CloseableHttpClient httpClient,
            RestTemplateBuilder builder) {
        // 使用springboot自动初始化的builder,已加载各种RestTemplateRequestCustomizer
        return builder.requestFactory(() -> new HttpComponentsClientHttpRequestFactory(httpClient)).build();
    }

注意事项:

  1. CloseableHttpResponse实例需要关闭,才会把tcp连接进行复用;CloseableHttpClient对象不能关闭,否则连接池里的连接全部关闭
  2. 默认加载的DefaultConnectionKeepAliveStrategy解析到response header里Keep-Alive项的timeout值后,才会主动关闭空闲过久的tcp连接;否则等待服务端来关闭tcp连接,让服务端遗留TIME_WAIT记录
  3. 默认加载的DefaultHttpRequestRetryHandler会对get请求重试3次,如果域名解析失败或建立tcp连接失败则不会重试

二、反向代理

(一)nginx与请求端保持长连接

http {
    # nginx默认支持开启与请求端的长连接;
    # 一个tcp长连接上处理了1000个请求后,nginx就会返回Connection: close,让客户端主动断开tcp连接
    keepalive_requests 1000;
    # 通知客户端,空闲连接继续保持60s再关闭(Keep-Alive: timeout=60);
    # 如果客户端未按要求在60s时关闭空闲连接,nginx实际在90s后也会主动关闭此连接,把TIME_WAIT记录留在nginx节点上
    keepalive_timeout 90s 60s;
    # 要与请求端保持http1.1通讯,就不能关闭chunked机制;
    # 否则nginx会在完成响应后主动关闭与请求端的tcp连接
    chunked_transfer_encoding on;
}

注意事项:

  1. 关闭chunked_transfer_encoding,nginx等待upstream返回全部response body后,才能组装新的body和计算Content-Length;并在返回body给请求端后主动断开与请求端之间的tcp连接,使得TIME_WAIT记录留在nginx节点上,相当于降级为http1.0短连接
  2. 如果不设置keepalive_timeout的第二个参数来返回空闲长连接的超时信息,客户端通常不会主动关闭超时的空闲连接,而是等待nginx来关闭连接,进而把TIME_WAIT记录留在nginx节点上(服务端TIME_WAIT的危害见文末)

(二)nginx与服务端(upstream)保持长连接

http {
    # 尝试与upstream服务器建立tcp连接的最大耗时,默认值60s极为不合理,影响nginx排除死机节点的速度
    proxy_connect_timeout 3s;
    proxy_send_timeout 30s;
    proxy_read_timeout 300s;

    upstream myapp {
        # nginx与upstream服务器之间的空闲连接最大数量(必须配置)
        keepalive 10;
        keepalive_requests 1024;
        # 与upstream的空闲连接保持多久后,主动断开连接
        keepalive_timeout 15s;
        server 10.10.0.8:8080;
        server 10.10.0.9:8080;
    }

    server {
        listen 80 default;

        location / {
            proxy_pass http://myapp;
            # 指定HTTP 1.1协议,并且Connection不为Close时,才会与upstream保持长连接
            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};
        }
    }
}

注意事项:

  1. upstream里返回的response header里需要包含Keep-Alive信息,指示nginx等待多久关闭与upstream的空闲连接,否则nginx会在60s后关闭空闲连接(keepalive_timeout的默认值)
  2. Keep-Alive属于hop-by-hop header,nginx不会把upstream传来的此信息原封不动传递到请求端,而是会使用keepalive_timeout配置里的信息
  3. 请求端的响应等待超时时间,如果短于proxy_connect_timeout,则请求端主动关闭超时连接时,也会触发nginx立即把当前等待建立的连接置为TIME_WAIT,nginx在日志打印499响应状态
  4. 和HttpURLConnection的行为类似,如果对服务端(upstream)的并发的请求数超过keepalive设置,则请求还是正常建立新连接,但是完成请求后会立即关闭tcp连接

(三)F5 big-ip与前后端保持长连接

f5用作7层负载的时候,与后端的tcp连接,并没有使用连接池管理,而是与请求端的连接一一对应;
并且f5会保留并传递response header里的Connection、Keep-Alive等信息。

只要服务端、请求端都不主动发起FIN操作,那么F5就会保持两端空闲的tcp连接。

三、防火墙

防火墙一般会有几个限制,影响tcp连接的建立或者保持:

  1. 空闲tcp连接保留时长(一般至少10分钟);请求端的响应超时配置、空闲长连接保持时间都需短于这个时长,以避免因tcp连接被防火墙默默阻断而发生的通讯异常
  2. 最大tcp连接数(比如限制单个请求IP最多同时存在20个连接);如果和服务端有较大的并发,需把请求IP告知服务端防火墙管理员,增大请求IP的可用并发

注意事项:

  1. 防火墙如使用reject策略阻断超出的并发连接,则请求端会收到连接拒绝信息,报错是connect refused
  2. 防火墙如使用drop策略,则请求端会一直等待连接超时后再主动放弃连接,报错是connect timed out

四、服务端

springboot默认使用tomcat;
tomcat默认使用并保持长连接20秒
修改默认配置(演示代码里配置的参数实际是默认值):

@Configuration
public class HttpApplication implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
	@Override
	public void customize(TomcatServletWebServerFactory factory) {
		factory.addConnectorCustomizers((connector) -> {
			if (connector.getProtocolHandler() instanceof Http11NioProtocol) {
				Http11NioProtocol protocolHandler = (Http11NioProtocol) connector.getProtocolHandler();
				// 接收连接后,等待request header到达的时间
				protocolHandler.setConnectionTimeout(20000);
				protocolHandler.setUseKeepAliveResponseHeader(true);
				protocolHandler.setKeepAliveTimeout(60000);				
				protocolHandler.setMaxKeepAliveRequests(100);
			}
		});

	}
}

五、使用短连接导致的网络阻断

(一)超出防火墙SYN Rate Limit

WAF/NGFW会限制单个请求IP在单位时间内发起tcp连接的数量,TPS高且采用短连接就会超过限制

(二)请求端的大量TIME_WAIT记录

成功开启tcp_tw_reuse(握手时两端还需都开启tcp_timestamps),则客户端能够复用本地TIME_WAIT表里存在超过1s的socket地址;
否则,如果可用请求端口与服务地址组成的socket已经全部存在于本地TIME_WAIT表里的话,客户端直接放弃建立新的socket。

(三)中间转发节点的大量NAT记录

比如k8s的nodePort进行转发时,需要把请求端地址和转发目标POD的地址进行记录;
如果大量进行短连接,那么作为中间转发节点的conntrack列表里,也会遗留大量TIME_WAIT记录。

# 查看NAT记录最大数、当前记录数
cat /proc/sys/net/netfilter/nf_conntrack_max && cat /proc/sys/net/netfilter/nf_conntrack_count
# 查看NAT明细
cat /proc/net/nf_conntrack

(四)服务端的大量TIME_WAIT记录

由服务端主动断开tcp连接的话,TIME_WAIT记录留在linux服务端1分钟;同ip的请求端如果使用相同的请求端口再次发起请求,连接请求有几率被服务端默默忽略(请求端SYN包里的ISN小于服务端TIME_WAIT记录的SN),引起请求端Connection timed out错误。尤其是位于F5后面的nginx,nginx收到的所有请求都来自同一个F5的源IP,更容易出现这种情况。
即使tcp协议规定了ISN是随机且递增的,但如果旧请求的平均数据上传速度超过250KB/s或者ISN重头开始计数,那么从同一请求端口发起的新请求还是会被服务端TIME_WAIT记录阻断(未成功开启PAWS机制的情况下):

   RFC 793 [RFC0793] suggests that the choice of the ISN of a connection is not arbitrary, but aims to reduce the chances of a stale segment
   from being accepted by a new incarnation of a previous connection.
   RFC 793 [RFC0793] suggests the use of a global 32-bit ISN generator that is incremented by 1 roughly every 4 microseconds.

如果整个链路上所有节点都配置好了对http1.1的支持,那么tcp连接的断开都是由请求端进行,不会有服务端存在大量TIME_WAIT的情况。

你可能感兴趣的:(java,微服务,java,开发语言)