【連載 21】效能測試實踐——超時結賬第一回合

FunTester發表於2025-03-11

3.7.1 超市結賬第一回合

讓我們把目光轉回小八超市。最近生意紅火,8 個收銀臺忙得團團轉,早高峰時連上廁所的時間都沒有。收銀員們叫苦不迭,紛紛建議老闆臨時增加 2 個收銀臺。小八思前想後,決定先對現有的 8 個收銀臺進行一次摸底,看看在滿負荷運轉的情況下,每分鐘能結賬多少顧客。根據摸底結果,再決定是否增加臨時收銀臺。

以此為背景,我們來設計一個效能測試用例。根據需求分析,我們選擇執行緒模型,也就是排隊模型,總併發數量為 8。測試內容就是模擬顧客結賬的流程,簡化為三個步驟:掃碼計價、付款結賬和打包走人。為了給收銀員留出熱身時間,我們設定了 2 分鐘的 Rump-Up 時間。

相信大家對這種場景已經駕輕就熟,下面是我設計的多執行緒類。為了增加一點挑戰性,我分別統計了三個步驟的耗時,並且支援非同步輸出實時資訊,這樣可以更快定位系統瓶頸,提升排查效率。

既然要增加額外的統計和非同步輸出功能,必然需要一個額外的執行緒來完成這個任務。為了避免執行緒開銷,我在 TaskExecutor 類中增加了一個屬性 realTimeThread:

/**
 * 實時資訊輸出執行緒,用於實時統計一段時間的TPS和平均耗時
 */
public Thread realTimeThread;

這樣在 start() 方法中稍加改造即可使用。當 realTimeThread 未賦值時,預設只統計當前實時 TPS 和 RT。

if (realTimeThread == null) realTimeThread = new Thread() {
    @Override
    public void run() {
        while (realTimeKey) {
            // 重複程式碼,省略
        }
    }
};
realTimeThread.start();

接下來我們編寫多執行緒任務類程式碼。增加了三個階段方法以及對應的統計屬性。本次資料統計採用了實時 TPS 和 RT 相同的方案,使用一個全域性執行緒安全物件計算總耗時,然後依據實時 TPS 計算平均耗時。

為了增加統計資料的波動性,在 pay() 方法中增加了時間相關變數作為休眠引數。多執行緒任務類程式碼如下:

package org.funtester.performance.books.chapter03.section7;

import org.funtester.performance.books.chapter03.common.ThreadTool;
import org.funtester.performance.books.chapter03.section3.ThreadTask;

import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 超市收銀臺效能測試用例
 */
public class SupermarketCheckoutTaskFirst extends ThreadTask {

    /**
     * 計價耗時統計
     */
    public static AtomicLong priceCostTime;

    /**
     * 支付耗時統計
     */
    public static AtomicLong payCostTime;

    /**
     * 打包耗時統計
     */
    public static AtomicLong packCostTime;

    /**
     * 構造方法
     * @param totalNum 執行的總次數
     */
    public SupermarketCheckoutTaskFirst(int totalNum) {
        this.totalNum = totalNum;
        this.costTime = new ArrayList<>(totalNum);
        priceCostTime = new AtomicLong();
        payCostTime = new AtomicLong();
        packCostTime = new AtomicLong();
    }

    /**
     * 業務操作,計價、支付、打包
     */
    @Override
    public void test() {
        long start = System.currentTimeMillis();
        price();
        long price = System.currentTimeMillis();
        priceCostTime.addAndGet(price - start);
        pay();
        long pay = System.currentTimeMillis();
        payCostTime.addAndGet(pay - price);
        pack();
        long pack = System.currentTimeMillis();
        packCostTime.addAndGet(pack - pay);
    }

    /**
     * 計價
     */
    public void price() {
        ThreadTool.sleep(10);
    }

    /**
     * 支付
     */
    public void pay() {
        ThreadTool.sleep((int) (System.currentTimeMillis() % 10000) / 100);
    }

