這個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