天天看點

JVM性能優化, Part 5:Java的伸縮性

很多程式員在解決jvm性能問題的時候,花開了很多時間去調優應用程式級别的性能瓶頸,當你讀完這本系列文章之後你會發現我可能更加系統地看待這類的問題。我說過jvm的自身技術限制了java企業級應用的伸縮性。首先我們先列舉一些主導因素。

主流的硬體伺服器提供了大量的記憶體

分布式系統有大量記憶體的需求,而且該需求在持續增長

一個普通java應用程式所持有的對空間大概在1gb~4gb,這遠遠低于一個硬體伺服器的記憶體管理能力以及一個分布式應用程式的記憶體需求量。這被稱之為java記憶體牆,如下圖所示(圖中表述java應用伺服器和正常java應用的記憶體使用量的演變史)。

圖 1 java記憶體牆(1980~2010)

JVM性能優化, Part 5:Java的伸縮性

java記憶體牆

上圖源自:azul systems

這給我們帶來了如下jvm性能課題:

1)    如果配置設定給應用程式的記憶體太小,将導緻記憶體不足。jvm 不能及時釋放記憶體空間給應用程式,最終将引發記憶體不足,或者jvm完全關閉。是以你必須提供更多的記憶體給應用程式。

2)    如果給對響應時間敏感的應用增加記憶體,如果不重新開機你的系統或者優化你的應用,java堆最終會碎片化。當碎片發生時,可能會導緻應用中斷100毫秒~100秒,這取決與你的java應用,java堆的大小以及其他的jvm調優參數。

關于停頓的讨論大部分都集中在平均停頓或者目标停頓,很少涉及到堆壓縮時的最壞停頓時間,在生産環境中堆中每千兆位元組的有效資料的都将會發生大約1秒的停頓。

2~4秒的停頓對大多數企業應用來說都是不能接受的,是以盡管實際的java應用執行個體可能需要更多的記憶體空間,但實際隻配置設定2~4gb的記憶體。在一些64位系統中帶有很多關于伸縮性的jvm調優項,使得這些系統可以運作16gb乃至20gb的堆空間,并能滿足典型響應時間的sla。但是這些離現實較遠,jvm目前的技術無法在進行堆壓縮時避免停頓應用程式。java應用開發人員苦于處理這兩個為我們大多數人所抱怨的任務。

架構/模組化在大量的執行個體池之上,随之而來的是複雜的監控和管理操作。

反複的jvm和應用程式調優以避免“stop the world“引起的停頓。大多數程式員希望停頓不要發生在系統峰值負載期間。我稱之為不可能的目标。

現在讓我們深入一點java的可伸縮性問題。

過度供給或過度執行個體化java部署

為了充分利用記憶體資源,普通的做法是将java應用部署在多個應用伺服器執行個體上而不是一個或者少數應用伺服器執行個體上。當一台server上運作16個應用伺服器執行個體可以充分利用所有的記憶體資源,但如此無法解決的是多執行個體的監控以及管理所帶來的成本,尤其是當你的應用部署在多個server上。

另一個問題來了,峰值負載時的記憶體資源不是每天都需要的,這樣就形成了巨大的浪費。有些情況下,一台實體機上可能隻不是不超過3個“大應用伺服器執行個體”,這樣的部署更加不夠經濟也不夠環保,尤其在非峰值負載期間。

讓我們來比較一下這兩種部署架構,下圖中左邊是多而小的應用伺服器執行個體部署模式,右邊是少而大的應用伺服器執行個體部署模式。兩種模式處理同樣的負載,究竟哪一種部署架構更具經濟性。

圖2 大應用伺服器部署場景

JVM性能優化, Part 5:Java的伸縮性

如我之前說過的,并發壓縮使得大應用伺服器部署模式變得可行,而且可以突破jvm可伸縮性的限制。目前隻有azul的zing jvm可以提供并發壓縮的技術,另外zing是server側的jvm,我們很樂意看到越來越多的開發者在jvm層面去挑戰java可伸縮性的問題。

由于性能調優仍然是我們解決java可伸縮性問題的主要手段,我們先來看有哪些主要的調優參數以及通過它們能達到什麼樣的效果。

調優參數:一些事例

最著名的調優參數莫過于”-xmx”了,通過該參數可以指定java的堆空間大小,實際上可能不同的jvm執行結果不太一樣。

