Java GC專家系列4:Apache的MaxClients設定及其對Tomcat Full GC的影響

segmentfault發表於2016-01-28

本文是GC專家系列中的第四篇。在第一篇理解Java垃圾回收中我們學習了幾種不同的GC演算法的處理過程,GC的工作方式,新生代與老年代的區別。所以,你應該已經瞭解了JDK 7中的5種GC型別,以及每種GC對效能的影響。

在第二篇Java垃圾回收的監控中介紹了在真實場景中JVM是如何執行GC,如何監控GC資料以及有哪些工具可用來方便進行GC監控。

在第三篇GC 調優中基於真實案例介紹了可用於GC調優的最佳選項。同時也描述瞭如何通過降低移動到老年代中物件的數量來縮短Full GC耗時,以及如何設定GC型別及記憶體大小。

本文將介紹Apache的MaxClients引數的重要性以及在GC發生時對系統整體效能的顯著影響。通過幾個例子,你將會更清晰的理解MaxClients值所引發的問題。最後會介紹如何依據系統的可用記憶體來為MaxClients設定合理的數值。

MaxClients對系統的影響

NHN的服務執行環境中有大量的流控(Throttle valve)選項,這些選項對系統的穩定執行具有重要作用。我們來看下Apache的MaxClients選項在Tomcat發生Full GC時會對系統帶來哪些影響。

大部分的開發人員都知道GC 發生中會伴隨著”stop the world(STW)現象”(具體詳情參考理解Java垃圾回收)。尤其是NHN的Java開發人員可能都經歷過在Tomcat中由GC相關問題而導致的系統崩潰。因為JVM管理記憶體,因此Java應用系統不可避免的會遇到GC引起的STW現象。

在你開發的線上系統中,GC每天都會發生很多次。在GC發生時,即便TTS沒有發生,卻依然可能會給使用者503的錯誤響應。

系統執行環境

根據結構特點,Web服務更適合於做橫向擴充套件而非單純的提高單一機器的效能。所以通常根據效能需要,Web服務的伺服器部署結構由一臺Apache伺服器和多臺Tomcat伺服器組成。在本文中,假設一個Apache服務和Tomcat服務部署在同一臺物理主機上,如下圖所示:


圖1: 本文假設的服務執行環境

作為參考,本文所述引數均是基於Apache 2.2.21(prefork MPM),Tomcat 6.0.35,jdk 1.6.0_24,並執行在CentOS 4.7.2(32位)作業系統上。

系統記憶體2GB,並使用ParallelOldGC垃圾回收,預設開啟了AdaptiveSizePolicy選項並設定堆大小為600MB。

STW 和 HTTP 503

假設Apache的流量為200QPS,並開啟10個httpd處理程式(儘管實際場景依賴於請求的響應時間)。在這種前提下,假設full GC導致的停頓耗時1秒,如果Tomcat發生了Full GC將會怎麼樣?

首先你能想到的是full GC導致Tomcat停頓,處理中的請求將得不到響應。如果這樣,Tomcat暫停,請求得不到處理,Apache將會怎麼樣?

即使Tomcat因Full GC而暫停處理,而請求卻仍以200 req/s的速度到達Apache。在full GC發生前,只需要10個或者稍微多一點的httpd程式就可以快速響應服務請求。但是現在Tomcat暫停了,為了處理新的請求Apache將持續建立新的httpd程式直到httpd.conf檔案中定義的MaxClients閥值。因為MaxClients預設值為256,所以200 req/s的請求並不會帶來太大問題。

這個時候,新建立的httpd 程式會怎麼樣?

Httpd 程式使用mod_jk模組管理的AJP連線池中的空閒連線把請求傳送到Tomcat。如果沒有空閒連線,則會要求建立新的連線。然而因為Tomcat處理暫停狀態,新建連線的請求將被拒絕。所以這些請求將會放到堆積佇列(backlog queue),佇列的長度是server.xml的AJP Connector中設定的。

如果請求資料超出了堆積佇列的長度,Apache將會收到連線拒絕錯誤,並把這個錯誤以HTTP 503的方式返回給使用者。

在本例的中,堆積佇列的長度預設設定為100,而請求速度為200 req/s,因此在由full GC導致Tomcat暫停的這1秒中,將有超過100的請求將會收到503錯誤。

Full GC結束之後,堆積佇列中的socket連線會被Tomcat接收並分配給工作執行緒(最大工作執行緒數由MaxThreads決定,預設值為200)來處理請求。

MaxClients和堆積佇列

