你們公司的 QPS 是怎么統(tǒng)計出來的?這五種常見方法我踩過一半的坑!
三年前做電商秒殺項目,運維同學說 “網(wǎng)關 QPS 已經(jīng)到 8000 了,趕緊擴容”,但我查應用監(jiān)控卻顯示 “接口 QPS 才 3000”—— 兩邊數(shù)據(jù)差了一倍多,最后發(fā)現(xiàn)是網(wǎng)關統(tǒng)計時把 “健康檢查請求” 也算進去了,白擴容了 3 臺服務器。
作為 Java 的老開發(fā),我太清楚 QPS 統(tǒng)計的重要性:它是判斷系統(tǒng)承載能力、決定是否擴容的核心依據(jù),統(tǒng)計不準會導致 “要么資源浪費,要么系統(tǒng)雪崩”。今天就從 業(yè)務場景、技術原理、核心代碼、踩坑經(jīng)驗 四個維度,拆解 5 種常見的 QPS 統(tǒng)計方法,幫你避開我曾踩過的坑。
一、先明確:不同業(yè)務場景,QPS 統(tǒng)計的 “粒度” 不一樣
在講方法前,得先搞清楚 “你要統(tǒng)計什么粒度的 QPS”—— 不同場景關注的重點完全不同:
業(yè)務場景 | 統(tǒng)計粒度 | 核心需求 |
電商秒殺 | 單個接口(如 /order/seckill) | 實時性(秒級更新)、準確性(排除無效請求) |
微服務集群監(jiān)控 | 服務維度(如訂單服務) | 全局視角(所有接口匯總)、低侵入 |
接口性能優(yōu)化 | 方法級(如 createOrder 方法) | 細粒度(定位慢方法)、結合響應時間 |
離線容量評估 | 全天 / 峰值時段匯總 | 數(shù)據(jù)完整性(不丟日志)、可回溯 |
二、5 種 QPS 統(tǒng)計方法:從網(wǎng)關到應用,從實時到離線
每種方法都有自己的適用場景,我會結合 Java 項目常用技術棧(Spring Boot、Nginx、Prometheus 等),給出可直接復用的代碼。
方法 1:網(wǎng)關層統(tǒng)計(全局視角,適合分布式項目)
適用場景:微服務集群,需要統(tǒng)計所有服務的總 QPS,或單個服務的入口 QPS(如 API 網(wǎng)關、Nginx)。原理:所有請求都經(jīng)過網(wǎng)關,在網(wǎng)關層攔截請求,記錄請求數(shù)和時間,按秒計算 QPS。
實戰(zhàn) 1:Nginx 統(tǒng)計 QPS(中小項目首選)
Nginx 的access_log會記錄每一次請求,配合ngx_http_stub_status_module模塊,能快速統(tǒng)計 QPS。關注工眾號:碼猿技術專欄,回復關鍵詞:1111 獲取阿里內(nèi)部Java性能調(diào)優(yōu)手冊!
- 配置 Nginx(
nginx.conf):
http {
# 開啟狀態(tài)監(jiān)控頁面
server {
listen 8080;
location /nginx-status {
stub_status on;
allow 192.168.0.0/24; # 只允許內(nèi)網(wǎng)訪問
deny all;
}
}
# 記錄詳細請求日志(用于離線分析)
log_format main '$remote_addr [$time_local] "$request" $status $request_time';
server {
listen 80;
server_name api.example.com;
access_log /var/log/nginx/api-access.log main; # 日志路徑
# 轉發(fā)到后端服務
location / {
proxy_pass http://backend-service;
}
}
}- 查看實時 QPS: 訪問
http://192.168.0.100:8080/nginx-status,會顯示:
Active connections: 200
server accepts handled requests
10000 10000 80000
Reading: 0 Writing: 10 Waiting: 190- QPS 計算:
requests/時間,比如 10 秒內(nèi)請求 80000 次,QPS=8000。 - 工具腳本:寫個 Shell 腳本定時統(tǒng)計(每 1 秒執(zhí)行一次):
while true; do
# 取當前請求數(shù)
current=$(curl -s http://192.168.0.100:8080/nginx-status | awk 'NR==3 {print $3}')
sleep 1
# 取1秒后請求數(shù)
next=$(curl -s http://192.168.0.100:8080/nginx-status | awk 'NR==3 {print $3}')
qps=$((next - current))
echo "當前QPS: $qps"
done實戰(zhàn) 2:Spring Cloud Gateway 統(tǒng)計 QPS(Java 微服務)
如果用 Spring Cloud Gateway,可通過自定義過濾器統(tǒng)計 QPS:
@Component
publicclass QpsStatisticsFilter implements GlobalFilter, Ordered {
// 存儲接口QPS:key=接口路徑,value=原子計數(shù)器
privatefinal Map<String, AtomicLong> pathQpsMap = new ConcurrentHashMap<>();
// 定時1秒清零計數(shù)器(避免數(shù)值過大)
@PostConstruct
public void init() {
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
executor.scheduleAtFixedRate(() -> {
// 遍歷所有接口,打印QPS后清零
pathQpsMap.forEach((path, counter) -> {
long qps = counter.getAndSet(0);
log.info("接口[{}] QPS: {}", path, qps);
});
}, 0, 1, TimeUnit.SECONDS);
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 獲取請求路徑(如/order/seckill)
String path = exchange.getRequest().getPath().value();
// 計數(shù)器自增(線程安全)
pathQpsMap.computeIfAbsent(path, k -> new AtomicLong()).incrementAndGet();
// 繼續(xù)轉發(fā)請求
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -1; // 過濾器優(yōu)先級:數(shù)字越小越先執(zhí)行
}
}踩坑經(jīng)驗:
- 網(wǎng)關統(tǒng)計會包含 “健康檢查請求”(如 /actuator/health),需要過濾:在
filter方法中加if (path.startsWith("/actuator")) return chain.filter(exchange);。 - 分布式網(wǎng)關(多節(jié)點)需匯總 QPS,可把數(shù)據(jù)推到 Prometheus,避免單節(jié)點統(tǒng)計不準。
方法 2:應用層埋點(細粒度,適合單服務接口統(tǒng)計)
適用場景:需要統(tǒng)計單個服務的接口級 QPS(如訂單服務的 /create 接口),或方法級 QPS(如 Service 層的 createOrder 方法)。原理:用 AOP 或 Filter 攔截請求 / 方法,記錄請求數(shù),按秒計算 QPS(適合 Java 應用)。
實戰(zhàn):Spring AOP 統(tǒng)計接口 QPS
- 引入依賴(Spring Boot 項目):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>- 自定義切面(統(tǒng)計 Controller 接口 QPS):
@Aspect
@Component
@Slf4j
publicclass ApiQpsAspect {
// 存儲接口QPS:key=接口名(如com.example.OrderController.createOrder),value=計數(shù)器
privatefinal Map<String, AtomicLong> apiQpsMap = new ConcurrentHashMap<>();
// 定時1秒打印QPS并清零
@PostConstruct
public void scheduleQpsPrint() {
Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {
apiQpsMap.forEach((api, counter) -> {
long qps = counter.getAndSet(0);
if (qps > 0) { // 只打印有請求的接口
log.info("[QPS統(tǒng)計] 接口: {}, QPS: {}", api, qps);
}
});
}, 0, 1, TimeUnit.SECONDS);
}
// 切入點:攔截所有Controller方法
@Pointcut("execution(* com.example.*.controller..*(..))")
public void apiPointcut() {}
// 環(huán)繞通知:統(tǒng)計請求數(shù)
@Around("apiPointcut()")
public Object countQps(ProceedingJoinPoint joinPoint) throws Throwable {
// 獲取接口名(類名+方法名)
String apiName = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
// 計數(shù)器自增
apiQpsMap.computeIfAbsent(apiName, k -> new AtomicLong()).incrementAndGet();
// 執(zhí)行原方法
return joinPoint.proceed();
}
}進階優(yōu)化:
- 過濾無效請求:在
countQps中判斷響應狀態(tài)碼,只統(tǒng)計 200/300 的有效請求; - 結合響應時間:在環(huán)繞通知中記錄方法執(zhí)行時間,同時統(tǒng)計 “QPS + 平均響應時間”:
// 記錄響應時間
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long cost = System.currentTimeMillis() - start;
// 存儲響應時間(key=接口名,value=時間列表)
timeMap.computeIfAbsent(apiName, k -> new CopyOnWriteArrayList<>()).add(cost);
// 計算平均響應時間
double avgTime = timeMap.get(apiName).stream().mapToLong(Long::longValue).average().orElse(0);踩坑經(jīng)驗:
- 并發(fā)安全:必須用
AtomicLong計數(shù),避免long變量的線程安全問題; - 性能影響:AOP 會增加微小開銷(單請求約 0.1ms),生產(chǎn)環(huán)境可通過
@Conditional控制只在非生產(chǎn)環(huán)境啟用,或用 Java Agent 替代 AOP 減少侵入。
方法 3:監(jiān)控工具統(tǒng)計(實時可視化,適合運維監(jiān)控)
適用場景:需要實時可視化 QPS、歷史趨勢分析、告警(如 QPS 超過閾值自動發(fā)告警),主流方案是Prometheus + Grafana。原理:應用埋點暴露指標(如 QPS、響應時間),Prometheus 定時拉取指標,Grafana 展示圖表。
實戰(zhàn):Spring Boot + Prometheus + Grafana 統(tǒng)計 QPS
- 引入依賴:
<!-- Micrometer:對接Prometheus的工具 -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>- 配置 Prometheus(
application.yml):
spring:
application:
name:order-service# 服務名,用于Prometheus識別
management:
endpoints:
web:
exposure:
include:prometheus# 暴露/prometheus端點
metrics:
tags:
application:${spring.application.name}# 給指標加服務名標簽
distribution:
percentiles-histogram:
http:
server:
requests:true# 開啟響應時間分位數(shù)統(tǒng)計- 埋點統(tǒng)計 QPS(用 Micrometer 的
MeterRegistry):
@RestController
@RequestMapping("/order")
publicclass OrderController {
// 注入MeterRegistry
privatefinal MeterRegistry meterRegistry;
@Autowired
public OrderController(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@PostMapping("/create")
public String createOrder() {
// 統(tǒng)計/create接口的QPS:meterRegistry會自動按秒聚合
Counter.builder("order.create.qps") // 指標名
.description("訂單創(chuàng)建接口QPS") // 描述
.register(meterRegistry)
.increment(); // 計數(shù)器自增
// 業(yè)務邏輯
return"success";
}
}- 配置 Prometheus 拉取指標(
prometheus.yml):
scrape_configs:
- job_name: 'order-service'
scrape_interval: 1s # 每秒拉取一次(實時性高)
static_configs:
- targets: ['192.168.0.101:8080'] # 應用地址(暴露的actuator端口)- Grafana 配置圖表:
導入 Prometheus 數(shù)據(jù)源,寫 QPS 查詢語句:sum(rate(order_create_qps_total[1m])) by (application)(1 分鐘內(nèi)的平均 QPS);
配置告警:當 QPS>5000 時,發(fā)送郵件 / 釘釘告警。
踩坑經(jīng)驗:
- 拉取間隔:
scrape_interval不要設太小(如 < 100ms),會增加應用和 Prometheus 的壓力; - 指標命名:按 “業(yè)務 + 接口 + 指標類型” 命名(如
order_create_qps),避免和其他指標沖突。
































