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
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相关的配置可以通过如下方式查看
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.
客户端修改默认值具体配置如下
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秒,不过也正常,毕竟是里层,这个设置是比较合理的,使用默认的就可以
另外,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
,默认128Nginx通过事件驱动来实现大量长连接的维护,具体可以查看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无关
参考资料
版权声明: 本文为 InfoQ 作者【陈德伟】的原创文章。
原文链接:【http://xie.infoq.cn/article/398b82c2b4300f928108ac605】。文章转载请联系作者。
评论