走進JavaWeb技術世界8:淺析Tomcat9請求處理流程與啓動部署過程

談談 Tomcat 請求處理流程

轉自:https://github.com/c-rainstorm/blog/blob/tomcat-request-process/reading-notes

《談談 Tomcat 架構及啓動過程[含部署]》已重新修訂!(與本文在 GitHub 同一目錄下)包括架構和 Tomcat Start 過程中的 MapperListener 相關描述。Connector 啓動相關的內容與請求處理關係比較緊密,所以就獨立出來放在本文中了。

建議結合《談談 Tomcat 架構及啓動過程[含部署]》一起看!

很多東西在時序圖中體現的已經非常清楚了,沒有必要再一步一步的作介紹,所以本文以圖爲主,然後對部分內容加以簡單解釋。

本文對 Tomcat 的介紹以 Tomcat-9.0.0.M22 爲標準。

Tomcat-9.0.0.M22 是 Tomcat 目前最新的版本,但尚未發佈,它實現了 Servlet4.0 及 JSP2.3 並提供了很多新特性,需要 1.8 及以上的 JDK 支持等等,詳情請查閱 Tomcat-9.0-doc


Overview

tomcat-request-process-model.jpg

添加描述

  1. Connector 啓動以後會啓動一組線程用於不同階段的請求處理過程。

  2. Acceptor 線程組。用於接受新連接,並將新連接封裝一下,選擇一個 Poller 將新連接添加到 Poller 的事件隊列中。

  3. Poller 線程組。用於監聽 Socket 事件,當 Socket 可讀或可寫等等時,將 Socket 封裝一下添加到 worker 線程池的任務隊列中。

  4. worker 線程組。用於對請求進行處理,包括分析請求報文並創建 Request 對象,調用容器的 pipeline 進行處理。

  • AcceptorPollerworker 所在的 ThreadPoolExecutor 都維護在 NioEndpoint 中。

Connector Init and Start

  1. initServerSocket(),通過 ServerSocketChannel.open() 打開一個 ServerSocket,默認綁定到 8080 端口,默認的連接等待隊列長度是 100, 當超過 100 個時會拒絕服務。我們可以通過配置 conf/server.xml 中 Connector 的 acceptCount 屬性對其進行定製。

  2. createExecutor() 用於創建 Worker 線程池。默認會啓動 10 個 Worker 線程,Tomcat 處理請求過程中,Woker 最多不超過 200 個。我們可以通過配置 conf/server.xml 中 Connector 的 minSpareThreads 和 maxThreads 對這兩個屬性進行定製。

  3. Pollor 用於檢測已就緒的 Socket。 默認最多不超過 2 個,Math.min(2,Runtime.getRuntime().availableProcessors());。我們可以通過配置 pollerThreadCount 來定製。

  4. Acceptor 用於接受新連接。默認是 1 個。我們可以通過配置 acceptorThreadCount 對其進行定製。

Requtst Process

Acceptor

tomcat-request-process-acceptor.png

添加描述

  1. Acceptor 在啓動後會阻塞在 ServerSocketChannel.accept(); 方法處,當有新連接到達時,該方法返回一個 SocketChannel

  2. 配置完 Socket 以後將 Socket 封裝到 NioChannel 中,並註冊到 Poller,值的一提的是,我們一開始就啓動了多個 Poller 線程,註冊的時候,連接是公平的分配到每個 Poller 的。NioEndpoint 維護了一個 Poller 數組,當一個連接分配給 pollers[index] 時,下一個連接就會分配給 pollers[(index+1)%pollers.length].

  3. addEvent() 方法會將 Socket 添加到該 Poller 的 PollerEvent 隊列中。到此 Acceptor 的任務就完成了。

Poller

tomcat-request-process-poller.png

添加描述

  1. selector.select(1000)。當 Poller 啓動後因爲 selector 中並沒有已註冊的 Channel,所以當執行到該方法時只能阻塞。所有的 Poller 共用一個 Selector,其實現類是 sun.nio.ch.EPollSelectorImpl

  2. events() 方法會將通過 addEvent() 方法添加到事件隊列中的 Socket 註冊到 EPollSelectorImpl,當 Socket 可讀時,Poller 纔對其進行處理

  3. createSocketProcessor() 方法將 Socket 封裝到 SocketProcessor 中,SocketProcessor 實現了 Runnable 接口。worker 線程通過調用其 run() 方法來對 Socket 進行處理。

  4. execute(SocketProcessor) 方法將 SocketProcessor 提交到線程池,放入線程池的 workQueue 中。workQueue 是 BlockingQueue 的實例。到此 Poller 的任務就完成了。

