深入理解Tomcat

前言

學習一個優秀的框架,總要循序漸進,瞭解->使用->原理->源碼->改造。

材料

  1. 下載Tomcat-8.5.37 程序 https://tomcat.apache.org/download-80.cgi
  2. 下載Tomcat-8.5.37 源碼 http://archive.apache.org/dist/tomcat/tomcat-8/v8.5.37/src/
  3. 準備調試代碼,如下
public static void main(String args[]) throws Exception {
        Bootstrap bootstrap=new Bootstrap();
        bootstrap.start();
        Thread.sleep(Integer.MAX_VALUE);
}

目錄

  • ** Tomcat 組件分析**
  • ** Tomcat 生產配置(網絡io模型,調優思路)**
  • ** Tomcat 是如何啓動的?NIO從接受socket到我們的servlet?**
  • ** Tomcat 如何打破雙親委派?**

0.簡介

Tomcat 是一個基於JAVA的WEB容器,其實現了JAVA EE中的 Servlet 與 jsp 規範,與Nginx,Apache 服務器不同在於一般用於動態請求處理;在架構設計上採用面向組件的方式設計,即整體功能是通過組件的方式拼裝完成。另外每個組件都可以被替換以保證靈活性。

1. Tomcat 組件分析

** 由上圖可知Tomcat組成如下 **

  • 一個 Server 和 多個Service
  • Connector 連接器
    • HTTP 1.1
    • SSL https
    • AJP( Apache JServ Protocol) apache 私有協議,用於apache 反向代理Tomcat
  • Container 啓動引擎
    • Engine 引擎 catalina
    • Host 虛擬機 基於域名 分發請求
    • Context 隔離各個WEB應用 每個Context的 ClassLoader都是獨立
  • Component
    • Manager (管理器)
    • logger (日誌管理)
    • loader (載入器)
    • pipeline (管道)
    • valve (管道中的閥,filter)

** 他們的關係如下圖 **

** 也可以從server.xml文件中加深理解 **


2. Tomcat 生產配置(io模型,調優思路)

** 一般的部署思路 **

  1. 複製WAR包至Tomcat webapp 目錄。
  2. 執行starut.bat 腳本啓動。
  3. 啓動過程中war 包會被自動解壓裝載。

** 問題 **:多個項目在一起配置相互影響問題多多

** 生產環境部署策略 **

  1. 實現Tomcat程序和應用部署目錄部署相互分離
  2. 實現各個應用相互不影響
  3. 使用腳本啓動
我們只需要在啓動時指定CATALINA_HOME 與  CATALINA_BASE 參數即可實現。

啓動參數--------描述說明
JAVA_OPTS--------jvm 啓動參數 , 設置內存  編碼等 -Xms100m -Xmx200m -Dfile.encoding=UTF-8
JAVA_HOME--------指定jdk 目錄,如果未設置從java 環境變量當中去找。
CATALINA_HOME--------Tomcat 程序根目錄 
CATALINA_BASE--------應用部署目錄,默認爲$CATALINA_HOME
CATALINA_OUT--------應用日誌輸出目錄:默認$CATALINA_BASE/log
CATALINA_TMPDIR--------應用臨時目錄:默認:$CATALINA_BASE/temp

創建目錄

payment
	├─webapps
	├─logs
	├─temp
	├─conf
	reload.sh
#!/bin/bash 
export JAVA_OPTS="-Xms100m -Xmx200m"
export JAVA_HOME=/root/svr/jdk/
export CATALINA_HOME=/usr/local/apache-tomcat-8.5.34
export CATALINA_BASE="`pwd`"

case $1 in
        start)
        $CATALINA_HOME/bin/catalina.sh start
                echo start success!!
        ;;
        stop)
                $CATALINA_HOME/bin/catalina.sh stop
                echo stop success!!
        ;;
        restart)
        $CATALINA_HOME/bin/catalina.sh stop
                echo stop success!!
                sleep 2
        $CATALINA_HOME/bin/catalina.sh start
        echo start success!!
        ;;
        version)
        $CATALINA_HOME/bin/catalina.sh version
        ;;
        configtest)
        $CATALINA_HOME/bin/catalina.sh configtest
        ;;
        esac
exit 0

** 線程模型 **

Tomcat支持的IO模型說明

  • BIO 阻塞式IO,即Tomcat使用傳統的java.io進行操作。該模式下每個請求都會創建一個線程,對性能開銷大,不適合高併發場景。優點是穩定,適合連接數目小且固定架構。
  • NIO 非阻塞式IO,jdk1.4 之後實現的新IO。該模式基於多路複用選擇器監測連接狀態在通知線程處理,從而達到非阻塞的目的。比傳統BIO能更好的支持併發性能。Tomcat 8.0之後默認採用該模式
  • APR 全稱是 Apache Portable Runtime/Apache可移植運行庫),是Apache HTTP服務器的支持庫。可以簡單地理解爲,Tomcat將以JNI的形式調用Apache HTTP服務器的核心動態鏈接庫來處理文件讀取或網絡傳輸操作。使用需要編譯安裝APR 庫
  • AIO 異步非阻塞式IO,jdk1.7後之支持 。與nio不同在於不需要多路複用選擇器,而是請求處理線程執行完程進行回調調知,已繼續執行後續操作。Tomcat 8之後支持。

