暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

浅析keepalive在Tomcat中的实现原理(之一)

中间件技术讨论圈 2016-06-23
2973

keepalive属性是web服务器非常重要的优化参数,其重要到什么程度呢,nginx,apache加上该参数,能在性能测试或者真实的大并发测试中,大幅提高吞吐量和web请求访问的次数,而这个参数也是在HTTP11之后,客户端和服务器端的默认选项。

本文首先讲解keepalive原理,其次结合Tomcat的配置,讲解keepalive在Tomcat实现的基本原理。

本文的第二篇,通过代码分析,细致讲解Tomcat关于keepalive的配置,在三个Tomcat前端通道的实现。


1.什么是keepalive

http协议的早期是,每开启一个http链接,是要进行一次socket,也就是新启动一个TCP链接。

使用keep-alive可以改善这种状态,即在一次TCP连接中可以持续发送多份数据而不会断开连接。通过使用keep-alive机制,可以减少tcp连接建立次数。

举一个例子,用户浏览一个网页时,除了网页本身外,还引用了多个 javascript 文件,多个 css 文件,多个图片文件,并且这些文件都在同一个 HTTP 服务器上,如下图所示:


上图页面html算作一个http请求,而如果浏览器支持keepalive的话,那么请求头中会有如下:


对于keepalive的部分,主要集中在Connection属性当中,这个属性可以设置两个值:

close(告诉WEB服务器或者代理服务器,在完成本次请求的响应后,断开连接,不要等待本次连接的后续请求了)。

keepalive(告诉WEB服务器或者代理服务器,在完成本次请求的响应后,保持连接,等待本次连接的后续请求)。

从整体可以再看看keepalive的优化的结果:


从上面的分析来看,keepalive这个选项相当好,是否所有的场景都适合开启keepalive呢?

情况1:如果用户浏览一个网页时,除了网页本身外,顶多能引入1,2个 javascript 文件,1,2个图片文件。
情况2:如果用户浏览的是一个动态网页,由程序即时生成内容,并且不引用其他内容。

当情况1的时候,keepalive的作用就不那么明显了,而情况2来说,keepalive开启与不开启没有任何的关系,因为整个网页是动态形成的,在服务器端对html页面进行组装的,因此开不开启都是一个TCP链接。

另外,需要澄清两个事情:

第一个,keep-alive与TIME_WAIT的关系,使用http keep-alive,可以减少服务端TIME_WAIT数量(因为由服务端httpd守护进程主动关闭连接)。道理很简单,相较而言,启用keep-alive,建立的tcp连接更少了,自然要被关闭的tcp连接也相应更少了。

什么是TIME_WAIT呢?

通信双方建立TCP连接后,主动关闭连接的一方就会进入TIME_WAIT状态。

客户端主动关闭连接时,会发送最后一个ack后,然后会进入TIME_WAIT状态,再停留2个MSL时间,进入CLOSED状态。

下图是以客户端主动关闭连接为例,说明这一过程的。


那么这个TIME_WAIT到底有什么作用呢?


主要有两个原因:

a)可靠地实现TCP全双工连接的终止

TCP协议在关闭连接的四次握手过程中,最终的ACK是由主动关闭连接的一端(后面统称A端)发出的,如果这个ACK丢失,对方(后面统称B端)将重发出最终的FIN,因此A端必须维护状态信息(TIME_WAIT)允许它重发最终的ACK。如果A端不维持TIME_WAIT状态,而是处于CLOSED 状态,那么A端将响应RST分节,B端收到后将此分节解释成一个错误(在java中会抛出connection reset的SocketException)。

因而,要实现TCP全双工连接的正常终止,必须处理终止过程中四个分节任何一个分节的丢失情况,主动关闭连接的A端必须维持TIME_WAIT状态 。

b)允许老的重复分节在网络中消逝 

TCP分节可能由于路由器异常而“迷途”,在迷途期间,TCP发送端可能因确认超时而重发这个分节,迷途的分节在路由器修复后也会被送到最终目的地,这个迟到的迷途分节到达时可能会引起问题。在关闭“前一个连接”之后,马上又重新建立起一个相同的IP和端口之间的“新连接”,“前一个连接”的迷途重复分组在“前一个连接”终止后到达,而被“新连接”收到了。为了避免这个情况,TCP协议不允许处于TIME_WAIT状态的连接启动一个新的可用连接,因为TIME_WAIT状态持续2MSL,就可以保证当成功建立一个新TCP连接的时候,来自旧连接重复分组已经在网络中消逝。

