Tomcat 7 伺服器關閉原理

預流發表於2019-02-26

之前的幾篇文章講了 Tomcat 的啟動過程,在預設的配置下啟動完之後會看到後臺實際上總共有 6 個執行緒在執行。即 1 個使用者執行緒,剩下 5 個為守護執行緒(下圖中的 Daemon Thread )。

Tomcat 7 伺服器關閉原理
如果對什麼叫守護執行緒的概念比較陌生,這裡再重複一下:

所謂守護執行緒,是指在程式執行的時候在後臺提供一種通用服務的執行緒,比如垃圾回收執行緒。這種執行緒並不屬於程式中不可或缺的部分,當所有的非守護執行緒結束時,程式也就終止了,同時會殺死程式中的所有守護執行緒。反過來說,只要任何非守護執行緒還在執行,程式就不會終止。

使用者執行緒和守護執行緒兩者幾乎沒有區別,唯一的不同之處就在於虛擬機器的離開:如果使用者執行緒已經全部退出執行了,只剩下守護執行緒存在了,虛擬機器也就退出了。因為沒有了被守護者,守護執行緒也就沒有工作可做了,也就沒有繼續執行程式的必要了。將執行緒轉換為守護執行緒可以通過呼叫 Thread 物件的 setDaemon(true) 方法來實現。

Tomcat 的關閉正是利用了這個原理,即只要將那唯一的一個使用者執行緒關閉,則整個應用就關閉了。

要研究這個使用者執行緒怎麼被關閉的得先從這個執行緒從何產生說起。在前面分析 Tomcat 的啟動時我們是從org.apache.catalina.startup.Bootstrap類的 main 方法作為入口,該類的 453 到 456 行是 Tomcat 啟動時會執行的程式碼:

Tomcat 7 伺服器關閉原理
前面的文章裡分析了 daemon.load 和 daemon.start 方法,這裡請注意 daemon.setAwait(true); 這句,它的作用是通過反射呼叫org.apache.catalina.startup.Catalina類的 setAwait(true) 方法,最終將 Catalina 類的例項變數 await 設值為 true 。

Catalina 類的 setAwait 方法程式碼:

    /**
     * Set flag.
     */
    public void setAwait(boolean await)
        throws Exception {

        Class<?> paramTypes[] = new Class[1];
        paramTypes[0] = Boolean.TYPE;
        Object paramValues[] = new Object[1];
        paramValues[0] = Boolean.valueOf(await);
        Method method =
            catalinaDaemon.getClass().getMethod("setAwait", paramTypes);
        method.invoke(catalinaDaemon, paramValues);

    }
複製程式碼

如前文分析,Tomcat 啟動時會呼叫org.apache.catalina.startup.Catalina類的 start 方法,看下這個方法的程式碼:

     
     1	    /**
     2	     * Start a new server instance.
     3	     */
     4	    public void start() {
     5	
     6	        if (getServer() == null) {
     7	            load();
     8	        }
     9	
    10	        if (getServer() == null) {
    11	            log.fatal("Cannot start server. Server instance is not configured.");
    12	            return;
    13	        }
    14	
    15	        long t1 = System.nanoTime();
    16	
    17	        // Start the new server
    18	        try {
    19	            getServer().start();
    20	        } catch (LifecycleException e) {
    21	            log.fatal(sm.getString("catalina.serverStartFail"), e);
    22	            try {
    23	                getServer().destroy();
    24	            } catch (LifecycleException e1) {
    25	                log.debug("destroy() failed for failed Server ", e1);
    26	            }
    27	            return;
    28	        }
    29	
    30	        long t2 = System.nanoTime();
    31	        if(log.isInfoEnabled()) {
    32	            log.info("Server startup in " + ((t2 - t1) / 1000000) + " ms");
    33	        }
    34	
    35	        // Register shutdown hook
    36	        if (useShutdownHook) {
    37	            if (shutdownHook == null) {
    38	                shutdownHook = new CatalinaShutdownHook();
    39	            }
    40	            Runtime.getRuntime().addShutdownHook(shutdownHook);
    41	
    42	            // If JULI is being used, disable JULI's shutdown hook since
    43	            // shutdown hooks run in parallel and log messages may be lost
    44	            // if JULI's hook completes before the CatalinaShutdownHook()
    45	            LogManager logManager = LogManager.getLogManager();
    46	            if (logManager instanceof ClassLoaderLogManager) {
    47	                ((ClassLoaderLogManager) logManager).setUseShutdownHook(
    48	                        false);
    49	            }
    50	        }
    51	
    52	        if (await) {
    53	            await();
    54	            stop();
    55	        }
    56	    }
複製程式碼

前文分析啟動時發現通過第 19 行 getServer().start() 的這次方法呼叫,Tomcat 接下來會一步步啟動所有在配置檔案中配置的元件。後面的程式碼沒有分析,這裡請關注最後第 52 到 55 行,上面說到已經將 Catalina 類的例項變數 await 設值為 true,所以這裡將會執行 Catalina 類的 await 方法:

    /**
     * Await and shutdown.
     */
    public void await() {
    
        getServer().await();
        
    }
