developerWorks 中國 技術主題 Java technology 文檔庫 Java 性能測試的四項原則

轉-https://www.ibm.com/developerworks/cn/java/j-lo-java-performance-testing/?cm_mmc=dwchina-_-homepa

Java 性能測試的四項原則

絕大數的開發人員在平常工做過程當中都會或多或少的碰見過性能問題,本文旨在闡述性能測試的理論,從而爲性能分析和開發人員作指導。本文對於那些剛剛接觸性能調優和正在解決問題的開發人員也能提供一些啓發性的思路。java

李 偉軍, 高級軟件工程師, IBM數據庫

楊 翔宇, 軟件工程師, IBM編程

宋 翰瀛, 資深軟件工程師, IBM緩存

2016 年 5 月 23 日服務器

  • expand內容

引言

計算機軟件做爲人類智慧的結晶,幫助咱們在這個突飛猛進的社會中完成了大量工做。我 們的平常生活中已經離不開軟件,玲琅滿目的軟件已經滲透到了咱們生活的各個角落,令咱們應接不暇。咱們都但願軟件變得更好,運行處理的速度更快,在當今硬 件性能日新月異的變革中,軟件性能的提高也是一個永不落伍的話題。軟件性能測試的實質,是從哲學的角度看問題,找出其內在聯繫,因果關係,形式內容關係, 重疊關係等等。假如這些關係咱們在分析過程當中理清了,那麼性能測試問題就會變得迎刃而解。架構

在軟件開發過程當中,性能測試每每在開發前期容易被 忽略。直到有一天問題暴露後,開發人員被迫的直面這個問題,大多數狀況下,這是令開發人員感受到很是痛苦事情。因此在軟件開發前期以及開發過程當中性能測試 的考量是必要的,那麼具有相應理論知識和實踐方法也是一個優秀工程師所應當具有的素養,這裏咱們歸納有四項原則,這些原則能夠幫助開發人員豐富、充實測試 理論,系統的開展性能測試工做,從而得到更有價值的結果。dom

實際項目中的性能測試纔有意義

第一個原則就是性能測試只有在實際項目中實施纔是有意義的,這樣才使得測試工做具備針對性,並且目標會更加明確。這個原則中有三個類別的基準能夠指導開發人員度量性能測試的結果,可是每一種方法都有它的優勢和劣勢,咱們將結合實際例子,來總結闡述。

  • 微 觀基準,能夠理解爲在某一個方法或某一個組件中進行的單元性能測試。好比檢測一個線程同步和一個非線程同步的方法運行時所須要的時間。或者對比建立一個單 獨線程和使用一個線程池的性能開銷。或者對比執行一個算法中的某一個迭代過程所須要的時間。當咱們遇到這些狀況時,咱們經常會選擇作一個方法層面的性能測 試。這些狀況的性能測試,均可以嘗試使用微觀基準的方法進行性能測試。微觀基準看似編寫起來簡單快捷,可是編寫可以準確反映性能問題的代碼並不是一件易事。 接下來經過例子讓咱們從代碼中發現一些問題。這是一個單線程的程序片斷,經過計算 50 次循環迭代來檢測執行方法所耗費的時間體現性能差別:

public void doTest() {
 double l;
 long then = System.currentTimeMillis();
 int nLoops = 50;
 for (int i = 0; i < nLoops; i++) {
 l = compute(50);
 }
 long now = System.currentTimeMillis();
 System.out.println("Elapsed time:" + (now - then));
 }

 private double compute(int n){
 if (n < 0)
 throw new IllegalArgumentException("Must be > 0");
 if (n == 0)
 return 0d;
 if (n == 1)
 return 1d;
 double d = compute(n - 2) + compute(n - 1);
 if (Double.isInfinite(d))
 throw new ArithmeticException("Overflow");
 return d;
}

執行這段代碼咱們會發現一個問題,那就是執行時間只有短短的幾秒。難道果然是程序性能很高?答案並不是如此,其實在整個執行過程當中 compute 計算方法並無調用而是被編譯器自動忽略了。那麼解決這個問題的辦法是將 double 類型的「l」換成 volatile 實例變量。這樣可以確保每個計算後所獲得的結果是能夠被記錄下來,用 volatile 修飾的變量,線程在每次使用變量的時候,都會讀取變量修改後的最後的值。