有的jvm包含了内部結構(如編譯器線程,垃圾回收器結構,代碼緩存等等)所需要的記憶體在“-xmx”的設定中,而有的則不包含。是以使用者java程序的大小不一定跟“-xmx”的設定相吻合。

如果你的應用程式配置設定對象的速率,對象的生命周期,或者對象的大小超過了jvm記憶體相關配置,一旦達到最大可使用記憶體的門檻值将會發生記憶體溢出,使用者程序則會停止。

當你的應用程式糾結于記憶體的可用性時,最有效的方法就是通過”-xmx”指定更大的記憶體去重新開機目前應用程序。為了避免頻繁的重新開機,大多數企業生産環境都傾向于指定峰值負載時所需要的記憶體,造成過度配置優化。

提示:生産環境負載的調整

java開發人員易犯的常見錯誤是在實驗下的做的堆記憶體設定,在移植到生産環境是忘記重新調整。生産環境和實驗室環境是不一樣的,謹記根據生産環境的負載重新調整堆記憶體設定。

分代垃圾回收器調優

還有一些其他的優化選項”-xns”和”-xx: newsize”,用來調整年輕代的大小,用來指定堆中專門負責新對象配置設定的空間大小。

大多數開發者都試圖基于實驗室環境調整年輕代的大小,這意味着在生産負載下存在失敗的風險。一般新生代的大小設定為堆大小的三分之一至二分之一左右,但這不是一個準則,畢竟實際還要視應用程式邏輯而定。是以最好先調查清楚年輕代到年老代的蛻變率以及年老代對象的大小,在此基礎上(確定年老代的大小,年老代過小會頻繁促發gc導緻記憶體溢出錯誤)盡可能地調大年輕代的空間。

還有一個與年輕代相關的調優項”-xx:survivorratio”,該選項用來指定年輕代中對象的生命周期,超過指定時長相關對象将被移至年老代。為了”正确”地設定該值,你需要知道年輕代空間回收的頻率,能夠估算到新對象在應用程式程序中被引用的時長,同時也取決于配置設定率。

并發垃圾回收調優

針對對停頓敏感的應用,建議使用并發垃圾回收,雖然并行的辦法能夠帶來非常好的吞吐量基準測試分數,但是并行gc不利于縮短響應時間。并發 gc 是目前唯一有效的實作一緻性和最少“stop the world”中斷的方法。不同的jvm提供不同的并發gc的設定,oracle jvm(hotspot)提供”-xx:+useconcmarksweepgc”,今後g1将成為oracle jvm預設的并發垃圾回收器。

性能調優并不是真正的解決辦法

或許你已經注意到上文中在讨論如何“正确“地設定調優此參數時,我刻意在”正确“二字上加了雙引号。那是因為就我個人經驗而言一旦涉及到性能參數調優,就沒有嚴格意義上的正确設定。每一個設定值都是針對特定的場景。考慮到應用場景會發生變化,jvm 性能調整充其量是一個權宜之計。

以堆的設定為例:如果2gb的堆可以應對20萬并發使用者,但是可能不能應付40萬的并發使用者。

我們再以”-xx:survivorratio”為例:當設定符合一個負載持續增長最高至每毫秒10000個交易的場景,當壓力到達每毫秒50000個交易時又會發生什麼呢?

大多數企業級應用負載都是動态的,java語言的動态記憶體管理以及動态編譯等技術使得java更加适合企業級應用。我們來看看一下兩個配置清單。

清單1. 應用程式(1)的啟動選項

>java -xmx12g -xx:maxpermsize=64m -xx:permsize=32m -xx:maxnewsize=2g

-xx:newsize=1g -xx:survivorratio=16 -xx:+useparnewgc

-xx:+useconcmarksweepgc -xx:maxtenuringthreshold=0

-xx:cmsinitiatingoccupancyfraction=60 -xx:+cmsparallelremarkenabled

-xx:+usecmsinitiatingoccupancyonly -xx:parallelgcthreads=12

-xx:largepagesizeinbytes=256m …

清單 2. 應用程式(2)的啟動選項

>java –xms8g –xmx8g –xmn2g -xx:permsize=64m -xx:maxpermsize=256m

-xx:-omitstacktraceinfastthrow -xx:survivorratio=2 -xx:-useadaptivesizepolicy -xx:+useconcmarksweepgc

-xx:+cmsconcurrentmtenabled -xx:+cmsparallelremarkenabled -xx:+cmsparallelsurvivorremarkenabled

