- Java性能分析是一門藝術和科學;科學指的是性能分析一般都包括大量的數字、測量和分析。絕大多數的性能工程師都有科學背景,運用科學的嚴謹是擷取最大性能的重要組成部分。藝術部分指的是什麼呢?性能調優是部分科學部分藝術的觀點是很早就有的,但是關于性能的主題很少會給定特定的知識,這就是藝術的部分了,它和我們平常接受到的教育訓練是不一樣的,教育訓練是确定了的。還有部分原因是對于某些人來說,性能調優是建立在深入的知識和經驗上面的。這裡藝術就是知識、經驗和直覺的使用。
- 這本書不能幫助我們提升經驗和直覺,但是可以幫助我們提升對知識的深入了解,我們持有這樣的觀點:多次運用知識能夠提升我們稱為Java性能工程師的能力。這本書的目标就是給予我們對Java平台性能方面的深入了解。
- 這種知識主要分為兩大部分:一是JVM本身的性能,研究JVM的配置是如何影響程式的各方面性能的。其它語言的有經驗的開發人員會發現這個調優是比較繁瑣的,盡管實時上JVM的調優和C++程式員在編譯的時候測試和選擇不同編譯器參數或者是PHP程式員在php.ini檔案設定合适的變量是一樣的。二是了解Java平台的特性是如何影響性能的。注意這裡的平台,一些特性(比如:線程和同步)是語言的一部分,另外一些特性(比如:XML解析性能)是Java的标準API。盡管Java語言和Java API是不同的,但是這裡我們把他們看成是類似的。
- JVM的性能很大程度上是和調優參數相關的,平台的性能則和應用代碼的優劣相關。很多時候,代碼開發人員和性能測試小組是分割開的,認為他們是不同方面的專家。隻有性能工程師能夠對JVM虛拟機進行調優,以榨取更多的性能;隻有開發人員才會關心代碼寫的好不好。區分這個是沒有什麼作用的--任何在Java平台上工作的人都需要了解這些。
平台和約定
- 本書使用的平台是:Java 7update 40和Java 8;Java7是進行性能調優的一個好的起點,因為它提供了很多新的特性,比如:G1垃圾回收器,同時還提供了一些和性能相關的工具,便于我們可視化的檢視Java應用的工作細節。Java8也做了較大幅度的改進(比如:引入lambda表達式)。
- 盡管所有平台都會做相容性測試,以便實作Java規範的功能;但對于本書來說,這裡的相容性是不夠的,特别是tuning flag,每個JVM都會實作一個或多個垃圾收集器,但是它們的調優參數一般是不一樣的,盡管這本書讨論的很多概念是适用于所有Java的實作平台的,但是對于一些調優參數和建議,那隻适用于Oracle的JVM(HotSpot)。
調優參數
- 除了少部分例外,JVM會接受兩種參數:boolean參數和帶有參數的參數。Boolean參數使用-XX:+FlagName來開啟參數,使用-XX:-FlagName來關閉參數。帶有參數的參數,使用-XX:FlagName=something來設定參數值。當介紹某個參數的時候都會讨論它的預設值。這個預設值是不同方面的組合,比如:目前JVM運作在什麼作業系統上面以及其它JVM指令行參數是什麼。如果有疑問,可以使用-XX:+PrintFlagsFinal來檢視在特定平台上面,給定JVM指令行參數會給其它參數什麼預設值。對于這些會根據其它參數和平台來自動標明調優參數的過程,我們叫做ergonomics(不知道怎麼翻譯)。
- Client class和Server class:當JVM運作在32-bit的windows伺服器上(不管有多少個CPU),或機器隻有一個CPU,那麼這台機器是Client class。其它機器都稱之為server class
性能
- 寫更好的算法
- 寫更少的代碼:需要編譯的代碼越多,代碼運作快需要更多的時間;配置設定和丢棄的對象越多,GC需要更多的工作;配置設定和回收更多的對象,GC周期會更長;加載的類越多,程式啟動需要更長的時間;執行的代碼越多,命中硬體緩存的可能性越小;執行的代碼越多,需要的時間越長。
- 不要過早優化
- 資料庫(或其它第三方資源)常常是瓶頸:
- 針對一般情況進行優化:1)對代碼進行分析,并關注分析中消耗時間最長的部分,這也不是說隻關注分析中的方法 2)使用奧卡姆剃刀定律來診斷性能問題,最新加入的代碼比系統配置更有可能引起性能問題,而系統配置比JVM或作業系統的bug更有可能引起性能瓶頸。一個測試用例很有可能會發現一個潛在的性能問題,但是我們不會立馬就去優化,而要看看這個測試用例是否是經常發生的。3)使用更簡單的算法來完成大多數的工作。
-
測試真正應用
第一個原則就是性能測試必須在即将上線的産品上進行測試。
Microbenchmarks
- Microbenchmarks是為了衡量非常小的代碼單元的性能而設計的測試。比如:使用Synchronized和不使用Synchronized的方法;使用線程池和不使用線程池的開銷;使用算術算法和另外一種實作的時間比較等等。Microbenchmarks寫的很準确是非常難的。比如:
public voiddoTest(){
// Main Loop
double l;
long then= System.currentTimeMillis();
11
for(int i= 0; i< nLoops; i++) {
l=fibImpl1(50);
}
long now= System.currentTimeMillis();
System.out.println("Elapsedtime: " + (now- then));
}
...
privatedouble fibImpl1(int n) {
if(n< 0)throw new IllegalArgumentException("Must be > 0");
if(n== 0)return 0d;
if(n== 1)return 1d;
double d= fibImpl1(n-2) +fibImpl(n- 1);
if(Double.isInfinite(d))throw new ArithmeticException("Overflow");
return d;
}
上面的程式存在幾個問題,第一,因為變量'l'沒有使用,是以編譯器會優化掉調用fibImpl1的所有代碼,是以實際執行的就是列印時間的代碼,根本測試不到fibImpl1的性能。第二,因為我們的目的是得到第50個Fibonacci的值,對于比較智能的編譯器,可能會去除循環。第三,必須要對正确的輸入進行測試,因為在實際運作的時候,輸入一般都是正确的。
注意:Java代碼有一個特征就是:執行的次數越多,它執行得越快(所謂:warm-up period)。是以在進行microbenchmark的時候需要包含warm-up period,以便給予編譯器産生優化代碼的機會。
- 還有一點要清楚的是:額外編譯效益(Complilation effects)。編譯器會使用分析回饋(profile feedback),編譯使用代碼的分析回饋來決定在編譯一個方法的時候使用的最佳優化手段。分析回饋(profile feedback)是基于:方法頻繁調用的程度、它們被調用時的棧深度以及它們參數的真正類型等等----一句話就是基于方法代碼運作的環境。編譯器對代碼的優化,在microbenchmark時候比在代碼運作在應用時更加頻繁。如果使用同樣的microbenchmark類來評測第二個Fibonacci的實作方法,那麼各種編譯效應(Complilation effects)都有可能會發生,特别是兩個Fibonacci實作分别在不同的類中。
- 最後要說明的是mircrobenchmark真正意味着什麼?在benchmark中,總時間(比如上面讨論的Fibonacci)可能是循環很多次得到的,一般以秒為機關;但是對于每一次循環的時間,可能是以納秒為機關。是的,随着納秒的增加,逐漸會變成一個性能瓶頸。但是,如果被測試的函數執行的次數很少,那麼在納秒級别進行優化就沒有意義了。
Macrobenchmarks
用于測量一個應用的性能的best thing(最佳東西?)就是應用本身,并結合它使用任何外部資源。如果應用調用了LDAP的API來确認使用者的身份,那麼測試的時候就會考慮LDAP調用。撇開LDAP進行測試對于子產品測試是有意義的,但是對于性能測試,必須要考慮到LDAP調用。
随着應用的增長,滿足上面說的準則(使用應用本身并結合它使用的外部資源進行測試)會變得更加重要,但是也變得更加困難。複雜系統比組成它的各個部分之和更加複雜,當把各個部分組合在一起的時候,它們的行為會變得非常不一樣。比如:如果我們mock了資料庫,那就意味着我們不用關注資料庫的性能了。但是,在真正運作的時候,資料庫連接配接會消耗很多heap space來存放它們的緩存資料;網絡會變得更加繁忙,因為傳輸資料會增加;代碼的優化會變得非常不同(簡單的代碼和複雜的代碼)。CPUpipeline和Cache在較短的代碼路徑上比在較長的代碼路徑上更高效等等。另外一些原因就是資源的配置設定。在理想環境下,對應用中的每一行代碼都有足夠的時間進行優化,但是在現實環境中,對優化消耗的時間是有要求的,并且隻優化複雜環境下的一部分也許不會立馬得到很好的效果。考慮下圖中,使用者發送資料到系統,首先确認使用者權限,然後做一些業務相關的計算,然後從資料庫中加載所需的資料,再做一些業務相關的計算,并把一些資料存入資料庫,最後向使用者發送響應。下圖中的每個框都是一個小子產品,框中小括号部分是該子產品最大處理的并發數。
Java性能優化指南系列(一):概述和性能測試方法 Java性能優化指南系列(一):概述和性能測試方法 -
從業務的角度來看,業務相關的計算是最重要的,這也是整個系統的目的;但是在上面的例子中,将它們的處理速度提升1倍是沒有意義的(因為LoadData的RPS隻有100)。任何應用(包括:單機JVM)都可以子產品化為上面這樣的一個一個步驟,每個步驟都以一定的速率向下一個步驟傳輸資料。
小貼士:如果有多個JVM同時運作在同一台機器上,我們必須要将所有JVM作為整體同時進行測試;因為有可能出現這種情況,單個JVM運作得很好,但是當多個JVM同時運作的時候,一些應用的性能會非常不同,比如:一些應用在GC的時候會占用比較多的CPU,當它在獨立運作的時候沒有問題,但是如果有其它應用一起運作的時候,它就可能得不到充分的CPU,導緻運作性能下降。這就是為什麼我們要對應用進行整體測試的一個理由。
Mesobenchmarks
- 對于JAVA SE和JAVA EE都有一系列稱之為microbenchmark的測試;對于Java SE工程師來說,microbenchmark意味着非常小的測試機關(比上文中的Fibonacci還要小);而Java EE工程師,microbenchmark意味着性能測試的某個方面(仍然需要執行比較多的代碼),舉個例子:從應用伺服器的某個JSP傳回結果有多快;但是在這個過程中有非常多的操作,比如:socket管理的代碼,請求處理等等,這個和Java SE裡面的microbenchmark測試是不一樣的。但是這個測試也不是Macrobenchmark,因為它沒有登陸,沒有會話管理,沒有使用Java EE的其它特性,我們稱之為Mesobenchmark。
- 本書中的樣例都是基于下面這樣一個應用程式:它用于計算一定時間範圍内某隻股票的曆史最高和最低的價格以及在這段時間内的價格偏差。
- 類:StockPrice用來存儲給定日期的該股票的價格範圍。
Java性能優化指南系列(一):概述和性能測試方法 - 樣例應用就是處理StockPrice的一個集合;這個集合代表這支股票一段時間的曆史(1年或25年),是以有下面的接口:
-
Java性能優化指南系列(一):概述和性能測試方法 - 這個類的基本實作就是從資料庫中加載一些列的價格
-
Java性能優化指南系列(一):概述和性能測試方法 - 注意到:curDate是按天進行增加的。
- 另外要注意的是這個類的性能和BigDecimal的性能是精密相關的;之所有選擇這個類,有兩點:1)提升計算的精度 2)對于我們做為例子來說,BigDecimal的計算量有助于增加業務的計算量
- 下面一個函數是對BIgDemial進行平方根求解(使用巴比倫方法):
Java性能優化指南系列(一):概述和性能測試方法 - 這個實作不是最高效的算法,但是這麼做是故意的,它可以增加些業務計算的時間。
- 接口StockPriceHistory的标準方差、平均價格以及直方圖實作都會産生新的值。在許多例子當中,這些值或者提前計算出來(在将資料從EntityManager中加載的時候)或者延遲進行計算(等到調用對應的函數)。同樣的,接口StockPrice引用了一個StockOptionPrice接口,它用來存放該股票給定日期的可選價格。這些價格也可以提前或延遲進行計算。無論是提前計算和延遲計算,這些接口的定義可以對在不同情況下,将這兩種方式進行比較。
- 這些接口也天然的複合J2EE應用:使用者通過通路一個JSP頁面,輸入股票的代碼和起止日期。這個請求會被一個Servlet進行處理(解析參數,通路stateless EJB),得到結果後,将響應轉到一個JSP頁面上,它負責将資料格式化為HTML頁面。
Java性能優化指南系列(一):概述和性能測試方法 -
這個類可以注入不同的history bean的實作(提前計算或延遲計算);它還可以緩存資料(或不緩存),這個對于一個企業應用是很平常的事情。
了解吞吐量、處理時間(批量操作)和響應時間
處理時間(批量操作)
- 測試性能的一個最簡單的方式就是看看應用在多長時間内能夠完成某個任務。比如:擷取10000隻股票的25年内的曆史紀錄,并計算這些價格的标準方差;生成某個公司5萬名員工的工資報告;執行1百萬次等等
- 對于非Java程式,這些測試都是很顯然的,寫好程式,然後進行執行并評估時間。但是對于java來說,就不是這樣了,因為它有JIT(Java即時編譯器)。這個過程會在第4章進行介紹,簡單來說就是Java代碼需要幾分鐘(或更長)的時間來達到最優化,這個時候代碼執行的性能才是最高的。是以,Java性能的研究非常關注warm-up period,性能測試通常在代碼執行了足夠長的時間後才啟動測試,因為這個時候的代碼才是最優化的。
- 注意:warm-up period通常是指JIT編譯和優化代碼的時間,但是還存在其它因素會影響warm-up period時間的長短的。比如:JPA通常會緩存資料;作業系統也會對檔案進行緩存等等。
- 但是另一方面,通常情況下應用的性能關注的是從啟動到結束的時間,使用者不會關注你的warm-upperiod的時間是多長。
- 吞吐量的測量是指在給定時間内,應用可以完成的工作量。一般情況下,測試吞吐量都需要一個用戶端不停的發送請求給服務端,但并不是所有吞吐量測試都需要這樣;對于獨立程式測試吞吐量就像測試程式處理時間一樣簡單。
- 吞吐量可以使用TPS、RPS或OPS來表示。
- 所有client-server測試的運作都存在一個風險:用戶端不能足夠快地發送資料到服務端。吞吐量測試比響應時間測試需要更少的client線程,因為吞吐量測試基本沒有什麼業務邏輯
- 吞吐量測試常常需要經過warm-up period時間後進行,特别是當被測量的應用工作集不是固定的(不知道什麼含義?)
- 響應時間是指請求從發送到接收到響應經曆的時長。響應時間測試和吞吐量測試的差別是響應時間測試的用戶端線程在操作之間會睡眠一段時間。這段時間,我們稱之為:think time。響應時間測試更加接近于模拟使用者的行為。
- 将響應時間考慮進測試當中,吞吐量就變得固定:給定數目的用戶端使用給定think time來執行請求,通常會産生同樣的TPS(當然,肯定有細微的變化)。在這個時候,請求的響應時間是更重要的測量目标:服務端的效率是通過它對固定數目的負載響應有多快來衡量的。
Java性能優化指南系列(一):概述和性能測試方法 -
在這種情況下,吞吐量在一定程度上依賴于響應時間。如果響應時間是1秒,那麼用戶端每31秒發送一個請求,是以吞吐量為0.032OPS;如果響應時間是2秒,那麼用戶端每32秒發送一個請求,吞吐量變為0.031OPS。
另外一種方式是使用cycle time(而不是think time)。Cycle time設定請求之間的總時間為30秒,是以用戶端睡眠的時間依賴于響應時間。
Java性能優化指南系列(一):概述和性能測試方法 - 這使得吞吐量固定為0.033OPS,而不管響應時間是多少(假定每個請求的響應時間都小于30秒)。在測試工具中think time常常是變化的,它們的平均時間大緻是某個值,但是為了更好地模拟使用者行為,每個請求的hink time是随機的。除了這個原因,線程排程也不可能是實時的,是以請求之間的真實時間都是有些不同的。是以,即使使用提供cycle time的工具,測試多次,每次測試的吞吐量也是會有所不同的。
- 這裡有兩種不同的方式來測試響應時間。1)平均時間:将每個請求的時間加在一起,除以請求的數目 2)百分點請求(percentile request),比如:90%請求的響應時間。如果90%的請求響應時間少于1.5秒,10%的請求響應時間大于1.5秒,那麼1.5秒就是90%的請求響應時間。
- 上面兩種方式的一個差別是極值對平均值計算的影響:因為它們被包含在平均值的計算當中,更大的極值對平均響應時間的計算影響更大。
Java性能優化指南系列(一):概述和性能測試方法 -
Java性能優化指南系列(一):概述和性能測試方法 - 而上圖中,90%的請求響應時間為1秒,但是平均響應時間卻為6秒,這就是極值對響應時間産生了巨大的影響。
- 盡管上面的極值在實際情況中比較少見,但是對于Java應用卻是比較容易發生的,因為GC引入了pause time。(這裡不是說GC會導緻100秒的pause time,而是在測試響應時間很小的應用時,pause time變成了極值)。在性能測試的時候,我們通常關注90%的請求響應時間(當然95%或99%都是可以)。如果我們隻能關注其中一個,百分比請求響應時間是更好的選擇,因為百分比請求響應時間更小對絕大多數使用者都是有好處的。但是最好兩個都要看,請求平均響應時間和至少一個百分比響應時間,以便我們不會丢失有比較大極值的情況(以便發現問題)。
- 負載生成器:有很多開源和商用的負載生成工具。本書使用Faban,一個開源的,基于Java的負載生成器,它可以用來簡單測試一個URL的性能,比如:
Java性能優化指南系列(一):概述和性能測試方法 Java性能優化指南系列(一):概述和性能測試方法 -
上面的例子,使用25個用戶端(-c)産生請求到stock servlet上面(SDO來訓示);每個請求有1秒的Cycle time(-w 1000)。這個測試有300秒的warm-up period,然後是5分鐘的測試,然後是1分鐘的ramp-downperiod( -r 300/300/60)。
了解波動性
- 第三個原則就是了解每次測試的結果是如何每次都不一樣的。處理同樣資料集的程式每次都會産生不一樣的結果。伺服器上的背景程序會影響應用,網絡或多或少地會和正在運作的程式進行CPU的競争等等。一個好的測試方案不會每次讓服務端都處理相同的資料集,它們應該産生随機的資料集以便更好地模拟現實情況。但這個會導緻一個問題,當對多個測試結果進行比較的時候,是回歸了,還是因為測試中各種變化因素導緻的?
- 這個問題可以通過多次測試,然後對結果求平均值來解決。但是問題并不是這麼簡單,要了解兩次測試結果的不同是由于回歸還是因為測試中變化因素導緻的,這個是非常困難的。