TCP和HTTP中的KeepAlive机制总结

什么是KeepAlive

  • KeepAlive可以简单理解为一种状态保持或重用机制,比如当一条连接建立后,我们不想它立刻被关闭,如果实现了KeepAlive机制,就可以通过它来实现连接的保持
  • HTTP的KeepAlive在HTTP 1.0版本默认是关闭的,但在HTTP1.1是默认开启的;操作系统里TCP的KeepAlive默认也是关闭,但一般应用都会修改设置来开启。因此网上TCP流量中基于KeepAlive的是主流
  • HTTP的KeepAlive和TCP的KeepAlive有一定的依赖关系,名称又一样,因此经常被混淆,但其实是不同的东西,下面具体分析一下

TCP为什么要做KeepAlive

  • 我们都知道TCP的三次握手和四次挥手。当两端通过三次握手建立TCP连接后,就可以传输数据了,数据传输完毕,连接并不会自动关闭,而是一直保持。只有两端分别通过发送各自的 FIN 报文时,才会关闭自己侧的连接。
  • 这个关闭机制看起来简单明了,但实际网络环境千变万化,衍生出了各种问题。假设因为实现缺陷、突然崩溃、恶意攻击或网络丢包等原因,一方一直没有发送 FIN 报文,则连接会一直保持并消耗着资源,为了防止这种情况,一般接收方都会主动中断一段时间没有数据传输的TCP连接,比如LVS会默认中断90秒内没有数据传输的TCP连接,F5会中断5分钟内没有数据传输的TCP连接
  • 但有的时候我们的确不希望中断空闲的TCP连接,因为建立一次TCP连接需要经过一到两次的网络交互,且由于TCP的 slow start 机制,新的TCP连接开始数据传输速度是比较慢的,我们希望通过连接池模式,保持一部分空闲连接,当需要传输数据时,可以从连接池中直接拿一个空闲的TCP连接来全速使用,这样对性能有很大提升
  • 为了支持这种情况,TCP实现了KeepAlive机制。KeepAlive机制并不是TCP规范的一部分,但无论Linux和Windows都实现实现了该机制。TCP实现里KeepAlive默认都是关闭的,且是每个连接单独设置的,而不是全局设置

Implementors MAY include "keep-alives" in their TCP implementations, although this practice is not universally accepted. If keep-alives are included, the application MUST be able to turn them on or off for each TCP connection, and they MUST default to off.

  • 另外有一个特殊情况就是,当某应用进程关闭后,如果还有该进程相关的TCP连接,一般来说操作系统会自动关闭这些连接

如何开启TCP的KeepAlive

  • TCP的KeepAlive默认不是开启的,如果想使用,需要在自己的应用中为每个TCP连接设置SO_KEEPALIVE 才会生效
  • 在Java中,应用程序一般通过设置 java.net.SocketOptions 来开启TCP连接的KeepAlive
/**
 *  When the keepalive option is set for a TCP socket and no data
 *  has been exchanged across the socket in either direction for
 *  2 hours (NOTE: the actual value is implementation dependent),
 *  TCP automatically sends a keepalive probe to the peer. This probe is a
 *  TCP segment to which the peer must respond.
 *  One of three responses is expected:
 *  1. The peer responds with the expected ACK. The application is not
 *     notified (since everything is OK). TCP will send another probe
 *     following another 2 hours of inactivity.
 *  2. The peer responds with an RST, which tells the local TCP that
 *     the peer host has crashed and rebooted. The socket is closed.
 *  3. There is no response from the peer. The socket is closed.
 *
 *  The purpose of this option is to detect if the peer host crashes.
 *
 *  Valid only for TCP socket: SocketImpl
 *
 * @see Socket#setKeepAlive
 * @see Socket#getKeepAlive
 */
@Native public final static int SO_KEEPALIVE = 0x0008;
  • Java Docs里对 SO_KEEPALIVE 的工作机制做了比较详细的说明,具体来说就是,如果某连接开启了TCP KeepAlive,当连接空闲了两个小时(依赖操作系统的 net.ipv4.tcp_keepalive_time 设置),TCP会自动发送一个KeepAlive探测报文给对端。对端必须回复这个探测报文,假设对端正常,就可以回复ACK报文,收到ACK后该连接就会继续维持,直到再次出现两个小时空闲然后探测;假设对端不正常,比如重启了,应该回复一个RST报文来关闭该连接。假设对端没有任何响应,TCP会每隔75秒(依赖操作系统的 net.ipv4.tcp_keepalive_intvl 设置)再次重试,重试9次(依赖OS的 net.ipv4.tcpkeepaliveprobes 设置)后如果依然没有回复则关闭连接
  • Linux中KeepAlive相关的配置可以通过如下方式查看
