Connection和Keep-Alive

Http头中的Connection

Connection决定了请求结束后是否关闭网络连接,keep-alive不会关闭连接,后续请求可以复用该连接,close反之。

Http头中的Keep-Alive

Connection指令中除了close就是以逗号分隔的HTTP头,但是通常只有Keep-Alive。

Keep-Alive指令中的参数有timeout和max,用逗号隔开。

timeout单位为秒,表示空闲连接保持打开状态的最小时长。

max表示在连接关闭之前,在此连接可以发送的请求的最大值。

Spring Cloud Netflix Zuul的处理方式

在spring cloud netflix zuul(H版本)中ribbon发送请求的时候会默认过滤掉connection。

RibbonRoutingFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
protected RibbonCommandContext buildCommandContext(RequestContext context) {
HttpServletRequest request = context.getRequest();

// 这里,一
MultiValueMap<String, String> headers = this.helper
.buildZuulRequestHeaders(request);
MultiValueMap<String, String> params = this.helper
.buildZuulRequestQueryParams(request);
String verb = getVerb(request);
InputStream requestEntity = getRequestBody(request);
if (request.getContentLength() < 0 && !verb.equalsIgnoreCase("GET")) {
context.setChunkedRequestBody();
}

String serviceId = (String) context.get(SERVICE_ID_KEY);
Boolean retryable = (Boolean) context.get(RETRYABLE_KEY);
Object loadBalancerKey = context.get(LOAD_BALANCER_KEY);

String uri = this.helper.buildZuulRequestURI(request);

// remove double slashes
uri = uri.replace("//", "/");

long contentLength = useServlet31 ? request.getContentLengthLong()
: request.getContentLength();

return new RibbonCommandContext(serviceId, verb, uri, retryable, headers, params,
requestEntity, this.requestCustomizers, contentLength, loadBalancerKey);
}

ProxyRequestHelper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public MultiValueMap<String, String> buildZuulRequestHeaders(
HttpServletRequest request) {
RequestContext context = RequestContext.getCurrentContext();
MultiValueMap<String, String> headers = new HttpHeaders();
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
// 这里,二
if (isIncludedHeader(name)) {
Enumeration<String> values = request.getHeaders(name);
while (values.hasMoreElements()) {
String value = values.nextElement();
headers.add(name, value);
}
}
}
}
Map<String, String> zuulRequestHeaders = context.getZuulRequestHeaders();
for (String header : zuulRequestHeaders.keySet()) {
if (isIncludedHeader(header)) {
headers.set(header, zuulRequestHeaders.get(header));
}
}
if (!headers.containsKey(HttpHeaders.ACCEPT_ENCODING)) {
headers.set(HttpHeaders.ACCEPT_ENCODING, "gzip");
}
return headers;
}

ProxyRequestHelper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public boolean isIncludedHeader(String headerName) {
String name = headerName.toLowerCase();
RequestContext ctx = RequestContext.getCurrentContext();
if (ctx.containsKey(IGNORED_HEADERS)) {
Object object = ctx.get(IGNORED_HEADERS);
if (object instanceof Collection && ((Collection<?>) object).contains(name)) {
return false;
}
}
switch (name) {
case "host":
if (addHostHeader) {
return true;
}
// 这里,三
case "connection":
case "content-length":
case "server":
case "transfer-encoding":
case "x-application-context":
return false;
default:
return true;
}
}

你以为tomcat收到的请求的头就没有connection了吗,no no no。

2.3.9.png

我们看到,还是有connection的,值为Keep-Alive指令,请注意大小写,后面通过挖tomcat的代码就知道为什么了。

接下来我们先来看下客户端对于response中的keepalive是如何处理的。

ribbon下的httpclient中对于keepalive的处理逻辑

根据timeout设置连接有效时间,并立马标记可用,如果是keepalive,否则就标记不可用。

MainClientExec

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (reuseStrategy.keepAlive(response, context)) {
// Set the idle duration of this connection
final long duration = keepAliveStrategy.getKeepAliveDuration(response, context);
if (this.log.isDebugEnabled()) {
final String s;
if (duration > 0) {
s = "for " + duration + " " + TimeUnit.MILLISECONDS;
} else {
s = "indefinitely";
}
this.log.debug("Connection can be kept alive " + s);
}
connHolder.setValidFor(duration, TimeUnit.MILLISECONDS);
connHolder.markReusable();
} else {
connHolder.markNonReusable();
}

继续。。。

Tomcat的处理方式

tomcat会判断连接是否是keepalive(具体逻辑见代码),如果不是,会在headers中加上Connection: close,如果是keepalive并且不是http1.1(http 1.1默认开启),
就会在headers中加上Connection: Keep-Alive,就是上图中红色框框出来的地方,大小写都一样。

然后会返回Keep-Alive,超时时间会取keepAliveTimeout,具体配置见 https://tomcat.apache.org/tomcat-8.5-doc/config/http.html

