用户您好!请先登录!

深入解析Keepalive机制

深入解析Keepalive机制

很多同学包括自己对keepalive理解不清晰,经常搞不清楚,TCP也有keepalive,HTTP也有keepalive,Nginx有keepalive,高可用也有叫keepalived的工具,经常混淆这几个概念。很多时候keepalive对于开发人员几乎是透明的,但从架构的角度,大家还是有必要认真了解一下keepalive后面那些事。

1. HTTP中的keep-alive

1.1 为什么HTTP是短连接

众所周知,HTTP是短连接,client向server发送一个request,得到response后,连接就关闭。之所以这样设计使用,主要是考虑到实际情况。例如,用户通过浏览器访问一个web站点上的某个网页,当网页内容加载完毕之后,用户可能需要花费几分钟甚至更多的时间来浏览网页内容,此时完全没有必要继续维持底层连。当用户需要访问其他网页时,再创建新的连接即可。

因此,HTTP连接的寿命通常都很短。这样做的好处是,可以极大的减轻服务端的压力。一般而言,一个站点能支撑的最大并发连接数也是有限的,面对这么多客户端浏览器,不可能长期维持所有连接。每个客户端取得自己所需的内容后,即关闭连接,更加合理。

1.2 为什么要引入keep-alive

通常一个网页可能会有很多组成部分,除了文本内容,还会有诸如:js、css、图片等静态资源,有时还会异步发起AJAX请求。只有所有的资源都加载完毕后,我们看到网页完整的内容。然而,一个网页中,可能引入了几十个js、css文件,上百张图片,如果每请求一个资源,就创建一个连接,然后关闭,代价实在太大了。

基于此背景,我们希望连接能够在短时间内得到复用,在加载同一个网页中的内容时,尽量的复用连接,这就是HTTP协议中keep-alive属性的作用。

  • HTTP 1.0中默认是关闭的,需要在http头加入”Connection: Keep-Alive”,才能启用Keep-Alive;
  • http 1.1中默认启用Keep-Alive,如果加入”Connection: close “,才关闭。

1.3 如何处理keep-alive

对于客户端来说,不论是浏览器,还是手机App,或者我们直接在Java代码中使用HttpUrlConnection,只是负责在请求头中设置Keep-Alive。而具体的连接复用时间的长短,通常是由web服务器控制的

这里有个典型的误解,经常听到一些同学会说,通过设置http的keep-alive来保证长连接。通常我们所说的长连接,指的是一个连接创建后,除非出现异常情况,否则从应用启动到关闭期间,连接一直是建立的。例如在RPC框架,如dubbo,服务的消费者在启动后,就会一直维护服务提供者的底层TCP连接。

在HTTP协议中,Keep-Alive属性保持连接的时间长短是由服务端决定的,通常配置都是在几十秒左右。 例如,在tomcat中,我们可以server.xml中配置以下属性:

  • maxKeepAliveRequests:一个连接上,最多可以发起多少次请求,默认100,超过这个次数后会关闭。
  • keepAliveTimeout:底层socket连接最多保持多长时间,默认60秒,超过这个时间连接会被关闭。

当然,这不是所有内容,在一些异常情况下,keepalive也会失效。tomcat会根据http响应的状态码,判断是否需要丢弃连接(笔者这里看的是tomcat 9.0.19的源码)。

org.apache.coyote.http11.Http11Processor#statusDropsConnection

另外,值得一提的是,Tomcat 7版本支持三种运行模式:NIO、BIO、APR,且默认在BIO模式下运行。由于每个请求都要创建一个线程来处理,线程开销较大,因此针对BIO,额外提供了一个disableKeepAlivePercentage参数,根据工作线程池中繁忙线程数动态的对keepalive进行开启或者关闭:

由于Tomcat 8版本之后,废弃了BIO,默认在NIO模式下运行,对应的也取消了这个参数。

Anyway,我们知道了,在HTTP协议中keep-alive的连接复用机制主要是由服务端来控制的,笔者也不认为其实真正意义上的长连接。

1.4 JDK对keep-alive的支持