总结一点,就是TIME_WAIT可以保证TCP的实现可靠的,而keepalive的设置可以让服务器端主动关闭减少,从而减少了这个状态,这样就会极大的提升服务器的吞吐量。


第二个,我们在这里讨论的是keep-alive,也就是http的keep-alive选项,而tcp也有keepalive的概念。

http keep-alive与tcp keep-alive,不是同一回事,意图不一样。http keep-alive是为了让tcp活得更久一点,以便在同一个连接上传送多个http,提高socket的效率。而tcp keep-alive是TCP的一种检测TCP连接状况的保鲜机制。

tcp keep-alive保鲜定时器,支持三个系统内核配置参数:

echo 1800 > /proc/sys/net/ipv4/tcp_keepalive_time

echo 15 > /proc/sys/net/ipv4/tcp_keepalive_intvl

echo 5 > /proc/sys/net/ipv4/tcp_keepalive_probes

keepalive是TCP保鲜定时器,当网络两端建立了TCP连接之后,闲置idle(双方没有任何数据流发送往来)了tcp_keepalive_time后,服务器内核就会尝试向客户端发送侦测包,来判断TCP连接状况(有可能客户端崩溃、强制关闭了应用、主机不可达等等)。如果没有收到对方的回答(ack包),则会在 tcp_keepalive_intvl后再次尝试发送侦测包,直到收到对对方的ack,如果一直没有收到对方的ack,一共会尝试 tcp_keepalive_probes次,每次的间隔时间在这里分别是15s, 30s, 45s, 60s, 75s。如果尝试tcp_keepalive_probes,依然没有收到对方的ack包,则会丢弃该TCP连接。TCP连接默认闲置时间是2小时,一般设置为30分钟足够了。

总结一下,实际上tcp keep-alive是一个协议级别的心跳检测实现,当超过规定的时间,tcp就断开,而这边是讨论的http的keepalive,描述的http高层多次tcp链接共享,根本不是一个网络层级的东西,一定注意不要混淆。


2.Tomcat的keepalive的配置实现

在不同的web服务器中,肯定都有keepalive的配置,一般都是三个参数,如Apache中的配置

路径在 apache_home/conf/extra/httpd-default.conf:


相应的配置,在Tomcat中Connector中也可以进行配置:


在tomcat中,http11之后,keepalive默认就是开启的

keepAliveTimeout:此时间过后连接就close了,单位是milliseconds 。

maxKeepAliveRequests: 最大长连接个数(1表示禁用,-1表示不限制个数,默认100个。一般设置在100~200之间). 


3.Tomcat的keepalive的基本实现原理

Tomcat的keepalive实现比较复杂,我们可以先从一个整体的技术框图来分析:



对于keepalive的请求,在Tomcat中分成三个通道进行接收,前面已经讲过,不同通道的线程模型是不同的,如上图所示,对于BIO通道中的没有Poller线程,所以对于http协议的处理都直接放到了工作线程池中(这也就是BIO之所以称之为阻塞通道的原因),而对于NIO和APR通道,Acceptor线程仅仅负责socket事件监听,一旦有事件,就转到Poller线程中进行处理和分析对应的事件event的key,最后调用Http11Processor进行工作线程池工作

如上图所示,实际上对Keepalive的处理最终都是在工作线程中SocketPorcessor中进行分析和处理,最终会调用Http11Processor(对应的实现都在AbstractHttp11Processor类中)的process方法中。

这个方法比较关键,主要的作用是将http请求头中的属性解析为Request,然后准备好Response,通过CoyAdaptor.service继续往容器中调用,而如果是keepalive的话,那么相当于请求响应的处理和普通的一次请求是不一样的,而区别就在于这个地方。

我们来具体分解一下这个方法,一共分成几个步骤:


步骤1,准备


