这个bug困扰咱们很长一段时间,最初是在生产环境发现的,为了确保项目发布,紧急状况下让应用切换成了BIO。后来没能重现,你们没足够重 视,一直没有去跟这个问题,直到最近再次发现这个问题,发现是NIO模式默认对静态资源启用了sendfile以提高性能,但这里存在bug所致。官方已 经在7051后续版本修复了这个问题,最好升级到最新版本。或者在server.xml的Connector节点里增长: useSendfile=”false” 来避免。 html
下面是相关的异常信息,若是你的tomcat是7051以前的版本,采用NIO而且没有显式的关闭sendfile,应用里有静态资源,访问静态资源时tomcat日志里出现了下面的异常(若是前边有nginx或apache返回502),极可能是同一问题: java
java.lang.NullPointerException at org.apache.catalina.connector.Request.notifyAttributeAssigned(Request.java:1565) at org.apache.catalina.connector.Request.setAttribute(Request.java:1556) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:178) at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:100) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:410) at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1043) at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:603) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1721) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1679) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) at java.lang.Thread.run(Thread.java:744) java.lang.NullPointerException at org.apache.coyote.http11.InternalNioOutputBuffer.addToBB(InternalNioOutputBuffer.java:210) at org.apache.coyote.http11.InternalNioOutputBuffer.commit(InternalNioOutputBuffer.java:202) at org.apache.coyote.http11.AbstractHttp11Processor.action(AbstractHttp11Processor.java:781) at org.apache.coyote.Response.action(Response.java:172) at org.apache.coyote.http11.AbstractOutputBuffer.endRequest(AbstractOutputBuffer.java:302) at org.apache.coyote.http11.InternalNioOutputBuffer.endRequest(InternalNioOutputBuffer.java:120) at org.apache.coyote.http11.AbstractHttp11Processor.endRequest(AbstractHttp11Processor.java:1743) at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1087) at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:603) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1721) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1679) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) at java.lang.Thread.run(Thread.java:744) java.lang.NullPointerException at org.apache.tomcat.util.buf.MessageBytes.toBytes(MessageBytes.java:244) at org.apache.catalina.connector.CoyoteAdapter.parsePathParameters(CoyoteAdapter.java:807) at org.apache.catalina.connector.CoyoteAdapter.postParseRequest(CoyoteAdapter.java:579) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:405) at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1043) at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:603) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1721) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1679) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) at java.lang.Thread.run(Thread.java:744) java.lang.NullPointerException at org.apache.coyote.http11.InternalNioOutputBuffer.flushBuffer(InternalNioOutputBuffer.java:233) at org.apache.coyote.http11.InternalNioOutputBuffer.endRequest(InternalNioOutputBuffer.java:121) at org.apache.coyote.http11.AbstractHttp11Processor.endRequest(AbstractHttp11Processor.java:1743) at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1087) at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:603) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1721) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1679) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) at java.lang.Thread.run(Thread.java:744)
这个脚本用于定位应用classpath下有哪些jar包冲突,列出它们的类似度,以及冲突的class个数,执行效果以下: node
$ ./cp-check.sh . Similarity DuplicateClasses File1 File2 %100 502 jackson-mapper-asl-1.9.13.jar jackson-mapper-lgpl-1.9.6.jar %100 21 org.slf4j.slf4j-api-1.5.6.jar slf4j-api-1.5.8.jar %100 9 jcl-over-slf4j-1.5.8.jar org.slf4j.jcl-over-slf4j-1.5.6.jar %100 6 org.slf4j.slf4j-log4j12-1.5.6.jar slf4j-log4j12-1.5.8.jar %99 120 jackson-core-asl-1.9.13.jar jackson-core-lgpl-1.9.6.jar %98 513 jboss.jboss-netty-3.2.5.Final.jar netty-3.2.2.Final.jar %98 256 jakarta.log4j-1.2.15.jar log4j-1.2.14.jar %98 97 json-lib-2.2.3.jar json-lib-2.4-jdk15.jar %87 186 fastjson-1.1.15.jar fastjson-1.1.30.jar %85 215 cglib-nodep-3.1.jar sourceforge.cglib-0.0.0.jar %83 93 commons-beanutils-1.7.0.jar commons-beanutils-core-1.7.0.jar %21 6 commons-logging-1.1.1.jar org.slf4j.jcl-over-slf4j-1.5.6.jar %21 6 commons-logging-1.1.1.jar jcl-over-slf4j-1.5.8.jar %16 18 commons-beanutils-1.7.0.jar commons-beanutils-bean-collections-1.7.0.jar %04 8 batik-ext-1.7.jar xml-apis-1.0.b2.jar %02 10 commons-beanutils-core-1.7.0.jar commons-collections-3.2.1.jar %02 10 commons-beanutils-1.7.0.jar commons-collections-3.2.1.jar See /tmp/cp-verbose.log for more details.
脚本同时会输出一个包含全部冲突的class文件:/tmp/cp-verbose.log这个verbose文件内容大体以下,记录每一个有冲突的class位于哪些jar包,定位问题时能够去查: linux
org/jboss/netty/util/internal/SharedResourceMisuseDetector.class jboss.jboss-netty-3.2.5.Final.jar,netty-3.2.2.Final.jar org/jboss/netty/util/internal/StackTraceSimplifier.class jboss.jboss-netty-3.2.5.Final.jar,netty-3.2.2.Final.jar org/jboss/netty/util/internal/StringUtil.class jboss.jboss-netty-3.2.5.Final.jar,netty-3.2.2.Final.jar
脚本内容: nginx
#!/bin/bash if [ $# -eq 0 ];then echo "please enter classpath dir" exit -1 fi if [ ! -d "$1" ]; then echo "not a directory" exit -2 fi tmpfile="/tmp/.cp$(date +%s)" tmphash="/tmp/.hash$(date +%s)" verbose="/tmp/cp-verbose.log" declare -a files=(`find "$1" -name "*.jar"`) for ((i=0; i < ${#files[@]}; i++)); do jarName=`basename ${files[$i]}` list=`unzip -l ${files[$i]} | awk -v fn=$jarName '/\.class$/{print $NF,fn}'` size=`echo "$list" | wc -l` echo $jarName $size >> $tmphash echo "$list" done | sort | awk 'NF{ a[$1]++;m[$1]=m[$1]","$2}END{for(i in a) if(a[i] > 1) print i,substr(m[i],2) }' > $tmpfile awk '{print $2}' $tmpfile | awk -F',' '{i=1;for(;i<=NF;i++) for(j=i+1;j<=NF;j++) print $i,$j}' | sort | uniq -c | sort -nrk1 | while read line; do dup=${line%% *} jars=${line#* } jar1=${jars% *} jar2=${jars#* } len_jar1=`grep -F "$jar1" $tmphash | grep ^"$jar1" | awk '{print $2}'` len_jar2=`grep -F "$jar2" $tmphash | grep ^"$jar2" | awk '{print $2}'` len=$(($len_jar1 > $len_jar2 ? $len_jar1 : $len_jar2)) per=$(echo "scale=2; $dup/$len" | bc -l) echo ${per/./} $dup $jar1 $jar2 done | sort -nr -k1 -k2 | awk 'NR==1{print "Similarity DuplicateClasses File1 File2"}{print "%"$0}'| column -t sort $tmpfile | awk '{print $1,"\n\t\t",$2}' > $verbose echo "See $verbose for more details." rm -f $tmpfile rm -f $tmphash
这个是改良过的脚本;第一次实现的时候是采用常规思路,用冒泡的方式比较两个jar文件的类似度,测试一二十个jar包的时候没有问题,找一个有 180多个jar包的应用来跑的时候发现很是慢,上面改良后的脚本在个人mac上检查这个应用大概3秒左右,在linux上检测一个300个jar左右的 应用4~5秒,基本上够用了。 web
为了兼容mac(有些命令在linux与mac/bsd上方式不一样),大部分状况下采用awk来处理,不过我对awk也不太熟,只好采用逐步拼接的 方式,若是经过一个awk脚原本实现或许性能能够高一些,但也比较有限,大头仍是在获取jar里的class列表那块。几个tips: docker
脚本已放到服务器上,能够经过下面的方式运行: shell
$ bash <(curl -s http://hongjiang.info/cpcheck.sh) libdir
tomcat默认是开启keep-alive的,有3个相关的参数能够配置: apache
表示在复用一个链接时,两次请求之间的最大间隔时间;超过这个间隔服务器会主动关闭链接。默认值同connectionTimeout参数,即20秒。不作限制的话能够设置为-1. json
表示一个链接最多可复用多少次请求,默认是100。不作限制能够设置为-1. 注意若是tomcat是直接对外服务的话,keep-alive特性可能被一些DoS攻击利用,好比以很慢的方式发生数据,能够长时间持有这个链接;从而 可能被恶意请求耗掉大量链接拒绝服务。tomcat直接对外的话这个值不宜设置的太大。
注意,这个参数是BIO特有的,默认状况BIO在线程池里的线程使用率超过75%时会取消keep-alive,若是不取消的话能够设置为100.
tomcat对每一个请求的超时时间是经过connectionTimeout参数设置的。默认的server.xml里的设置是20秒,若是不设置这个参数代码里会使用60秒。
这个参数也会对POST请求有影响,但并非指上传完的时间限制,而是指两次数据发送中间的间隔超过connectionTimeout会被服务器断开。能够模拟一下,先修改server.xml,把connectionTimeout设置为2秒:
<Connector port="7001" protocol="HTTP/1.1" connectionTimeout="2000" redirectPort="8443" />
先看看是否已生效:
$ time telnet localhost 7001 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. Connection closed by foreign host. telnet localhost 7001 0.01s user 0.00s system 0% cpu 2.016 total
telnte后没有发送数据,看到2秒左右被服务器关闭了,证实配置生效了。
如今经过telnet发送数据:
$ telnet localhost 7001 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. POST /main HTTP/1.1 host: localhost:7001 Content-type:application/x-www-form-urlencoded Content-length:10 a
上面咱们模拟一次POST请求,指定的长度是10,但指发送了一个字符,这里等待2秒,会被服务器端认为超时,被强制关闭。response信息以下:
HTTP/1.1 200 OK Server: Apache-Coyote/1.1 Content-Length: 10 Date: Thu, 04 Sep 2014 08:20:08 GMT done: null Connection closed by foreign host.
若是想对POST状况不使用connectionTimeout来限制,还有另外两个参数可用。这两个参数必须配合使用才行:
disableUploadTimeout="false" connectionUploadTimeout="10000"
必需要设置disableUploadTimeout为false(默认是true),才能够对POST请求发送数据超时使用其余参数来设置,这样在发送数据的过程当中最大能够等待的时间间隔就再也不由connectionTimeout决定,而是由connectionUploadTimeout决定。
Q: tomcat的关闭过程是怎么触发的?是经过系统信号吗?若是存在多个tomcat进程,关闭时怎么保证不会误杀?
A: 这个过程能够跟踪一下关闭时的脚本就知道了。
$ bash -x ./catalina.sh stop ... eval '"/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home/bin/java"' -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Dlog4j.defaultInitOverride=true -Dorg.apache.tomcat.util.http.ServerCookie.ALLOW_EQUALS_IN_VALUE=true -Dorg.apache.tomcat.util.http.ServerCookie.ALLOW_HTTP_SEPARATORS_IN_V0=true ' -Djava.endorsed.dirs="/data/server/tomcat/endorsed"' -classpath '"/data/server/tomcat/bin/bootstrap.jar:/data/server/tomcat/bin/tomcat-juli.jar"' ' -Dcatalina.base="/data/server/tomcat"' ' -Dcatalina.home="/data/server/tomcat"' ' -Djava.io.tmpdir="/data/server/tomcat/temp"' org.apache.catalina.startup.Bootstrap stop
可见是新启了一个java进程,调用org.apache.catalina.startup.Bootstrap的main方法,传入的stop参数。
跟踪一下这个新的java进程执行过程,堆栈大体以下:
at java.net.Socket.(Socket.java:208) at org.apache.catalina.startup.Catalina.stopServer(Catalina.java:477) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:601) at org.apache.catalina.startup.Bootstrap.stopServer(Bootstrap.java:371) at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:452)
在Bootstrap的main方法里的,对stop参数会执行stopServer的操做:
... else if (command.equals("stop")) { daemon.stopServer(args); }
stopServer是经过反射调用的Catalina.stopServer,它经过解析当前CATALINA_HOME/conf/server.xml从中获得正在运行的tomcat实例的关闭端口(server port, 默认是8005)和关闭指令(默认是SHUTDOWN),而后经过socket链接到这个目标端口上,发送关闭指令。若是咱们直接telnet到目标端口,而后输入指令也是同样的:
因此经过默认脚本关闭tomcat,并不关心tomcat进程pid,而是socket通信的方式。若是存在多个tomcat实例,每一个tomcat的server port都是不一样的。
若是不经过8005端口的方式,而是系统信号的方式,tomcat则是经过了ShutdownHook来确保在进程退出前关闭服务的。这时若是有多个tomcat进程实例,就须要明确进程pid了,一些改进的脚本会在启动时把进程pid记录在某个文件来以便后续使用。
tomcat在处理每一个链接时,Acceptor角色负责将socket上下文封装为一个任务SocketProcessor而后提交给线程池处理。在BIO和APR模式下,每次有新请求时,会建立一个新的SocketProcessor实例(在以前的tomcat对keep-alive的实现逻辑里也介绍过能够简单的经过SocketProcessor与SocketWrapper实例数对比socket的复用状况);而在NIO里,为了追求性能,对SocketProcessor也作了cache,用完后将对象状态清空而后放入cache,下次有新的请求过来先从cache里获取对象,获取不到再建立一个新的。
这个cache是一个ConcurrentLinkedQueue,默认最多可缓存500个对象(见SocketProperties)。能够经过socket.processorCache来设置这个缓存的大小,注意这个参数是NIO特有的。
接下来在SocketProcessor执行过程当中,真正的业务逻辑是经过一个org.apache.coyote.Processor的接口来封装的,默认这个Processor的实现是org.apache.coyote.http11.Http11Processor。咱们看一下SocketProcessor.process(...)方法的大体逻辑:
public SocketState process(SocketWrapper<S> wrapper, SocketStatus status) { ... // 针对长轮询或upgrade状况 Processor<S> processor = connections.get(socket); ... if (processor == null) { // 1) 尝试从回收队列里获取对象 processor = recycledProcessors.poll(); } if (processor == null) { // 2) 没有再建立新的 processor = createProcessor(); } ... state = processor.process(wrapper); ... release(wrapper, processor, ...); ... return SocketState.CLOSED; }
上面的方法是在AbstractProtocol模板类里,因此BIO/APR/NIO都走这段逻辑,这里使用了一个回收队列来缓存Processor,这个回收队列是ConcurrentLinkedQueue的一个子类,队列的长度可经过server.xml里connector节点的processorCache属性来设置,默认值是200,若是不作限制的话能够设置为-1,这样cache的上限将是最大链接数maxConnections的大小。
在原有的一张ppt上加工了一下把这两个缓存队列所在位置标示了一下,图有点乱,重点是两个绿颜色的cache队列:
图中位于上面的socket.processorCache队列是NIO独有的,下面的processorCache是三种链接器均可以设置的。processorCache这个参数在并发量比较大的状况下也蛮重要的,若是设置的过小,可能引发瓶颈。咱们模拟一下,看看这个瓶颈是怎么回事。先修改server.xml里的connector节点,把processorCache设置为0:
<Connector port="7001" protocol="org.apache.coyote.http11.Http11NioProtocol" connectionTimeout="20000" redirectPort="8443" processorCache="0"/>
启动tomcat后,使用ab模拟并发请求:
$ ab -n100000 -c10 http://localhost:7001/main
而后在ab的执行过程当中马上执行jstack观察堆栈信息,会发现一大半线程阻塞在AbstractConnectionHandler.register或AbstractConnectionHandler.unregister方法上:
"http-nio-7001-exec-11" #34 daemon prio=5 os_prio=31 tid=0x00007fd05ab05000 nid=0x8903 waiting for monitor entry [0x000000012b3b7000] java.lang.Thread.State: BLOCKED (on object monitor) at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.register(AbstractProtocol.java:746) - waiting to lock <0x00000007403b8950> (a org.apache.coyote.http11.Http11NioProtocol$Http11ConnectionHandler) at org.apache.coyote.http11.Http11NioProtocol$Http11ConnectionHandler.createProcessor(Http11NioProtocol.java:277) at org.apache.coyote.http11.Http11NioProtocol$Http11ConnectionHandler.createProcessor(Http11NioProtocol.java:139) at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:585) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1720) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1679) ... "http-nio-7001-exec-4" #27 daemon prio=5 os_prio=31 tid=0x00007fd0593e3000 nid=0x7b03 waiting for monitor entry [0x000000012aca2000] java.lang.Thread.State: BLOCKED (on object monitor) at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.unregister(AbstractProtocol.java:773) - locked <0x00000007403b8950> (a org.apache.coyote.http11.Http11NioProtocol$Http11ConnectionHandler) at org.apache.coyote.AbstractProtocol$RecycledProcessors.offer(AbstractProtocol.java:820) at org.apache.coyote.http11.Http11NioProtocol$Http11ConnectionHandler.release(Http11NioProtocol.java:219) at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:690) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1720)
register和unregister分别是在建立和回收processor的时候调用的;看一下createProcessor方法里的大体逻辑:
public Http11NioProcessor createProcessor() { Http11NioProcessor processor = new Http11NioProcessor(...); processor.setXXX(...); ... // 这里,注册到jmx register(processor); return processor; }
tomcat对jmx支持的很是好,运行时信息也有不少能够经过jmx获取,因此在每一个新链接处理的时候,会在建立processor对象的时候注册一把,而后在processor处理完回收的时候再反注册一把;但这两个方法的实现都是同步的,同步的锁是一个全局的ConnectionHandler对象,形成了多个线程会在这里串行。
绝大部分应用没有特别高的访问量,一般并不须要调整processorCache参数,但对于网关或代理一类的应用(尤为是使用servlet3的状况)这个地方能够设置的大一些,好比调到1000或者-1。
tomcat的最大链接数参数是maxConnections,这个值表示最多能够有多少个socket链接到 tomcat上。BIO模式下默认最大链接数是它的最大线程数(缺省是200),NIO模式下默认是10000,APR模式则是8192(windows 上则是低于或等于maxConnections的1024的倍数)。若是设置为-1则表示不限制。
在tomcat里经过一个计数器来控制最大链接,好比在Endpoint的Acceptor里大体逻辑以下:
while (running) { ... //if we have reached max connections, wait countUpOrAwaitConnection(); //计数+1,达到最大值则等待 ... // Accept the next incoming connection from the server socket socket = serverSock.accept(); ... processSocket(socket); ... countDownConnection(); //计数-1 closeSocket(socket); }
计数器是经过LimitLatch锁来实现的,它内部主要经过一个java.util.concurrent.locks.AbstractQueuedSynchronizer的实现来控制。
咱们在server.xml里对Connector增长maxConnections="1"这个参数,而后模拟2个链接:
for i in {1..2}; do ( { echo -ne "POST /main HTTP/1.1\nhost: localhost:7001\n\n"; sleep 20 } | telnet localhost 7001 )&; done
而后经过jstack能够看到acceptor线程阻塞在countUpOrAwaitConnection方法上:
"http-bio-7001-Acceptor-0" #19 daemon prio=5 os_prio=31 tid=0x00007f8acbcf1000 nid=0x6903 waiting on condition [0x0000000129c58000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x0000000740353f40> (a org.apache.tomcat.util.threads.LimitLatch$Sync) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836) at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java:997) at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1304) at org.apache.tomcat.util.threads.LimitLatch.countUpOrAwait(LimitLatch.java:115) at org.apache.tomcat.util.net.AbstractEndpoint.countUpOrAwaitConnection(AbstractEndpoint.java:755) at org.apache.tomcat.util.net.JIoEndpoint$Acceptor.run(JIoEndpoint.java:214) at java.lang.Thread.run(Thread.java:745)
对于NIO和APR的最大链接数默认值比较大,适合大量链接的场景;若是是BIO模式线程池又设置的比较小的话,就须要注意一下链接的处理是否够快,若是链接处理的时间较长,或新涌入的链接量比较大是不太适合用BIO的,调大BIO的线程数也可能存在利用率不高的状况。
若是没有对connector配置额外的线程池的话,maxThreads参数用来设置默认线程池的最大线程数。tomcat默认是200,对通常访问量的应用来讲足够了。
对于acceptCount这个参数,含义跟字面意思并非特别一致(我的感受),容易跟maxConnections,maxThreads等参数混淆;实际上这个参数在tomcat里会被映射成backlog:
static { replacements.put("acceptCount", "backlog"); replacements.put("connectionLinger", "soLinger"); replacements.put("connectionTimeout", "soTimeout"); replacements.put("rootFile", "rootfile"); }
backlog表示积压待处理的事物,是socket的参数,在bind的时候传入的,好比在Endpoint里的bind方法里:
public void bind() throws Exception { serverSock = ServerSocketChannel.open(); ... serverSock.socket().bind(addr,getBacklog()); ... }
这个参数跟tcp底层实现的半链接队列和彻底链接队列有什么关系呢?咱们在tomcat默认BIO模式下模拟一下它的效果。
模拟的思路仍是简单的经过shell脚本,创建一个长链接发送请求,持有20秒再断开,好有时间观察网络状态。注意BIO模式下默认超过75%的线 程时会关闭keep-alive,须要把这个百分比调成100,这样就不会关闭keep-alive了。修改后的connector以下,最后边的三行参 数是新增的:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" maxThreads="1" disableKeepAlivePercentage="100" acceptCount="2" />
上面的配置里咱们把tomcat的最大线程数设置为1个,一直开启keep-alive,acceptCount设置为2。在linux上能够经过ss命令检测参数是否生效:
$ ss -ant State Recv-Q Send-Q Local Address:Port Peer Address:Port LISTEN 0 2 :::7001 :::*
能够看到7001端口是LISTEN状态,send-q的值是2,也就是咱们设置的backlog的值。若是咱们不设置,tomcat默认会设置为100,java则默认是50。
而后用下面的脚本模拟一次长链接:
$ { echo -ne "POST /main HTTP/1.1\nhost: localhost:7001\n\n"; sleep 20 } | telnet localhost 7001
这个时候看服务器端socket的情况,是ESTABLISHED,而且Recv-Q和Send-Q都是没有堆积的,说明请求已经处理完
$ netstat -an | awk 'NR==2 || $4~/7001/' Proto Recv-Q Send-Q Local Address Foreign Address (state) tcp4 0 0 127.0.0.1.7001 127.0.0.1.54453 ESTABLISHED
如今咱们模拟多个链接:
$ for i in {1..5}; do ( { echo -ne "POST /main HTTP/1.1\nhost: localhost:7001\n\n"; sleep 20 } | telnet localhost 7001 )&; done
上面发起了5个连接,服务器端只有1个线程,只有第一个链接上的请求会被处理,另外4次链接,有2个链接仍是完成了创建(ESTABLISHED状态),还有2个链接则由于服务器端的链接队列已满,没有响应,发送端处于SYN_SENT状态。下面列出发送端的tcp状态:
$ netstat -an | awk 'NR==2 || $5~/7001/' Proto Recv-Q Send-Q Local Address Foreign Address (state) tcp4 0 0 127.0.0.1.51389 127.0.0.1.7001 SYN_SENT tcp4 0 0 127.0.0.1.51388 127.0.0.1.7001 SYN_SENT tcp4 0 0 127.0.0.1.51387 127.0.0.1.7001 ESTABLISHED tcp4 0 0 127.0.0.1.51386 127.0.0.1.7001 ESTABLISHED tcp4 0 0 127.0.0.1.51385 127.0.0.1.7001 ESTABLISHED
再看tomcat端的状态:
$ netstat -an | awk 'NR==2 || $4~/7001/' Proto Recv-Q Send-Q Local Address Foreign Address (state) tcp4 45 0 127.0.0.1.7001 127.0.0.1.51387 ESTABLISHED tcp4 45 0 127.0.0.1.7001 127.0.0.1.51386 ESTABLISHED tcp4 0 0 127.0.0.1.7001 127.0.0.1.51385 ESTABLISHED
有3个连接,除了第一条链接请求的Recv-Q是0,另外两个链接的Recv-Q则有数据堆积(大小表示发送过来的字节长度)。注意,在ESTABLISHED状态下看到的Recv-Q或Send-Q的大小与在LISTEN状态下的含义不一样,在LISTEN状态下的大小表示队列的长度,而非数据的大小。
从上面的模拟能够看出acceptCount参数是指服务器端线程都处于busy状态时(线程池已满),还可接受的链接数,即tcp的彻底链接队列的大小。对于彻底队列的计算,在linux上是:
min(backlog,somaxconn)
即backlog参数和proc/sys/net/core/somaxconn这两个值哪一个小选哪一个。
不过acceptCount/backlog参数还不只仅决定彻底链接队列的大小,对于半链接队列也有影响。参考同事飘零的blog,在linux 2.6.20内核以后,它的计算方式大体是:
table_entries = min(min(somaxconn,backlog),tcp_max_syn_backlog) roundup_pow_of_two(table_entries + 1)
第二行的函数roundup_pow_of_two表示取最近的2的n次方的值,举例来讲:假设somaxconn为128,backlog值为50,tcp_max_syn_backlog值为4096,则第一步计算出来的为50,而后roundup_pow_of_two(50 + 1),找到比51大的2的n次方的数为64,因此最终半链接队列的长度是64。
因此对于acceptCount这个值,须要慎重对待,若是请求量不是很大,一般tomcat默认的100也ok,但若访问量较大的状况,建议这个值设置的大一些,好比1024或更大。若是在tomcat前边一层对synflood攻击的防护没有把握的话,最好也开启syn cookie来防护。
在非jvm crash引发的tomcat进程意外退出的故障里,oom-killer是见过的比例最多的状况,排查这类问题时应首先判断是否由oom-killer所致。这个问题在答疑中遇到好几回,记录一下给新人了解。
定位oom-killer一般比较简单,直接经过dmesg便可看到:
$ sudo dmesg | grep java | grep -i oom-killer [6989889.606947] java invoked oom-killer: gfp_mask=0x280da, order=0, oom_adj=0, oom_score_adj=0 [7061818.494917] java invoked oom-killer: gfp_mask=0x201da, order=0, oom_adj=0, oom_score_adj=0 [7108961.621382] java invoked oom-killer: gfp_mask=0x280da, order=0, oom_adj=0, oom_score_adj=0
或者在日志中按java关键字搜索,会看到相似下面的日志:
[7250516.311246] Out of memory: Kill process 15041 (java) score 869 or sacrifice child [7250516.311255] Killed process 15041, UID 505, (java) total-vm:2307028kB, anon-rss:1780636kB, file-rss:872kB
不过这里有个问题,日志的格式,不能之间看出被kill时的信息,除非你肯定被kill的java进程id就是以前tomcat的进程id(在ali-tomcat会记录在一个文件里)。
在高版本的dmesg命令里,有一个很人性化的参数-T来以正常的时间格式来显示日志的,但不少时候会碰到比较低的版本:
$ rpm -qf /bin/dmesg util-linux-2.13-0.56.el5
小于util-linux-2.20版本的没法使用这个参数,只有变通的经过下面的方式转换一下,从stackoverflow上学到的:
dmesg_with_human_timestamps () { $(type -P dmesg) "$@" | perl -w -e 'use strict; my ($uptime) = do { local @ARGV="/proc/uptime";<>}; ($uptime) = ($uptime =~ /^(\d+)\./); foreach my $line (<>) { printf( ($line=~/^\[\s*(\d+)\.\d+\](.+)/) ? ( "[%s]%s\n", scalar localtime(time - $uptime + $1), $2 ) : $line ) }' } alias dmesg=dmesg_with_human_timestamps
把上面的函数和alias加到.bashrc里,source一下,能够获得正常的日期格式了:
$ dmesg | grep "(java)" [Thu Aug 28 20:50:14 2014] Out of memory: Kill process 18078 (java) score 872 or sacrifice child [Thu Aug 28 20:50:14 2014] Killed process 18078, UID 505, (java) total-vm:2390108kB, anon-rss:1784964kB, file-rss:2048kB [Fri Aug 29 14:48:06 2014] Out of memory: Kill process 15041 (java) score 869 or sacrifice child [Fri Aug 29 14:48:06 2014] Killed process 15041, UID 505, (java) total-vm:2307028kB, anon-rss:1780636kB, file-rss:872kB
开启oom-killer的话,在/proc/pid下对每一个进程都会多出3个与oom打分调节相关的文件,若是想要关闭,可能涉及运维的管理,要跟各方沟通好。临时对某个进程能够忽略oom-killer可使用下面的方式:
$ echo -17 > /proc/$(pidof java)/oom_adj
更多有关oom-killer的可参看这篇。
在docker/centos系统里启动官方的tomcat时,发现启动过程很慢,须要几十秒,即便只用官方默认自带的几个应用启动也同样。
一查日志,发现是session引发的随机数问题致使的:
INFO: Deploying web application directory /data/server/install/apache-tomcat-7.0.55/webapps/ROOT Aug 29, 2014 1:14:02 AM org.apache.catalina.util.SessionIdGenerator createSecureRandom INFO: Creation of SecureRandom instance for session ID generation using [SHA1PRNG] took [27,537] milliseconds.
这个问题以前在以前的这篇JVM上的随机数与熵池策略 已经分析过了,咱们在ali-tomcat里为避免随机数引发的阻塞,设置过使用非阻塞熵池策略:
if [[ "$JAVA_OPTS" != *-Djava.security.egd=* ]]; then JAVA_OPTS="$JAVA_OPTS -Djava.security.egd=file:/dev/./urandom" fi
修改事后,马上从以前的27秒降到了0.5秒:
INFO: Deploying web application directory /data/server/install/apache-tomcat-7.0.55/webapps/ROOT Aug 29, 2014 2:10:13 AM org.apache.catalina.startup.HostConfig deployDirectory INFO: Deployment of web application directory /data/server/install/apache-tomcat-7.0.55/webapps/ ROOT has finished in 515 ms