Worker

tomcat-request-process-worker.png

添加描述

  1. worker 線程被創建以後就執行 ThreadPoolExecutor 的 runWorker() 方法,試圖從 workQueue 中取待處理任務,但是一開始 workQueue 是空的,所以 worker 線程會阻塞在 workQueue.take() 方法。

  2. 當新任務添加到 workQueue後,workQueue.take() 方法會返回一個 Runnable,通常是 SocketProcessor,然後 worker 線程調用 SocketProcessor 的 run() 方法對 Socket 進行處理。

  3. createProcessor() 會創建一個 Http11Processor, 它用來解析 Socket,將 Socket 中的內容封裝到 Request 中。注意這個 Request 是臨時使用的一個類,它的全類名是 org.apache.coyote.Request

  4. postParseRequest() 方法封裝一下 Request,並處理一下映射關係(從 URL 映射到相應的 HostContextWrapper)。

  5. CoyoteAdapter 將 Rquest 提交給 Container 處理之前,並將 org.apache.coyote.Request 封裝到 org.apache.catalina.connector.Request,傳遞給 Container 處理的 Request 是 org.apache.catalina.connector.Request

  6. connector.getService().getMapper().map(),用來在 Mapper 中查詢 URL 的映射關係。映射關係會保留到 org.apache.catalina.connector.Request 中,Container 處理階段 request.getHost() 是使用的就是這個階段查詢到的映射主機,以此類推 request.getContext()request.getWrapper() 都是。

  7. connector.getService().getContainer().getPipeline().getFirst().invoke() 會將請求傳遞到 Container 處理,當然了 Container 處理也是在 Worker 線程中執行的,但是這是一個相對獨立的模塊,所以單獨分出來一節。

Container

tomcat-request-process-container.png

添加描述

  1. 需要注意的是,基本上每一個容器的 StandardPipeline 上都會有多個已註冊的 Valve,我們只關注每個容器的 Basic Valve。其他 Valve 都是在 Basic Valve 前執行。

  2. request.getHost().getPipeline().getFirst().invoke() 先獲取對應的 StandardHost,並執行其 pipeline。

  3. request.getContext().getPipeline().getFirst().invoke() 先獲取對應的 StandardContext,並執行其 pipeline。

  4. request.getWrapper().getPipeline().getFirst().invoke() 先獲取對應的 StandardWrapper,並執行其 pipeline。

  5. 最值得說的就是 StandardWrapper 的 Basic Valve,StandardWrapperValve

  6. allocate() 用來加載並初始化 Servlet,值的一提的是 Servlet 並不都是單例的,當 Servlet 實現了 SingleThreadModel 接口後,StandardWrapper 會維護一組 Servlet 實例,這是享元模式。當然了 SingleThreadModel在 Servlet 2.4 以後就棄用了。

  7. createFilterChain() 方法會從 StandardContext 中獲取到所有的過濾器,然後將匹配 Request URL 的所有過濾器挑選出來添加到 filterChain 中。

  8. doFilter() 執行過濾鏈,當所有的過濾器都執行完畢後調用 Servlet 的 service() 方法。

談談 Tomcat 架構及啓動過程[含部署]

這個題目命的其實是很大的,寫的時候還是很忐忑的,但我儘可能把這個過程描述清楚。因爲這是讀過源碼以後寫的總結,在寫的過程中可能會忽略一些前提條件,如果有哪些比較突兀就出現,或不好理解的地方可以給我提 Issue,我會盡快補充修訂相關內容。

很多東西在時序圖中體現的已經非常清楚了,沒有必要再一步一步的作介紹,所以本文以圖爲主,然後對部分內容加以簡單解釋。

  1. tomcat-architecture.pu

  2. tomcat-init.pu

  3. tomcat-start.pu

  4. tomcat-context-start.pu

  5. tomcat-background-thread.pu

本文對 Tomcat 的介紹以 Tomcat-9.0.0.M22 爲標準。