要 特別值得注意的是,當考慮爲多線程寫一個微基準性能測試用例時,假如幾個線程同時執行一小段業務邏輯代碼,這可能會引起潛在的線程同步所帶來的性能開銷和 瓶頸。此時微觀微基準測試的結果每每引導開發人員爲了保持同步進行不斷的優化,這樣會浪費不少時間,對於解決更緊迫的性能問題,這樣作就顯得得不償失。

咱們再試想這樣一個例子,微基準測試兩個線程調用同步方法的狀況,由於基準代碼很小,那麼測試用例大部分時間將消耗在同步過程當中。即便微基準測試在總體的同步過程當中只佔 50%,那麼兩個線程嘗試執行同步方法的概率也是至關高的。基準運行將會很是緩慢,添加額外的線程會形成更大的性能問題。

基 於微觀基準的測試過程當中,是不能含有額外的對性能產生影響的操做,咱們知道執行 compute(1000) 和 compute(1) 在性能上是有很大差別的,假如咱們的目標是對比兩個不一樣實現方法之間的性能差別,那麼就應當考慮一系列的輸入測試值做爲前提,傳遞給測試目標,參數就須要 多樣化。這裏以咱們的經驗解決的辦法就是使用隨機值:

for (int i = 0; i < nLoops; i++) {
 l = compute(random.nextInt());
 }

如今,產生隨機數的時間也包含在了整個循環執行過程當中,所以測試結果中包含了隨機數生成所須要的時間,這並不能客觀的體現 compute 方法真實的性能。因此在構建微觀基準時,輸入的測試值必須是預先準備好的,且不會對性能測試產生額外的影響。正確的作法以下:

public void doTest() {
 double l;
 int nLoops = 10;
 Random random = new Random();
 int[] input = new int[nLoops];
 for (int i = 0; i < nLoops; i++) {
 input[i] = random.nextInt();
 }
 long then = System.currentTimeMillis();
 for (int i = 0; i < nLoops; i++) {
 try {
 l = compute(input[i]);
 } catch (IllegalArgumentException iae) {

 }

 }
 long now = System.currentTimeMillis();
 System.out.println("Elapsed time:" + (now - then));
}

微觀基準中輸入的測試值必須是符合業務邏輯的。全部的輸入的值並不必定會被代碼用到,實際的業務可能對輸入的數據有特定 的要求,不合理的輸入值可能致使代碼在執行過程當中就拋出異常而中斷,從而使得咱們難以判斷代碼執行的效率。因此在準備測試數據的時候應當考慮到輸入數據的 有效性,保證代碼執行的完整性。好比下面的例子輸入的參數若是是大於 1476 ,執行會當即中斷,從而影響了真實性能結果的產生。

public double ImplSlow(int n) {
 if (n < 0) throw new IllegalArgumentException("Must be > 0");
 if (n > 1476) throw new ArithmeticException("Must be < 1476");
 return verySlowImpl(n);
}

一般狀況下,對參與到實際業務計算的值提早檢測對提高性能是有幫助的,可是假如用戶大多數輸入的值是合理的,那麼提早檢查數據的有效性就顯得冗餘了。因此編寫核心邏輯代碼的時候,咱們建議只針對通常狀況作處理,保證執行的效率的高效性。假設訪問一個 collection 對象時,每一次可以節省幾毫秒的話,那麼在屢次的訪問狀況下就會對性能的提高產生重大的意義。

public class Test1 {

 private volatile double l;
 private int nLoops;
 private int[] input;
 
 
 private Test1(int n) {
 nLoops = n;
 input = new int[nLoops];
 Random random = new Random();
 for (int i = 0; i < nLoops; i++) {
 input[i] = random.nextInt(50);
 }
 }

 public void doTest(boolean isWarmup) {
 long then = System.currentTimeMillis();

 for (int i = 0; i < nLoops; i++) {
 try {
 l = compute(input[i]);
 } catch (IllegalArgumentException iae) {
 }
 if (!isWarmup) {
 long now = System.currentTimeMillis();
 System.out.println("Elapsed time:" + (now - then));
 }
 }

 }

 private double compute(int n) {
 if (n < 0)
 throw new IllegalArgumentException("Must be > 0");
 if (n == 0)
 return 0d;
 if (n == 1)
 return 1d;
 double d = compute(n - 2) + compute(n - 1);
 if (Double.isInfinite(d))
 throw new ArithmeticException("Overflow");
 return d;
 }

 public static void main(String[] args) {
 // TODO Auto-generated method stub
 Test1 test1 = new Test1(Integer.parseInt("10");));
 test1.doTest(true);
 test1.doTest(false);
 }

}

