文档章节

转载一篇同行记录的使用tomcat的troubleshooting

m
 miscellanea
发布于 2015/07/31 11:16
字数 6208
阅读 84
收藏 1

tomcat nio模式下sendfile引起的NPE

这个bug困扰我们很长一段时间,最初是在生产环境发现的,为了确保项目发布,紧急情况下让应用切换成了BIO。后来没能重现,大家没足够重 视,一直没有去跟这个问题,直到最近再次发现这个问题,发现是NIO模式默认对静态资源启用了sendfile以提升性能,但这里存在bug所致。官方已 经在7051后续版本修复了这个问题,最好升级到最新版本。或者在server.xml的Connector节点里增加: useSendfile=”false” 来避免。

下面是相关的异常信息,如果你的tomcat是7051之前的版本,采用NIO并且没有显式的关闭sendfile,应用里有静态资源,访问静态资源时tomcat日志里出现了下面的异常(如果前边有nginx或apache返回502),很可能是同一问题:

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个数,执行效果如下:

$ ./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包,定位问题时可以去查:

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

脚本内容:

#!/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秒,基本上够用了。

为了兼容mac(有些命令在linux与mac/bsd上方式不同),大部分情况下采用awk来处理,不过我对awk也不太熟,只好采用逐步拼接的 方式,如果通过一个awk脚本来实现或许性能可以高一些,但也比较有限,大头还是在获取jar里的class列表那块。几个tips:

  • 测试发现unzip -l要比jar tvf快出一个数量级以上
  • shell里的字符串拼接尤其是比较大的字符串拼接非常耗性能
  • mac/bsd下的sed非常难用,linux上的sed用法无法在mac上运行,跨平台脚本尽量避免sed,除非都安装的是gnu-sed
  • bash4.0版本才支持map,而实际环境还运行的还是低版本的,只能自己模拟一个map,简单的做法可以基于临时文件

脚本已放到服务器上,可以通过下面的方式运行:

$ bash <(curl -s http://hongjiang.info/cpcheck.sh) libdir
本条目发布于 2014-09-15。属于 shell分类,被贴了 shelltomcat 标签。

tomcat-connector的微调(5): keep-alive相关的参数

tomcat默认是开启keep-alive的,有3个相关的参数可以配置:

1) keepAliveTimeout

表示在复用一个连接时,两次请求之间的最大间隔时间;超过这个间隔服务器会主动关闭连接。默认值同connectionTimeout参数,即20秒。不做限制的话可以设置为-1.

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 标签。

本文转载自:http://hongjiang.info/tag/tomcat/

m
粉丝 6
博文 86
码字总数 22525
作品 0
海淀
私信 提问
你的论文能否中顶会?这篇分析同行评审结果的论文可帮助你

  选自arXiv   作者:Dongyeop Kang等   机器之心编译   参与:Nurhachu Null、李亚洲、李泽南      在人工智能领域,会议论文是证明研究人员学术水平的重要一环。是否存在一些「...

机器之心
2018/05/02
0
0
tomcat的缺少tcnative-1.dll的解决

tomcat启动出现如下问题: The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the Java.library.path: C:Program......

小麋鹿666
2017/01/05
0
0
如何使用逆向工程分析开源框架源码呢?

最近在啃tomcat的源码,研究tomcat的运行机制,在网上翻时看一位同行写的读感,印象深刻,感觉自己的思路清晰了许多,自己又重读了一次感觉了解有深入了一些,这位同行提到可是使用逆向工程来分析源...

乔康007
2013/01/29
335
1
Confluence 6 通过 SSL 或 HTTPS 运行 - 备注和问题解决

备注 在创建证书时候的背景信息: '' 命令将会创建秘钥对,包括公钥和关联的私钥,然后存储到 keystore 中。这个命令打包公钥为 X.509 v3 自签名证书,同时存储为证书链中的单一元素。这个证...

honeymose
2018/08/09
7
0
ClassLoader.getSystemResourceAsStream("file.xml") NULL 错误的经验分享

今天遇到小问题搞了好几个小时才算搞定?! 是不是太笨了!!记录一下,分享给遇到类似问题的童鞋。 关于 ClassLoader.getSystemResourceAsStream($fileName)的在Java应用程序中没有错误,放...

lvgm
2013/06/21
4K
4

没有更多内容

加载失败,请刷新页面

加载更多

spring源码分析6: ApplicationContext的初始化与BeanDefinition的搜集入库

先前几篇都是概念的讲解:回顾下 BeanDefinition 是物料 Bean是成品 BeanFactory是仓库,存储物料与成品 ApplicationContext初始化搜集物料入库,触发生产线,取出物料生产Bean 本文研究spr...

星星之焱
24分钟前
5
0
彻底解决tomcat乱码问题

本地项目请求访问,浏览器中文输出没问题。 部署到服务器上面之后,返回到浏览器的中文就乱码了。 尝试办法: 1.修改tomcat下的conf中的service.xml中的配置信息: 重新启动后,没有效果还是...

诗书易经
40分钟前
5
0
Java开发需要掌握的IDEA插件大全

1、Lombok 解释:这是最基本的插件,2017年就火了,还没用的百度一下吧。 博客链接:Intellij IDEA 安装lombok及使用详解 2、PlantUML integration 解释:各种类之间的关联图,高级开发必备。...

木九天
40分钟前
6
0
python学习10.05:Python range()快速初始化数字列表

实际场景中,经常需要存储一组数字。例如在游戏中,需要跟踪每个角色的位置,还可能需要跟踪玩家的几个最高得分。在数据可视化中,处理的几乎都是由数字(如温度、距离、人口数量、经度和纬度...

太空堡垒185
48分钟前
4
0
java单元测试,PowerMockito模拟方法内new对象

在做单元测试中有时候需要对方法内new出来的对象进行隔离,这是我们需要使用PowerMockito。 添加依赖 <dependency> <groupId>org.powermock</groupId> <artifactId>......

如梦之猿
49分钟前
5
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部