当前位置 博文首页 > andy_yin的专栏:HTTP 与 TCP 的 KeepAlive 是一个东西吗?

    andy_yin的专栏:HTTP 与 TCP 的 KeepAlive 是一个东西吗?

    作者:[db:作者] 时间:2021-08-06 18:53

    本文转自田守枝的技术博客

    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 中配置以下属性:

    640?wx_fmt=png

    说明如下:

    • maxKeepAliveRequests一个连接上,最多可以发起多少次请求,默认 100,超过这个次数后会关闭。

    • keepAliveTimeout底层 Socket 连接最多保持多长时间,默认 60 秒,超过这个时间连接会被关闭。

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

    org.apache.coyote.http11.Http11Processor#statusDropsConnection

    640?wx_fmt=png

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

    640?wx_fmt=png

    ?? ?? ??由于 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 连接信息

    640?wx_fmt=png

    ?? ?? ?运行这段代码,然后通过?netstat?命令观察 TCP?的 Socket 连接信息

    640?wx_fmt=png

    可以看到,当我们创建 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 的默认行为,如下:

    640?wx_fmt=png

    说明:

    • 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 相关的配置参数有三个:

    640?wx_fmt=png

    其中:

    • 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 端也加上一个连接监控信息可视化的功能。

    喜欢本文的朋友们,欢迎长按下图关注订阅号涤生的博客,收看更多精彩内容

    640?wx_fmt=jpeg

    更多精彩内容:

    • JVM 源码解读之 CMS GC 触发条件

    • JVM 源码解读之 CMS 何时会进行 Full GC

    • 高吞吐低延迟 Java 应用的 GC 优化

    • CMS GC 新生代默认是多大?

    • 再次剖析?“一个?JVM?参数引发的频繁?CMS?GC”

    • 一个?JVM?参数引发的频繁?CMS?GC

    • 一次 Young GC 的优化实践(FinalReference 相关)

    • 依赖包滥用 System.gc() 导致的频繁 Full GC

    • 服务框架的技术栈

    • PhantomReference导致CMS?GC耗时严重

    • 长连接和心跳那些事儿

    • System.gc()?源码解读

    • Long?Polling长轮询详解

    cs
    下一篇:没有了