前文讲解了HTTP协议中,以tomcat为例说明了server端是如何处理keepalive的。但这并不意味着在client端,除了设置keep-alive请求头之外,就什么也不用考虑了。

在客户端,我们可以通过HttpUrlConnection来进行网络请求。当我们创建一个HttpUrlConnection对象时,其底层实际上会创建一个对应的Socket对象。我们要复用的不是HttpUrlConnection,而是底层的Socket

当然,我们的重点是Java如何帮我们实现底层socket链接的复用。JDK对keepalive的支持是透明的,keepAlive默认就是开启的。我们需要做的是,学会正确的使用姿势。

官网上有说明:当通过URLConnection.getInputStream()读取响应数据之后(在这里是HttpUrlConnection),应该调用InputStream的close方法关闭输入流,JDK http协议处理器会将这个连接放到一个连接缓存中,以便之后的HTTP请求进行复用。

翻译成代码,当发送一次请求,得到响应之后,不是调用HttpURLConnection.disconnect方法关闭,这会导致底层的socket连接被关闭。我们应该通过如下方式关闭,才能进行复用:

InputStream in=HttpURLConnection.getInputStream();

//处理

in.close()

这里并不打算提供完整的代码,官方已经给出的了代码示例,可参考上述链接。在实际开发中,通常是一些第三方sdk,如http-client、ok-http、RestTemplate等。

需要说明的是,只要我们的使用姿势正确。JDK对keepalive的支持对于我们来说是透明的,不过jdk也提供了相关系统属性配置来控制keeplive的默认行为,如下:

  • http.keepAlive:默认值为true。也就说是,即使我们不显示指定keep-alive,HttpUrlConnection也会自动帮我们加上。
  • http.maxConnections:的默认值是5。表示对于同一个目标IP地址,进行KeepAlive的连接数量。举例来说,你针对同一个IP同时创建了10个HttpUrlConnection,对应底层10个socket,但是JDK底层只会其中5个进行KeepAlive,多余的会关闭底层socket连接。

最后,尽管你可能不直接使用HttpUrlConnection,习惯于使用http-client、ok-http或者其他第三方类库。但是了解JDK原生对keep-alive的支持,也是很重要的。首先,你在看第三方类库的源码时,可能就利用到了这些特性。

2. TCP协议中的keepalive

首先介绍一下HTTP协议中keepalive与TCP中keepalive的区别:

  • HTTP协议(七层)的Keep-Alive意图在于连接复用,希望可以短时间内在同一个连接上进行多次请求/响应。
  • TCP协议(四层)的KeepAlive机制意图在于保活、心跳,检测连接错误。当一个TCP连接两端长时间没有数据传输时(通常默认配置是2小时),发送keepalive探针,探测链接是否存活。核心在于:虽然频率低,但是持久。

回到TCP keepalive探针,对于一方发起的keepalive探针,另一方必须响应。响应可能是以下三种形式之一:

  • 对方回应了ACK。说明一切OK。如果接下来2小时还没有数据传输,那么还会继续发送keepalive探针,以确保连接存活。
  • 对方回复RST,表示这个连接已经不存在。例如一方服务宕机后重启,此时接收到探针,因为不存在对应的连接。
  • 没有回复。说明socket已经被关闭了。

linux中的tcp的参数(参见 man tcp):

  • tcp_keepalive_intvl:keepalive探测包的发送间隔,默认为75秒
  • tcp_keepalive_probes:如果对方不予应答,探测包的最大发送次数,默认为9次。即连续9次发送,都没有应答的话,则关闭连接。
  • tcp_keepalive_time:连接的最大空闲(idle)时间,默认为7200秒,即2个小时。需要注意的是,这2个小时,指的是只有keepalive探测包,如果期间存在其他数据传输,则重新计时。

这些的默认配置值在/proc/sys/net/ipv4 目录下可以找到,文件中的值,就是默认值,可以直接用cat来查看文件的内容 。

一些编程语言支持在代码层面覆盖默认的配置。在使用Java 中,我们可以通过Socket设置keepAlive为true:

Socket socket=new Socket();

socket.setKeepAlive(true);//开启keep alive

socket.connect(new InetSocketAddress(“127.0.0.1”,8080));

