深刻拆解Tomcat & Jetty-學習筆記(3)模塊二之tomcat如何實現一鍵啓停和頂層組件

tomcat如何實現一鍵啓停

靜態類圖

動態啓動

若是想讓一個系統可以對外提供服務,咱們須要建立、組裝並啓動這些組件;在服務中止的時候,咱們還須要釋放資源,銷燬這些組件,所以這是一個動態的過程。也就是說,Tomcat 須要動態地管理這些組件的生命週期。html

組件之間的關係

  • 第一層關係是組件有大有小,大組件管理小組件,好比 Server 管理 Service,Service 又管理鏈接器和容器。
  • 第二層關係是組件有外有內,外層組件控制內層組件,好比鏈接器是外層組件,負責對外交流,外層組件調用內層組件完成業務功能。 也就是說,請求的處理過程是由外層組件來驅動的。這兩層關係決定了系統在建立組件時應該遵循必定的順序。
  • 第一個原則是先建立子組件,再建立父組件,子組件須要被「注入」到父組件中。
  • 第二個原則是先建立內層組件,再建立外層組件,內層組建須要被「注入」到外層組件。

一鍵式啓停:Lifecycle 接口

設計就是要找到系統的變化點和不變點。
  • 組件建立所須要經歷的 建立,初始化,啓動着幾個過程是不變的。
  • 變化的只是不一樣的組件,這些方法的實現不同。 所以,咱們把不變點抽象出來成爲一個接口,這個接口跟生命週期有關,叫做 Lifecycle。Lifecycle 接口裏應該定義這麼幾個方法:init、start、stop 和 destroy,每一個具體的組件去實現這些方法。

組合模式的

在父組件的 init 方法裏須要建立子組件並調用子組件的 init 方法。一樣,在父組件的 start 方法裏也須要調用子組件的 start 方法,所以調用者能夠無差異的調用各組件的 init 方法和 start 方法,這就是組合模式的使用java

觀察者模式

組件的 init 和 start 調用是由它的父組件的狀態變化觸發的,上層組件的初始化會觸發子組件的初始化,上層組件的啓動會觸發子組件的啓動,所以咱們把組件的生命週期定義成一個個狀態,把狀態的轉變看做是一個事件。而事件是有監聽器的,在監聽器裏能夠實現一些邏輯,而且監聽器也能夠方便的添加和刪除,這就是典型的觀察者模式git

在LifecycleBase已然使用觀察者模式

private final List<LifecycleListener> lifecycleListeners = new CopyOnWriteArrayList<>();


    /** * Allow sub classes to fire {@link Lifecycle} events. * * @param type Event type * @param data Data associated with event. */
    protected void fireLifecycleEvent(String type, Object data) {
        LifecycleEvent event = new LifecycleEvent(this, type, data);
        for (LifecycleListener listener : lifecycleListeners) {
            listener.lifecycleEvent(event);
        }
    }
複製代碼
模板模式

接口可能有多個實現類,爲了不每一個實現類都去實現一些重複的邏輯。能夠定義一個基類來實現共同的邏輯。而基類中每每會定義一些抽象方法,所謂的抽象方法就是說基類不會去實現這些方法,而是調用這些方法來實現骨架邏輯。抽象方法是留給各個子類去實現的,而且子類必須實現,不然沒法實例github

注意下圖註釋中的描述。數據庫

/** * {@inheritDoc} */
    @Override
    public final synchronized void start() throws LifecycleException {

        if (LifecycleState.STARTING_PREP.equals(state) || LifecycleState.STARTING.equals(state) ||
                LifecycleState.STARTED.equals(state)) {

            if (log.isDebugEnabled()) {
                Exception e = new LifecycleException();
                log.debug(sm.getString("lifecycleBase.alreadyStarted", toString()), e);
            } else if (log.isInfoEnabled()) {
                log.info(sm.getString("lifecycleBase.alreadyStarted", toString()));
            }

            return;
        }

        if (state.equals(LifecycleState.NEW)) {
            init();
        } else if (state.equals(LifecycleState.FAILED)) {
            stop();
        } else if (!state.equals(LifecycleState.INITIALIZED) &&
                !state.equals(LifecycleState.STOPPED)) {
            invalidTransition(Lifecycle.BEFORE_START_EVENT);
        }

        try {
            setStateInternal(LifecycleState.STARTING_PREP, null, false);
            startInternal();  //骨架代碼中調用的預留的抽象方法,未來每一個子類在使用本方法的時候,就是調用的它本身實現了startInternal
            if (state.equals(LifecycleState.FAILED)) {
                stop();
            } else if (!state.equals(LifecycleState.STARTING)) {
                invalidTransition(Lifecycle.AFTER_START_EVENT);
            } else {
                setStateInternal(LifecycleState.STARTED, null, false);
            }
        } catch (Throwable t) {
            handleSubClassException(t, "lifecycleBase.startFail", toString());
        }
    }
複製代碼

生週期管理整體類圖

connector的線程池是隻處理鏈接的線程池,仍是後臺任務都共用的線程池?

Connector上的線程池負責處理這個它所接收到的全部請求。一個Connector有一個線程池。其餘的後臺任務也有專門的線程池,不會佔用Connector的線程池。bootstrap

其它精彩連接

  • 和你們分享一篇介紹Tomcat啓動的文章, 從startup.bat的源碼開始分析的.www.cnblogs.com/tanshaoshen…
  • 怎麼啓動Tomcat的源碼、調試。怎麼讀源碼。建議跟SpringBoot那樣,用嵌入式方式啓動Tomcat,這裏有例子https://github.com/heroku/devcenter-embedded-tomcat

Tomcat的「高層們」都負責作什麼?

執行startup.sh啓動腳本後,執行了什麼?

  • Tomcat 本質是 Java 程序, startup.sh 啓動 JVM 運行 Tomcat 啓動類 bootstrap
  • Bootstrap 初始化類加載器, 建立 Catalina
  • Catalina 解析 server.xml, 建立相應組件, 調用 Server start 方法
  • Server 組件管理 Service 組件並調用其 start 方法
  • Service 負責管理鏈接器和頂層容器 Engine, 其會調用 Engine start 方法- 這些類不處理具體請求, 主要管理下層組件, 並分配請求
  • 一個比喻: 若是咱們把 Tomcat 比做是一家公司,
    • 那麼 Catalina 應該是公司創始人,由於 Catalina 負責組建團隊,也就是建立 Server 以及它的子組件。
    • Server 是公司的 CEO,負責管理多個事業羣,每一個事業羣就是一個 Service。
    • Service 是事業羣總經理,它管理兩個職能部門:一個是對外的市場部,也就是鏈接器組件;另外一個是對內的研發部,也就是容器組件。
    • Engine 則是研發部經理,由於 Engine 是最頂層的容器組件。

Catalina 功能

  • 解析 server.xml, 建立定義的各組件, 調用 server init 和 start 方法
  • 處理異常狀況, 例如 ctrl + c 關閉 Tomcat. 其會在 JVM 中註冊"關閉鉤子"
    • 關閉鉤子, 在關閉 JVM 時作清理工做, 例如刷新緩存到磁盤
    • 關閉鉤子是一個線程, JVM 中止前會執行器 run 方法, 該 run 方法調用 server stop 方法
//建立並註冊關閉鉤子
     if (useShutdownHook) {
                if (shutdownHook == null) {
                    shutdownHook = new CatalinaShutdownHook();
                }
                Runtime.getRuntime().addShutdownHook(shutdownHook);//關閉鉤子其實就是一個實現了run方法的線程對象。
    }            
複製代碼

Server 組件

  • 實現類 StandServer 繼承了 LifeCycleBase
  • 子組件是 Service, 須要管理其生命週期(調用其 LifeCycle 的方法), 用數組保存多個 Service 組件, 動態擴容數組來添加組件
  • 啓動一個 socket Listen中止端口, Catalina 啓動時, 調用 Server await 方法, 其建立 socket Listen 8005 端口, 並在死循環中等鏈接, 檢查到 shutdown 命令, 調用 stop 方法

Service 組件,

  • 實現類 StandService   包含 Server, Connector, Engine 和 Mapper 組件的成員變量
  • 還包含 MapperListener 成員變量, 以支持熱部署, 其Listen容器變化, 並更新 Mapper, 是觀察者模式
  • 需注意各組件啓動順序, 根據其依賴關係肯定
  • 先啓動 Engine, 再啓動 Mapper Listener, 最後啓動鏈接器, 而中止順序相反.
Engine 組件
  • 實現類 StandEngine 繼承 ContainerBase
  • ContainerBase 實現了維護子組件的邏輯, 用 HaspMap 保存子組件, 所以各層容器可重用邏輯
  • ContainerBase 用專門線程池啓動子容器, 並負責子組件啓動/中止, "增刪改查"
  • 請求到達 Engine 以前, Mapper 經過 URL 定位了容器, 並存入 Request 中. Engine 從 Request 取出 Host 子容器, 並調用其 pipeline 的第一個 valve

在實際應用場景中,tomcat在shutdown的時候,沒法殺死java進程,還得kill,這是爲什麼呢?

Tomcat會調用Web應用的代碼來處理請求,可能Web應用代碼阻塞在某個地方。數組

tomcat通常生產環境線程數大小建議怎麼設置呢

理論上:線程數=((線程阻塞時間 + 線程忙綠時間) / 線程忙碌時間) * cpu核數若是線程始終不阻塞,一直忙碌,會一直佔用一個CPU核,所以能夠直接設置 線程數=CPU核數。可是現實中線程可能會被阻塞,好比等待IO。所以根據上面的公式肯定線程數。那怎麼肯定線程的忙碌時間和阻塞時間?要通過壓測,在代碼中埋點統計.緩存

connector的線程池設置成800合理嗎?

問題:剛到新公司,看他們把一個tomact的connector的線程池設置成800,這個太誇張了吧,connector的線程池只是用來處理接收的http請求,線程池不會用來處理其餘業務自己的事情,設置再大也只能提升請求的併發,並不能提升系統的響應,讓這個線程池幹其餘的事情,並且線程數過高,線程上下文切換時間也高,反而會下降系統的響應速度吧?我理解是否是對的,老師?還有一個問題就是設置的connector線程數,是tomcat啓動的時候就會初始化這麼多固定的線程仍是這只是一個上限,還有就是若是線程處於空閒狀態,會不會進行上下文切換呢?tomcat

解答:這裏誤解了Connector中的線程池,這個線程池就是用來處理業務的。另外你提到線程數設的過高,會有線程切換的開銷,這是對的。線程數具體設多少,根據具體業務而定,若是你的業務是IO密集型的,好比大量線程等待在數據庫讀寫上,線程數應該設的越高。若是是CPU密集型,徹底沒有阻塞,設成CPU核數就行。800這個數有點高,我猜大家的應用屬於IO密集型。併發

備註

本文是我我的學習了李號雙老師的專欄課程以後,結合專欄文章和老師對同窗答疑整理的學習筆記。僅供分享。更多精彩內容,你們能夠掃描下方二位碼,在極客時間訂閱李號雙老師的《深刻拆解Tomcat & Jetty》。獲取一手學習資料。

相關文章
相關標籤/搜索