1. 程式人生 > >tomcat無法正常關閉問題分析及解決

tomcat無法正常關閉問題分析及解決

x64 second catch 監聽 files 執行 發現 int find

技術分享圖片

問題描述

通常,我們都會直接使用tomcat提供的腳本執行關閉操作,如下:

# sh bin/shutdown.sh 
Using CATALINA_BASE:   /usr/local/apache-tomcat-7.0.59
Using CATALINA_HOME:   /usr/local/apache-tomcat-7.0.59
Using CATALINA_TMPDIR: /usr/local/apache-tomcat-7.0.59/temp
Using JRE_HOME:        /usr/local/jdk1.8.0_121
Using CLASSPATH:       /usr/local/apache-tomcat-7.0.59/bin/bootstrap.jar:/usr/local/apache-tomcat-7.0.59/bin/tomcat-juli.jar

但是執行該關閉操作之後,有時候會發現tomcat進程依然存在:

# ps uax |grep tomcat
root       1199  0.0  0.0   9120   468 ?        Ss   21:53   0:00 /sbin/dhclient -H centosx64_tomcat1 -1 -q -lf /var/lib/dhclient/dhclient-eth2.leases -pf /var/run/dhclient-eth2.pid eth2
root       2081  9.7 60.7 2192828 295224 pts/0  Sl   22:04   1:04 /usr/local/jdk1.8.0_121/bin/java -Djava.util.logging.config.file=/usr/local/apache-tomcat-7.0.59/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Dmoc.debug=true -Djava.endorsed.dirs=/usr/local/apache-tomcat-7.0.59/endorsed -classpath /usr/local/apache-tomcat-7.0.59/bin/bootstrap.jar:/usr/local/apache-tomcat-7.0.59/bin/tomcat-juli.jar -Dcatalina.base=/usr/local/apache-tomcat-7.0.59 -Dcatalina.home=/usr/local/apache-tomcat-7.0.59 -Djava.io.tmpdir=/usr/local/apache-tomcat-7.0.59/temp org.apache.catalina.startup.Bootstrap start
root       2192  0.0  0.1 103332   848 pts/0    S+   22:15   0:00 grep tomcat

這時我們就只能通過強制殺死進程的方式停止Tomcat了:kill -9 <tomcat_process_id>
那麽,為什麽使用shutdown.sh無法正常停止Tomcat進程呢?

原因分析

停止Tomcat原理分析

我們先來看看tomcat實現關閉的原理是什麽?如下為shutdown.sh腳本內容:

PRGDIR=`dirname "$PRG"`
EXECUTABLE=catalina.sh

# Check that target executable exists
if $os400; then
  # -x will Only work on the os400 if the files are:
  # 1. owned by the user
  # 2. owned by the PRIMARY group of the user
  # this will not work if the user belongs in secondary groups
  eval
else
  if [ ! -x "$PRGDIR"/"$EXECUTABLE" ]; then
    echo "Cannot find $PRGDIR/$EXECUTABLE"
    echo "The file is absent or does not have execute permission"
    echo "This file is needed to run this program"
    exit 1
  fi
fi

exec "$PRGDIR"/"$EXECUTABLE" stop "$@"

顯然,shutdown.sh只是一個執行入口,真正執行關閉操作是在catalina.sh中實現的,繼續查看catalina.sh腳本內容,在其中關於調用stop方法的地方可以看到如下信息:

eval "\"$_RUNJAVA\"" $LOGGING_MANAGER $JAVA_OPTS     -Djava.endorsed.dirs="\"$JAVA_ENDORSED_DIRS\"" -classpath "\"$CLASSPATH\""     -Dcatalina.base="\"$CATALINA_BASE\""     -Dcatalina.home="\"$CATALINA_HOME\""     -Djava.io.tmpdir="\"$CATALINA_TMPDIR\""     org.apache.catalina.startup.Bootstrap "$@" stop
# stop failed. Shutdown port disabled? Try a normal kill.
  if [ $? != 0 ]; then
    if [ ! -z "$CATALINA_PID" ]; then
      echo "The stop command failed. Attempting to signal the process to stop through OS signal."
      kill -15 `cat "$CATALINA_PID"` >/dev/null 2>&1
    fi
  fi

首先需要調用Tomcat的Bootstrap類,然後再通過kill命名停止Tomcat進程。但是註意 到在這裏使用kill命令發送的信號為SIGTERM(15),也就是說有可能不能停止Tomcat進程(如:進程未釋放系統資源)。

下面先追蹤一下Bootstrap類的實現:

if (command.equals("startd")) {
    args[args.length - 1] = "start";
    daemon.load(args);
    daemon.start();
} else if (command.equals("stopd")) {
    args[args.length - 1] = "stop";
    daemon.stop();
} else if (command.equals("start")) {
    daemon.setAwait(true);
    daemon.load(args);
    daemon.start();
} else if (command.equals("stop")) {
    daemon.stopServer(args);
} else if (command.equals("configtest")) {
    daemon.load(args);
if (null==daemon.getServer()) {
    System.exit(1);
}
    System.exit(0);
} else {
    log.warn("Bootstrap: command \"" + command + "\" does not exist.");
}

在Bootstrap的main方法中會根據在catalina.sh腳本傳遞的不同參數(start,stop)執行不同的方法。其中,當參數為stop時會調用stopServer()方法。

/**
 * Stop the standalone server.
 * @param arguments Command line arguments
 * @throws Exception Fatal stop error
 */
public void stopServer(String[] arguments)
    throws Exception {

    Object param[];
    Class<?> paramTypes[];
    if (arguments==null || arguments.length==0) {
        paramTypes = null;
        param = null;
    } else {
        paramTypes = new Class[1];
        paramTypes[0] = arguments.getClass();
        param = new Object[1];
        param[0] = arguments;
    }
    Method method =
    catalinaDaemon.getClass().getMethod("stopServer", paramTypes);
    method.invoke(catalinaDaemon, param);
}

實際上,最終的停止服務操作是通過反射方式執行了Catalina類中的stopServer()方法,如下所示:

public void stopServer(String[] arguments) {

    if (arguments != null) {
        arguments(arguments);
    }
    Server s = getServer();
    if (s == null) {
        // Create and execute our Digester
        Digester digester = createStopDigester();
        File file = configFile();
        try (FileInputStream fis = new FileInputStream(file)) {
            InputSource is =
            new InputSource(file.toURI().toURL().toString());
            is.setByteStream(fis);
            digester.push(this);
            digester.parse(is);
        } catch (Exception e) {
            log.error("Catalina.stop: ", e);
            System.exit(1);
        }
    } else {
        // Server object already present. Must be running as a service
        try {
            s.stop();
        } catch (LifecycleException e) {
            log.error("Catalina.stop: ", e);
        }
        return;
    }

    // Stop the existing server
    s = getServer();
    if (s.getPort()>0) {
        try (Socket socket = new Socket(s.getAddress(), s.getPort());
            OutputStream stream = socket.getOutputStream()) {
            String shutdown = s.getShutdown();
            for (int i = 0; i < shutdown.length(); i++) {
                stream.write(shutdown.charAt(i));
            }
            stream.flush();
        } catch (ConnectException ce) {
            log.error(sm.getString("catalina.stopServer.connectException",
                                       s.getAddress(),
                                       String.valueOf(s.getPort())));
            log.error("Catalina.stop: ", ce);
            System.exit(1);
        } catch (IOException e) {
            log.error("Catalina.stop: ", e);
            System.exit(1);
        }
    } else {
        log.error(sm.getString("catalina.stopServer"));
        System.exit(1);
    }
}

如上所示,Tomcat進程的關閉操作需要做2件事:
第一:調用Bootstrap類的方法釋放Tomcat進程所占用的資源。
第二:使用kill命令停止Tomcat進程:kill -15 <tomcat_process_id>

為什麽停止Tomcat之後進程依然存在

Tomcat是一個Servlet容器,用於部署Serlvet程序(我們通常寫的各種Java Web應用本質上就是一個Servlet程序)。也就說,在停止Tomcat時不僅僅需要釋放Tomcat進程本身所占用的資源,還需要釋放Serlvet程序所占用的資源。而出現“停止Tomcat之後進程依然存在”這種現象的主要原因就是:我們自己寫的Java Web應用在Tomcat容器停止時沒有正常釋放所占用的系統資源,比如:線程池未關閉,輸入輸出流未關閉等等。我在實際開發中就曾遇到因Kafka客戶端未關閉到導致Tomcat無法正常停止的情況。然而,這卻是很多做Web應用開發的程序員未引起註意的地方。往往都是不能正常關閉就直接強制殺死進程,當然達到了目的,但這並不是一個很好的做法。
技術分享圖片

解決方案

我們必須確保在容器退出時正確地釋放相應資源,如:實現ServletContextListener監聽器接口,在contextDestroyed()方法中執行相應的關閉操作。

public class ResListener implements ServletContextListener {
    public void contextInitialized(ServletContextEvent sce) {
        //TODO:初始化資源
    }

    // 釋放資源,否則容器無法正常關閉
    public void contextDestroyed(ServletContextEvent sce) {
        //TODO:釋放資源
    }
}

【參考】
[1]. http://han.guokai.blog.163.com/blog/static/1367182712010731149286/ Tomcat無法正常關閉

tomcat無法正常關閉問題分析及解決