然而,tcp的keepalive机制,还是有一些潜在问题的:

  • keepalive只能检测连接是否存活,不能检测连接是否可用。例如,某一方发生了死锁,无法在连接上进行任何读写操作,但是操作系统仍然可以响应网络层keepalive包。
  • TCP keepalive 机制依赖于操作系统的实现,灵活性不够,默认关闭,且默认的 keepalive 心跳时间是 两个小时, 时间较长。
  • 代理(如socks proxy)、或者负载均衡器,会让tcp keep-alive失效

基于此,我们需要加上应用层的心跳。应用层的心跳的作用,取决于你想干啥。笔者理解:

从服务端的角度来说,主要是为了资源管理和监控。例如大家都知道,访问mysql时,如果连接8小时没有请求,服务端就会主动断开连接。这是为了节省连接资源,mysql服务端有一个配置项max_connections,限制最大连接数。如果一个应用建立了连接,又不执行SQL,典型的属于占着茅坑不拉屎,mysql就要把这个连接回收。还可以对连接信息进行监控,例如mysql 中我们可以执行”show processlist”,查看当前有哪些客户端建立了连接。

从客户端的角度来说, 主要是为了保证连接可用。很多RPC框架,在调用方没有请求发送时,也会定时的发送心跳sql,保证连接可用。例如,很多数据库连接池,都会支持配置一个心跳sql,定时发送到mysql,以保证连接存活。

Netty中也提供了IdleSateHandler,来支持心跳机制。笔者的建议是,如果仅仅只是配置了IdleSateHandler,保证连接可用。有精力的话,server端也加上一个连接监控信息可视化的功能。

3. 高可用的keeplived

keepalived是一个基于VRRP协议来实现的服务高可用方案,可以利用其来避免IP单点故障。它的作用是检测服务器的状态,如果有一台服务器宕机,或出现故障,Keepalived将检测到,使用其他服务器代替该服务器的工作,当服务器工作正常后Keepalived自动将服务器加入到服务器群中。

Keepalived一般不会单独出现,而是与其它负载均衡技术(如lvs、haproxy、nginx)一起工作来达到集群的高可用。

一个简单的使用例子,将域名解析到一台负载均衡机器上,然后负载均衡反向代理到WEB机器上。中间的负载均衡只有一台,没法做到高可用,至少需要做到两台,那配置成两台机器之后,Keepalived就可以保证服务只有一个对外的虚拟IP,如果MASTER的负载均衡出现故障的时候,自动切换到BACKUP负载均衡上,服务不受任何影响。

我们以前有过一套稍显复杂的服务配置,Keepalived给HAProxy提供高可用,然后HAProxy给Twemproxy提供高可用和负载均衡,Twemproxy给Redis集群提供高可用和负载均衡。提供负载均衡服务的基本都会保证高可用,我们使用最多的Nginx作为反向代理服务器的时候,就能保证web服务的高可用。

nginx+keepalived 搭建高可用的服务教程有很多。感兴趣的可以自己试下搭建。

4. Nginx 的keepalive

既然TCP层已经有keepalive,为什么应用层的Nginx还需要keepalive?我理解的是,使用TCP的keepalive的保证传输层连接的可用性,默认配置都是2小时的检测周期。Nginx的keepalive来保证应用层的连接的可用性。

一个在第四层传输层上保证可用性,一个在第七层应用层上保证应用层协议连接的可用性。有本书里面有提到:为什么TCP keepalive不能替代应用层心跳?心跳除了说明应用程序还活着(进程还在,网络通畅),更重要的是表明应用程序还能正常工作。而TCP keepalive由操作系统负责探查,即便进程死锁或者阻塞,操作系统也会如常收发TCP keepalive信息,对方无法得知这一异常。

nginx keepalive跟TCP的配置基本一致,只不过名字不一样罢了。配置说明如下

so_keepalive=on|off|[keepidle]:[keepintvl]:[keepcnt]

so_keepalive=30m::10 表示开启tcp侦测,30分钟后无数据收发会发送侦测包,时间间隔使用系统默认的,发送10次侦测包。

行走的code

要发表评论,您必须先登录