總得說來,微觀基準做用是有限的,在頻繁調用的方法中使用微觀基準的度量方法會幫助咱們檢測代碼的性能,若是用在不會被頻繁調用的方法中是不合適的,應當考慮其它方法。

  • 宏觀基準,當咱們測量應用程序性能時,應當縱覽整個系統,影響應用程序性能的緣由多是多方面的,不能片面的認爲性能瓶頸只會在程序自己上。經過下面這個例子咱們將探討離開宏觀基準的性能測試是不可能找到影響應用程序性能真正的瓶頸。

Figure xxx. Requires a heading

上 圖數據來自客戶實體,觸發應用程序的核心業務計算方法,該方法從數據庫加載數據,並傳導給核心業務中的計算方法,獲得結果保存到數據庫,最終響應客戶的請 求。每一個圖形中的數字分別表明了這個模塊所能處理客戶請求的數量。核心業務模塊的優化多數狀況是受限於業務的要求。假設咱們優化這些核心模塊,使其能夠處 理 200 RPS 時,咱們發現加載數據的模塊依然只能處理 100 RPS,也就是說整個系統的吞吐能力其實仍然爲 100 RPS,最終對應用程序總體的性能提高是沒有任何幫助的。從這個例子咱們得知,咱們花費再多的精力在覈心業務上的優化意義並不大,咱們應當從總體運行狀況 來看,發現真正影響性能的瓶頸來解決問題,這就是宏觀基準原則的意義。

  • 折 衷基準,相比微觀基準和宏觀基準,一個單獨功能模塊的性能測試,或者一系列特定操做的性能測試被稱爲折衷基準。它是介於微觀基準和宏觀基準之間的折衷方 案。基於微觀基準測試的正確性是較難把握的,性能瓶頸的判斷毫不能僅僅依賴於此。若是咱們要使用微觀基準做爲性能的測量方法,那麼不妨在此以前先嚐試基於 宏觀基準的測試。它能夠幫助咱們瞭解系統以及代碼是如何工做的,從而造成一個系統總體邏輯結構圖。接下來能夠考慮基於折衷基準的測試,來真正發現潛在的性 能瓶頸。須要明確的是折衷基準的測試方法並非完整應用程序測試的替代方法,更多狀況下咱們認爲它更適用於一個功能模塊的自動測試。

批量,吞吐量和響應時間的測量方法

性能測試中的第二個重要的原則是引入多樣的測量方法來分析程序的性能。

  • 批 量執行所用時間的測量方法(耗時法),這是種簡單而快速有效的方法,經過測量完成特定任務所消耗的時間來測量總體性能。可是須要特別注意,假如所測試的應 用程序中使用緩存數據技術來爲了得到更好的性能表現時,屢次循環使用該方法可能沒法徹底反應性能問題。那麼能夠嘗試在初始狀態開始時應用耗時法作一次性能 的評估,而後當緩存創建後,再次嘗試此方法。

  • 吞吐量的測量方法,在一段時間內考察完成任務的數量的能力,被稱爲吞吐量測 量方法。在測試客戶服務器的應用程序時,吞吐量的測量意味着客戶端發送請求到服務器是沒有任何延遲的,當客戶端接收到響應後,應當當即發出新的請求,直到 最終結束,統計客戶端完成任務的總數。這種相對理想的測試方法一般稱之爲「Zero-think-time」。但是一般狀況下,客戶端可能會有多個線程作 同一件事情,吞吐量則意味着每秒鐘內全部客戶端的操做數,而不是測量的某一個時段內的全部操做總數。這種測量常常稱爲每秒事務/(TPS),每秒請求 (RPS),或每秒操做數 (OPS)。

測 試全部基於客戶端和服務器端應用程序都存在一種風險,客戶端不能以足夠快的速度發送數據到服務器端,這種狀況的發生多是因爲客戶端此時沒有足夠的 CPU 資源去運行須要數量的線程,或者客戶端必須耗用更長的時間來處理當前的請求。這種狀況下,實際上測量的是客戶端的性能,而非服務器的性能,與吞吐量測量方 法是背道而馳的。其實這種風險是由每一個客戶端線程處理任務的數量和硬件配置決定的。「Zero-think-time」在吞吐量測試中可能常常會碰見以上 的狀況,因爲每一個客戶端線程都須要處理大量的任務,所以吞吐量測試一般被應用於較少的客戶端線程程序。吞吐量測量方法也一樣適應用於帶有緩存技術的應用程 序,尤爲是當測試的數據是一個並不固定的狀況下。

  • 響應時間的測量方 法,響應時間的測量方法是指客戶端發出一個請求後直到接收到服務器的響應返回後的時間消耗。響應時間測量方法不一樣於吞吐量測量方法,在響應時間測試過程 中,客戶端線程可能會在操做的過程當中某一時刻休眠,這就引出「think- time」這個關鍵詞,當「think- time」被引入到測試過程當中,也就是意味着待處理任務量是固定的,測量的是服務器響應請求的速率是怎樣的。大多數狀況下,響應時間的測量方法用來模擬用 戶真實操做,從而測量應用程序的性能。

