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); 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。
我们看到,还是有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)) { 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) { 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) { 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 () { 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的连接,提高宿主的性能。