在上面的場景中,如何設定才能避免給使用者返回503錯誤?

首先我們需要知道,應該設定足夠的堆積佇列長度以容納在Tomcat Full GC導致的暫停期間流入的請求。因此堆積佇列最小長度至少為200(上文中QPS為200)。

這樣配置以後,是否還有其他問題?

把堆積佇列長度設定為200後,我們再次重複上面的場景。結果問題卻比之前更加嚴重。

正常情況下系統記憶體使用量維持在50%,而在發生Full GC時記憶體使用卻迅速上升到100%,引起記憶體交換區(swap)使用量的極劇增加。更為嚴重的是Full GC導致的響應停頓由原來的1秒增加到了4秒,直接後果就是期間系統像掛掉了一樣,不能響應任何請求。

在之前的場景中,只有100左右的請求會收到 503 的錯誤,而增加堆積佇列到200後卻導致了500甚至更多的請求被掛起至少3秒不能收到任何響應。

這個例子很好的證明了如果不能準備的理清配置資訊之間的因果關係,可能會對系統帶來極為嚴重的影響。

為什麼會這樣?

原理就是要清楚MaxClients選項的特性。

MaxClients的值不易設定過大,設定MaxClients的關鍵在於即便建立了MaxClients數量的httpd程式,也要需要維持應用系統的記憶體使用量不應超過80%。

系統交換區預設值為60,因此如果記憶體使用超過80%,系統將會發生頻繁的記憶體交換。

我們再來看下為什麼這個特性會導致上面所述的嚴重後果。

當請求的QPS為200時,Tomcat會被Full GC暫停響應,然後把堆積列隊容量設定為200。起初大約有100個額外的httpd 程式會被Apache建立,緊接著記憶體使用量超過了80%,引起作業系統主動的使用交換區的記憶體空間,而因GC存活在JVM老年代中的物件被作業系統誤認為長時間未使用,從而導致這些物件被移動到交換區。

最後,當GC過程中涉及到交換區時,耗時就會迅速增加。而後httpd程式數繼續增加,導致記憶體使用量達到了100%,從而出現了上述的嚴重後果。

上述案例的前後區別僅在於堆積佇列的長度:100和200。但為什麼在200時會出現更嚴重的狀況?

原因是堆積佇列不同的長度導致了httpd程式數的不同。當值為100時,在發生Full GC時100個請求所要求建立的連線被置於堆積佇列中。再有新的請求會被拒絕並返回503錯誤,所以系統的整個httpd的程式數僅超出100很少的數量。

但當佇列長度設定為200時,有200個請求被接收並置於佇列中。從而導致httpd程式的數量超過200,並觸發了作業系統進行記憶體交換的閥值。

所以,如果不顧記憶體使用情況而一味的加大MaxClients的數值,將會導致Full GC時httpd程式數迅速增加,引進記憶體交換並最終降低系統的整體效能。

所以如何設定MaxClients,如何找到當前系統的閥值?

MaxClients 取值的計算方式

如果系統總記憶體為2GB,設定MaxClient的值需要保證在任何時候記憶體的使用量不超過80%即1.6GB,從而避免因記憶體交換導致的效能下降。也就是說僅有1.6GB空間供Apache, Tomcat和其他預設安裝的代理程式共享和分配記憶體。

假如預設安裝的代理程式佔用200M記憶體;Tomcat的堆空間設定-Xmx為600M,如下圖所示,Tomcat總佔用量將725M (持久代 + 本地堆空間)。Apache可使用的空間為剩下的700M。


圖 2:Top命令的截圖

對於Apache的700M記憶體,該如何設定合理的MaxClients值?

當然這也取決於Apache載入的模組型別和數量。以NHN的Web服務為例,把Apache當作簡單的代理使用,根據上圖RES顯示,4M空間對於每個httpd程式來說已足夠使用。因此700M空間能設定的MaxClients為175。

總結

可靠的服務配置要能夠在滿載的情況下降低系統停頓時間並能夠最大範圍的保證成功響應使用者請求。對於Java應用來說,必須要確認在Full GC引起的SWT情況下,系統的配置是否能夠提供足夠可靠的服務。

如果為了應對單純的請求增加和防止DDos攻擊,在不考慮記憶體使用的情況下把MaxClients設定過大,那麼MaxClients不但會失去作為流控的用途,反而會帶來更為嚴重的後果。

在這個案例中,解決問題的最優途徑是加大系統的記憶體,或者設定MaxClients為175(上面的計算結果)以保證只有QPS超過175時才會出現503錯誤。

相關文章