首先准备SocketWrapper,SocketWrapper实际就是socket的包装类,而通过这个包装类加上一些属性,例如keepaliveout时间,keepaliveRequest的次数;其次,keepalive默认就是true,如果当前发现SocketWrapper包装类是不支持keepalive的,这种情况直接keepalive就是false,后续任凭你咋配置tomcat的keepalive的属性,keepalive也不能工作。

我们需要重点关注一下,最后一个disableKeepAlive方法:


这个方法通过代码分析,keepalive的开启和关闭,即使socket支持,协议也支持,但是当前线程池的比较忙的时候,超过一定的阈值,keepalive可能就会被禁用。

这个是因为当前线程数已经占据最大线程的一定的比例时,说明系统并发现在已经非常高了,这个时候其实缺少的是工作线程,因为现在已经有很多工作线程在排队,这个时候明智之举就是暂时放弃对keepAlive的支持(keepAlive=1),这样做的目的是为了支持更多的用户,因为keepAlive会让连接被同一个请求在工作线程中占用比较长时间,对于BIO来讲会一直占用。

通过这一点其实就可以更深刻理解,上面讲的keepAlive不是设置上就好,当大量短连接请求并发来的时候,请求的页面中的css和js非常非常多的时候,这个时候会出现新请求响应不了的请求,因为原有的请求通过keepalive占据着线程链接。


步骤二,启动大循环,识别该请求没有结束(是否keepalive模式开启后,连续的几个请求)跳出循环,释放或者出让工作线程


首先开启一个大循环,然后判断请求是否是该keepalive期间的最后的一个请求,如果是的话,那么在这里直接就进行break掉,释放掉该工作线程,因为活都已经干完了嘛,如果发现不是最后一个请求,或者后续还有可能有请求,那么这里务必需要将keepalive的模式的状态还要保持住,这些属性如openSocket和readComplete等状态,来保证下一次请求这些状态能正常工作。

通过这段代码就可以分析,在keepalive期间,工作线程池是可以进行释放或者出让的,至少从程序的逻辑上来看,保留了入口。

对于工作线程是否在keepalive期间出让或者释放的问题,详细见下一篇文章,这里就不再缀余了。


步骤三,通过prepareRequest方法解析请求头,基于客户端状态设置keepalive


这一步其实比较清晰,就是解析http请求头,看看是否支持keepalive;

先看看http协议,再看看请求头中的Connection字段,如果不是keepalive的话,是close的话,那么就需要强制关闭了,

最后看看客户端浏览器的agent是否支持,如果上面都可以的话,keepalive就可以设置了,如果一点不行,那么这里面直接就不能执行keepalive的逻辑,如果是Connection:close的话,处理完直接链接关闭。

从这一步上来看,keepalive也不是那么容易就开启的;


步骤四,设置Tomcat的keepalive:


到这一步了,说明至少环境上是可以满足keepalive了,但是前面讲过Tomcat的配置可以让keepalive停掉;

例如maxKeepAliveRequests如果设置成1了,这里直接keepalive就为false,相当于给禁止了,如果maxKeepAliveRequests大于0,走到这里执行了一次,需要减1,这就用到了前面准备阶段中的SocketWrapper的计数器。


步骤五,执行Tomcat容器部分,如果出现异常,关掉Keepalive


这一步就是执行容器,然后基于反馈,如果错误,直接置响应头为Connection:close,keepalive直接就没用了,链接都关了;


步骤六,设置request的keepalive阶段,看是否各变量符合跳出大循环


到这里,大循环任务已经完成,最后检验一下,如果出现错误,这里就会通过breakKeepAliveLoop跳出大循环;

如果一切正常,当前的Request的阶段就是STAGE_KEEPALIVE阶段;


步骤七,当大循环通过上述第二个步骤break掉,说明工作线程可以出让,这里需要设置SocketState为Open


代表该Socket是已经开启的状态,请求下一次过来后,一看到这个标识,说明工作线程这回执行的是一个keepalive,和上一次请求处理使用的是同一个Socket。


总结:

本文关注与keepalive的原理,Tomcat中的配置,与Tomcat中对keepalive的基本实现,下一篇会以线程池的视角,看看通过不同通道在keepalive下,究竟有哪些异同,从而分析出keepalive参数对性能为什么这么关键的原因。


文章转载自中间件技术讨论圈,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论