1. 程式人生 > >Tomcat 6 效能優化小結

Tomcat 6 效能優化小結

(一)

       目前市場上常用的開源Java Web容器TomcatResinJetty。其中Resin從V3.0後需要購買才能用於商業目的,而其他兩種則是純開源的。可以分別從他們的網站上下載最新的二進位制包和原始碼。

作為Web容器,需要承受較高的訪問量,能夠同時響應不同使用者的請求,能夠在惡劣環境下保持較高的穩定性和健壯性。在HTTP伺服器領域,Apache HTTPD的效率是最高的,也是最為穩定的,但它只能處理靜態頁面的請求,如果需要支援動態頁面請求,則必須安裝相應的外掛,比如mod_perl可以處理Perl指令碼,mod_python可以處理Python指令碼。

上面介紹的三中Web容器

,都是使用Java編寫的HTTP伺服器,當然他們都可以嵌到Apache中使用,也可以獨立使用。分析它們處理客戶請求的方法有助於瞭解Java多執行緒和執行緒池的實現方法,為設計強大的多執行緒伺服器打好基礎。

Tomcat是使用最廣的Java Web容器,功能強大,可擴充套件性強。最新版本的Tomcat(5.5.17)為了提高響應速度和效率,使用了Apache Portable Runtime(APR)作為最底層,使用了APR中包含Socket、緩衝池等多種技術,效能也提高了。APR也是Apache HTTPD的最底層。可想而知,同屬於ASF(Apache Software Foundation)中的成員,互補互用的情況還是很多的,雖然使用了不同的開發語言。

Tomcat 的執行緒池位於tomcat-util.jar檔案中,包含了兩種執行緒池方案。方案一:使用APR的Pool技術,使用了JNI;方案二:使用Java實現的ThreadPool。這裡介紹的是第二種。如果想了解APR的Pool技術,可以檢視APR的原始碼。

ThreadPool預設建立了5個執行緒,儲存在一個200維的執行緒陣列中,建立時就啟動了這些執行緒,當然在沒有請求時,它們都處理“等待”狀態(其實就是一個while迴圈,不停的等待notify)。如果有請求時,空閒執行緒會被喚醒執行使用者的請求。

具體的請求過程是: 服務啟動時,建立一個一維執行緒陣列(maxThread=200個),並建立空閒執行緒(minSpareThreads=5個)隨時等待使用者請求。 當有使用者請求時,呼叫 threadpool.runIt(ThreadPoolRunnable)方法,將一個需要執行的例項傳給ThreadPool中。其中使用者需要執行的例項必須實現ThreadPoolRunnable介面。 ThreadPool 首先查詢空閒的執行緒,如果有則用它執行要執行ThreadPoolRunnable;如果沒有空閒執行緒並且沒有超過maxThreads,就一次性建立 minSpareThreads個空閒執行緒;如果已經超過了maxThreads了,就等待空閒執行緒了。總之,要找到空閒的執行緒,以便用它執行例項。找到後,將該執行緒從執行緒陣列中移走。 接著喚醒已經找到的空閒執行緒,用它執行執行例項(ThreadPoolRunnable)。 執行完ThreadPoolRunnable後,就將該執行緒重新放到執行緒陣列中,作為空閒執行緒供後續使用。

由此可以看出,Tomcat的執行緒池實現是比較簡單的,ThreadPool.java也只有840行程式碼。用一個一維陣列儲存空閒的執行緒,每次以一個較小步伐(5個)建立空閒執行緒並放到執行緒池中。使用時從陣列中移走空閒的執行緒,用完後,再“歸還”給執行緒池

(二)

     ThreadPool提供的僅僅是執行緒池的實現,而如何使用執行緒池也是有很大學問的。讓我們看看Tomcat是如何使用ThreadPool的吧。

Tomcat有兩種EndPoint,分別是AprEndpoint和PoolTcpEndpoint。前者自己實現了一套執行緒池(其實這和Tomcat 老版本的方案是相同的,至今Tomcat中還保留著老版本的執行緒池,PoolTcpEndpoint也有類似的程式碼,通過“策略”可以選擇不同的執行緒池方案)。我們只關注PoolTcpEndpoint如何使用ThreadPool的。

首先,PoolTcpEndpoint建立了一個ThreadPoolRunnable例項——LeaderFollowerWorkerThread,實際上該例項就是接收(Accept)並處理(Process)使用者socket請求。接著將該例項放進ThreadPool中並執行,此時就可以接收使用者的請求了。

當有Socket請求時,LeaderFollowerWorkerThread首先獲得了Socket例項,注意此時LeaderFollowerWorkerThread並沒有急著處理該Socket,而是在響應Socket訊息前,再次將LeaderFollowerWorkerThread放進ThreadPool中,從而它(當然是另外一個執行緒了)可以繼續處理其他使用者的Socket請求;接著,擁有Socket的LeaderFollowerWorkerThread再來處理該使用者的Socket請求。

整個過程與傳統的處理使用者Socket請求是不同的,也和Tomcat老版本不同。傳統的處理方法是:有一個後臺執行的監聽執行緒負責統一處理接收(注意只是“接收”)Socket請求,當有新的Socket請求時,將它賦值給一個Worker執行緒(通常是喚醒該執行緒),並有後者處理Socket請求,監聽執行緒繼續等待其他Socket請求。所以整個過程中有一個從Listener到Worker切換的過程。

而新版本Tomcat很有創造性的使用了另外一種方法,正如前文所描述的,接收和處理某個使用者Socket請求的始終是由一個執行緒全程負責,沒有切換到其他執行緒處理,少了這種執行緒間的切換是否更有效率呢?我還不能確認。不過這種使用方式確實有別於傳統模式,有種耳目一新的感覺。

(三)

除了Tomcat外,Jetty是另外一個重要的Java Web容器,號稱“最小的”Web容器,從Jetty的原始碼規模可以看出它確實比較小。而且它的ThreadPool的實現也非常簡單,整個程式碼ThreadPool程式碼只有450行左右,可見小巧之極。

ThreadPool程式碼位於com.mortbty.thread包中,其中最重要的方法是dispatch()和內部類PoolThread。顧名思義,dispatch方法主要是將Runnable例項派給執行緒池中的空閒PoolThread,由後者執行Runnable。

還是看看整個過程吧。首先,ThreadPool建立_minThreads個空閒PoolThread,並把它們新增到空閒執行緒佇列中。當需要執行 Runnable時,首先查詢是否有空閒的PoolThread,如果有空閒的,這由它處理;如果沒有並且PoolThread並沒有超過 _maxThreads個時,則建立一個新的PoolThread,並由這個新建立的PoolThread執行Runnable;如果 PoolThread超過了_maxThreads,則一直等待有空閒的PoolThread出現。在PoolThread執行之前,必須把該 PoolThread從空閒執行緒佇列中移走。

再來看看PoolThread的實現吧。和所有的Worker執行緒一樣,用一個while(flag){wait();}迴圈等待Runnable的到來,當有Runnable被ThreadPool.dispatch()時,該PoolThread就執行Runnable;當執行完成後,再“歸還”給空閒執行緒佇列。

Jetty如何使用ThreadPool?整個Jetty只使用了一個ThreadPool例項,具體入口在 org.mortbay.jetty.Server中被例項化的,Connector中也使用Server的ThreadPool處理使用者的Socket 請求。Connector是處理使用者Socket請求的入口,一個Connector建立_acceptors個Acceptor,由Acceptor處理使用者Socket請求時,當有Socket請求時,就建立一個Connection放到執行緒池中處理,而Acceptor繼續處理其他的Socket請求。這是個傳統的Listener和Worker處理方式。

(四)

     在這些Java Web容器中,Resin算得上很特別的,小巧穩定,而且效率很高。在這些Java Web容器中,算它的效率最高了。很多大型的網站中都能找到它的身影。Resin從3.0版本後開始走“特色”的開源路,與MySql很相似——如果用於商業目的,則需要買它的License。但對於個人研究而言,這已經不錯了,在網站上可以下載除了涉及License的原始碼外其他所有程式碼。

說Resin特別,還主要是由於它的效能出眾,即使在很多企業級應用中也能派上用場。Resin的資料庫連線池做的很不錯,效率非常高。不過這裡我們討論它的執行緒池,看看有何特別之處。

Resin的ThreadPool位於com.caucho.util.ThreadPool中,不過這個類的命名有點蹊蹺,更恰當的命名是 ThreadPoolItem,因為它確實只是一個普通的Thread。那執行緒排程何管理在哪裡呢?也在這個類中,不過都是以靜態函式方式提供的,所以這個類起到了兩重作用:執行緒池排程和Worker執行緒。也由於這種原因,Resin例項中只有一個執行緒池,不像Tomcat和Jetty可以同時執行多個執行緒池,不過對於一個系統而言,一個執行緒池足夠了。