** 使用指定IO模型的配置方式 **

配置 server.xml 文件當中的 <Connector  protocol="HTTP/1.1">    修改即可。
默認配置 8.0  protocol=“HTTP/1.1” 8.0 之前是 BIO 8.0 之後是NIO
BIO
protocol=“org.apache.coyote.http11.Http11Protocol“
NIO
protocol=”org.apache.coyote.http11.Http11NioProtocol“
AIO
protocol=”org.apache.coyote.http11.Http11Nio2Protocol“
APR
protocol=”org.apache.coyote.http11.Http11AprProtocol“

** BIO 線程模型 (Tomcat8之後移除)**

Acceptor 負責接受連接(封裝提交task) 給線程池(去分配work線程),每個請求都又一個線程去處理

** NIO 線程模型講解 **

Acceptor 負責接受連接socket,然後會給Poller使用NIO select ,再 進行分配線程處理

** NIO2 線程模型講解 **

基於NIO,不再使用多路複用selector,基於事件的異步通知實現,監聽各種CompletionHandler,那麼爲什麼有了AIO 還需要NIO?適用場景不同,NIO適合處理較快的場景,不然要一直輪詢,如果處理時間長,效率就低下了。AIO 適合時間長的,例如相冊服務器,長時間的讀取,異步通知效率高

** 源碼實現 **

  • 1.Http11Protocol Http BIO協議解析器
    • JIoEndpoint
      • Acceptor implements Runnable
      • SocketProcessor implements Runnable
  • 2.Http11NioProtocol Http Nio協議解析器
    • NioEndpoint
      • Acceptor implements Runnable
      • Poller implements Runnable
      • SocketProcessor implements Runnable

** 調優 **

Connector
連接器:用於接收 指定協議下的連接 並指定給唯一的Engine 進行處理。
主要屬性:
protocol 監聽的協議,默認是http/1.1
port 指定服務器端要創建的端口號
minThread	服務器啓動時創建的處理請求的線程數
maxThread	最大可以創建的處理請求的線程數
enableLookups	如果爲true,則可以通過調用request.getRemoteHost()進行DNS查詢來得到遠程客戶端的實際主機名,若爲false則不進行DNS查詢,而是返回其ip地址
redirectPort	指定服務器正在處理http請求時收到了一個SSL傳輸請求後重定向的端口號
acceptCount	指定當所有可以使用的處理請求的線程數都被使用時,可以放到處理隊列中的請求數,超過這個數的請求將不予處理。內部有隊列
connectionTimeout	指定超時的時間數(以毫秒爲單位)
SSLEnabled 是否開啓 sll 驗證,在Https 訪問時需要開啓。

調休的關鍵點:爲什麼這麼調?不要憑着感覺調優

https://gitee.com/zhangxiangfeng/apache-tomcat-8-5-14-prod

  • 使用線程池,優化策略
  • 採用多個Acceptor 加速接受連接,處理加速 合理設置acceptCount acceptorThreadCount
  • JVM設置合理的堆內存

3. Tomcat 是如何啓動的?

1.從這裏看到 執行 startup.sh == catalina.sh start

2.從catalina.sh 可以看出 org.apache.catalina.startup.Bootstrap “$@” start

3.到這裏就明白了簡單來說就是 Bootstrap.start()啓動,也徹底證明了Tomcat是純Java實現的應用容器

** 源代碼 **

