转载一篇同行记录的使用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 标签。
相关文章
相关标签/搜索