本文转自田守枝的技术博客
KeepAlive 已经不是什么新鲜的概念了,HTTP 协议中有 KeepAlive 的概念,TCP 协议中也有 KeepAlive 的概念。二者的作用是不同的。本文将详细的介绍 HTTP 中的 KeepAlive,介绍 Tomcat 在 Server 端是如何对 KeepAlive 进行处理,以及 JDK 对 HTTP 协议中 KeepAlive 的支持。同时会详细介绍 TCP 中的 KeepAlive 机制以及应用层的心跳。
1. HTTP 中的 KeepAlive
1.1 为什么 HTTP 是短连接
众所周知,HTTP 一般是短连接,Client 向 Server发送一个 Request,得到 Response后,连接就关闭。之所以这样设计使用,主要是考虑到实际情况。例如,用户通过浏览器访问一个web站点上的某个网页,当网页内容加载完毕之后,用户可能需要花费几分钟甚至更多的时间来浏览网页内容,此时完全没有必要继续维持底层连。当用户需要访问其他网页时,再创建新的连接即可。
因此,HTTP 连接的寿命通常都很短。这样做的好处是,可以极大的减轻服务端的压力。一般而言,一个站点能支撑的最大并发连接数也是有限的,面对这么多客户端浏览器,不可能长期维持所有连接。每个客户端取得自己所需的内容后,即关闭连接,更加合理。
1.2 为什么要引入 KeepAlive
通常一个网页可能会有很多组成部分,除了文本内容,还会有诸如:js、css、图片等静态资源,有时还会异步发起AJAX请求。只有所有的资源都加载完毕后,我们看到网页完整的内容。然而,一个网页中,可能引入了几十个js、css文件,上百张图片,如果每请求一个资源,就创建一个连接,然后关闭,代价实在太大了。
基于此背景,我们希望连接能够在短时间内得到复用,在加载同一个网页中的内容时,尽量的复用连接,这就是 HTTP 协议中 KeepAlive 属性的作用。
HTTP 1.0 中默认是关闭的,需要在 HTTP 请求头部加入"Connection: Keep-Alive",才能启用 KeepAlive;
HTTP 1.1中默认启用 KeepAlive,如果在 HTTP 请求头部加入"Connection: close ",才会关闭。
1.3 如何处理 KeepAlive
对于客户端来说,不论是浏览器,还是手机 App,或者我们直接在 Java 代码中使用 HttpUrlConnection,只是负责在请求头中设置 Keep-Alive。而具体的连接复用时间的长短,通常是由 Web 服务器控制的。
这里有个典型的误解,经常听到一些同学会说,通过设置 HTTP 的 KeepAlive 来保证长连接。通常我们所说的长连接,指的是一个连接创建后,除非出现异常情况,否则从应用启动到关闭期间,连接一直是建立的。例如在 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协议中 KeepAlive 的连接复用机制主要是由服务端来控制的,笔者也不认为其实真正意义上的长连接。
1.4 JDK 对 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
When the application finishes reading the response body or when the application calls close()
on the InputStream returned by URLConnection.getInputStream(),
the JDK's HTTP protocol handler will try to clean up the connection and if successful,
put the connection into a connection cache for reuse by future HTTP requests.
这段话的含义是:当通过 URLConnection.getInputStream() 读取响应数据之后(在这里是HttpUrlConnection),应该调用 InputStream 的 close 方法关闭输入流,JDK HTTP协议处理器会将这个连接放到一个连接缓存中,以便之后的 HTTP 请求进行复用。
翻译成代码,当发送一次请求,得到响应之后,不是调用 HttpURLConnection.disconnect 方法关闭,这会导致底层的 Socket 连接被关闭。我们应该通过如下方式关闭,才能进行复用:
InputStream in=HttpURLConnection.getInputStream();
//处理
in.close()
这里并不打算提供完整的代码,官方已经给出的了代码示例,可参考上述链接。在实际开发中,通常是一些第三方 SDK,如 HttpClient、OkHttp、RestTemplate 等。
需要说明的是,只要我们的使用姿势正确。JDK 对 KeepAlive 的支持对于我们来说是透明的,不过 JDK 也提供了相关系统属性配置来控制 KeepAlive 的默认行为,如下:
说明:
http.keepAlive:默认值为 true。也就说是,即使我们不显示指定 keep-alive,HttpUrlConnection 也会自动帮我们加上。
http.maxConnections:的默认值是 5。表示对于同一个目标 IP 地址,进行 KeepAlive 的连接数量。举例来说,你针对同一个 IP 同时创建了 10 个 HttpUrlConnection,对应底层 10 个 Socket,但是 JDK 底层只会其中 5 个进行 KeepAlive,多余的会关闭底层 Socket 连接。
最后,尽管你可能不直接使用 HttpUrlConnection,习惯于使用 HttpClient、OkHttp 或者其他第三方类库。但是了解 JDK 原生对 KeepAlive 的支持,也是很重要的。首先,你在看第三方类库的源码时,可能就利用到了这些特性。另外,也许你可以干翻面试官。
2. TCP 协议中的 KeepAlive
首先介绍一下 HTTP 协议中 KeepAlive 与 TCP 中 KeepAlive 的区别:
HTTP 协议(七层)的 KeepAlive 意图在于连接复用,希望可以短时间内在同一个连接上进行多次请求/响应。举个例子,你搞了一个好项目,想让马云爸爸投资,马爸爸说,"我很忙,最多给你3分钟”,你需要在这三分钟内把所有的事情都说完。核心在于:时间要短,速度要快。
TCP 协议(四层)的 KeepAlive 机制意图在于保活、心跳,检测连接错误。当一个 TCP 连接两端长时间没有数据传输时(通常默认配置是 2 小时),发送 KeepAlive 探针,探测链接是否存活。例如,我和厮大聊天,开了语音,之后我们各自做自己的事,一边聊天,有一段时间双方都没有讲话,然后一方开口说话,首先问一句,"老哥,你还在吗?”,巴拉巴拉..。又过了一会,再问,"老哥,你还在吗?”。核心在于:虽然频率低,但是持久。
回到 TCP KeepAlive 探针,对于一方发起的 KeepAlive 探针,另一方必须响应。响应可能是以下三种形式之一:
对方回应了 ACK。说明一切 OK。如果接下来 2 小时还没有数据传输,那么还会继续发送 KeepAlive 探针,以确保连接存活。
对方回复 RST,表示这个连接已经不存在。例如一方服务宕机后重启,此时接收到探针,因为不存在对应的连接。
没有回复。说明 Socket 已经被关闭了。
用 man 命令,可以查看 linux 的 TCP 的参数:
man 7 tcp
其中 KeepAlive 相关的配置参数有三个:
其中:
tcp_keepalive_intvl:KeepAlive 探测包的发送间隔,默认为 75 秒
tcp_keepalive_probes:如果对方不予应答,探测包的最大发送次数,默认为 9 次。即连续 9 次发送,都没有应答的话,则关闭连接。
tcp_keepalive_time:连接的最大空闲(idle)时间,默认为 7200 秒,即 2 个小时。需要注意的是,这 2 个小时,指的是只有 KeepAlive 探测包,如果期间存在其他数据传输,则重新计时。
这些的默认配置值在 /proc/sys/net/ipv4 目录下可以找到,文件中的值,就是默认值,可以直接用 cat 来查看文件的内容 。
$ ls /proc/sys/net/ipv4 | grep tcp_keepalive
tcp_keepalive_intvl
tcp_keepalive_probes
tcp_keepalive_time
可以通过 sysctl 命令来查看和修改:
# 查询
cat /proc/sys/net/ipv4/tcp_keepalive_time
#修改
sysctl net.ipv4.tcp_keepalive_time=3600
可以看到,TCP 中的 SO_KEEPALIVE 是一个开关选项,默认关闭,需要在应用程序需要代码中显式的开启。当开启之后,在通信双方没有数据传输时,操作系统底层会定时发送 KeepAlive 探测包,以保证连接的存活。
一些编程语言支持在代码层面覆盖默认的配置。在使用 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 KeepAlive 失效
基于此,我们需要加上应用层的心跳。应用层的心跳的作用,取决于你想干啥。笔者理解:
从服务端的角度来说,主要是为了资源管理和监控。例如大家都知道,访问 mysql 时,如果连接 8 小时没有请求,服务端就会主动断开连接。这是为了节省连接资源,mysql 服务端有一个配置项 max_connections,限制最大连接数。如果一个应用建立了连接,又不执行 SQL,典型的属于占着茅坑不拉屎,mysql 就要把这个连接回收。还可以对连接信息进行监控,例如 mysql 中我们可以执行“show processlist”,查看当前有哪些客户端建立了连接。
从客户端的角度来说, 主要是为了保证连接可用。很多 RPC 框架,在调用方没有请求发送时,也会定时的发送心跳 SQL,保证连接可用。例如,很多数据库连接池,都会支持配置一个心跳 SQL,定时发送到 mysql,以保证连接存活。
Netty 中也提供了 IdleSateHandler,来支持心跳机制。笔者的建议是,如果仅仅只是配置了 IdleSateHandler,保证连接可用。有精力的话,Server 端也加上一个连接监控信息可视化的功能。
喜欢本文的朋友们,欢迎长按下图关注订阅号涤生的博客,收看更多精彩内容
更多精彩内容: