四张技术架构图,分别展示了ruoyi-vue-pro的单体架构、yudao-cloud的微服务架构、支付系统界面及数据可视化大屏。

三年前参与一个电商秒杀项目,运维同事看着监控大屏说:“网关QPS已经冲到8000了,得赶紧扩容!” 然而,我这边应用层的监控数据显示,核心接口的QPS才3000出头。两边的数据差了一倍多,经过一番排查,发现是网关在统计时,把大量频繁的健康检查请求(如 /actuator/health)也算了进去,导致我们白白多扩容了3台服务器。
作为有多年经验的Java开发者,我深知QPS(Queries Per Second,每秒查询率)统计的份量。它是评估系统承载能力、决策是否需要进行弹性伸缩的核心依据。统计一旦失准,后果无非两种:要么造成资源浪费,成本飙升;要么导致系统在流量洪峰下崩溃。今天,我们就从业务场景、技术原理、核心代码、踩坑经验这四个维度,系统地拆解五种常见的QPS统计方法,希望能帮你避开我曾经踩过的那些“坑”。
一、先明确:不同业务场景,QPS统计的“粒度”不一样
在探讨具体方法之前,首先要搞清楚一个根本问题:你需要统计什么粒度的QPS? 不同的业务场景,关注的重点截然不同。
| 业务场景 |
统计粒度 |
核心需求 |
| 电商秒杀 |
单个接口(如 /order/seckill) |
实时性(秒级更新)、准确性(排除无效请求) |
| 微服务集群监控 |
服务维度(如订单服务) |
全局视角(所有接口汇总)、低侵入性 |
| 接口性能优化 |
方法级(如 createOrder() 方法) |
细粒度(定位慢方法)、结合响应时间分析 |
| 离线容量评估 |
全天 / 峰值时段汇总 |
数据完整性(不丢日志)、可回溯性 |
二、5种QPS统计方法:从网关到应用,从实时到离线
每种方法都有其最适合的应用场景。下面我将结合Java项目常用的技术栈(如Spring Boot、Nginx、Prometheus等),给出可直接参考或复用的代码示例。
方法1:网关层统计(全局视角,适合分布式项目)
适用场景:微服务集群,需要统计所有服务的总QPS,或单个服务的入口QPS(如通过API网关或Nginx的流量)。
原理:所有外部请求均经由网关,在网关层进行拦截和记录,统计请求数与时间窗口,进而计算QPS。
实战1:Nginx统计QPS(中小项目首选)
Nginx的 access_log 会记录每一次请求,结合 ngx_http_stub_status_module 模块,可以快速获取QPS数据。
-
配置Nginx(nginx.conf):
http {
# 开启状态监控页面
server {
listen 8080;
location /nginx-status {
stub_status on;
allow 192.168.0.0/24; # 只允许内网访问
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; # 日志路径
# 转发到后端服务
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
实战2:Spring Cloud Gateway统计QPS(Java微服务)
如果使用的是Spring Cloud Gateway,可以通过自定义全局过滤器(GlobalFilter)来实现QPS统计。
@Component
public class QpsStatisticsFilter implements GlobalFilter, Ordered {
// 存储接口QPS:key=接口路径,value=原子计数器
private final Map<String, AtomicLong> pathQpsMap = new ConcurrentHashMap<>();
// 定时1秒清零计数器(避免数值无限增长)
@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();
// 计数器自增(线程安全)
pathQpsMap.computeIfAbsent(path, k -> new AtomicLong()).incrementAndGet();
// 继续转发请求
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -1; // 过滤器优先级:数字越小越先执行
}
}
踩坑经验:
- 过滤无效请求:网关统计会包含如
/actuator/health 之类的健康检查请求,需要在 filter 方法中增加过滤逻辑,例如:if (path.startsWith("/actuator")) return chain.filter(exchange);。
- 分布式汇总:在分布式网关(多节点)场景下,单节点的统计数据不具备全局意义。需要将各节点的数据推送到统一的监控中心(如Prometheus)进行聚合计算。
方法2:应用层埋点(细粒度,适合单服务接口统计)
适用场景:需要精确统计单个服务内部某个接口(如订单服务的 /create 接口)甚至某个业务方法的QPS。
原理:利用AOP(面向切面编程)或Servlet Filter在请求处理流程中植入统计逻辑,记录计数并按秒聚合。
实战:Spring AOP统计接口QPS
- 引入依赖(Spring Boot项目):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
-
自定义切面(统计Controller层接口的QPS):
@Aspect
@Component
@Slf4j
public class ApiQpsAspect {
// 存储接口QPS:key=接口名(类名+方法名),value=计数器
private final 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统计] 接口: {}, QPS: {}", api, qps);
}
});
}, 0, 1, TimeUnit.SECONDS);
}
// 切入点:拦截所有Controller包下的方法
@Pointcut("execution(* com.example.*.controller..*(..))")
public void apiPointcut() {}
// 环绕通知:统计请求数
@Around("apiPointcut()")
public Object countQps(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取接口名(类名+方法名)
String apiName = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
// 计数器自增
apiQpsMap.computeIfAbsent(apiName, k -> new AtomicLong()).incrementAndGet();
// 执行原方法
return joinPoint.proceed();
}
}
进阶优化:
- 过滤无效请求:在
countQps 方法中获取响应对象或后续判断状态码,只统计HTTP状态码为2xx/3xx的成功请求。
- 结合响应时间:在环绕通知中记录方法执行耗时,可以同时统计“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);
踩坑经验:
- 并发安全:必须使用
AtomicLong 或类似的线程安全类进行计数,直接使用 long 型变量在多线程环境下会导致计数不准。
- 性能影响:AOP会增加微小的性能开销(单次请求约0.1ms)。在生产环境中,可通过
@Conditional 注解控制切面仅在特定环境(如预发环境)生效,或考虑使用Java Agent等更低侵入的技术。
方法3:监控工具统计(实时可视化,适合运维监控)
适用场景:需要实时可视化的QPS仪表盘、历史趋势分析、以及基于阈值的自动告警。目前的主流方案是 Prometheus + Grafana 组合。
原理:应用通过埋点暴露指标(Metrics),Prometheus定时拉取(Pull)这些指标并存储,Grafana则从Prometheus查询数据并渲染成图表。
实战:Spring Boot + Prometheus + Grafana 统计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>
- 配置应用(
application.yml):
spring:
application:
name: order-service # 服务名,用于Prometheus识别
management:
endpoints:
web:
exposure:
include: prometheus # 暴露 /actuator/prometheus 端点
metrics:
tags:
application: ${spring.application.name} # 给所有指标打上服务名标签
distribution:
percentiles-histogram:
http:
server:
requests: true # 开启HTTP请求响应时间的分位数统计(如p95, p99)
3. **埋点统计QPS**(使用Micrometer的 `MeterRegistry`):
```java
@RestController
@RequestMapping("/order")
public class OrderController {
// 注入MeterRegistry
private final MeterRegistry meterRegistry;
@Autowired
public OrderController(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@PostMapping("/create")
public String createOrder() {
// 统计/create接口的QPS:meterRegistry会自动按秒聚合
Counter.builder("order.create.qps") // 指标名
.description("订单创建接口QPS") // 描述
.register(meterRegistry)
.increment(); // 计数器自增
// 业务逻辑
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数据源。
- 创建Panel,查询语句示例:
sum(rate(order_create_qps_total[1m])) by (application),表示计算过去1分钟内,order.create.qps 指标的平均增长速率(即QPS)。
踩坑经验:
- 拉取间隔:
scrape_interval 不宜设置过小(如低于100ms),否则会给应用和Prometheus服务器带来不必要的压力。通常1s-15s是合理区间。
- 指标命名规范:建议遵循“业务.功能.指标类型”的命名风格(如
order.create.qps),避免与系统或其他业务指标冲突。
方法4:日志分析统计(离线,适合容量评估与问题回溯)
适用场景:需要离线分析历史QPS数据(如评估昨日秒杀活动的峰值压力),或排查过去某时间段内的问题(如上周三QPS突增的原因)。
原理:应用将每一次请求的详细信息(时间戳、接口、状态码、耗时等)以结构化格式(如JSON)输出到日志文件,然后使用日志分析系统(如ELK Stack:Elasticsearch, Logstash, Kibana)或流处理框架(如Flink)进行收集、处理和可视化分析。
实战:ELK统计离线QPS
- 应用打印结构化日志(配置Logback):
<!-- logback-spring.xml -->
<appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/order-service/request.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/var/log/order-service/request.%d{yyyy-MM-dd}.log</fileNamePattern>
</rollingPolicy>
<!-- 输出JSON格式日志,便于后续解析 -->
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeMdcKeyName>requestPath</includeMdcKeyName>
<includeMdcKeyName>requestTime</includeMdcKeyName>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="JSON_FILE" />
</root>
2. **通过MDC(Mapped Diagnostic Context)埋点记录请求信息**:
```java
@Component
public class RequestLogFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
try {
// 记录请求路径到MDC
MDC.put("requestPath", request.getRequestURI());
// 记录请求时间
MDC.put("requestTime", String.valueOf(System.currentTimeMillis()));
chain.doFilter(request, response);
} finally {
// 清除MDC,避免线程池复用导致数据污染
MDC.clear();
}
}
}
- Logstash收集并解析日志,写入Elasticsearch(
logstash.conf):
input {
file {
path => "/var/log/order-service/request.*.log" # 监听日志文件
start_position => "beginning"
sincedb_path => "/dev/null" # 仅测试时使用,每次从头读
}
}
filter {
json {
source => "message" # 解析JSON格式的日志内容
}
提取时间字段并转换为Elasticsearch标准时间格式
date {
match => [ "requestTime", "UNIX_MS" ] # 根据MDC中存储的时间戳格式匹配
target => "@timestamp"
}
}
output {
elasticsearch {
hosts => ["192.168.0.102:9200"] # Elasticsearch地址
index => "order-request-%{+YYYY.MM.dd}" # 按天创建索引
}
}
4. **Kibana分析QPS**:
* 在Kibana中创建针对 `order-request-*` 索引模式的视图。
* 使用“Visualize”功能创建柱状图(Bar Chart),X轴选择时间字段(`@timestamp`),并设置间隔为“1秒”,Y轴选择“计数(Count)”,生成的即是每秒请求数的趋势图,也就是QPS。
**踩坑经验**:
* **日志切割与清理**:必须配置合理的日志滚动策略(如按天或按大小切割),避免单个日志文件过大(超过10GB),导致Logstash读取缓慢甚至阻塞。
* **字段清洗与过滤**:在Logstash的filter阶段,应过滤掉DEBUG级别等无效日志,并只保留必要的字段,以减轻Elasticsearch的存储和索引压力。
### 方法5:数据库层辅助统计(间接手段,适合排查数据库瓶颈)
**适用场景**:当应用QPS突增,怀疑数据库成为瓶颈时,可以通过数据库层面的相关指标间接推断应用层的压力。
**原理**:应用层的业务请求通常会转化为对数据库的查询(SELECT)、更新(UPDATE)等操作。数据库的连接数、每秒查询数(QPS)等指标的变化,与应用的请求压力存在正相关关系,可用于辅助判断。
#### 实战:MySQL统计连接数和慢查询
1. **查看MySQL实时连接数与查询数**:
```sql
-- 查看当前活跃连接数(应用压力大时,连接数通常会增长)
SHOW STATUS LIKE 'Threads_connected';
-- 查看数据库自启动以来的总查询数,通过间隔计算可得到数据库层的QPS
SHOW STATUS LIKE 'Queries';
- 启用并分析慢查询日志(
my.cnf配置):
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1 # 执行时间超过1秒的查询将被记录
- 分析慢查询与QPS的关系:当应用QPS突增时,数据库压力增大,可能导致更多的查询变慢。观察慢查询数量的增长趋势,可以辅助定位因高并发导致的数据库性能瓶颈SQL。
踩坑经验:
- 间接统计有误差:数据库QPS并不等同于应用QPS。一个用户下单请求,可能对应着插入订单、更新库存、记录日志等多个数据库操作。因此,该方法只能作为辅助参考和瓶颈定位的手段。
- 避免监控命令本身成为瓶颈:频繁执行
SHOW STATUS 等命令会消耗数据库资源。建议通过监控系统(如Prometheus的mysql_exporter)定期采集,而非在应用代码中实时查询。
三、经验总结:QPS统计的选型指南和避坑清单
1. 选型指南(根据场景选择方法)
| 需求场景 |
推荐方法 |
优点 |
缺点 |
| 实时全局QPS监控 |
网关层(Nginx/Spring Cloud Gateway) + Prometheus |
全局视角、实时性高 |
分布式环境下配置稍复杂 |
| 单服务接口级细粒度统计 |
应用层AOP埋点 + Micrometer |
粒度细、侵入性相对较低 |
分布式场景需聚合各实例数据 |
| 离线容量评估与问题回溯 |
ELK日志分析 |
数据可长期存储、完整、可回溯 |
实时性差(分钟级延迟) |
| 快速排查数据库瓶颈 |
数据库层指标辅助分析 |
无需修改应用代码 |
误差较大,仅为间接参考 |
2. 避坑清单(前人踩过的坑)
- 过滤无效请求:务必在统计源头(网关或应用层)过滤掉健康检查(如
/actuator/health)、爬虫、静态资源等无效或非业务请求,避免QPS数据“虚高”,误导决策。
- 保证并发安全:任何在内存中进行的计数操作,必须使用线程安全的类,如
AtomicLong、LongAdder 或Micrometer的 Counter,切忌使用普通的 long 变量。
- 平衡实时性与性能:监控数据的采集频率(如Prometheus的
scrape_interval)需要根据业务敏感度和系统资源权衡。过高频率会影响性能,过低则可能错过短时峰值。
- 处理多节点数据汇总:在微服务或分布式网关场景下,必须将多个实例的监控数据汇总到统一平台(如Prometheus)进行计算和展示,以获取准确的集群整体QPS。
- 结合业务上下文解读数据:QPS绝对值的高低需要结合具体业务场景判断。大促秒杀时的QPS和日常流量的QPS标准完全不同。统计的目的是服务业务决策,而非追求一个孤立数字的精确性。
最后:QPS统计的本质是“为决策服务”
多年实践下来,我发现一个常见的误区:过于纠结QPS数值的绝对精确性。实际上,QPS统计的核心目的,是为了判断系统在当前或预期的流量压力下是否健康,以及是否需要采取扩容、限流、优化等行动。
例如在秒杀场景,只要通过可靠的统计方法判断出QPS已超过系统预设的容量阈值(比如4000),那么决策就是扩容。至于这个峰值是4100还是4200,对于“是否需要扩容”这个决策而言,影响微乎其微。关键在于,你所采用的统计方法是否避免了上述那些常见的“坑”,从而使得QPS数据足以支撑你做出正确的技术决策。
因此,下次再讨论“你们系统的QPS是怎么统计的”时,或许可以更深入地聊聊:在你们的业务场景下,选择了哪种粒度的统计?用了哪一层的技术方案?又为此避开了哪些潜在的陷阱?这才是经验的价值所在。
如果你对系统设计、高并发处理或微服务架构中的监控实践有更多兴趣,欢迎到 云栈社区 与更多开发者交流探讨。社区内积累了丰富的后端架构、运维监控以及数据库优化等领域的实战经验和资源。