-xx:cmsmaxabortableprecleantime=10000 -xx:+usecmsinitiatingoccupancyonly

-xx:cmsinitiatingoccupancyfraction=63 -xx:+useparnewgc –xnoclassgc …

兩者的配置差別很大,因為他們是兩個不同應用程式。感覺根據各自的應用特設都做了”正确“的配置與調優。在實驗室環境下都運作良好,但在生産環境中最終會表現出疲态。清單1由于沒有考慮到動态負載,到了生産環境即表現不良。清單2沒有考慮到應用程式在生産環境中的特性變化。這兩種情況應該歸咎于開發團隊,但是該歸咎于何處呢?

變通辦法可行嗎?

有些企業通過精确測量交易對象的大小定義極緻的對象回收空間并”精簡“其架構來适配該空間。這也許是辦法來削減碎片以應對一整天的交易(在不做堆壓縮的情況下)。還有一個辦法就是通過程式設計確定對象被引用的時間在一個比較短的時間内進而阻止其在survivorratio時間之後不被遷往年老代而直接被回收,避免記憶體壓縮的場景。這兩種辦法都可以,但是對應用開發人員和設計人員有一定的挑戰。

誰保障應用程式的性能?

一個門戶應用可能會在其活動負載峰值點出現故障;一個交易應用可能會在每次市場下跌和上升時無法正常運作;電子商務網站可能會無法應對節假日購物高峰期。這些都是真實世界的案例基本都是jvm性能參數調優導緻的。當産生了經濟損失,開發團隊就會受到責備。也許某些場合下開發團隊應該要受到責備,但是jvm的提供商又應該負起什麼樣兒的責任呢?

首先jvm提供商應該要提供調優參數的優先順序,至少這在短期内還是很有意義的。有一些新的調優選項是針對特定的、 新興的企業應用程式場景。更多的調優選項是為了減輕jvm支援團隊的工作負荷而将性能優化轉嫁到應用開發者身上。但我個人認為這或将導緻更加漫長的支援負荷,一些針對最糟糕場景的調優選項也将被延期,當然不是無限延期。

毋庸置疑jvm的開發團隊也在努力地進行着他們的工作,同時也隻有應用實施者才會更加清楚他們應用的特定需求。但是應用的實施者或開發者是無法預測期動态的負載需求。在過去,jvm提供商也會去分析關于java的性能與可擴充性問題,哪些是他們能夠解決的。不是提供調優參數,而是直接去優化或創新垃圾回收的算法。更有趣是我們可以想象一下如果openjdk的社群聚集在一起重新考慮java垃圾回收器将會發生什麼!

jvm性能的基準測試

調優參數有時被jvm提供商作為其競争的工具,因為不同的調優可以改善他們的jvm在可預見的環境中的性能表現,本系列的最後一片文章中将調查這些基準測試來衡量jvm的性能。

真正的企業級可伸縮性需求是要求jvm能夠适應動态靈活的應用負載。這是在特定吞吐量和響應時間内保證持續穩定性能的關鍵。這是jvm開發者才能完成曆史使命,是以是時候号召我們java開發者社群來迎接真正的java可伸縮性的挑戰。

持續調優:對于給定的應用,在一開始需要告知其需要多大的記憶體,之後的工作都應該有jvm來負責 ,jvm需要适配動态的應用負載和運作場景。

jvm執行個體數 vs. 執行個體的可擴充性:現在的伺服器都支援很大的記憶體,那麼為什麼jvm執行個體不能有效地利用它呢?将應用拆分部署許多小的應用伺服器執行個體上,這從經濟和環保角度都是一種浪費。現代的jvm需要跟上硬體和應用的發展潮流。

真實世界的性能和可伸縮性:企業不需要為其應用的性能需求去做極緻的性能調優。jvm提供商和openjdk社群需要去解決java可伸縮性的核心問題以及消除“stop the world“的操作。

如果jvm做了這樣的工作,并且提供了并發壓縮的垃圾回收算法,jvm也不再成為java可伸縮性的限制因素,java應用開發者不需要花費痛苦的時間了解怎樣配置jvm去獲得最佳性能,進而将會有更多的有趣的java應用層面的創新,而不是無休止的jvm調優。我要挑戰jvm開發人員以及提供商所需要做的事情來響應甲骨文所提倡的“make the java future“的活動。