

TCP和HTTP中的KeepAlive机制总结
source link: https://cloud.tencent.com/developer/news/696654
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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_requests
和http
区块里的一样是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连接数的是操作系统允许打开的最大文件数,要修改的主要有以下几处
- /proc/sys/fs/file-max:操作系统所有进程一共可以打开的文件数
- /proc/sys/fs/nr_open:单个进程能分配的最大文件数
- ulimit的open files:当前shell以及由它启动的进程可以打开的最大文件数,如果超过了nropen,要先调整nropen的值
Tomcat的KeepAlive配置
- Tomcat7以上都默认开启了keepalive支持。两个主要参数maxKeepAliveRequest和KeepAliveTimeout
- maxKeepAliveRequest:一个长连接能接受的最大请求数,默认100
- KeepAliveTimeout:一个长连接最长空闲时间,否则被关闭,默认为connectionTimeout的值,默认60s
- Tomcat里的应用作为发起方的时候,是否支持KeepAlive是由应用自行决定的,和Tomcat无关
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK