标签归档:tomcat

spring-boot 1.4.x遇到的cpu高的问题

如果你的spring-boot应用里tomcat线程耗cpu较高,并主要耗在做读取jar的操作上(堆栈类似下面),可能跟我们遇到同样的问题。

    CRC32.update(byte[], int, int) line: 76
    JarInputStream(ZipInputStream).read(byte[], int, int) line: 200
    JarInputStream.read(byte[], int, int) line: 207
    JarInputStream(ZipInputStream).closeEntry() line: 140
    JarInputStream(ZipInputStream).getNextEntry() line: 118
    JarInputStream.getNextEntry() line: 142
    JarInputStream.getNextJarEntry() line: 179
    JarWarResourceSet.getArchiveEntries(boolean) line: 112
    JarWarResourceSet(AbstractArchiveResourceSet).getResource(String) line: 256
    StandardRoot.getResourceInternal(String, boolean) line: 280
    CachedResource.validateResource(boolean) line: 95
    Cache.getResource(String, boolean) line: 69
    StandardRoot.getResource(String, boolean, boolean) line: 215
    StandardRoot.getResource(String) line: 205
    Mapper.internalMapWrapper(Mapper$ContextVersion, CharChunk, MappingData) line: 1027
    Mapper.internalMap(CharChunk, CharChunk, String, MappingData) line: 842
    Mapper.map(MessageBytes, MessageBytes, String, MappingData) line: 698
    CoyoteAdapter.postParseRequest(Request, Request, Response, Response) line: 672
    CoyoteAdapter.service(Request, Response) line: 344
    Http11Processor.service(SocketWrapperBase<?>) line: 784
    Http11Processor(AbstractProcessorLight).process(SocketWrapperBase<?>, SocketEvent) line: 66
    AbstractProtocol$ConnectionHandler<S>.process(SocketWrapperBase<S>, SocketEvent) line: 802
    NioEndpoint$SocketProcessor.doRun() line: 1410
    NioEndpoint$SocketProcessor(SocketProcessorBase<S>).run() line: 49
    ThreadPoolExecutor(ThreadPoolExecutor).runWorker(ThreadPoolExecutor$Worker) line: 1142
    ThreadPoolExecutor$Worker.run() line: 617
    TaskThread$WrappingRunnable.run() line: 61
    TaskThread(Thread).run() line: 745  

这种情况只发生在 spring-boot 1.4.x版本(及1.3.x版本,更早的没有确认),1.5.x已经没有这个问题。

主要的改变在org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory的内部类StoreMergedWebXmlListeneronStart方法:

// TomcatEmbeddedContext 启动时触发了该监听器
private void onStart(Context context) {
    ServletContext servletContext = context.getServletContext();
    if (servletContext.getAttribute(MERGED_WEB_XML) == null) {
        servletContext.setAttribute(MERGED_WEB_XML, getEmptyWebXml());
    }
    // 注意最后这句,1.5.3版本已经去掉了这句,它导致变慢
    TomcatResources.get(context).addClasspathResources(); 
}

addClasspathResources方法里对于jar资源的处理,不同的tomcat版本方式有所不同,spring-boot 中如果使用嵌入式的 tomcat8 的话这些jar资源会记录到StandardRoot里的jarResources集合里,它们会被定时清理。

tomcat容器的后台线程(ContainerBackgroundProcessor)会触发StandardRoot里的清理逻辑

    public void backgroundProcess() {
        cache.backgroundProcess();
        gc();
    }

    public void gc() {
        for (List<WebResourceSet> list : allResources) {
            for (WebResourceSet webResourceSet : list) {
                webResourceSet.gc();
            }
        }
    }
    
    // JarWarResourceSet里的gc方法
    public void gc() {
        synchronized (archiveLock) {
            if (archive != null && archiveUseCount == 0) {
                try {
                    archive.close();
                } catch (IOException e) {
                    // Log at least WARN
                }
                archive = null;
                archiveEntries = null;
            }
        }
    }

请求过来时,Mapper阶段会根据请求路径去找映射的资源,Cache不管命中还是未命中,都会对资源进行validate,在validateResource时要去遍历WebResourceRoot里所有的资源(包括所有的jar资源),若应用依赖的jar比较多时,会导致cpu较高。

spring-boot 1.5 版本里不会再将 BOOT-INF/lib 下的所有jar添加到tomcat的WebResourceRoot里,升级到1.5.3后这个情况没有再发生。

tomcat8.5.8遇到的两个问题

压力测试场景,前端nginx反向代理到4个tomcat实例,在其中的一个实例上产生了大量的countDownConnection Incorrect connection count警告

 WARNING [http-nio-8080-exec-48] org.apache.tomcat.util.net.AbstractEndpoint.countDownConnection Incorrect connection count, multiple socket.close called on the same socket.

另外一个异常是4个tomcat实例上都看到的NPE异常:

Exception in thread "http-nio-8080-AsyncTimeout" java.lang.NullPointerException
  at org.apache.coyote.AbstractProcessor.doTimeoutAsync(AbstractProcessor.java:528)
  at org.apache.coyote.AbstractProcessor.timeoutAsync(AbstractProcessor.java:518)
  at org.apache.coyote.AbstractProtocol$AsyncTimeout.run(AbstractProtocol.java:1130)
  at java.lang.Thread.run(Thread.java:745)

使用了servlet3.0,connector配置如下:

 <Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"
           connectionTimeout="20000" redirectPort="8443" maxParameterCount="2000" maxKeepAliveRequests="-1"
       maxThreads="200" maxPostSize="20971520" acceptCount="1024" useBodyEncodingForURI="true"
        URIEncoding="UTF-8"/>

已将bug提交到了bugzilla,在这里记录一下,后续跟踪。

nginx与tomcat之间的keep-alive配置

今天碰到的一个情况,tomcat与前端nginx之间的存在大量的TIME_WAIT状态的连接,第一反应是这里可能没有配置keep-alive。问ops,回复说启用了;要来nginx的配置看了一下,发现upstream里设置了keepalive参数:

upstream  tomcat {
    server x.x.x.x:8080;
    ...
    keepalive 16;
}

不确定这个参数是不是http的keep-alive,在nginx的网站上找了一下

Syntax: keepalive connections;
Default:    —
Context:    upstream

The connections parameter sets the maximum number of idle keepalive connections to upstream servers that are preserved in the cache of each worker process.

它并不是与后端节点开启http-alive方式的意思,nginx作为反向代理后端并不局限http协议,这里的keepalive设置相当于每个worker连接池的最大空闲keepalive连接数,跟http里的keep-alive不是一回事。

在官方文档里明确要对后端节点使用http keep-alive 可以指定http版本为1.1:

location /http/ {
    proxy_pass http://http_backend;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    ...
}

或者仍使用http 1.0协议,但显式的设置http header里Connection参数为Keep-Alive,这是因为keep-alive是http1.1的默认特性,在1.0里最初并未实现是后来从1.1里backport到1.0的,需要显式设定这个参数才启用:

location /http/ {
    proxy_pass http://http_backend;
    proxy_http_version 1.0;
    proxy_set_header Connection "Keep-Alive";
    ...
}

为了确认有效性,可以对tomcat的logger增加一句

org.apache.coyote.http11.level = FINE

这样可以在tomcat日志里看到每个请求的http header信息:

FINE: Received [GET / HTTP/1.0
Connection: Keep-Alive
Host: localhost:8080
User-Agent: curl/7.37.1
Accept: */*

确实在header里增加了。建议还是配置为http 1.1协议,支持chunked等特性。

ps, keepalive 这个词可能是指连接池或者tcp层面的参数,要看上下文。在http里是keep-alive,注意中间有个连字符。

servlet模型(3.1之前)对InputStream的处理是阻塞模式

同事聊起这个话题,当请求建立时是否tomcat就为每个连接分配线程了? 是否只要发起足够多的连接不必发送任何数据就可以DDoS了?对于DDoS这个话题不展开,这里仅仅说一下连接过来时,服务器端是否就一定分配了线程。这取决于tomcat配置的connector模式,只讨论一下bio和nio的情况。

以bio模式启动tomcat,然后建立3个连接(不发送任何数据):

$ nc localhost 8080 &
$ nc localhost 8080 &
$ nc localhost 8080 &

这时我们来看tomcat的bio执行线程是与连接数一对一的:

$ ./appctrl.sh jstack | grep http-bio-8080-exec
"http-bio-8080-exec-3" #24 daemon prio=5 os_prio=31 tid=0x00007fdd7a840800 nid=0x6607 runnable [0x000000011d78b000]
"http-bio-8080-exec-2" #23 daemon prio=5 os_prio=31 tid=0x00007fdd7a83f800 nid=0x6507 runnable [0x000000011d618000]
"http-bio-8080-exec-1" #22 daemon prio=5 os_prio=31 tid=0x00007fdd7a800000 nid=0x4107 runnable [0x000000011addd000]

而在nio模式下,请求建立时,并不会有执行线程,只有接收到数据发时,才会有线程分配:

$ nc localhost 8080
POST /test/post?name=foo HTTP/1.1
Host: localhost:8080
Context-Length: 10000 

上面在nc建立连接后,发送了一段http header(还未发送body,请求保持中),这时才会对这些请求数据分配线程阻塞执行:

$ ./appctrl.sh jstack | grep http-nio-8080-exec
"http-nio-8080-exec-3" #26 daemon prio=5 os_prio=31 tid=0x00007ffefb001800 nid=0x6007 waiting on condition [0x0000000123af7000]
"http-nio-8080-exec-2" #25 daemon prio=5 os_prio=31 tid=0x00007ffefa029000 nid=0x5d07 waiting on condition [0x000000012351c000]
"http-nio-8080-exec-1" #24 daemon prio=5 os_prio=31 tid=0x00007ffef900a000 nid=0x680b waiting on condition [0x000000012328f000] 

上面的测试是在tomcat7下测试的,nio在处理http request的时候是非阻塞的,但读取数据的时候是模拟阻塞的,因为servlet3.1之前对input的处理就是一种阻塞模式,参考以前的一张截图:

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)         

检查应用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

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.

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"

必须要设置disableUploadTimeoutfalse(默认是true),才可以对POST请求发送数据超时使用其他参数来设置,这样在发送数据的过程中最大可以等待的时间间隔就不再由connectionTimeout决定,而是由connectionUploadTimeout决定。

答疑: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.Bootstrapmain方法,传入的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)

Bootstrapmain方法里的,对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记录在某个文件来以便后续使用。