和其他執行緒池實現方式不同的是,Resin採用連結串列儲存執行緒。如果有請求時,就將Head移走並喚醒該執行緒;待執行完成後,該執行緒就變成空閒狀態並且被新增到連結串列的Head部分。另外,每一個執行緒執行時都要判斷當前空閒執行緒數是否超過_minSpareThreads,如果超過了,該執行緒就會退出(狀態變成Dead),也從連結串列中刪除。

Resin如何使用該ThreadPool?所有需要用執行緒池的地方,只需呼叫ThreadPool. Schedule(Runnable)即可。該方法就是一個靜態函式,顧名思義,就是將Runnable加到ThreadPool中待執行。

Resin使用的還是傳統方法:監聽執行緒(com.caucho.server.port.Port),系統中可以有多個Port例項,前提埠號不同,比如有80和8080埠;另外就是Worker執行緒,其實就是ThreadPool中的空閒執行緒。Port本身是一個Thread,在啟動時,會在 ThreadPool中執行5個執行緒——TcpConnection同時等待使用者請求,當有使用者請求時,其中的一個會處理。其他繼續等待。當處理使用者請求完成後,還可以重用這些TcpConnection,這與Jetty的有所不同,Jetty是當有使用者請求時,才建立連線,處理完成後也不會重用這些連線,效率會稍差一些。

另外Resin有兩個後臺執行執行緒:ThreadLauncher和ScheduleThread,前者負責當空閒執行緒小於最小空閒執行緒時建立新的執行緒;而後者則負責執行實際的Runnable。我覺得有的負責,沒有必要用一個執行緒來建立新執行緒,多此一舉。不過ScheduleThread是必須的,因為它就是Worker執行緒。

(五)

介紹了tomcat、jetty和resin三種Java Web容器的執行緒池後,按照慣例應該比較它們的優缺點。不過先總結執行緒池的特點。

執行緒池作為提高程式處理資料能力的一種方案,應用非常廣泛。大量的伺服器都或多或少的使用到了執行緒池技術,不管是用Java還是C++實現,執行緒池都有如下的特點:

執行緒池一般有三個重要引數:
1. 最大執行緒數。在程式執行的任何時候,執行緒數總數都不會超過這個數。如果請求數量超過最大數時,則會等待其他執行緒結束後再處理。
2. 最大共享執行緒數,即最大空閒執行緒數。如果當前的空閒執行緒數超過該值,則多餘的執行緒會被殺掉。
3. 最小共享執行緒數,即最小空閒執行緒數。如果當前的空閒數小於該值,則一次性建立這個數量的空閒執行緒,所以它本身也是一個建立執行緒的步長。

執行緒池有兩個概念:
1. Worker執行緒。工作執行緒主要是執行執行程式碼,有兩種狀態:空閒狀態和執行狀態。在空閒狀態時,類似“休眠”,等待任務;處理執行狀態時,表示正在執行任務(Runnable)。
2. 輔助執行緒。主要負責監控執行緒池的狀態:空閒執行緒是否超過最大空閒執行緒數或者小於最小空閒執行緒數等。如果不滿足要求,就調整之。

如果按照上述標準去考察這三個容器就會發現:Tomcat實現的執行緒池是最完備的,Resin次之,而Jetty最為簡單。Jetty沒有控制空閒執行緒的數量,可能最後空閒執行緒數會達到最大執行緒數,影像效能,畢竟即使是休眠執行緒也會耗費CPU時鐘的。

談談Resin的執行緒池。Resin的實現比Tomcat複雜些。也有上述三個引數,也有兩個概念,這與Tomcat相當。但考慮到如何使用ThreadPool時,Resin也要複雜些。

或許由於Resin的ThreadPool是單間模式的,所有使用ThreadPool的執行緒都是相同設定,比如相同的最大執行緒數,最大空閒執行緒數等,在使用它時會多些考慮。比如在控制最大Socket連線數時,com.caucho.server.port.Port還要有自己的一套控制“數量”的機制,而無法使用ThreadPool所特有的控制機制。所以使用起來比Tomcat複雜。

Tomcat使用ThreadPool卻很簡單。由於Tomcat的ThreadPool可以有不同的例項存在,很方便的定製屬於自己的“數量”控制,直接用ThreadPool控制Socket連線數量。所以程式碼也比較清爽。

如果要使用執行緒池,那就用Tomcat的ThreadPool吧。