Tomcat 啓動初始化和停止

Tomcat 通過 server.xml 配置文件裝配一系列組件,並且爲組件設計生命週期接口,在容器啓停時,協調控制組件的啓動、初始化和停止。容器通常使用腳本啓動,腳本主要是檢查 Java 環境、設置 JVM 參數,調用 Bootstrap.start 啓動。

Bootstrap 是 Catalina 的引導加載類,它構造了一個 commonLoader 類加載器,加載 ${catalina.base}/lib 目錄下的類,目的是與應用程序級的類隔離,接下來詳細分析每個過程。

在此之前可先了解一下,官網對啓動過程的描述,以及提供的啓動UML序列圖,Startup.txt&UML,這個圖囊括了啓動過程中類的調用,但我第一次看這個圖,也是一臉懵,以下描述的則是通過 DEBGUG 跟出來的,當回過頭再看這個圖,確實很有用。

初始化

初始化涉及到 server.xml 的解析,Tomcat 使用 Digester 解析,其工作原理理解了很簡單,使用 sax 解析,在元素開始和結束,藉助一個 ObjectStack 對象棧和一系列解析規則完成組件的初始化,它主要有三個基本規則:

  • ObjectCreateRule:根據指定的 ClassName 創建一個實例,元素開始時,壓入對象棧,結束時,彈出對象棧
  • SetPropertiesRule:元素開始時,根據其屬性,反射調用棧頂元素對應成員變量的 set 方法
  • SetNextRule:元素結束時,使用set方法建立棧頂元素(child)和(top-1)元素(parent)的父子(組合)關係

各組件都是使用這三個規則進行配置解析,解析過程不再贅述,(這裏) 提供一份源碼註釋,可加斷點跟一下,着重關注棧內對象的入棧和出棧,值得注意的是在 GlobalResourcesLifecycleListener 初始化時會觸發加載解析 mbeans-descriptors.xml,這個過程太長,可使用斷點跳過。

調用 StandardServer.initialize 開始組件的初始化,觸發 INIT_EVENT 事件,詳細過程:

  1. 初始化 StandardServer:首先觸發各 Listener(具體有哪些,可查看xml的配置)的執行,然後初始化內部的 Services;
  2. Service 主要是初始化定義的 Connectors;
  3. 初始化 Connector:初始化 Adapter 和 ProtocolHandler,處理器有兩種不同實現:
    • Http11NioProtocol 初始化:NioEndpoint 設置線程池名稱、設置ConnectionHandler、設置接收和發送 ByteBuffer 容量;SSL相關實現初始化:
      • 初始化 NioEndpoint:初始化 ServerSocketChannel,設置成阻塞模式,綁定端口;設置 Acceptor、Poller 線程數目;初始化 SSL 信息;初始化 NioSelectorPool;
    • Http11Protocol 初始化:JIoEndpoint 設置線程池名和ConnectionHandler,初始化 ServerSocketFactory:
      • 初始化 JioEndpoint:設置 Acceptor 線程數;創建 ServerSocket 並綁定端口。
  4. 初始化完畢

以上就是在組件在init生命週期事件中完成的設置,注意在 Digester 解析過程中,也完成了一系列的設置。

啓動

