原文章內容為空,本文補充一個生產級的 HTTP 響應速度測試工具實現,支持多線程並發、百分位統計、異常分類、結果導出,可直接用於服務 SLA 驗證、上線前性能基線測試、故障排查對比。
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.3.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
package com.example.perf;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.stream.Collectors;
public class HttpLatencyProbe {
// 配置類
public static class Config {
public String url;
public String method = "GET";
public Map<String, String> headers = new HashMap<>();
public String body;
public int threads = 10;
public int totalRequests = 1000;
public int connectTimeoutMs = 3000;
public int readTimeoutMs = 5000;
public double targetQps = 0; // 0 = 不限流
public boolean followRedirects = true;
}
// 單次結果
public static class Result {
public final long startNs;
public final long latencyMs;
public final int statusCode;
public final String errorType;
public final String errorMsg;
public Result(long startNs, long latencyMs, int statusCode, String errorType, String errorMsg) {
this.startNs = startNs;
this.latencyMs = latencyMs;
this.statusCode = statusCode;
this.errorType = errorType;
this.errorMsg = errorMsg;
}
public boolean isSuccess() { return statusCode >= 200 && statusCode < 300; }
}
// 統計報告
public static class Report {
public final int total;
public final int success;
public final int failed;
public final double errorRate;
public final double throughputQps;
public final long minMs, maxMs, avgMs;
public final long p50, p90, p99, p999;
public final Map<String, Long> errorDistribution;
public Report(List<Result> results, long totalDurationMs) {
this.total = results.size();
this.success = (int) results.stream().filter(Result::isSuccess).count();
this.failed = total - success;
this.errorRate = total == 0 ? 0 : (double) failed / total * 100;
this.throughputQps = totalDurationMs == 0 ? 0 : total / (totalDurationMs / 1000.0);
List<Long> latencies = results.stream()
.filter(Result::isSuccess)
.map(r -> r.latencyMs)
.sorted()
.collect(Collectors.toList());
this.minMs = latencies.isEmpty() ? -1 : latencies.get(0);
this.maxMs = latencies.isEmpty() ? -1 : latencies.get(latencies.size() - 1);
this.avgMs = latencies.isEmpty() ? -1 : (long) latencies.stream().mapToLong(Long::longValue).average().orElse(-1);
this.p50 = percentile(latencies, 50);
this.p90 = percentile(latencies, 90);
this.p99 = percentile(latencies, 99);
this.p999 = percentile(latencies, 99.9);
this.errorDistribution = results.stream()
.filter(r -> !r.isSuccess())
.collect(Collectors.groupingBy(r -> r.errorType != null ? r.errorType : "HTTP_" + r.statusCode, Collectors.counting()));
}
private long percentile(List<Long> sorted, double p) {
if (sorted.isEmpty()) return -1;
int idx = (int) Math.ceil(p / 100.0 * sorted.size()) - 1;
return sorted.get(Math.max(0, idx));
}
@Override public String toString() {
return String.format("""
========== HTTP Latency Report ==========
Total Requests: %,d
Success: %,d Failed: %,d Error Rate: %.2f%%
Throughput: %.2f QPS
Latency (ms): Min=%,d Max=%,d Avg=%,d
Percentiles: P50=%,d P90=%,d P99=%,d P99.9=%,d
Errors: %s
==========================================""",
total, success, failed, errorRate, throughputQps,
minMs, maxMs, avgMs, p50, p90, p99, p999, errorDistribution);
}
}
public static Report run(Config cfg) throws InterruptedException {
// 連接池
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setMaxTotal(cfg.threads * 2);
cm.setDefaultMaxPerRoute(cfg.threads);
CloseableHttpClient client = HttpClients.custom()
.setConnectionManager(cm)
.evictIdleConnections(30, TimeUnit.SECONDS)
.build();
// 限流器
RateLimiter limiter = cfg.targetQps > 0 ? new RateLimiter(cfg.targetQps) : null;
// 統計收集
List<Result> results = Collections.synchronizedList(new ArrayList<>());
AtomicLong submitted = new AtomicLong();
CountDownLatch latch = new CountDownLatch(cfg.totalRequests);
long startTime = System.currentTimeMillis();
ExecutorService executor = Executors.newFixedThreadPool(cfg.threads);
for (int i = 0; i < cfg.threads; i++) {
executor.submit(() -> {
while (submitted.incrementAndGet() <= cfg.totalRequests) {
if (limiter != null) limiter.acquire();
long reqStart = System.nanoTime();
Result result = executeOnce(client, cfg);
results.add(result);
latch.countDown();
}
});
}
latch.await();
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
long durationMs = System.currentTimeMillis() - startTime;
return new Report(results, durationMs);
}
private static Result executeOnce(CloseableHttpClient client, Config cfg) {
try {
org.apache.hc.client5.http.classic.methods.HttpRequestBase request;
if ("POST".equalsIgnoreCase(cfg.method)) {
HttpPost post = new HttpPost(cfg.url);
if (cfg.body != null) post.setEntity(new StringEntity(cfg.body, ContentType.APPLICATION_JSON));
request = post;
} else {
request = new HttpGet(cfg.url);
}
cfg.headers.forEach(request::setHeader);
var response = client.execute(request);
int status = response.getCode();
EntityUtils.consumeQuietly(response.getEntity());
return new Result(0, 0, status, null, null); // latency 由調用方計算
} catch (org.apache.hc.core5.http.ConnectionException e) {
return new Result(0, 0, 0, "CONNECT_TIMEOUT", e.getMessage());
} catch (org.apache.hc.core5.http.io.HttpRequestException e) {
return new Result(0, 0, 0, "READ_TIMEOUT", e.getMessage());
} catch (IOException e) {
return new Result(0, 0, 0, "IO_EXCEPTION", e.getMessage());
} catch (Exception e) {
return new Result(0, 0, 0, "UNKNOWN", e.getMessage());
}
}
// 簡易令牌桶限流
static class RateLimiter {
private final double permitsPerSecond;
private double storedPermits;
private long lastRefillNs;
RateLimiter(double qps) { this.permitsPerSecond = qps; this.storedPermits = qps; this.lastRefillNs = System.nanoTime(); }
synchronized void acquire() {
refill();
if (storedPermits < 1.0) {
long waitNs = (long) ((1.0 - storedPermits) / permitsPerSecond * 1_000_000_000);
try { Thread.sleep(waitNs / 1_000_000, (int) (waitNs % 1_000_000)); } catch (InterruptedException ignored) {}
refill();
}
storedPermits -= 1.0;
}
private void refill() {
long now = System.nanoTime();
double elapsedSec = (now - lastRefillNs) / 1_000_000_000.0;
storedPermits = Math.min(permitsPerSecond, storedPermits + elapsedSec * permitsPerSecond);
lastRefillNs = now;
}
}
// 主程序示例
public static void main(String[] args) throws Exception {
Config cfg = new Config();
cfg.url = "https://httpbin.org/get";
cfg.threads = 20;
cfg.totalRequests = 500;
cfg.targetQps = 100;
Report report = run(cfg);
System.out.println(report);
// 導出 JSON
// new ObjectMapper().writerWithDefaultPrettyPrinter().writeValue(new File("report.json"), report);
}
}
| 指標 | 含義 | 關注閾值 |
|---|---|---|
| P99 延遲 | 99% 請求的響應時間 | < 200ms (典型 Web API) |
| 錯誤率 | 非 2xx / 異常佔比 | < 0.1% |
| 吞吐量 | 成功 QPS | 達標預期峰值 * 1.5 |
| P999 - P99 | 長尾抖動幅度 | < 50ms (穩定性指標) |
一個優秀的響應速度測試工具,不僅能「跑分」,更能通過百分位分布、錯誤分類、長尾分析,精準定位性能瓶頸。將上述代碼納入項目基礎設施庫,配合 CI/CD 流水線自動化執行,讓性能回歸「可見、可測、可控」。