Tomcat-9.0.0.M22 是 Tomcat 目前最新的版本,但尚未發佈,它實現了 Servlet4.0 及 JSP2.3 並提供了很多新特性,需要 1.8 及以上的 JDK 支持等等,詳情請查閱 Tomcat-9.0-doc

Overview

  1. Bootstrap 作爲 Tomcat 對外界的啓動類,在 $CATALINA_BASE/bin 目錄下,它通過反射創建 Catalina 的實例並對其進行初始化及啓動。

  2. Catalina 解析 $CATALINA_BASE/conf/server.xml 文件並創建 StandardServerStandardServiceStandardEngineStandardHost 等

  3. StandardServer 代表的是整個 Servlet 容器,他包含一個或多個 StandardService

  4. StandardService 包含一個或多個 Connector,和一個 EngineConnector 和 Engine 都是在解析 conf/server.xml 文件時創建的,Engine 在 Tomcat 的標準實現是 StandardEngine

  5. MapperListener 實現了 LifecycleListener 和 ContainerListener 接口用於監聽容器事件和生命週期事件。該監聽器實例監聽所有的容器,包括 StandardEngineStandardHostStandardContextStandardWrapper,當容器有變動時,註冊容器到 Mapper

  6. Mapper 維護了 URL 到容器的映射關係。當請求到來時會根據 Mapper 中的映射信息決定將請求映射到哪一個 HostContextWrapper

  7. Http11NioProtocol 用於處理 HTTP/1.1 的請求

  8. NioEndpoint 是連接的端點,在請求處理流程中該類是核心類,會重點介紹。

  9. CoyoteAdapter 用於將請求從 Connctor 交給 Container 處理。使 Connctor 和 Container 解耦。

  10. StandardEngine 代表的是 Servlet 引擎,用於處理 Connector 接受的 Request。包含一個或多個 Host(虛擬主機), Host 的標準實現是 StandardHost

  11. StandardHost 代表的是虛擬主機,用於部署該虛擬主機上的應用程序。通常包含多個 Context (Context 在 Tomcat 中代表應用程序)。Context 在 Tomcat 中的標準實現是 StandardContext

  12. StandardContext 代表一個獨立的應用程序,通常包含多個 Wrapper,一個 Wrapper 容器封裝了一個 Servlet,Wrapper的標準實現是 StandardWrapper

  13. StandardPipeline 組件代表一個流水線,與 Valve(閥)結合,用於處理請求。 StandardPipeline 中含有多個 Valve, 當需要處理請求時,會逐一調用 Valve 的 invoke 方法對 Request 和 Response 進行處理。特別的,其中有一個特殊的 Valve 叫 basicValve,每一個標準容器都有一個指定的 BasicValve,他們做的是最核心的工作。

  • StandardEngine 的是 StandardEngineValve,他用來將 Request 映射到指定的 Host;

  • StandardHost 的是 StandardHostValve, 他用來將 Request 映射到指定的 Context;

  • StandardContext 的是 StandardContextValve,它用來將 Request 映射到指定的 Wrapper

  • StandardWrapper 的是 StandardWrapperValve,他用來加載 Rquest 所指定的 Servlet,並調用 Servlet 的 Service 方法。

Tomcat init

tomcat-start.png

