这篇文章主旨在于讲明白:
- 什么是http的keepalive
- keepalive在客户端和服务端都有什么不同表现,影响如何(Httpclient/4与tomcat/8为例)
- keepalive是通过什么方式实现链接复用的
先上一张图,回顾下基本知识,这张图包含了一个完整的TCP链接的生命周期。这张图内容很丰富,每个节点都要看仔细咯,不能忽略。
现在一般默认都是用HTTP/1.1 进行网络访问,Http/1.1是支持长连接的,一个连接可以被多次使用,发起不同的http请求,这个长连接就是基于Http Header参数Connection: keep-alive进行控制,随便查看一个http请求的报文,都可以发现类型以下的请求头:
表明上面是一个长连接。
∞
创建一个HttpClient的代码一般如下格式:定义ConnectionManager、RequestConfig 用来初始化httpClient,变量timeToLive可以认为是keepalive超时时间。
HttpClientConnectionManager connectionManager = connectionManagerFactory
.newConnectionManager(false, maxTotalConnections,
maxConnectionsPerHost, timeToLive, ttlUnit, registryBuilder);
RequestConfig defaultRequestConfig = RequestConfig.custom()
.setConnectTimeout(connectTimeout)
.setRedirectsEnabled(followRedirects).build();
CloseableHttpClient httpClient = httpClientFactory.createBuilder().
setDefaultRequestConfig(defaultRequestConfig)
.setConnectionManager(connectionManager).build();
其中的ConnectionManager最终被初始化为一个PoolingHttpClientConnectionManager实例,也就是说,创建一个http的连接池来管理连接;连接池的特性就是连接用完后直接放回池中(这个ConnectionManager还有些其他的处理,稍后再分析),下次直接从池中取出链接使用。
服务端keepalive配置代码如下:
@Configuration public class ServerConfig { @Bean public EmbeddedServletContainerFactory getEmbeddedServletContainerFactory() { TomcatEmbeddedServletContainerFactory containerFactory = new TomcatEmbeddedServletContainerFactory(); containerFactory .addConnectorCustomizers( (TomcatConnectorCustomizer) connector -> ((AbstractProtocol) connector.getProtocolHandler()).setKeepAliveTimeout(2000)); return containerFactory; } }
下图是一个正常情况下,完整的一次http交互,3次握手建立连接,4次握手关闭链接
服务端keepalive有效期为2秒,客户端keepalive有效期为10秒,与上图时间吻合。关闭链接有服务端主动发起,但客户端没有立即响应。
可以通过命令查看已建立的链接及数量
netstat -an | grep 8090 | wc -l
下图是一个链接复用的例子,同一个连接(50188 <-> 8090)处理了多次请求,提高了IO的利用率。
∞
- 如果客户端的 keepalive有效期(5秒)比服务端(10秒)的短,一个http请求的过程有什么变化,看下图
这次是客户端主动发起的关闭链接请求(可以通过两种方式进行关闭,后续进行说明),服务端及时响应。
- 如果一次请求过程中,服务端响应时间超过了keepalive的有效期,链接并不会提前中断,如下图,keepalive超时时间为10秒,而服务端返回结果用了13秒,连接是在正常返回后中断的。
- 如果客户端keepalive的有效期(100秒)远远超过了服务端的keepalive的有效期(10秒钟),会出现什么样的结果,这也是本篇最关注的问题,同样通过抓包查看连接的交互过程。
下图是我通过手动点击发送请求对应的网络抓包,可以看到交互顺利,还有连接复用,貌似一切正常。
但如果将上面的配置放到生产环境或者进行一个简单压力测试,将会时不时地发生failed to respond这个异常,但具有一定的偶然性。
异常抛出点如下
抛出这个异常的条件为读取到了连接结束的标记 i=-1。为何会出现这个情况,和httpclient维护连接的方式有关,简单描述如下:
httpclient默认使用PoolingHttpClientConnectionManager这个类管理连接,主要用到的是
归还连接的方法
public void releaseConnection(final HttpClientConnection managedConn, final Object state, final long keepalive, final TimeUnit tunit)
获取连接的方法
public ConnectionRequest requestConnection( final HttpRoute route, final Object state)
- 连接归还
归还连接的过程相对简单,主要是根据参数keepalive更新连接过期时间并放入连接池.
keepalive这个参数是从服务端返回的HttpHeader中获取的,如请求头中 Keep-Alive: timeout=5, max=100,表示5毫秒后超时,还能请求100次。
参考下面的代码:
事实上,tomcat返回的头部中没有关于keepalive的参数,所以httpclient任务此链接永久有效,会打印如下日志:
Connection [id: 18][route: {}->http://localhost:8090] can be kept alive indefinitely
- 获取连接
获取链接的过程稍微复杂一下,跟踪代码,最终会进入org.apache.http.pool.AbstractConnPool#getPoolEntryBlocking方法,如图
getPoolEntryBlocking方法比较长,重点关注上图中的这几行就可以,其中的entry就可以认为是一个连接,取出连接的过程做了一些判断,尽可能的保障了连接的可用性(之所以是尽可能,是因为存在某些巧合,导致虽然检测通过了,但是连接依旧是不可用的)
检测包括两部分:
- 是否过期,isExpired方法,根据创建时间和keepalive超时时间和当前时间进行比较
- 是否可用,根据参数validateAfterInactivity(默认值2000)每超过一定时间,判断连接是否可用,也就是存在一个时间窗口,在这个范围内是不检测的,这就给异常埋下了伏笔。
第二项检测会出现异常的道理很简单,如果这个时间窗口内服务端关闭了链接,客户端是不知晓的,或者超出了本时间窗口且检查通过,但发送请求前服务端主动关闭了链接,客户端也不知晓。
抛出这种异常情况,客户端就可以重复使用连接池中的对象,发送请求,达到连接复用的效果。
还有一种清除过期连接的方式,配合使用效果更好,启动定时任务,定期清理过期连接:
this.connectionManagerTimer.schedule(new TimerTask() { @Override public void run() { connectionManager.closeExpiredConnections(); } }, 1000, 10000);
还可以在出事化httpclient时,配置好HttpRequestRetryHandler,DefaultHttpRequestRetryHandler.INSTANCE似乎不能处理NoHttpResponseException,最好自定义,这样在发生NoHttpResponseException异常时可以进行重试,一般也能解决问题。
this.httpClient = httpClientFactory.createBuilder().setRetryHandler(DefaultHttpRequestRetryHandler.INSTANCE). setDefaultRequestConfig(defaultRequestConfig) .setConnectionManager(connectionManager).build();
当然这些都是补救措施,最好的方式应该是协调好客户端与服务端keepalive配置,客户端keepalive时间不要超过服务端keepalive时间。
经过这些改造,在进行测试,就没有再发现相关错误。
注:
其实,一个事情验证它有比较容易,如何验证没有呢?理论证明加实验,很难穷举所有情况,但是能确保绝大多数情况下正常。
参考:
https://blog.csdn.net/liyantianmin/article/details/82505634
https://zzc1684.iteye.com/blog/2189254