調用 StandardServer.start 開啓組件的啓動過程,觸發 BEFORE_START_EVENT、START_EVENT、AFTER_START_EVENT 事件:

  1. 啓動 StandardServer:觸發執行各 Listener;啓動內部的 Services;
  2. Service 默認沒有 Listener,首先啓動 Engines 、接着啓動 Executors、最後啓動 Connectors;
  3. 啓動 Engine,它調用 super.start() 進行啓動或觸發以下的動作:
    • 嘗試啓動 Manager、Cluster、Realm(LockOutRealm);
    • 啓動子容器 Hosts;
    • EngineConfig 監聽器的執行 - START_EVENT,STOP_EVENT;
    • 啓動後臺線程,定期檢查會話超時。
  4. 啓動 Host:設置 ErrorReportValve,並添加到自己的 pipeline 中,觸發 ADD_VALVE_EVENT 容器事件,調用 super.start():
    • 啓動子容器 Contexts;
    • 啓動 pipeline 中實現 Lifecycle 的 Valve。
    • 觸發 HostConfig 監聽器的執行:
      • PERIODIC_EVENT:檢查所有Web應用程序的狀態;
      • START_EVENT:創建 Context,啓動並部署 webapps & conf/Catalina/localhost/*.xml;
      • STOP_EVENT:取消已部署的所有應用。
  5. 啓動 Context,部署 Web App
    • 根據 context.xml(或默認的)使用 Digester 創建 StandardContext 對象,添加 ContextConfig 監聽器,通過 host.addChild 啓動 Context;
    • 初始化用於解析 web.xml 的 Digester,創建設置 WebappLoader 且不使用"標準委託模型";
    • 先解析默認的 conf/web.xml,然後處理 WEB-INF/web.xml,創建 StandardWrapper 封裝 Servlet。
  6. 啓動 Connector,無 Listener,它主要是啓動 ProtocolHandler:
    • Http11NioProtocol - 啓動 NioEndpoint - 啓動 Poller 線程,啓動 Acceptor 線程;
    • Http11Protocol - 啓動 JioEndpoint - 啓動 Acceptor 線程。
  7. 註冊 ShutdownHook,阻塞 main 線程,啓動完畢。

接下來就該處理請求了,這部分下一篇會介紹。

停止

當調用 Bootstrap.stop 或接收到 SHUTDOWN 指令或者捕捉到系統關閉信號(如ctrl+c,shutdown,logoff)時,開始調用 Catalina.stop 方法,關閉組件,觸發 BEFORE_STOP_EVENT、STOP_EVENT 事件:停止Server、Service,暫停 Connector,停止內部組件,停止 Connector,關閉線程池,打斷 awaitThread 即 main 線程,結束。

如果 stop 命令執行失敗,嘗試使用系統信號終止,首先使用 kill -15 ,進程收到後進行處理;如果還是失敗那麼,等待 5s 後,使用 kill -9 強制終止。

小結

當我看到 Web應用加載的時候,耐心有點不足,感覺捋順了整個過程,其實還有很多地方經不起推敲,爲了看得更透徹,那麼這個"簡單"的啓動過程,又有哪些值得思考的呢?

內部啓動的線程和線程池中的線程爲什麼設置爲守護線程(Daemon)?

Java 中有兩類線程:守護線程與用戶線程,區別是守護線程不會阻止 JVM 的退出。從 Tomcat 的停止流程來看,就算用非守護線程也不會出現什麼問題,沒有搜到官方對此的描述,這裏按照理解強行解釋一波 :)。

守護線程一般是服務提供者,運行系統代碼,比如 GC 線程,就像操作系統中也區分用戶線程和內核線程那樣,Tomcat 將內部線程設爲 daemon,也能很好的在語義上區分 Servlet 啓動的線程,並且對於 Servlet 來說 Tomcat 就是它的操作系統。

生命週期的設計有什麼好處?

保證組件啓動和停止的一致性,爲生命週期事件添加監聽器,這些監聽器處理其感興趣的事件,來做一些額外的操作。這是觀察者模式的應用。

多應用間如何實現隔離?

應用隔離的本質就是類隔離,主要防止類衝突。類是否相等是由其全限定名和類加載器共同決定的,容器就是通過自定義 ClassLoader 實現應用間隔離,Tomcat 類加載器結構:

classloader

當要求類加載器加載類時,它首先將請求委託給父加載器,然後在父加載器找不到所請求的類時,查找自己的存儲庫。而 Webapp 加載器略有不同,它首先會在自己的資源庫中搜索,而不是向上委託,打破了標準的委託機制,其類加載時按以下順序查找資源庫:

  • Bootstrap 和 System 已加載的類
  • /WEB-INF/classes 和 /WEB-INF/lib/*.jar
  • Common 已加載的類

應用熱加載和熱部署?

當在啓動 Engine 時,會新建一個名爲 ContainerBackgroundProcessor[StandardEngine[Catalina]] 的線程,默認 10s 檢查是否要重新加載或重新部署,對應方法在 Loader 接口定義分別是 backgroundProcess 和 modified。

默認 web.xml 配置了什麼?

  • 提供一個 DefaultServlet,用於處理靜態資源和未找到匹配 Servlet 的請求;
  • 用於編譯和執行 JSP 的 JspServlet;
  • Session 默認超時 30 minutes;
  • 默認 MIME Type 映射和 Welcome Files

其他能夠想到的點

採用都是常用的數據結構,如 ArrayList、數組、HashMap等,Pipeline 採用鏈表實現,Digester 使用棧來解析。歡迎補充。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章