複製程式碼

該方法就一句話,意思是呼叫org.apache.catalina.core.StandardServer類的 await 方法:


     1	    /**
     2	     * Wait until a proper shutdown command is received, then return.
     3	     * This keeps the main thread alive - the thread pool listening for http 
     4	     * connections is daemon threads.
     5	     */
     6	    @Override
     7	    public void await() {
     8	        // Negative values - don't wait on port - tomcat is embedded or we just don't like ports
     9	        if( port == -2 ) {
    10	            // undocumented yet - for embedding apps that are around, alive.
    11	            return;
    12	        }
    13	        if( port==-1 ) {
    14	            try {
    15	                awaitThread = Thread.currentThread();
    16	                while(!stopAwait) {
    17	                    try {
    18	                        Thread.sleep( 10000 );
    19	                    } catch( InterruptedException ex ) {
    20	                        // continue and check the flag
    21	                    }
    22	                }
    23	            } finally {
    24	                awaitThread = null;
    25	            }
    26	            return;
    27	        }
    28	
    29	        // Set up a server socket to wait on
    30	        try {
    31	            awaitSocket = new ServerSocket(port, 1,
    32	                    InetAddress.getByName(address));
    33	        } catch (IOException e) {
    34	            log.error("StandardServer.await: create[" + address
    35	                               + ":" + port
    36	                               + "]: ", e);
    37	            return;
    38	        }
    39	
    40	        try {
    41	            awaitThread = Thread.currentThread();
    42	
    43	            // Loop waiting for a connection and a valid command
    44	            while (!stopAwait) {
    45	                ServerSocket serverSocket = awaitSocket;
    46	                if (serverSocket == null) {
    47	                    break;
    48	                }
    49	    
    50	                // Wait for the next connection
    51	                Socket socket = null;
    52	                StringBuilder command = new StringBuilder();
    53	                try {
    54	                    InputStream stream;
    55	                    try {
    56	                        socket = serverSocket.accept();
    57	                        socket.setSoTimeout(10 * 1000);  // Ten seconds
    58	                        stream = socket.getInputStream();
    59	                    } catch (AccessControlException ace) {
    60	                        log.warn("StandardServer.accept security exception: "
    61	                                + ace.getMessage(), ace);
    62	                        continue;
    63	                    } catch (IOException e) {
    64	                        if (stopAwait) {
    65	                            // Wait was aborted with socket.close()
    66	                            break;
    67	                        }
    68	                        log.error("StandardServer.await: accept: ", e);
    69	                        break;
    70	                    }
    71	
    72	                    // Read a set of characters from the socket
    73	                    int expected = 1024; // Cut off to avoid DoS attack
    74	                    while (expected < shutdown.length()) {
    75	                        if (random == null)
    76	                            random = new Random();
    77	                        expected += (random.nextInt() % 1024);
    78	                    }
    79	                    while (expected > 0) {
    80	                        int ch = -1;
    81	                        try {
    82	                            ch = stream.read();
    83	                        } catch (IOException e) {
    84	                            log.warn("StandardServer.await: read: ", e);
    85	                            ch = -1;
    86	                        }
    87	                        if (ch < 32)  // Control character or EOF terminates loop
    88	                            break;
    89	                        command.append((char) ch);
    90	                        expected--;
    91	                    }
    92	                } finally {
    93	                    // Close the socket now that we are done with it
    94	                    try {
    95	                        if (socket != null) {
    96	                            socket.close();
    97	                        }
    98	                    } catch (IOException e) {
    99	                        // Ignore
   100	                    }
   101	                }
   102	
   103	                // Match against our command string
   104	                boolean match = command.toString().equals(shutdown);
   105	                if (match) {
   106	                    log.info(sm.getString("standardServer.shutdownViaPort"));
   107	                    break;
   108	                } else
   109	                    log.warn("StandardServer.await: Invalid command '"
   110	                            + command.toString() + "' received");
   111	            }
   112	        } finally {
   113	            ServerSocket serverSocket = awaitSocket;
   114	            awaitThread = null;
   115	            awaitSocket = null;
   116	
   117	            // Close the server socket and return
   118	            if (serverSocket != null) {
   119	                try {
   120	                    serverSocket.close();
   121	                } catch (IOException e) {
   122	                    // Ignore
   123	                }
   124	            }
   125	        }
   126	    }
複製程式碼

這段程式碼就不一一分析,總體作用如方法前的註釋所說,即“一直等待到接收到一個正確的關閉命令後該方法將會返回。這樣會使主執行緒一直存活——監聽http連線的執行緒池是守護執行緒”。

熟悉 Java 的 Socket 程式設計的話對這段程式碼就很容易理解,就是預設地址(地址值由例項變數 address 定義,預設為localhost)的預設的埠(埠值由例項變數 port 定義,預設為8005)上監聽 Socket 連線,當發現監聽到的連線的輸入流中的內容與預設配置的值匹配(該值預設為字串SHUTDOWN)則跳出迴圈,該方法返回(第 103 到 107 行)。否則該方法會一直迴圈執行下去。 一般來說該使用者主執行緒會阻塞(第 56 行)直到有訪問localhost:8005的連線出現。 正因為如此才出現開頭看見的主執行緒一直 Running 的情況,而因為這個執行緒一直 Running ,其它守護執行緒也會一直存在。

