常见的http服务器有apache,nginx,iis,tomcat等。HTTP服务器本质上也是一种应用程序——它通常运行在服务器之上,绑定服务器的IP地址并监听某一个tcp端口来接收并处理HTTP请求,这样客户端(一般来说是IE, Firefox,Chrome这样的浏览器)就能够通过HTTP协议来获取服务器上的网页(HTML格式)、文档(PDF格式)、音频(MP4格式)、视频(MOV格式)等等资源。
Tomcat运行在JVM之上,它和HTTP服务器一样,绑定IP地址并监听TCP端口,同时还包含以下职责:
- 管理Servlet程序的生命周期。
- 将URL映射到指定的Servlet进行处理。
- 与Servlet程序合作处理HTTP请求——根据HTTP请求生成HttpServletResponse对象并传递给Servlet进行处理,将Servlet中的HttpServletResponse对象生成的内容返回给浏览器。
自己的第一门语言就是java,所以tomcat一直在用,但也基本只是在开发环境用一下,具体的参数也就改过JVM内存,没做过深入研究,主要还是因为自己工作一直没有需要去优化过tomcat。
最近由于工作上的原因,需要对tomcat并发进行优化。在tomcat的server.xml文件中有关于请求连接的配置Connector。本文就具体属性进行一下说明。
Connector的protocol属性,三种运行模式(BIO, NIO, APR)
1)BIO:一个线程处理一个请求。缺点:并发量高时,线程数较多,浪费资源。Tomcat7或以下在Linux系统中默认使用这种方式。
2)NIO:利用Java的异步IO处理,可以通过少量的线程处理大量的请求。Tomcat8在Linux系统中默认使用这种方式。Tomcat7必须修改Connector配置来启动(conf/server.xml配置文件) protocol="org.apache.coyote.http11.Http11NioProtocol"
3)APR(Apache Portable Runtime):从操作系统层面解决io阻塞问题。Linux如果安装了apr和native,Tomcat直接启动就支持apr。在Tomcat.7.0.30版本开始,tomcat自带tcnative-1.dll等文件,windows上默认是运行apr模式。
linux安装apr和native
apr是比较恶心的一个东东,因为用直接用yum install apr安装apr后,当再安装其他东西需要apr环境时候 经常还是找不到,尽管已经安装它了。需要的几个环境:
#yum -y install autoconf // 安装autoconf
#yum -y install libtool // 安装libtool
#yum -y install openssl openssl-devel // 安装openssl
这样的话我们只能通过下面这两个参数来指定他们的位置了
apr官网下载地址 http://apr.apache.org/download.cgi
安装apr:
#tar xvzf apr-1.5.2.tar.gz // 解压apr-1.5.2.tar.gz
#cd apr-1.5.2 // 进入apr-1.5.2目录
#./configure --prefix=/usr/apr // 指定安装到/usr/apr目录
#make & make install
安装tomcat-native:
#cd /usr/tomcat/apache-tomcat-7.0.59/bin // 切换目录,在tomcat/bin目录下找到tomcat-native.tar.gz;
#tar xvzf tomcat-native.tar.gz // 解压tomcat-native.tar.gz
#cd /usr/tomcat/apache-tomcat-7.0.59/bin/tomcat-native-1.1.32-src/jni/native // 切换目录
#./configure --with-apr=/usr/local/apr --with-java-home=/usr/java/jdk1.8.0_11 --with-ssl=/usr/bin --prefix=/usr/local/apr // 指定之前安装的目录
#make & make install
配置APR本地库到系统共享库搜索路径中
编辑$TOMCAT_HOME/bin/catalina.sh文件,在虚拟机启动参数JAVA_OPTS中添加java.library.path参数,指定apr库的路径
JAVA_OPTS="$JAVA_OPTS -Djava.library.path=/usr/local/apr/lib"
Tomcat8以下版本,需要指定运行模式,将protocol从HTTP/1.1改成org.apache.coyote.http11.Http11AprProtocol
这里重点比较一下BIO和NIO,在百度很多文章都是说BIO是一个请求一个线程,NIO可以一个线程服务多个请求。字面意思很清楚,但具体细节却不是很明白。虽然网上也有测试说明NIO在高并发的时候比BIO性能要好。我也做了一个简单的实验。
这里需要优化的就是网关tomcat,这里的网关tomcat主要起到的作用主要是验证请求者,然后通过http请求调用后端服务,没有复杂的业务逻辑处理。后端服务的提供者有很多,图中只举例了两个,各个服务由于业务不同,处理时间也不一样,有耗时的服务,也有快速返回的服务。网关tomcat如果按照默认的配置,则网关tomcat的服务器数量必须大于等于后端服务中并发量最大的那个服务的机器数量。这就形成了网关tomcat成为了整个系统的一个瓶颈,但网关tomcat只是起到一个网关作用,需要可以像nginx代理服务器那样可以抗大并发的能力。
由于后端机器使用的低配置的阿里云,10秒的耗时应用的并发能力不到100,也就是两台机器通过nginx负载均衡后200的并发。这里为了测试方便把网关tomcat的maxThreads设置为50。
首先做一个测试比较protocol分别为BIO和NIO时,并发请求耗时应用的情况。
这里测试了三种情况,分别为并发50,并发100,并发200,总请求数都是500,每种情况测试3次。通过图片数据可以看到BIO和NIO的结果没有什么区别。NIO并有像描述的那样一个线程可以处理更多的请求连接。网上也有实验数据说明NIO确实在并发处理上比BIO效果要好很多,但这个实验就不行,为什么呢?后来在知乎上的一篇文章上找到了点原因。
这张表格引用自apache-tomcat官方网站,对于connector的三种模式有很好的对比,NIO也不是完全非阻塞(读body和写response是模拟阻塞行为)的地方用红色突出了一下,于header的处理,NIO不会阻塞,只有在body的读取时,NIO采取模拟阻塞的方式。这是由于servlet规范,tomcat要实现servlet规范所以不能最大发挥NIO的特性。在read http body 以及 response的情况下,即使tomcat选择 NIO的 connector也是模拟阻塞的行为,因为servlet规范里定义了ServletInputStream在读数据时是阻塞模式。
关于Wait for next Request
它表示的是当开启keep-alive的情况下三种模式对等待下一次请求是否阻塞。
在BIO模式下,如果请求是开启keep-alive的话,socket在请求结束后仍处于OPEN状态,下一次请求仍可以复用当前socket而不必重新创建,在 finally 块里会判断连接状况如果是keep-alive会再次封装为同样的任务提交给线程池,重复这段逻辑,相当于一个循环,只不过每次执行的线程不一定相同。如果socket上已经没有请求了,最终socket会因超时或客户端close造成的EOFException而关闭。也就是说在BIO下的keep-alive的请求,只有等到keep-alive超时或者达到maxKeepAliveRequests时才会把线程放回tomcat的线程池,服务其他请求。
在NIO和APR模式下,即使开启keep-alive,在Wait for next Request时候,这个线程也是可以服务其他请求的。
看了上面这么多也就知道为什么会出现实验的效果了,NIO的非阻塞并不能完全解决我这个业务场景,因为即使是NIO下,在read body时也还是阻塞的,而我当前的业务场景是在serlvet中请求服务耗时。servlet的规范ServletInputStream在读数据时是阻塞模式这一特性是问题根本节点。通过调整maxThreads会是解决这种并发的一个方式。
在查找上面BIO和NIO比较的时候,我看到有人说到了servlet3.0中有异步serlvet,可以解决serlvet阻塞的问题。于是我使用异步servlet重试了上诉实验:
通过实验结果可以看到在NIO下使用异步serlvet确实可以达到想要的效果,tomcat网关在线程数50时也能处理并发200个请求。
AsyncContext asyncContext = request.startAsync();
asyncContext.start();//如果使用start方法,虽然把请求交给了另一个线程,但这个线程还是使用的tomcat的线程。
//这里最好使用一个线程池,为了简化测试,我直接使用new Thread
new Thread(new AsyncRequest(asyncContext)).start();//这样就可以把tomcat的线程还回给tomcat线程池接收新请求了。
Connector的其他属性
connectionTimeout:网络连接超时,单位:毫秒。设置为0表示永不超时,通常可设置为20000毫秒。
URIEncoding:指定Tomcat 容器的URL编码格式。
enableLookups:如果为true,则可以通过调用request.getRemoteHost()进行DNS查询来得到远程客户端的实际主机名,若为false则不进行DNS查询,而是返回其ip地址。建议设置false,能提高部分性能。
maxThreads:tomcat起动的最大线程数,即同时处理的任务个数,默认值为200
acceptCount:当tomcat起动的线程数达到最大时,接受排队的请求个数,默认值为100
minSpareThreads:表示即使没有人使用也开这么多空线程等待
maxConnections:这个貌似应该是新加的参数吧,我没求证。服务器能接受和处理的最大数量,bio默认是maxThreads的值,nio默认10000,APR/native 默认是8192。
keepAliveTimeout:这个是保持长连接的时间限制,默认就是connectionTimeout设置的时间。
关于keepAlive,tomcat长连接短连接
1.WEB应用有很多,下面就两个典型的应用(管理页面和接口服务)做对比。
管理页面:多涉及到用户的登录和长时间的频繁操作处理,这些操作都集中在一个session中,建议采用长连接;
接口服务:比如常见的webservice,操作集中在很短时间内完成,不需要对session进行维护,建议采用短连接。
HTTP1.1默认采用长连接,需要去掉长连接的话,只需修改默认配置参数maxKeepAliveRequests。把maxKeepAliveRequests="5",意思是每个连接只响应5个请求,然后就shutdown连接.不用等到keepAliveTimeout就关闭这个连接
Apache优化之KeepAlive
1)KeepAlive是在HTTP1.1中定义的,用来保持客户机和服务器的长连接,通过减少建立TCP Session的次数来提高性能。
2)常用的配置参数有{KeepAlive,KeepAliveTimeout,MaxKeepAliveRequests}
KeepAlive是决定开启KeepAlive支持;
KeepAliveTimeout决定一 个KeepAlive的连接能保持多少时间,Timeout就尽快shutdown链接,若还有数据必须再建立新的连接了;
MaxKeepAliveRequests于KeepAliveTimeout相似,意思是服务多少个请求就shutdown连接。
3)对于KeepAlive的配置需要慎重,错误的参数可能导致严重的性能问题。
一个高负载的Server,如果建立的很多长连接将无法继续服 务新的连接。因此需要根据server的性质调整KeepAliveTimeout或是MaxKeepAliveRequests的值。
其他:tomcat内存优化
主要是在bin/catalina.bat或bin/catalina.sh 配置文件中进行。linux上,在catalina.sh中添加:
JAVA_OPTS="-server -Xms1G -Xmx2G -Xss256K -Djava.awt.headless=true -Dfile.encoding=utf-8 -XX:MaxPermSize=256m -XX:PermSize=128M -XX:MaxPermSize=256M"
其中:
• -server:启用jdk的server版本。
• -Xms:虚拟机初始化时的最小堆内存。
• -Xmx:虚拟机可使用的最大堆内存。
-Xms与-Xmx设成一样的值,避免JVM因为频繁的GC导致性能大起大落
• -XX:PermSize:设置非堆内存初始值,默认是物理内存的1/64。
• -XX:MaxNewSize:新生代占整个堆内存的最大值。
• -XX:MaxPermSize:Perm(俗称方法区)占整个堆内存的最大值,也称内存最大永久保留区域。
1)错误提示:java.lang.OutOfMemoryError:Java heap space
Tomcat默认可以使用的内存为128MB,在较大型的应用项目中,这点内存是不够的,有可能导致系统无法运行。常见的问题是报Tomcat内存溢出错误,Outof Memory(系统内存不足)的异常,从而导致客户端显示500错误,一般调整Tomcat的-Xms和-Xmx即可解决问题,通常将-Xms和-Xmx设置成一样,堆的最大值设置为物理可用内存的最大值的80%。
set JAVA_OPTS=-Xms512m-Xmx512m
2)错误提示:java.lang.OutOfMemoryError: PermGenspace
PermGenspace的全称是Permanent Generationspace,是指内存的永久保存区域,这块内存主要是被JVM存放Class和Meta信息的,Class在被Loader时就会被放到PermGenspace中,它和存放类实例(Instance)的Heap区域不同,GC(Garbage Collection)不会在主程序运行期对PermGenspace进行清理,所以如果你的应用中有很CLASS的话,就很可能出现PermGen space错误,这种错误常见在web服务器对JSP进行precompile的时候。如果你的WEB APP下都用了大量的第三方jar, 其大小超过了jvm默认的大小(4M)那么就会产生此错误信息了。解决方法:
setJAVA_OPTS=-XX:PermSize=128M
3)在使用-Xms和-Xmx调整tomcat的堆大小时,还需要考虑垃圾回收机制。如果系统花费很多的时间收集垃圾,请减小堆大小。一次完全的垃圾收集应该不超过3-5 秒。如果垃圾收集成为瓶颈,那么需要指定代的大小,检查垃圾收集的详细输出,研究垃圾收集参数对性能的影响。一般说来,你应该使用物理内存的 80% 作为堆大小。当增加处理器时,记得增加内存,因为分配可以并行进行,而垃圾收集不是并行的。