Http11Processor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
if (keepAlive && statusDropsConnection(statusCode)) {
keepAlive = false;
}
if (!keepAlive) {
// Avoid adding the close header twice
if (!connectionClosePresent) {
headers.addValue(Constants.CONNECTION).setString(
Constants.CLOSE);
}
} else if (!getErrorState().isError()) {
if (!http11) {
headers.addValue(Constants.CONNECTION).setString(Constants.KEEP_ALIVE_HEADER_VALUE_TOKEN);
}

if (protocol.getUseKeepAliveResponseHeader()) {
boolean connectionKeepAlivePresent =
isConnectionToken(request.getMimeHeaders(), Constants.KEEP_ALIVE_HEADER_VALUE_TOKEN);

if (connectionKeepAlivePresent) {
int keepAliveTimeout = protocol.getKeepAliveTimeout();

if (keepAliveTimeout > 0) {
String value = "timeout=" + keepAliveTimeout / 1000L;
headers.setValue(Constants.KEEP_ALIVE_HEADER_NAME).setString(value);

if (http11) {
// Append if there is already a Connection header,
// else create the header
MessageBytes connectionHeaderValue = headers.getValue(Constants.CONNECTION);
if (connectionHeaderValue == null) {
headers.addValue(Constants.CONNECTION).setString(Constants.KEEP_ALIVE_HEADER_VALUE_TOKEN);
} else {
connectionHeaderValue.setString(
connectionHeaderValue.getString() + ", " + Constants.KEEP_ALIVE_HEADER_VALUE_TOKEN);
}
}
}
}
}
}

Spring boot 中的 keepAliveTimeout 取值

2.3.12的spring boot,使用tomcat做为web服务器,keepAliveTimeout默认值等于connectionTimeout,connectionTimeout默认值为60s。

AbstractHttp11Protocol

1
2
3
4
5
6
7
8
public AbstractHttp11Protocol(AbstractEndpoint<S,?> endpoint) {
super(endpoint);
// 这里
setConnectionTimeout(Constants.DEFAULT_CONNECTION_TIMEOUT);
ConnectionHandler<S> cHandler = new ConnectionHandler<>(this);
setHandler(cHandler);
getEndpoint().setHandler(cHandler);
}

Spring boot 中的 keepAliveTimeout 配置

目前2.3.x不支持,2.5.x会支持,具体见此pr https://github.com/spring-projects/spring-boot/pull/25815

在2.3.x中,如果实现WebServerFactoryCustomizer - TomcatServletWebServerFactory中的customize,能不能配置呢,答案是可以的。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class TomcatCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
private static final Log LOGGER = LogFactory.get(TomcatCustomizer.class);

@Override
@SuppressWarnings("rawtypes")
public void customize(TomcatServletWebServerFactory factory) {
factory.addConnectorCustomizers(connector -> {
AbstractHttp11Protocol protocol = (AbstractHttp11Protocol) connector.getProtocolHandler();
protocol.setKeepAliveTimeout(65 * 1000);
});
}
}

Spring boot 中的 maxKeepAliveRequests 配置

和keepAliveTimeout一样,配置文件暂不支持配置,那代码可以配置么?答案是不行的(。。。。)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class TomcatCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
private static final Log LOGGER = LogFactory.get(TomcatCustomizer.class);

@Override
@SuppressWarnings("rawtypes")
public void customize(TomcatServletWebServerFactory factory) {
factory.addConnectorCustomizers(connector -> {
AbstractHttp11Protocol protocol = (AbstractHttp11Protocol) connector.getProtocolHandler();
// 这里无法配置,详见下面解析
protocol.setMaxKeepAliveRequests(10);
});
}
}

为什么不能配置

其实set后endpoint中的值确实是10,但是在取值的时候会根据 是否有界 这个状态判断,如果是无界,就是1,默认状态就是无界的,所以看上去是无法配置。

AbstractEndpoint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private volatile BindState bindState = BindState.UNBOUND;

public void setMaxKeepAliveRequests(int maxKeepAliveRequests) {
this.maxKeepAliveRequests = maxKeepAliveRequests;
}

public int getMaxKeepAliveRequests() {
// Disable keep-alive if the server socket is not bound
if (bindState.isBound()) {
return maxKeepAliveRequests;
} else {
return 1;
}
}

总结

到这里,tomcat(9.0.x)的keepalive指令中的timeout和max的大体逻辑我们清楚了,如果不改服务的配置,这个值就是60s(spring boot 2.3.x)。

之前某版本的tomcat并不会返回keep-alive头,即禁用keep-alive,这个时候请求的时候会频繁出现 zuul org.apache.http.nohttpresponseexception:
192.168.1.116:8403 failed to respond, 因为bug,客户端自己给自己加上了Keep-Alive: timeout=4这样的头,客户端以为还可以重用这个连接,
事实上是使用了已关闭的连接。

提醒

将所有web服务器和7层代理服务器的keep-alive都设置成一样的时间,有助于减少wait的连接,提高宿主的性能。