原文链接:http://www.cubrid.org/blog/dev-platform/maxclients-in-apache-and-its-effect-on-tomcat-during-full-gc/
本文是GC专家系列中的第四篇。在第一篇理解Java垃圾回收中我们学习了几种不同的GC算法的处理过程,GC的工作方式,新生代与老年代的区别。所以,你应该已经了解了JDK 7中的5种GC类型,以及每种GC对性能的影响。
在第二篇Java垃圾回收的监控中介绍了在真实场景中JVM是如何运行GC,如何监控GC数据以及有哪些工具可用来方便进行GC监控。
在第三篇GC 调优中基于真实案例介绍了可用于GC调优的最佳选项。同时也描述了如何通过降低移动到老年代中对象的数量来缩短Full GC耗时,以及如何设置GC类型及内存大小。
本文将介绍Apache的MaxClients
参数的重要性以及在GC发生时对系统整体性能的显著影响。通过几个例子,你将会更清晰的理解MaxClients
值所引发的问题。最后会介绍如何依据系统的可用内存来为MaxClients
设置合理的数值。
MaxClients对系统的影响
NHN的服务运行环境中有大量的流控(Throttle valve)选项,这些选项对系统的稳定运行具有重要作用。我们来看下Apache的MaxClients
选项在Tomcat发生Full GC时会对系统带来哪些影响。
大部分的开发人员都知道GC 发生中会伴随着"stop the world(STW)现象"(具体详情参考理解Java垃圾回收)。尤其是NHN的Java开发人员可能都经历过在Tomcat中由GC相关问题而导致的系统崩溃。因为JVM管理内存,因此Java应用系统不可避免的会遇到GC引起的STW现象。
在你开发的线上系统中,GC每天都会发生很多次。在GC发生时,即便TTS没有发生,却依然可能会给用户503的错误响应。
系统运行环境
根据结构特点,Web服务更适合于做横向扩展而非单纯的提高单一机器的性能。所以通常根据性能需要,Web服务的服务器部署结构由一台Apache服务器和多台Tomcat服务器组成。在本文中,假设一个Apache服务和Tomcat服务部署在同一台物理主机上,如下图所示:
图1: 本文假设的服务运行环境
作为参考,本文所述参数均是基于Apache 2.2.21(prefork MPM),Tomcat 6.0.35,jdk 1.6.0_24,并运行在CentOS 4.7.2(32位)操作系统上。
系统内存2GB,并使用ParallelOldGC垃圾回收,默认开启了AdaptiveSizePolicy
选项并设置堆大小为600MB。
STW 和 HTTP 503
假设Apache的流量为200QPS,并开启10个httpd处理进程(尽管实际场景依赖于请求的响应时间)。在这种前提下,假设full GC导致的停顿耗时1秒,如果Tomcat发生了Full GC将会怎么样?
首先你能想到的是full GC导致Tomcat停顿,处理中的请求将得不到响应。如果这样,Tomcat暂停,请求得不到处理,Apache将会怎么样?
即使Tomcat因Full GC而暂停处理,而请求却仍以200 req/s的速度到达Apache。在full GC发生前,只需要10个或者稍微多一点的httpd进程就可以快速响应服务请求。但是现在Tomcat暂停了,为了处理新的请求Apache将持续创建新的httpd进程直到httpd.conf文件中定义的MaxClients
阀值。因为MaxClients
默认值为256,所以200 req/s的请求并不会带来太大问题。
这个时候,新创建的httpd 进程会怎么样?
Httpd 进程使用mod_jk模块管理的AJP连接池中的空闲连接把请求发送到Tomcat。如果没有空闲连接,则会要求创建新的连接。然而因为Tomcat处理暂停状态,新建连接的请求将被拒绝。所以这些请求将会放到堆积队列(backlog queue),队列的长度是server.xml的AJP Connector中设置的。
如果请求数据超出了堆积队列的长度,Apache将会收到连接拒绝错误,并把这个错误以HTTP 503的方式返回给用户。
在本例的中,堆积队列的长度默认设置为100,而请求速度为200 req/s,因此在由full GC导致Tomcat暂停的这1秒中,将有超过100的请求将会收到503错误。
Full GC结束之后,堆积队列中的socket连接会被Tomcat接收并分配给工作线程(最大工作线程数由MaxThreads
决定,默认值为200)来处理请求。
MaxClients和堆积队列
在上面的场景中,如何设置才能避免给用户返回503错误?
首先我们需要知道,应该设置足够的堆积队列长度以容纳在Tomcat Full GC导致的暂停期间流入的请求。因此堆积队列最小长度至少为200(上文中QPS为200)。
这样配置以后,是否还有其他问题?
把堆积队列长度设置为200后,我们再次重复上面的场景。结果问题却比之前更加严重。
正常情况下系统内存使用量维持在50%,而在发生Full GC时内存使用却迅速上升到100%,引起内存交换区(swap)使用量的极剧增加。更为严重的是Full GC导致的响应停顿由原来的1秒增加到了4秒,直接后果就是期间系统像挂掉了一样,不能响应任何请求。
在之前的场景中,只有100左右的请求会收到 503 的错误,而增加堆积队列到200后却导致了500甚至更多的请求被挂起至少3秒不能收到任何响应。
这个例子很好的证明了如果不能准备的理清配置信息之间的因果关系,可能会对系统带来极为严重的影响。
为什么会这样?
原理就是要清楚MaxClients
选项的特性。
MaxClients
的值不易设置过大,设置MaxClients
的关键在于即便创建了MaxClients
数量的httpd进程,也要需要维持应用系统的内存使用量不应超过80%。
系统交换区默认值为60,因此如果内存使用超过80%,系统将会发生频繁的内存交换。
我们再来看下为什么这个特性会导致上面所述的严重后果。
当请求的QPS为200时,Tomcat会被Full GC暂停响应,然后把堆积列队容量设置为200。起初大约有100个额外的httpd 进程会被Apache创建,紧接着内存使用量超过了80%,引起操作系统主动的使用交换区的内存空间,而因GC存活在JVM老年代中的对象被操作系统误认为长时间未使用,从而导致这些对象被移动到交换区。
最后,当GC过程中涉及到交换区时,耗时就会迅速增加。而后httpd进程数继续增加,导致内存使用量达到了100%,从而出现了上述的严重后果。
上述案例的前后区别仅在于堆积队列的长度:100和200。但为什么在200时会出现更严重的状况?
原因是堆积队列不同的长度导致了httpd进程数的不同。当值为100时,在发生Full GC时100个请求所要求创建的连接被置于堆积队列中。再有新的请求会被拒绝并返回503错误,所以系统的整个httpd的进程数仅超出100很少的数量。
但当队列长度设置为200时,有200个请求被接收并置于队列中。从而导致httpd进程的数量超过200,并触发了操作系统进行内存交换的阀值。
所以,如果不顾内存使用情况而一味的加大MaxClients
的数值,将会导致Full GC时httpd进程数迅速增加,引进内存交换并最终降低系统的整体性能。
所以如何设置MaxClients,如何找到当前系统的阀值?
MaxClients 取值的计算方式
如果系统总内存为2GB,设置MaxClient
的值需要保证在任何时候内存的使用量不超过80%即1.6GB,从而避免因内存交换导致的性能下降。也就是说仅有1.6GB空间供Apache, Tomcat和其他默认安装的代理程序共享和分配内存。
假如默认安装的代理程序占用200M内存;Tomcat的堆空间设置-Xmx
为600M,如下图所示,Tomcat总占用量将725M (持久代 + 本地堆空间)。Apache可使用的空间为剩下的700M。
图 2:Top命令的截图
对于Apache的700M内存,该如何设置合理的MaxClients值?
当然这也取决于Apache加载的模块类型和数量。以NHN的Web服务为例,把Apache当作简单的代理使用,根据上图RES显示,4M空间对于每个httpd进程来说已足够使用。因此700M空间能设置的MaxClients
为175。
总结
可靠的服务配置要能够在满载的情况下降低系统停顿时间并能够最大范围的保证成功响应用户请求。对于Java应用来说,必须要确认在Full GC引起的SWT情况下,系统的配置是否能够提供足够可靠的服务。
如果为了应对单纯的请求增加和防止DDos攻击,在不考虑内存使用的情况下把MaxClients
设置过大,那么MaxClients
不但会失去作为流控的用途,反而会带来更为严重的后果。
在这个案例中,解决问题的最优途径是加大系统的内存,或者设置MaxClients
为175(上面的计算结果)以保证只有QPS超过175时才会出现503错误。
作者:Dongsoon Choi,游戏服务技术支持团队高级工程师,NHN公司