多變性

性能測試的第三個原則是理解測試結果如何隨時間改變,即便每一次測試使用一樣的數據,可能得到的結果也是不一樣的。一些客觀因素,好比後臺運行的進程,網絡的負載狀況,這些均可能帶來測試結果的不一樣,因此在測試過程當中存在着一些隨機性的因素。這就產生了一個問題: 當比較兩次運行獲得的測試結果時,它們之間的差別是由迴歸測試產生的,仍是是隨機變化而致使的呢?

咱們不能簡單的經過測量屢次運行迴歸測試的平均結果來評判性能的差別。這時咱們可使用統計分析的方法,假設兩種狀況的平均值是同樣的,而後經過幾率來判斷這樣的假設是成立的。若是假設不成立,那麼就說明有很高的機率證實平均數存在差別。

在迴歸測試中原始代碼被視爲基線,新增長的代碼稱爲樣本。三次運行基線和樣本,產生時間如表 1:

表 1. 三次運行基線和樣本結果
次數 基準 樣本

1

1.0

0.5

2

0.8

1.25

3

1.2

0.5

平均

1

0.75

看起來樣本的平均值顯示有 25%的提高,可事實證實樣本和基線有相同性能的機率是 43%。也就是說 57%的機率存在性能上的不一樣。43%是基於 T 檢驗所獲得的結果,T 檢驗主要用於樣本含量較小(例如 n<30),整體標準差σ未知的正態分佈資料。t 檢驗是用 t 分佈理論來推論差別發生的機率,從而比較兩個平均數的差別是否顯著。它與 z 檢驗、卡方檢驗並列。如今的 T 檢驗結果告訴咱們這樣一個信息::57%機率顯示樣本和基線存在性能差別,差別最大值是 25%。也能夠理解爲性能差有 57%的置信度向理想發現發展,結果有 25%的改善。

在考量回歸測試的結果時,離開了統計分析的方法,而只關注平均值來作出判斷,含糊的理解這些數字的含義是不可取的。性能工程師的工做是看數據,理解這些機率,基於全部可用的數據肯定在何處花時間。

儘早測試,常常測試

第 四個原則就是工程師應該視性能測試是整個開發過程必要的部分,儘早進行性能測試,常常進行性能的測試,是一個好的工程師應該作到的。在代碼提交到代碼庫之 前,就應當作性能測試,由於性能問題也會致使迴歸測試失敗。因此提前發現問題會提升整個項目的質量,減少交付的風險性。

在一個典型的項目開發週期過程當中,項目計劃經常是創建一個功能提交的時間表,全部功能的開發必需要在某一個時間點所有提交到代碼庫中,在項目發佈以前,全部的精力都致力於解決功能上的 Bug,那麼頗有可能在這個過程當中發現性能問題,這會致使兩個問題產生:

  • 開發人員在時間的約束下不得不提交代碼以知足時間表,一旦發現出嚴重的性能問題他們會很是畏懼,因此開發人員在測試開始的早期解決性能問題可以產生 1%的迴歸測試代價,而若是開發人員一直在等待晚上的凍結功能開發的時候纔開始檢查代碼將會致使 20%的迴歸測試的代價。

  • 任何爲解決性能作出的修改都有可能帶來巨大的成本,有時不只僅是代碼的修改,更有多是軟件架構的修改。因此最好在軟件設計之時就充分的考慮到將來可能帶來的性能問題。

儘早測試性能有如下四點可做爲指導:

  • 提前準備測試用戶以及測試環境的設計和建立;

  • 性能測試應該考慮儘可能用腳原本完成;

  • 經過性能監控工具儘可能收集有可能獲得的運行信息,爲未來分析提供便利;

  • 必定要在一個能真實模擬多數用戶的機器環境下進行性能測試。

總結

最後,基於咱們講過的方法做爲基礎,構建一個自動化的測試系統來收集測試過程當中產生的各類信息,可以很好的幫助咱們分析發現性能瓶頸。

參考資料

相關文章
相關標籤/搜索