添加描述

  • 當通過 ./startup.sh 腳本或直接通過 java 命令來啓動 Bootstrap 時,Tomcat 的啓動過程就正式開始了,啓動的入口點就是 Bootstrap 類的 main 方法。

  • 啓動的過程分爲兩步,分別是 init 和 start,本節主要介紹 init;

  1. 初始化類加載器。[關於 Tomcat 類加載機制,可以參考我之前寫的一片文章:談談Java類加載機制]

  2. 通過從 CatalinaProperties 類中獲取 common.loader 等屬性,獲得類加載器的掃描倉庫。CatalinaProperties 類在的靜態塊中調用了 loadProperties() 方法,從 conf/catalina.properties 文件中加載了屬性.(即在類創建的時候屬性就已經加載好了)。

  3. 通過 ClassLoaderFactory 創建 URLClassLoader 的實例

  4. 通過反射創建 Catalina 的實例並設置 parentClassLoader

  5. setAwait(true)。設置 Catalina 的 await 屬性爲 true。在 Start 階段尾部,若該屬性爲 true,Tomcat 會在 main 線程中監聽 SHUTDOWN 命令,默認端口是 8005.當收到該命令後執行 Catalina 的 stop() 方法關閉 Tomcat 服務器。

  6. createStartDigester()Catalina 的該方法用於創建一個 Digester 實例,並添加解析 conf/server.xml 的 RuleSet。Digester 原本是 Apache 的一個開源項目,專門解析 XML 文件的,但我看 Tomcat-9.0.0.M22 中直接將這些類整合到 Tomcat 內部了,而不是引入 jar 文件。Digester 工具的原理不在本文的介紹範圍,有興趣的話可以參考 The Digester Component - Apache 或 《How Tomcat works》- Digester [推薦] 一章

  7. parse() 方法就是 Digester 處理 conf/server.xml 創建各個組件的過程。值的一提的是這些組件都是使用反射的方式來創建的。特別的,在創建 Digester 的時候,添加了一些特別的 rule Set,用於創建一些十分核心的組件,這些組件在 conf/server.xml 中沒有但是其作用都比較大,這裏做下簡單介紹,當 Start 時用到了再詳細說明:

  8. EngineConfigLifecycleListener 的實現類,觸發 Engine 的生命週期事件後調用,這個監聽器沒有特別大的作用,就是打印一下日誌

  9. HostConfigLifecycleListener 的實現類,觸發 Host 的生命週期事件後調用。這個監聽器的作用就是部署應用程序,這包括 conf/<Engine>/<Host>/ 目錄下所有的 Context xml 文件 和 webapps 目錄下的應用程序,不管是 war 文件還是已解壓的目錄。 另外後臺進程對應用程序的熱部署也是由該監聽器負責的。

  10. ContextConfigLifecycleListener 的實現類,觸發 Context 的生命週期事件時調用。這個監聽器的作用是配置應用程序,它會讀取併合並 conf/web.xml 和 應用程序的 web.xml,分析 /WEB-INF/classes/ 和 /WEB-INF/lib/*.jar中的 Class 文件的註解,將其中所有的 Servlet、ServletMapping、Filter、FilterMapping、Listener 都配置到 StandardContext 中,以備後期使用。當然了 web.xml 中還有一些其他的應用程序參數,最後都會一併配置到 StandardContext 中。

  11. reconfigureStartStopExecutor() 用於重新配置啓動和停止子容器的 Executor。默認是 1 個線程。我們可以配置 conf/server.xml 中 Engine 的 startStopThreads,來指定用於啓動和停止子容器的線程數量,如果配置 0 的話會使用 Runtime.getRuntime().availableProcessors() 作爲線程數,若配置爲負數的話會使用 Runtime.getRuntime().availableProcessors() + 配置值,若和小與 1 的話,使用 1 作爲線程數。當線程數是 1 時,使用 InlineExecutorService 它直接使用當前線程來執行啓動停止操作,否則使用 ThreadPoolExecutor 來執行,其最大線程數爲我們配置的值。

  12. 需要注意的是 Host 的 init 操作是在 Start 階段來做的, StardardHost 創建好後其 state 屬性的默認值是 LifecycleState.NEW,所以在其調用 startInternal() 之前會進行一次初始化。

Tomcat Start[Deployment]

tomcat-start.png

添加描述

  1. 圖中從 StandardHost Start StandardContext 的這步其實在真正的執行流程中會直接跳過,因爲 conf/server.xml 文件中並沒有配置任何的 Context,所以在 findChildren() 查找子容器時會返回空數組,所以之後遍歷子容器來啓動子容器的 for 循環就直接跳過了。

  2. 觸發 Host 的 BEFORE_START_EVENT 生命週期事件,HostConfig 調用其 beforeStart() 方法創建 $CATALINA_BASE/webapps$CATALINA_BASE/conf/<Engine>/<Host>/ 目錄。

  3. 觸發 Host 的 START_EVENT 生命週期事件,HostConfig 調用其 start() 方法開始部署已在 $CATALINA_BASE/webapps & $CATALINA_BASE/conf/<Engine>/<Host>/ 目錄下的應用程序。

  4. 解析 $CATALINA_BASE/conf/<Engine>/<Host>/ 目錄下所有定義 Context 的 XML 文件,並添加到 StandardHost。這些 XML 文件稱爲應用程序描述符。正因爲如此,我們可以配置一個虛擬路徑來保存應用程序中用到的圖片,詳細的配置過程請參考 開發環境配置指南 - 6.3. 配置圖片存放目錄

  5. 部署 $CATALINA_BASE/webapps 下所有的 WAR 文件,並添加到 StandardHost

  6. 部署 $CATALINA_BASE/webapps 下所有已解壓的目錄,並添加到 StandardHost

  • 特別的,添加到 StandardHost 時,會直接調用 StandardContext 的 start() 方法來啓動應用程序。啓動應用程序步驟請看 Context Start 一節。

  1. 在 StandardEngine 和 StandardContext 啓動時都會調用各自的 threadStart() 方法,該方法會創建一個新的後臺線程來處理該該容器和子容器及容器內各組件的後臺事件。StandardEngine 會直接創建一個後臺線程,StandardContext 默認是不創建的,和 StandardEngine 共用同一個。後臺線程處理機制是週期調用組件的 backgroundProcess() 方法。詳情請看 Background process 一節。

  2. MapperListener

  • addListeners(engine) 方法會將該監聽器添加到 StandardEngine 和它的所有子容器中

  • registerHost() 會註冊所有的 Host 和他們的子容器到 Mapper 中,方便後期請求處理時使用。

  • 當有新的應用(StandardContext)添加進來後,會觸發 Host 的容器事件,然後通過 MapperListener 將新應用的映射註冊到 Mapper 中。

  1. Start 工作都做完以後 Catalina 會創建一個 CatalinaShutdownHook 並註冊到 JVM。CatalinaShutdownHook 繼承了 Thread,是 Catalina 的內部類。其 run 方法中直接調用了 Catalina 的 stop() 方法來關閉整個服務器。註冊該 Thread 到 JVM 的原因是防止用戶非正常終止 Tomcat,比如直接關閉命令窗口之類的。當直接關閉命令窗口時,操作系統會向 JVM 發送一個終止信號,然後 JVM 在退出前會逐一啓動已註冊的 ShutdownHook 來關閉相應資源。

Context Start

tomcat-context-start.png

添加描述

  1. StandRoot 類實現了 WebResourceRoot 接口,它容納了一個應用程序的所有資源,通俗的來說就是部署到 webapps 目錄下對應 Context 的目錄裏的所有資源。因爲我對 Tomcat 的資源管理部分暫時不是很感興趣,所以資源管理相關類只是做了簡單瞭解,並沒有深入研究源代碼。

  2. resourceStart() 方法會對 StandardRoot 進行初始配置

  3. postWorkDirectory() 用於創建對應的工作目錄 $CATALINA_BASE/work/<Engine>/<Host>/<Context>, 該目錄用於存放臨時文件。

  4. StardardContext 只是一個容器,而 ApplicationContext 則是一個應用程序真正的運行環境,相關類及操作會在請求處理流程看完以後進行補充。

  5. StardardContext 觸發 CONFIGURE_START_EVENT 生命週期事件,ContextConfig 開始調用 configureStart() 對應用程序進行配置。

  6. 這個過程會解析併合並 conf/web.xml & conf/<Engine>/<Host>/web.xml.default & webapps/<Context>/WEB-INF/web.xml 中的配置。

  7. 配置配置文件中的參數到 StandardContext, 其中主要的包括 Servlet、Filter、Listener。

  8. 因爲從 Servlet3.0 以後是直接支持註解的,所以服務器必須能夠處理加了註解的類。Tomcat 通過分析 WEB-INF/classes/ 中的 Class 文件和 WEB-INF/lib/ 下的 jar 包將掃描到的 Servlet、Filter、Listerner 註冊到 StandardContext

  9. setConfigured(true),是非常關鍵的一個操作,它標識了 Context 的成功配置,若未設置該值爲 true 的話,Context 會啓動失敗。

Background process

tomcat-background-thread.png

添加描述

  1. 後臺進程的作用就是處理一下 Servlet 引擎中的週期性事件,處理週期默認是 10s。

  2. 特別的 StandardHost 的 backgroundProcess() 方法會觸發 Host 的 PERIODIC_EVENT 生命週期事件。然後 HostConfig 會調用其 check() 方法對已加載並進行過重新部署的應用程序進行 reload 或對新部署的應用程序進行熱部署。熱部署跟之前介紹的部署步驟一致, reload() 過程只是簡單的順序調用 setPause(true)、stop()、start()、setPause(false),其中 setPause(true) 的作用是暫時停止接受請求。



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