我們經常會談論性能、並發等問題,但是衡量性能不是說寫段代碼循環幾百次這麼簡單。最近從項目上的同事了解到了代碼化的測試性能測試工具 k6,以及結合之前用過的Java 微基準測試 (JMH)、AB (Apache Benchmark) 測試、Jmeter 做一下總結。
談性能,實際上結合實際的業務背景、網路條件、測試數據的選擇等因素影響非常大,單純的談 QPS 等數據意義不大。
這裡介紹的幾個工具剛好能滿足平時開發工作中不同場景下衡量性能的需求,因此整理出來。
- Java 微基準測試 (JMH) 可以用于衡量一段 Java 代碼到底性能如何,例如我們平時總是談 StringBuilder 比 new String() 快很多。我們有一個很好地量化方法,就可以很直觀的展示出一段代碼的性能優劣。
- AB (Apache Benchmark) 測試是 Apache 伺服器內置的一個 http web 壓測工具,非常簡單易用。Mac 預裝了 Apache,因此可以隨手使用來測試一個頁面或者 API 的性能。貴在簡單易用,無需額外安裝。
- k6 一款使用 go 語言編寫,支持用戶編寫測試腳本的測試套件。彌補了 ab 測試功能不足,以及 jemeter 不容易代碼化的缺點。也是項目上需要使用,從同事那裡了解到的。
- Jmeter 老牌的性能測試工具,有大量專門講 jmeter 的資料,本文不再贅述。
那我們從 JMH 開始從來看下這幾個工具的特點和使用吧。
Java 微基準測試
StringBuilder 到底比 new String() 快多少呢?
我們可以使用 JMH 來測試一下。JMH 是一個用於構建、運行和分析 Java 方法運行性能工具,可以做到 nano/micro/mili/macro 時間粒度。JMH 不僅可以分析 Java 語言,基於 JVM 的語言都可以使用。
JMH 由 OpenJDK 團隊開發,由一次下載 OpenJDK 時注意到官網還有這麼一個東西。
OpenJdk 官方運行 JMH 測試推的方法是使用 Maven 構建一個單獨的項目,然後把需要測試的項目作為 Jar 包引入。這樣能排除項目代碼的干擾,得到比較可靠地測試效果。當然也可以使用 IDE 或者 Gradle 配置到自己項目中,便於和已有項目集成,代價是配置比較麻煩並且結果沒那麼可靠。
使用 Maven 構建基準測試
根據官網的例子,我們可以使用官網的一個模板項目。
mvn archetype:generate
-DinteractiveMode=false
-DarchetypeGroupId=org.openjdk.jmh
-DarchetypeArtifactId=jmh-java-benchmark-archetype
-DgroupId=org.sample
-DartifactId=test
-Dversion=1.0
創建一個項目,導入 IDE,Maven 會幫我們生成一個測試類,但是這個測試類沒有任何內容,這個測試也是可以運行的。
先編譯成 jar
mvn clean install
然後使用 javar -jar 來運行測試
java -jar target/benchmarks.jar
運行後可以看到輸出信息中包含 JDK、JVM 等信息,以及一些用於測試的配置信息。
#JMH version: 1.22
# VM version: JDK 1.8.0_181, Java HotSpot(TM) 64-Bit Server VM, 25.181-b13
# VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/bin/java
# VM options:
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: org.sample.MyBenchmark.testSimpleString
下面是一些配置信息說明
- Warmup 因為 JVM 即時編譯的存在,所以為了更加準確有一個預熱環節,這裡是預熱 5,每輪 10s。
- Measurement 是真實的性能測量參數,這裡是 5輪,每輪10s。
- Timeout 每輪測試,JMH 會進行 GC 然後暫停一段時間,默認是 10 分鐘。
- Threads 使用多少個線程來運行,一個線程會同步阻塞執行。
- Benchmark mode 輸出的運行模式,常用的有下面幾個。Throughput 吞吐量,即每單位運行多少次操作。AverageTime 調用的平均時間,每次調用耗費多少時間。SingleShotTime 運行一次的時間,如果把預熱關閉可以測試代碼冷啟動時間
- Benchmark 測試的目標類
實際上還有很多配置,可以通過 -h 參數查看
java -jar target/benchmarks.jar -h
由於默認的配置停頓的時間太長,我們通過註解修改配置,並增加了 Java 中最基本的字元串操作性能對比。
@BenchmarkMode(Mode.Throughput)
@Warmup(iterations = 3)
@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)
@Threads(8)
@Fork(1)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class MyBenchmark {
@Benchmark
public void testSimpleString() {
String s = "Hello world!";
for (int i = 0; i < 10; i++) {
s += s;
}
}
@Benchmark
public void testStringBuilder() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10; i++) {
sb.append(i);
}
}
}
在控制台可以看到輸出的測試報告,我們直接看最後一部分即可。
Benchmark Mode Cnt Score Error Units
MyBenchmark.testSimpleString thrpt 10 226.930 ± 16.621 ops/ms
MyBenchmark.testStringBuilder thrpt 10 80369.037 ± 3058.280 ops/ms
Score 這列的意思是每毫秒完成了多少次操作,可見 StringBuilder 確實比普通的 String 構造器性能高很多。
更多有趣的測試
實際上平時 Java 開發中一些細節對性能有明顯的影響,雖然對系統整體來說影響比較小,但是注意這些細節可以低成本的避免性能問題堆積。
其中一個非常有意思細節是自動包裝類型的使用,即使是一個簡單的 for 循環,如果不小心講 int 使用成 Integer 也會造成性能浪費。
我們來編寫一個簡單的基準測試
@Benchmark
public void primaryDataType() {
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += i;
}
}
@Benchmark
public void boxDataType() {
int sum = 0;
for (Integer i = 0; i < 10; i++) {
sum += i;
}
}
運行測試後,得到下面的測試結果
AutoBoxBenchmark.boxDataType thrpt 5 312779.633 ± 26761.457 ops/ms
AutoBoxBenchmark.primaryDataType thrpt 5 8522641.543 ± 2500518.440 ops/ms
基本類型的性能高出了一個數量級。當然你可能會說基本類型這種性能問題比較微小,但是性能往往就是這種從細微處提高的。另外編寫 JMH 測試也會讓團隊看待性能問題更為直觀。
一份直觀的 Java 基礎性能報告
下面是我寫的常見場景的性能測試,例如 StringBuilder 比 new String() 速度快幾個數量級。

