KeepAlive 已经不是什么新鲜的概念了,HTTP 协议中有 KeepAlive 的概念,TCP 协议中也有 KeepAlive 的概念。二者的作用是不同的。本文将详细的介绍 HTTP 中的 KeepAlive,介绍 Tomcat 在 Server 端是如何对 KeepAlive 进行处理,以及 JDK 对 HTTP 协议中 KeepAlive 的支持。同时会详细介绍 TCP 中的 KeepAlive 机制以及应用层的心跳。
众所周知,HTTP 一般是短连接,Client 向 Server发送一个 Request,得到 Response后,连接就关闭。之所以这样设计使用,主要是考虑到实际情况。例如,用户通过浏览器访问一个web站点上的某个网页,当网页内容加载完毕之后,用户可能需要花费几分钟甚至更多的时间来浏览网页内容,此时完全没有必要继续维持底层连。当用户需要访问其他网页时,再创建新的连接即可。
因此,HTTP 连接的寿命通常都很短。这样做的好处是,可以极大的减轻服务端的压力。一般而言,一个站点能支撑的最大并发连接数也是有限的,面对这么多客户端浏览器,不可能长期维持所有连接。每个客户端取得自己所需的内容后,即关闭连接,更加合理。
通常一个网页可能会有很多组成部分,除了文本内容,还会有诸如:js、css、图片等静态资源,有时还会异步发起AJAX请求。只有所有的资源都加载完毕后,我们看到网页完整的内容。然而,一个网页中,可能引入了几十个js、css文件,上百张图片,如果每请求一个资源,就创建一个连接,然后关闭,代价实在太大了。
基于此背景,我们希望连接能够在短时间内得到复用,在加载同一个网页中的内容时,尽量的复用连接,这就是 HTTP 协议中 KeepAlive 属性的作用。
对于客户端来说,不论是浏览器,还是手机 App,或者我们直接在 Java 代码中使用 HttpUrlConnection,只是负责在请求头中设置 Keep-Alive。而具体的连接复用时间的长短,通常是由 Web 服务器控制的。
这里有个典型的误解,经常听到一些同学会说,通过设置 HTTP 的 KeepAlive 来保证长连接。通常我们所说的长连接,指的是一个连接创建后,除非出现异常情况,否则从应用启动到关闭期间,连接一直是建立的。例如在 RPC 框架 Dubbo 中,服务的消费者在启动后,就会一直维护服务提供者的底层 TCP 连接。
在 HTTP 协议中,Keep-Alive 属性保持连接的时间长短是由服务端决定的,通常配置都是在几十秒左右。 例如,在 Tomcat 中,我们可以 server.xml 中配置以下属性:
说明如下:
当然,这不是所有内容,在一些异常情况下,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协议中 KeepAlive 的连接复用机制主要是由服务端来控制的,笔者也不认为其实真正意义上的长连接。
前文讲解了 HTTP 协议中,以 Tomcat 为例说明了 Server 端是如何处理 KeepAlive 的。但这并不意味着在 Client 端,除了设置 Keep-Alive 请求头之外,就什么也不用考虑了。
在客户端,我们可以通过 HttpUrlConnection 来进行网络请求。当我们创建一个 HttpUrlConnection 对象时,其底层实际上会创建一个对应的 Socket 对象。我们要复用的不是HttpUrlConnection,而是底层的 Socket。
下面这个案例,演示了同时创建 5个 HttpUrlConnection,然后通过 netstat 命令观察 Socket 连接信息
运行这段代码,然后通过 netstat 命令观察 TCP 的 Socket 连接信息
可以看到,当我们创建 5 个 HttpUrlConnection 后,底层的确创建了对应数量的 TCP Socket 连接。其中,192.168.1.3 是本机 IP,220.181.57.216 是服务端 IP。
当然,我们的重点是 Java 如何帮我们实现底层 Socket 链接的复用。JDK 对 KeepAlive 的支持是透明的,KeepAlive 默认就是开启的。我们需要做的是,学会正确的使用姿势。
官网上有说明,参见:
https://docs.oracle.com/javase/8/docs/technotes/guides/net/http-keepalive.html
这段话的含义是:当通过 URLConnection.getInputStream() 读取响应数据之后(在这里是HttpUrlConnection),应该调用 InputStream 的 close 方法关闭输入流,JDK HTTP协议处理器会将这个连接放到一个连接缓存中,以便之后的 HTTP 请求进行复用。
翻译成代码,当发送一次请求,得到响应之后,不是调用 HttpURLConnection.disconnect 方法关闭,这会导致底层的 Socket 连接被关闭。我们应该通过如下方式关闭,才能进行复用:
这里并不打算提供完整的代码,官方已经给出的了代码示例,可参考上述链接。在实际开发中,通常是一些第三方 SDK,如 HttpClient、OkHttp、RestTemplate 等。
需要说明的是,只要我们的使用姿势正确。JDK 对 KeepAlive 的支持对于我们来说是透明的,不过 JDK 也提供了相关系统属性配置来控制 KeepAlive 的默认行为,如下:
说明:
最后,尽管你可能不直接使用 HttpUrlConnection,习惯于使用 HttpClient、OkHttp 或者其他第三方类库。但是了解 JDK 原生对 KeepAlive 的支持,也是很重要的。首先,你在看第三方类库的源码时,可能就利用到了这些特性。另外,也许你可以干翻面试官。
首先介绍一下 HTTP 协议中 KeepAlive 与 TCP 中 KeepAlive 的区别:
回到 TCP KeepAlive 探针,对于一方发起的 KeepAlive 探针,另一方必须响应。响应可能是以下三种形式之一:
用 man 命令,可以查看 linux 的 TCP 的参数:
其中 KeepAlive 相关的配置参数有三个:
其中:
这些的默认配置值在 /proc/sys/net/ipv4 目录下可以找到,文件中的值,就是默认值,可以直接用 cat 来查看文件的内容 。
可以通过 sysctl 命令来查看和修改:
可以看到,TCP 中的 SO_KEEPALIVE 是一个开关选项,默认关闭,需要在应用程序需要代码中显式的开启。当开启之后,在通信双方没有数据传输时,操作系统底层会定时发送 KeepAlive 探测包,以保证连接的存活。
一些编程语言支持在代码层面覆盖默认的配置。在使用 Java 中,我们可以通过 Socket 设置 KeepAlive 为 true:
然而,TCP 的 KeepAlive 机制,说实话,有一些鸡肋:
基于此,我们需要加上应用层的心跳。应用层的心跳的作用,取决于你想干啥。笔者理解:
从服务端的角度来说,主要是为了资源管理和监控。例如大家都知道,访问 mysql 时,如果连接 8 小时没有请求,服务端就会主动断开连接。这是为了节省连接资源,mysql 服务端有一个配置项 max_connections,限制最大连接数。如果一个应用建立了连接,又不执行 SQL,典型的属于占着茅坑不拉屎,mysql 就要把这个连接回收。还可以对连接信息进行监控,例如 mysql 中我们可以执行“show processlist”,查看当前有哪些客户端建立了连接。
从客户端的角度来说, 主要是为了保证连接可用。很多 RPC 框架,在调用方没有请求发送时,也会定时的发送心跳 SQL,保证连接可用。例如,很多数据库连接池,都会支持配置一个心跳 SQL,定时发送到 mysql,以保证连接存活。
Netty 中也提供了 IdleSateHandler,来支持心跳机制。笔者的建议是,如果仅仅只是配置了 IdleSateHandler,保证连接可用。有精力的话,Server 端也加上一个连接监控信息可视化的功能。