yunnick

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

这篇文章主旨在于讲明白:

  • 什么是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就可以认为是一个连接,取出连接的过程做了一些判断,尽可能的保障了连接的可用性(之所以是尽可能,是因为存在某些巧合,导致虽然检测通过了,但是连接依旧是不可用的)

检测包括两部分:

  1. 是否过期,isExpired方法,根据创建时间和keepalive超时时间和当前时间进行比较
  2. 是否可用,根据参数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

posted on 2019-08-06 17:38  yunnick  阅读(7850)  评论(0编辑  收藏  举报