> org.apache.catalina.startup.Bootstrap.start()  啓動入口函數
  > org.apache.catalina.startup.Bootstrap.init() 1.使用類加載器工廠創建不同的類加載器 2.構造 Catalina,反射調用其內部的start的方法
    > org.apache.catalina.startup.Catalina.start 1.調用start(啓動Acceptor(接受socket),SocketProcessor(處理socket,編解碼,然後給線程給run,出來HttpReq,HttpResp))
      > org.apache.catalina.startup.Catalina.load()
        > org.apache.catalina.startup.Catalina.initDirs() 初始化臨時目錄
        > org.apache.catalina.startup.Catalina.initNaming() 初始化命名
        > org.apache.catalina.startup.Catalina.createStartDigester() 構造主要的啓動者,設置了用來解析server.xml的參數
        > org.apache.catalina.startup.Catalina.configFile() 根據約定規則去默認的base_home目錄conf/server.xml去獲取server.xml文件,下面使用org.xml.sax.InputSource解析該xml
        > org.apache.tomcat.util.digester.Digester.parse(org.xml.sax.InputSource) 解析上一步的server文件,實例化Server,最終反射調用了org.apache.tomcat.util.IntrospectionUtils.callMethod1 調用setServer設置Server對象
          > org.apache.catalina.startup.SetAllPropertiesRule.begin 設置HttpReq,HttpResp,用於之後的處理
        > org.apache.catalina.Lifecycle.init() 初始化Server
      > org.apache.catalina.Lifecycle.start() 啓動Server
        > org.apache.catalina.core.StandardServer.startInternal:413 循環啓動所有的service(內部的engine+executors+mapperListener+connectors)都是調用其start方法
          > org.apache.catalina.Lifecycle.start 循環啓動service
            > org.apache.catalina.util.LifecycleBase.start 循環啓動connectors
              > org.apache.coyote.ProtocolHandler.start 啓動  ProtocolHandler 協議處理器
                > org.apache.tomcat.util.net.NioEndpoint.startInternal 內部先啓動(pollers,其次是啓動所有的Acceptor,代碼如下)
                  >// Start poller threads
                       pollers = new Poller[getPollerThreadCount()];
                       for (int i=0; i<pollers.length; i++) {
                           pollers[i] = new Poller();
                           Thread pollerThread = new Thread(pollers[i], getName() + "-ClientPoller-"+i);
                           pollerThread.setPriority(threadPriority);
                           pollerThread.setDaemon(true);
                           pollerThread.start();
                       }
                  //startAcceptorThreads();
                  > org.apache.tomcat.util.net.NioEndpoint.Poller#run 開始通過 selector.selectNow || selector.select(selectorTimeout)監聽
                    > org.apache.tomcat.util.net.NioEndpoint.Poller#events 循環所有進來的event,異步執行,放入
                      > org.apache.tomcat.util.net.NioEndpoint.PollerEvent#run 通過 socket.getIOChannel().keyFor(socket.getPoller().getSelector()) 處理某一個event
                        > java.nio.channels.SelectionKey.interestOps(int)
                    > org.apache.tomcat.util.net.NioEndpoint.Poller.processKey 處理NIO的selectKey
                      > org.apache.tomcat.util.net.AbstractEndpoint.processSocket 進一步處理
                        > org.apache.tomcat.util.net.SocketProcessorBase.run
                          > org.apache.tomcat.util.net.SocketProcessorBase.doRun 此方法是抽象的,又此類實現例如 NioEndpoint,Nio2Endpoint,AprEndpoint
                            > org.apache.coyote.Processor.process
                              > org.apache.coyote.AbstractProcessorLight.service
                                > org.apache.coyote.http11.Http11Processor.service 在這裏開始解析協議
                                  > org.apache.catalina.connector.CoyoteAdapter.postParseRequest 開始實現Request,Response
                                    > org.apache.catalina.core.ApplicationFilterChain.internalDoFilter  Use potentially wrapped request from this point,Filter執行完畢就開始Servlet.service調用
                                      > javax.servlet.Servlet#service 開始我們的應用
                  > org.apache.tomcat.util.net.AbstractEndpoint.startAcceptorThreads 啓動所有的Acceptor(請求入口接收者)
      > java.lang.Runtime.addShutdownHook 添加鉤子函數,如果應用被系統kill掉,這裏就會調用stop方法優雅退出

源代碼調試比較複雜,簡單來說

  1. 構建Catalina 調用start
  2. 解析server.xml
  3. 初始化啓動Server
  4. Server 循環啓動所有的service(內部的engine+executors+mapperListener+connectors+pollers+startAcceptorThreads)都是調用其start方法
  5. 添加 addShutdownHook 鉤子函數,清理資源

4. Tomcat 如何打破雙親委派

篇幅夠長了,這裏不介紹什麼是雙親委派了

  • 如何打破雙親委派 1.重寫loadcalss 2.使用線程上下文類加載器

    • 第一次破壞:JDK1.2引入雙親委派,之前的代碼都是自定義類加載器,爲了兼容,出現loadclass
    • 第二次破壞: 雙親委派解決了基礎向上加載的問題,但是基礎的類依賴用戶的代碼如何處理,例如JNDI管理髮現用戶的資源,出現了 線程上下文類加載器(Thread Context ClassLoader)
    • 第三次破壞: jrebel 熱加載和熱部署等的出現,java開頭的類用系統加載器,否則依賴自己的加載,樹狀加載轉網加載
  • 爲什麼tomca要打破類加載,tomcat有自己的lib目錄,一些類要自己去加載

1.啓動Tomcat有三個線程異步啓動
2.org.apache.catalina.startup.Bootstrap.initClassLoaders 分別設置了上下文類加載器(用於加載約定lib下的jar,打破雙親委派)

    private void initClassLoaders() {
        try {
            commonLoader = createClassLoader("common", null);
            if( commonLoader == null ) {
                // no config file, default to this loader - we might be in a 'single' env.
                commonLoader=this.getClass().getClassLoader();
            }
            catalinaLoader = createClassLoader("server", commonLoader);
            sharedLoader = createClassLoader("shared", commonLoader);
        } catch (Throwable t) {
            handleThrowable(t);
            log.error("Class loader creation threw exception", t);
            System.exit(1);
        }
    }

Tomcat 如果使用默認的類加載機制行不行?應用的不同lib如何隔離? 看這裏 https://blog.csdn.net/qq_38182963/article/details/78660779

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