    /**
     * 打包
     */
    public void pack() {
        ThreadTool.sleep(10);
    }
}

測試用例如下:

package org.funtester.performance.books.chapter03.section7;

import org.funtester.performance.books.chapter03.common.ThreadTool;
import org.funtester.performance.books.chapter03.section3.ThreadTask;
import org.funtester.performance.books.chapter03.section4.TaskExecutor;

import java.util.ArrayList;
import java.util.List;

/**
 * 超市收銀臺效能測試用例
 */
public class SupermarketCheckoutCase {

    public static void main(String[] args) throws InterruptedException {
        int total = 1000;
        List<ThreadTask> tasks = new ArrayList<>();
        for (int i = 0; i < 8; i++) {
            SupermarketCheckoutTaskFirst supermarketCheckoutTask = new SupermarketCheckoutTaskFirst(total);
            tasks.add(supermarketCheckoutTask);
        }
        TaskExecutor taskExecutor = new TaskExecutor(tasks, "超市收銀臺效能測試用例", 120);
        Thread thread = new Thread() {
            @Override
            public void run() {
                while (taskExecutor.realTimeKey) {
                    ThreadTool.sleep(1000);
                    long sumCost = TaskExecutor.realTimeCostTime.sumThenReset();
                    long sumTimes = TaskExecutor.realTimeCostTimes.sumThenReset();
                    System.out.println(String.format("實時統計TPS: %d, 平均耗時: %d", sumTimes, sumTimes == 0 ? 0 : sumCost / sumTimes));
                    long price = SupermarketCheckoutTaskFirst.priceCostTime.getAndSet(0);
                    long pay = SupermarketCheckoutTaskFirst.payCostTime.getAndSet(0);
                    long pack = SupermarketCheckoutTaskFirst.packCostTime.getAndSet(0);
                    System.out.println(String.format("實時統計各階段耗時: price: %d, pay: %d, pack: %d", price / sumTimes, pay / sumTimes, pack / sumTimes));
                }
            }
        };
        taskExecutor.realTimeThread = thread;
        taskExecutor.start();
    }
}

控制檯輸出資訊如下(省略了重複內容):

實時統計各階段耗時: price: 11, pay: 51, pack: 11
實時統計TPS: 12, 平均耗時: 84
實時統計各階段耗時: price: 10, pay: 52, pack: 11
實時統計TPS: 23, 平均耗時: 84
// 其他Rump-Up階段資訊省略
Rump-Up結束,開始執行測試任務!
實時統計TPS: 88, 平均耗時: 90
實時統計各階段耗時: price: 11, pay: 67, pack: 11
實時統計TPS: 80, 平均耗時: 100
實時統計各階段耗時: price: 11, pay: 78, pack: 11
任務執行完畢! 預期執行次數: 1000, 實際執行次數 1000, 錯誤次數 0, 耗時收集數量: 1000
// 此處省略相同多執行緒任務結束日誌
測試TPS: 129, 平均耗時: 62
測試TPS: 129, 總執行次數: 8000
最小值:20
最大值:127
平均值:62
50分位值:57
90分位值:107
95分位值:116
99分位值:124
999分位值:126
任務執行完畢! 壓測時長: 62 秒, 預期執行次數: 8000, 實際執行次數 8000, 錯誤次數 0, 耗時收集數量: 8000

透過這次測試,小八超市的收銀臺效能一目瞭然,接下來就是根據資料做決策了。正所謂 “磨刀不誤砍柴工”,有了這些資料支援,小八的決策會更加科學合理。

FunTester 原創精華
【連載】從 Java 開始效能測試
故障測試與 Web 前端
服務端功能測試
效能測試專題
Java、Groovy、Go
白盒、工具、爬蟲、UI 自動化
理論、感悟、影片
如果覺得我的文章對您有用,請隨意打賞。您的支援將鼓勵我繼續創作!
打賞支援
暫無回覆。

相關文章