chendw@chendw-PC:~$ sysctl -a | grep keepalive
net.ipv4.tcp_keepalive_time = 7200
net.ipv4.tcp_keepalive_intvl = 75
net.ipv4.tcp_keepalive_probes = 9

HTTP为什么要做KeepAlive

  • HTTP虽然是基于有连接状态的TCP,但本身却是一个无连接状态的协议,客户端建立连接,发出请求,获取响应,关闭连接,然后整个流程就结束了;当有新的HTTP请求,则使用新建立的TCP连接。老的连接一般会被客户端浏览器或服务器关闭,此时由于是两端主动发的 FIN 报文,因此即使TCP已经设置了KeepAlive,TCP连接也会被正常关闭
  • 这种模式下每个HTTP请求都会经过三次握手创建新的TCP,再加上TCP慢启动的影响,以及单个网页里包含越来越多的资源请求,因此效果并不理想。为了提升性能,HTTP规范也提出了KeepAlive机制,HTTP请求携带头部 Connection: Keep-Alive 信息,告知服务器不要关闭该TCP连接,当服务器收到该请求,完成响应后,不会主动主动关闭该TCP连接。而浏览器当然也不会主动关闭,而是在后续请求里复用该TCP连接来发送下一个HTTP请求
  • HTTP1.0默认不开启KeepAlive,因此要使用的话需要浏览器支持,在发送HTTP请求时主动携带

Connection: Keep-Alive头部,应用服务器同样也要支持;而HTTP1.1规范明确规定了要默认开启KeepAlive,所以支持HTTP1.1的浏览器不需要显式指定,发送请求时会自动携带该头部,只有在想关闭时可以通过设置 Connection: Close 头部告知对端

  • 另外,HTTP的KeepAlive机制还提供了头部 Keep-Alive: max=5, timeout=120 来控制连接关闭时间,比如如上头部就表示该TCP连接还会保持120秒,max表示可以发送的请求数,不过在非管道连接下会被忽略,我们基本都是非管道连接,因此可以忽略
  • HTTP/2为每个域名使用单个TCP连接,本身就是连接复用,因此请求不再需要携带头部来开启KeepAlive

HTTP的KeepAlive和TCP的KeepAlive的关系

  • 从上面可以看出,虽然都叫KeepAlive且有依赖关系,但HTTP的KeepAlive和TCP的KeepAlive是两个完全不同的概念
  • TCP的KeepAlive是由操作系统内核来控制,通过 keep-alive 报文来防止TCP连接被对端、防火墙或其他中间设备意外中断,和上层应用没有任何关系,只负责维护单个TCP连接的状态,其上层应用可以复用该TCP长连接,也可以关闭该TCP长连接
  • HTTP的KeepAlive机制则是和自己的业务密切相关的,浏览器通过头部告知服务器要复用这个TCP连接,请不要随意关闭。只有到了 keepalive 头部规定的 timeout 才会关闭该TCP连接,不过这具体依赖应用服务器,应用服务器也可以根据自己的设置在响应后主动关闭这个TCP连接,只要在响应的时候携带 Connection: Close 告知对方
  • 所以很多时候我们可以把HTTP连接理解为TCP连接,但HTTP KeepAlive则不能当成TCP的KeepAlive看待
  • 假设我们不开启TCP长连接而只开启HTTP长连接,是不是HTTP的KeepAlive就不起作用了?并不是的,此时HTTP的KeepAlive还会正常起作用,TCP连接还会被复用,但被复用的TCP连接出现故障的概率就高很多。由于没有开启TCP的KeepAlive,防火墙或负载转发服务等中间设备可能因为该TCP空闲太长而悄悄关闭该连接,当HTTP从自己的连接池拿出该TCP连接时,可能并不知道该连接被关闭,继续使用就会出现错误
  • 为了减少错误,一般来说开启HTTP的KeepAlive的应用都会开启TCP的KeepAlive
  • 默认的 net.ipv4.tcp_keepalive_time 为2个小时,是不是太长了?感觉太长了,2小时监测一次感觉黄花菜都凉了。我们公司F5后面的Nginx服务器配置了30分钟,但应该也是太长了吧,F5维持空闲连接5分钟,那超时监测不应该低于这个值吗 ???,比如Google Cloud说其防火墙允许10分钟空闲连接,因此建议 net.ipv4.tcp_keepalive_time 设置为6分钟

