轉載一篇同行記錄的使用tomcat的troubleshooting

tomcat nio模式下sendfile引發的NPE

這個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)
本條目發佈於 2014-10-01。屬於 java分類,被貼了 tomcat 標籤。

檢查應用jar衝突的腳本

這個腳本用於定位應用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

  • 測試發現unzip -l要比jar tvf快出一個數量級以上
  • shell裏的字符串拼接尤爲是比較大的字符串拼接很是耗性能
  • mac/bsd下的sed很是難用,linux上的sed用法沒法在mac上運行,跨平臺腳本儘可能避免sed,除非都安裝的是gnu-sed
  • bash4.0版本才支持map,而實際環境還運行的仍是低版本的,只能本身模擬一個map,簡單的作法能夠基於臨時文件

腳本已放到服務器上,能夠經過下面的方式運行: shell

$ bash <(curl -s http://hongjiang.info/cpcheck.sh) libdir
本條目發佈於 2014-09-15。屬於 shell分類,被貼了 shelltomcat 標籤。

tomcat-connector的微調(5): keep-alive相關的參數

tomcat默認是開啓keep-alive的,有3個相關的參數能夠配置: apache

1) keepAliveTimeout

表示在複用一個鏈接時,兩次請求之間的最大間隔時間;超過這個間隔服務器會主動關閉鏈接。默認值同connectionTimeout參數,即20秒。不作限制的話能夠設置爲-1. json

2) maxKeepAliveRequests

表示一個鏈接最多可複用多少次請求,默認是100。不作限制能夠設置爲-1. 注意若是tomcat是直接對外服務的話,keep-alive特性可能被一些DoS攻擊利用,好比以很慢的方式發生數據,能夠長時間持有這個鏈接;從而 可能被惡意請求耗掉大量鏈接拒絕服務。tomcat直接對外的話這個值不宜設置的太大。

3) disableKeepAlivePercentage

注意,這個參數是BIO特有的,默認狀況BIO在線程池裏的線程使用率超過75%時會取消keep-alive,若是不取消的話能夠設置爲100.

本條目發佈於 2014-09-04。屬於 java分類,被貼了 keep-alivetomcat 標籤。

tomcat-connector的微調(4): 超時相關的參數

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決定。

本條目發佈於 2014-09-04。屬於 java分類,被貼了 tomcat 標籤。

答疑:tomcat關閉腳本怎麼確保不誤殺其餘進程

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記錄在某個文件來以便後續使用。

本條目發佈於 2014-09-03。屬於 java分類,被貼了 tomcat 標籤。

tomcat-connector的微調(3): processorCache與socket.processorCache

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。

本條目發佈於 2014-09-03。屬於 java分類,被貼了 tomcat 標籤。

tomcat-connector的微調(2): maxConnections, maxThreads

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的線程數也可能存在利用率不高的狀況

2) 最大線程數

若是沒有對connector配置額外的線程池的話,maxThreads參數用來設置默認線程池的最大線程數。tomcat默認是200,對通常訪問量的應用來講足夠了。

本條目發佈於 2014-09-02。屬於 java分類,被貼了 tomcat 標籤。

tomcat-connector的微調(1): acceptCount參數

對於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來防護。

本條目發佈於 2014-09-01。屬於 java分類,被貼了 linuxnetstatsstcptomcat 標籤。

tomcat進程意外退出: oom-killer

在非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的可參看這篇

本條目發佈於 2014-08-30。屬於 java分類,被貼了 linuxoom-killertomcat 標籤。

Docker中apache-tomcat啓動慢的問題

在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
本條目發佈於 2014-08-29。屬於 java分類,被貼了 dockerrandomtomcat 標籤。
相關文章
相關標籤/搜索