代碼倉庫和持續更新的基準測試可以看下面的倉庫。
https://github.com/linksgo2011/jmh-reports
Apache Benchmark 測試
我想用命令行快速簡單的壓測一下網站該怎麼辦呢?
Apache Benchmark (簡稱 ab,不同於產品領域的 A/B 測試) 是 Apache web 伺服器自帶的性能測試工具,在 windows 或者 linux 上安裝了 Apache 伺服器就可以在其安裝位置的 bin 目錄中找到 ab 這個程序。
ab 使用起來非常簡單,一般只需要 -n 參數指明發出請求的總數,以及 -c 參數指明測試期間的並發數。
例如對 ThoughtWorks 官網首頁發出 100 個請求,模擬並發數為 10:
ab -n 100 -c 10 https://thoughtworks.com/
需要注意的是 ab 工具接收一個 url 作為參數,僅僅是一個域名是不合法的,需要增加 / 表示首頁。稍等片刻後就可以看到測試報告:
Server Software: nginx/1.15.6
Server Hostname: thoughtworks.com
Server Port: 443
SSL/TLS Protocol: TLSv1.2,ECDHE-RSA-AES256-GCM-SHA384,2048,256
Server Temp Key: ECDH P-256 256 bits
TLS Server Name: thoughtworks.com
Document Path: /
Document Length: 162 bytes
Concurrency Level: 10
Time taken for tests: 42.079 seconds
Complete requests: 100
Failed requests: 0
Non-2xx responses: 100
Total transferred: 42500 bytes
HTML transferred: 16200 bytes
Requests per second: 2.38 [#/sec] (mean)
Time per request: 4207.888 [ms] (mean)
Time per request: 420.789 [ms] (mean, across all concurrent requests)
Transfer rate: 0.99 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 1056 2474 3006.1 1144 23032
Processing: 349 740 1003.5 379 8461
Waiting: 349 461 290.9 377 2265
Total: 1411 3214 3273.9 1674 23424
Percentage of the requests served within a certain time (ms)
50% 1674
66% 2954
75% 3951
80% 4397
90% 6713
95% 9400
98% 14973
99% 23424
100% 23424 (longest request)
從這個報告中可以看到伺服器的一些基本信息,以及請求的統計信息。比較重要的指標是 Requests per second 每秒鐘完成的請求數量,不嚴格的說也就是我們的平時說的 QPS。
ab 測試是專為 http 請求設計的,因此 ab 的其他參數和 curl 的參數比較類似,也可以指定 http method 以及 cookies 等參數。
K6 測試套件
我需要編寫複雜的測試腳本,並保留壓測的腳本、參數、數據,以及版本化該怎麼做呢?
k6 是一個壓力測試套件,使用 golang 編寫。主要特性有:
- 提供了友好的 CLI 工具
- 使用 JavaScript 代碼編寫測試用例
- 可以根據性能條件設置閾值,表明成功還是失敗
k6 沒有使用 nodejs 而是 golang 程序,通過包裹了一個 JavaScript 運行時來運行 JavaScript 腳本,因此不能直接使用 npm 包以及 Nodejs 提供的一些 API。
同時,k6 在運行測試時,沒有啟動瀏覽器,主要用於測試頁面以及 API 載入速度。k6 提供了通過網路請求(HAR)生成測試腳本的方法,實現更簡便的測試腳本編寫,以及 session 的維護。
使用
在 Mac 上比較簡單,直接使用 HomeBrew 即可安裝:
brew install k6
其他平台官網也提供了相應的安裝方式,比較特別的是提供了 Docker 的方式運行。
直接使用 k6 的命令運行測試,官網提供了一個例子:
k6 run github.com/loadimpact/k6/samples/http_get.js
也可以編寫自己的測試腳本:
import http from "k6/http";
import { sleep } from "k6";
export default function() {
http.get("https://www.thoughtworks.com/");
sleep(1);
};
保存文件 script.js 後運行 k6 命令
k6 run script.js
然後可以看到 http 請求的各項指標
/ |‾‾| /‾‾/ /‾/
/ / | |_/ / / /
/ / | | / ‾‾
/ | |‾ | (_) |
/ __________ |__| __ ___/ .io
execution: local
output: -
script: k6.js
duration: -, iterations: 1
vus: 1, max: 1
done [==========================================================] 1 / 1
data_received..............: 108 kB 27 kB/s
data_sent..................: 1.0 kB 252 B/s
http_req_blocked...........: avg=2.35s min=2.35s med=2.35s max=2.35s p(90)=2.35s p(95)=2.35s
http_req_connecting........: avg=79.18ms min=79.18ms med=79.18ms max=79.18ms p(90)=79.18ms p(95)=79.18ms
http_req_duration..........: avg=639.03ms min=639.03ms med=639.03ms max=639.03ms p(90)=639.03ms p(95)=639.03ms
http_req_receiving.........: avg=358.12ms min=358.12ms med=358.12ms max=358.12ms p(90)=358.12ms p(95)=358.12ms
http_req_sending...........: avg=1.79ms min=1.79ms med=1.79ms max=1.79ms p(90)=1.79ms p(95)=1.79ms
http_req_tls_handshaking...: avg=701.46ms min=701.46ms med=701.46ms max=701.46ms p(90)=701.46ms p(95)=701.46ms
http_req_waiting...........: avg=279.12ms min=279.12ms med=279.12ms max=279.12ms p(90)=279.12ms p(95)=279.12ms
http_reqs..................: 1 0.249921/s
iteration_duration.........: avg=4s min=4s med=4s max=4s p(90)=4s p(95)=4s
iterations.................: 1 0.249921/s
vus........................: 1 min=1 max=1
vus_max....................: 1 min=1 max=1
k6 提供的性能指標相對 ab 工具多很多,也可以通過腳本自己計算性能指標。和 ab 工具中表明每秒鐘處理完的請求數是 http_reqs,上面的測試默認只有一個用戶的一次請求,如果通過參數增加更多請求,可以看到和 ab 工具得到的結果比較接近。
運行壓力測試時,需要增加更多的虛擬用戶(VU),vus 參數和持續時間的參數:
k6 run –vus 10 –duration 30s script.js
編寫測試腳本的一些規則
default 方法是用於給每個 VU 以及每次迭代重複運行的,因此需要把真正的測試代碼放到這個方法中,例如訪問某個頁面。
為了保證測試的準確性,一些初始化的代碼不應該放到 default 方法中。尤其是文件的讀取等依賴環境上下文的操作不能放到 default 方法中執行,這樣做也會丟失 k6 分散式運行的能力。
前面提到的命令行參數,例如指定虛擬用戶數量 –vus 10 ,這些參數也可以放到腳本代碼中。通過暴露一個 options 對象即可。
export let options = {
vus: 10,
duration: "30s"
};
為了更為真實的模擬用戶訪問的場景,k6 提供了在整個測試期間讓用戶數量和訪問時間呈階段性變化的能力。只需要在 options 中增加 stages 參數即可:
export let options = {
stages: [
{ duration: "30s", target: 20 },
{ duration: "1m30s", target: 10 },
{ duration: "20s", target: 0 },
]
};
在測試過程中需要檢查網路請求是否成功,返回的狀態碼是否正確,以及響應時間是否符合某個閾值。在腳本中可以通過調用 check() 方法編寫檢查語句,以便 k6 能收集到報告。
import http from "k6/http";
import { check, sleep } from "k6";
export let options = {
vus: 10,
duration: "30s"
};
export default function() {
let res = http.get("https://www.thoughtworks.com/");
check(res, {
"status was 200": (r) => r.status == 200,
"transaction time OK": (r) => r.timings.duration < 200
});
sleep(1);
};
報告輸出
k6 默認將報告輸出到 stdout 控制台,同時也提供了多種格式報告輸出,包括:
- JSON
- CSV
- InfluxDB
- Apache Kafka
- StatsD
- Datadog
- Load Impact cloud platform
當然,我們在編寫測試的時候不可能只有一個用例,對多個場景可以在腳本中通過group 進行分組,分組後輸出的報告會按照分組排列。同時,也可以使用對一個組整體性能衡量的指標 group_duration。
import { group } from "k6";
export default function() {
group("user flow: returning user", function() {
group("visit homepage", function() {
// load homepage resources
});
group("login", function() {
// perform login
});
});
};
InfluxDB 等外部數據收集平台時,還可以打上標籤,供過濾和檢索使用。k6 提供了一些內置的標籤,並允許用戶自定義標籤。
總結
實際上用於性能測試的工具還有很多,也有一些專門的工具針對網路質量(iperf) 、資料庫(sysbench)、前端頁面(PageSpeed)等專門方面進行性能測試。
寫本文的初衷是想說評價性能,以及做性能優化的第一步應該是尋找到合適工具做一次基準測試,這樣的優化往往才有意義。我在使用 JMH 後不僅在工作中使用它對一些代碼片段進行測試以及優化,同時更重要的是,在codereview 中對某些操作關於性能的討論不再基於經驗,而是事實。
原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-tw/n/231144.html