如何使用HTTP的KeepAlive

  • 很明显,开启HTTP KeepAlive不需要用户做任何操作,只要浏览器和应用服务器支持即可,不过需要注意的是,HTTP KeepAlive的相关头部都是 hop-by-hop 类型的
  • 和TCP连接不同,一个完整的HTTP事务,可能会横跨多个TCP连接,比如浏览器请求某个网页,请求可能先通过浏览器与负载均衡之间的TCP连接传输,再经过负载均衡到Nginx的TCP连接,最后在经过Nginx与业务Tomcat服务器的TCP连接,Tomcat处理完请求并返回响应后,响应沿着同样的TCP连接路线返回
  • 因此HTTP的头部被分为了两部分:End-to-end 头部和 Hop-by-hop 头部,End-to-end 头部会被中间的代理原样转发,比如浏览器请求报文中的 host 头部,会被负载均衡、反向代理原样转发到Tomcat里,除非特意修改。而 Hop-by-hop 头部则只在当前TCP连接里有效,大部分头部都是 End-to-end ,但KeepAlive相关头部很明显和TCP连接有密切关系,因此是 Hop-by-hop

* End-to-end headers which are transmitted to the ultimate recipient of a request or response. End-to-end headers in responses MUST be stored as part of a cache entry and MUST be transmitted in any response formed from a cache entry. * Hop-by-hop headers which are meaningful only for a single transport-level connection and are not stored by caches or forwarded by proxies.

  • 也就是说,即使浏览器请求时携带了 Connection: Keep-Alive ,也只表示浏览器到负载均衡之间是长连接,但负载均衡到nginx、nginx到tomcat是否是长连接则需要具体分析。比如Nginx虽然支持HTTP的Keep-Alive,但由Nginx发起的HTTP请求默认不是长连接
  • 由于这种 Hop-by-hop 的特性,HTTP长连接中的 timeout 设置就十分可疑了,不过一般来说应用服务器都是根据自己的设置来管理TCP连接的,因此HTTP长连接中 Connection 头部每个请求都携带, keepalive 头部用的就比较少

Nginx的KeepAlive配置

  • Nginx与客户端的长连接
  • Nginx是支持HTTP KeepAlive的,因此只要client发送的http请求携带了KeepAlive头部,客户端和Nginx的长连接就能正常保持
  • 可以使用keepaliverequests和keepalivetimeout调整对client的长连接的单个连接承受的最大请求数,以及长连接最大空闲时长
  • 从上面可知,服务端可以根据客户端的 keepalive 头部来管理TCP连接,也可以根据自己的设置来管理,Nginx一般根据自己的设置来管理

Syntax: keepalive_requests number; Default: keepalive_requests 100; Context: http, server, location This directive appeared in version 0.8.0. Sets the maximum number of requests that can be served through one keep-alive connection. After the maximum number of requests are made, the connection is closed. Closing connections periodically is necessary to free per-connection memory allocations. Therefore, using too high maximum number of requests could result in excessive memory usage and not recommended. Syntax: keepalivetimeout timeout [headertimeout]; Default: keepalive_timeout 75s; Context: http, server, location The first parameter sets a timeout during which a keep-alive client connection will stay open on the server side. The zero value disables keep-alive client connections. The optional second parameter sets a value in the “Keep-Alive: timeout=time” response header field. Two parameters may differ.

  • 客户端修改默认值具体配置如下
http {
    keepalive_requests 100;
    keepalive_timeout 75s;
    upstream backend {
        server 192.167.61.1:8080;
    }
}
  • Nginx与Upstream Server的长连接
  • Nginx作为发起方的时候,默认还是不开启HTTP的KeepAlive的,因此需要主动设置
  • upstream 区块使用 keepalive 开启,数字表示每个work开启的最大长连接数
  • Nginx和上游交互时,默认 proxy_http_version 为1.0,因此需要配置 proxy_http_version ,并清空 connection,这样即使前一跳是短连接,Nginx与上游也可以是长连接
  • 另外 upstream 里的 keepalive_requestshttp 区块里的一样是100,但 keepalive_timeout 默认为60秒,比 http 区块里的少15秒,不过也正常,毕竟是里层,这个设置是比较合理的,使用默认的就可以