說完這個執行緒的產生,接下來看看這個執行緒的關閉,按照上面的分析,這個執行緒提供了一個關閉機制,即只要訪問localhost:8005,並且傳送一個內容為SHUTDOWN的字串,就可以關閉它了。

Tomcat 正是這麼做的,一般來說關閉 Tomcat 通過執行 shutdown.bat 或 shutdown.sh 指令碼,關於這段指令碼可參照分析啟動指令碼那篇文章,機制類似,最終會執行org.apache.catalina.startup.Bootstrap類的 main 方法,並傳入入參"stop",看下本文第 2 張圖片中org.apache.catalina.startup.Bootstrap類的第 458 行,接著將呼叫org.apache.catalina.startup.Catalina類 stopServer 方法:


     1	    public void stopServer(String[] arguments) {
     2	
     3	        if (arguments != null) {
     4	            arguments(arguments);
     5	        }
     6	
     7	        Server s = getServer();
     8	        if( s == null ) {
     9	            // Create and execute our Digester
    10	            Digester digester = createStopDigester();
    11	            digester.setClassLoader(Thread.currentThread().getContextClassLoader());
    12	            File file = configFile();
    13	            FileInputStream fis = null;
    14	            try {
    15	                InputSource is =
    16	                    new InputSource(file.toURI().toURL().toString());
    17	                fis = new FileInputStream(file);
    18	                is.setByteStream(fis);
    19	                digester.push(this);
    20	                digester.parse(is);
    21	            } catch (Exception e) {
    22	                log.error("Catalina.stop: ", e);
    23	                System.exit(1);
    24	            } finally {
    25	                if (fis != null) {
    26	                    try {
    27	                        fis.close();
    28	                    } catch (IOException e) {
    29	                        // Ignore
    30	                    }
    31	                }
    32	            }
    33	        } else {
    34	            // Server object already present. Must be running as a service
    35	            try {
    36	                s.stop();
    37	            } catch (LifecycleException e) {
    38	                log.error("Catalina.stop: ", e);
    39	            }
    40	            return;
    41	        }
    42	
    43	        // Stop the existing server
    44	        s = getServer();
    45	        if (s.getPort()>0) {
    46	            Socket socket = null;
    47	            OutputStream stream = null;
    48	            try {
    49	                socket = new Socket(s.getAddress(), s.getPort());
    50	                stream = socket.getOutputStream();
    51	                String shutdown = s.getShutdown();
    52	                for (int i = 0; i < shutdown.length(); i++) {
    53	                    stream.write(shutdown.charAt(i));
    54	                }
    55	                stream.flush();
    56	            } catch (ConnectException ce) {
    57	                log.error(sm.getString("catalina.stopServer.connectException",
    58	                                       s.getAddress(),
    59	                                       String.valueOf(s.getPort())));
    60	                log.error("Catalina.stop: ", ce);
    61	                System.exit(1);
    62	            } catch (IOException e) {
    63	                log.error("Catalina.stop: ", e);
    64	                System.exit(1);
    65	            } finally {
    66	                if (stream != null) {
    67	                    try {
    68	                        stream.close();
    69	                    } catch (IOException e) {
    70	                        // Ignore
    71	                    }
    72	                }
    73	                if (socket != null) {
    74	                    try {
    75	                        socket.close();
    76	                    } catch (IOException e) {
    77	                        // Ignore
    78	                    }
    79	                }
    80	            }
    81	        } else {
    82	            log.error(sm.getString("catalina.stopServer"));
    83	            System.exit(1);
    84	        }
    85	    }
複製程式碼

第 8 到 41 行是讀取配置檔案,可參照前面分析 Digester 的文章,不再贅述。從第 49 行開始,即向localhost:8005發起一個 Socket 連線,並寫入SHUTDOWN字串。 這樣將會關閉 Tomcat 中的那唯一的一個使用者執行緒,接著所有守護執行緒將會退出(由 JVM 保證),之後整個應用關閉。

以上分析 Tomcat 的預設關閉機制,但這是通過執行指令碼來關閉,我覺得這樣比較麻煩,那麼能不能通過一種線上訪問的方式關閉 Tomcat 呢?當然可以,比較暴力的玩法是直接改org.apache.catalina.core.StandardServer的原始碼第 500 行,將

boolean match = command.toString().equals(shutdown);
複製程式碼

改成

boolean match = command.toString().equals(“GET /SHUTDOWN HTTP/1.1”);  
複製程式碼

或者修改 server.xml 檔案,找到 Server 節點,將原來的

<Server port="8005" shutdown="SHUTDOWN">  
複製程式碼

改成

<Server port="8005" shutdown="GET /SHUTDOWN HTTP/1.1">  
複製程式碼

這樣直接在瀏覽器中輸入http://localhost:8005/SHUTDOWN就可以關閉 Tomcat 了,原理?看懂了上面的文章,這個應該不難。

相關文章