upstream backend {
    server 192.167.61.1:8080;
    server 192.167.61.1:8082 back;
    keepalive 100;
    # keepalive_requests 100;
    # keepalive_timeout 60s;
}

local /test {
    proxy_http_version 1.1;
    proxy_set_header Connection ""; // 传递给上游服务器的头信息

    proxy_pass http://backend;
}
  • 另外,Nginx还在 listner 指令上提供了一个 so_keepalive 选项,来开启Nginx对TCP长连接的支持,应该开启的是客户端与Nginx之间的TCP长连接,但一般没有人使用,那负载均衡和Nginx、Nginx和Tomcat之间是不需要TCP长连接吗?因为中间没有网络设备?否则TCP长连接是由谁来做检测?
  • 长连接的资源占用问题
  • 长连接带来的一个很明显的问题就是资源的占用,浏览器对同一个域名一般能并发建立6个连接,一般这些都是长连接,而这些连接会维护75秒,但客户端获得响应以后一般就结束了,下一次的客户是不同的源地址,因此无法复用前一个浏览器与服务器之间维护的长连接,这会造成服务端维护了大量不再被使用的连接,所以长连接的意义在于有大量资源持续请求的场景
  • 假设你就一个静态页面,里面包含几个资源,使用短连接对服务器并发更好
  • 另外,注意Nginx中 keepalive_requests 默认的100表示的是单个长连接能处理的最大请求数,而并不是Nginx能维护的长连接数。Nginx能维护的TCP连接数,为工作进程个数 worker_processes 乘以每个工作进程允许维护的最大连接数 worker_connections(默认512);如果想计算Nginx能服务的最大请求数,还需要在最大TCP连接数外,加上操作系统允许的排队等待数 net.core.somaxconn,默认128
  • Nginx通过事件驱动来实现大量长连接的维护,具体可以查看Nginx文档
  • 端口号与文件数
  • 由于端口在传输层使用16位来传输,因此取值范围只能是0到65535,再加上TCP连接关闭后端口并不能立刻被重用,而是要经过2MSL的TIME_WAIT闲置,所以经常有人以为一个服务器同时最大能维持的TCP数是 65000/2*60 ,大约500左右
  • 这个理解是有偏颇的。端口的限制只是对发起方来说的,即源端口。比如Nginx作为反向代理,和上游Tomcat建立连接时,源IP和目的IP肯定是固定的,目的端口也是固定的,比如Tomcat的8080端口,只有源端口可变,所以Nginx和上游Tomcat最多只能建立500左右的TCP连接,不过两端IP都是固定的,所以TCP连接重用效果非常好,并不会造成性能问题
  • 当Nginx作为接收方和客户端浏览器建立连接时,Nginx服务器提供固定的IP和端口,而客户端浏览器IP和端口都会正常变动,因此Nginx服务器上维护的与客户端的长连接是不受端口限制的,不过此时服务器又会遇到著名的C10K问题
  • 此时限制服务器维持TCP连接数的是操作系统允许打开的最大文件数,要修改的主要有以下几处
  1. /proc/sys/fs/file-max:操作系统所有进程一共可以打开的文件数
  2. /proc/sys/fs/nr_open:单个进程能分配的最大文件数
  3. ulimit的open files:当前shell以及由它启动的进程可以打开的最大文件数,如果超过了nropen,要先调整nropen的值

Tomcat的KeepAlive配置

  • Tomcat7以上都默认开启了keepalive支持。两个主要参数maxKeepAliveRequest和KeepAliveTimeout
  • maxKeepAliveRequest:一个长连接能接受的最大请求数,默认100
  • KeepAliveTimeout:一个长连接最长空闲时间,否则被关闭,默认为connectionTimeout的值,默认60s
  • Tomcat里的应用作为发起方的时候,是否支持KeepAlive是由应用自行决定的,和Tomcat无关

参考资料

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/398b82c2b4300f928108ac605
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券