开篇:那次因 QPS 统计不准差点背锅的经历
三年前做电商秒杀项目时,运维同学紧急反馈“网关 QPS 已经到 8000 了,赶紧扩容”,但我查看应用监控却显示“接口 QPS 才 3000”——两边数据相差一倍多。最后排查发现,网关统计时误将“健康检查请求”纳入其中,导致数据虚高,白白扩容了 3 台服务器。
作为拥有十年 Java 开发经验的从业者,我深知 QPS 统计的核心价值:它是判断系统承载能力、决定是否扩容的关键依据,统计不准轻则造成资源浪费,重则引发系统雪崩。今天,我将从业务场景、技术原理、核心代码、踩坑经验四个维度,拆解 5 种常见的 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; # 限制内网 IP 段访问 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- QPS 计算方式:
requests/时间,例如 10 秒内请求 80000 次,则 QPS = 80000 / 10 = 8000。
- QPS 计算方式:
-
Shell 脚本定时统计(每 1 秒执行一次):
while true; do # 读取当前请求数 current=$(curl -s http://192.168.0.100:8080/nginx-status | awk 'NR==3 {print $3}') sleep 1 # 读取 1 秒后请求数 next=$(curl -s http://192.168.0.100:8080/nginx-status | awk 'NR==3 {print $3}') # 计算并输出 QPS qps=$((next - current)) echo "当前 QPS: $qps" done
实战 2:Spring Cloud Gateway 统计 QPS(Java 微服务)
若项目使用 Spring Cloud Gateway,可通过自定义全局过滤器实现 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();
// 过滤健康检查请求(避免数据虚高)
if (path.startsWith("/actuator")) {
return chain.filter(exchange);
}
// 计数器自增(线程安全)
pathQpsMap.computeIfAbsent(path, k -> new AtomicLong()).incrementAndGet();
// 继续转发请求
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -1; // 过滤器优先级:数字越小越先执行
}
}
踩坑经验:
- 网关统计易包含“健康检查请求”(如 /actuator/health),需在过滤逻辑中明确排除;
- 分布式网关(多节点部署)需将 QPS 数据汇总到统一平台(如 Prometheus),避免单节点统计偏差。
方法 2:应用层埋点(细粒度,适合单服务接口统计)
适用场景:需统计单个服务的接口级 QPS(如订单服务的 /create 接口),或方法级 QPS(如 Service 层的 createOrder 方法)。
原理:通过 AOP 或 Filter 拦截请求/方法,记录请求数,按秒计算 QPS(适合 Java 应用)。
实战: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<>(); // 存储响应时间:key=接口名,value=时间列表(用于计算平均值) private final Map<String, CopyOnWriteArrayList<Long>> timeMap = new ConcurrentHashMap<>(); // 定时 1 秒打印 QPS 与平均响应时间,并清零计数器 @PostConstruct public void scheduleQpsPrint() { Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> { apiQpsMap.forEach((api, counter) -> { long qps = counter.getAndSet(0); if (qps > 0) { // 仅打印有请求的接口 // 计算平均响应时间 double avgTime = timeMap.getOrDefault(api, new CopyOnWriteArrayList<>()) .stream() .mapToLong(Long::longValue) .average() .orElse(0); log.info("[QPS统计] 接口: {}, QPS: {}, 平均响应时间: {:.2f}ms", api, qps, avgTime); // 清空响应时间列表 timeMap.get(api).clear(); } }); }, 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(); // 记录请求开始时间 long start = System.currentTimeMillis(); // 执行原方法(业务逻辑) Object result = joinPoint.proceed(); // 计算响应时间 long cost = System.currentTimeMillis() - start; // 计数器自增(线程安全) apiQpsMap.computeIfAbsent(apiName, k -> new AtomicLong()).incrementAndGet(); // 记录响应时间 timeMap.computeIfAbsent(apiName, k -> new CopyOnWriteArrayList<>()).add(cost); return result; } }
进阶优化:
- 过滤无效请求:在
countQps方法中判断响应状态码,仅统计 200/300 等成功状态的请求; - 控制埋点开关:通过
@Conditional注解实现“非生产环境启用、生产环境关闭”,减少性能损耗。
踩坑经验:
- 并发安全:必须使用
AtomicLong或ConcurrentHashMap,避免long变量的线程安全问题; - 性能影响:AOP 埋点会增加约 0.1ms/请求的开销,生产环境可改用 Java Agent 替代 AOP,进一步降低侵入性。
方法 3:监控工具统计(实时可视化,适合运维监控)
适用场景:需实时可视化 QPS、分析历史趋势、配置告警(如 QPS 超阈值自动发通知),主流方案为 Prometheus + Grafana。
原理:应用埋点暴露指标(QPS、响应时间等),Prometheus 定时拉取指标,Grafana 生成可视化图表并配置告警。
实战:Spring Boot + Prometheus + Grafana 统计 QPS
-
引入依赖:
<!-- Micrometer:对接 Prometheus 的工具 --> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> </dependency> <!-- Actuator:暴露监控端点 --> <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 端点(供 Prometheus 拉取指标) metrics: tags: application: ${spring.application.name} # 给指标添加服务名标签(便于筛选) distribution: percentiles-histogram: http: server: requests: true # 开启响应时间分位数统计(如 P95、P99) -
埋点统计 QPS(使用 Micrometer 的 MeterRegistry):
@RestController @RequestMapping("/order") public class OrderController { // 注入 MeterRegistry(Micrometer 核心类,用于注册指标) private final MeterRegistry meterRegistry; @Autowired public OrderController(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; } @PostMapping("/create") public String createOrder() { // 统计 /order/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 # 拉取间隔(1秒/次,实时性高) static_configs: - targets: ['192.168.0.101:8080'] # 应用地址(Actuator 暴露的端口) -
Grafana 配置图表与告警:
- 导入 Prometheus 数据源:在 Grafana 中添加“Prometheus”类型数据源,填写 Prometheus 地址(如
http://192.168.0.103:9090); - 创建 QPS 图表:使用查询语句
sum(rate(order_create_qps_total[1m])) by (application)(计算 1 分钟内的平均 QPS); - 配置告警:当 QPS > 5000 时,通过邮件/钉钉发送告警通知。
- 导入 Prometheus 数据源:在 Grafana 中添加“Prometheus”类型数据源,填写 Prometheus 地址(如
踩坑经验:
- 拉取间隔:
scrape_interval不宜过小(如 < 100ms),否则会增加应用与 Prometheus 的性能压力; - 指标命名:需遵循“业务+接口+指标类型”的格式(如
order_create_qps),避免与其他指标冲突。
方法 4:日志分析统计(离线,适合容量评估)
适用场景:需离线统计 QPS(如分析昨天秒杀的峰值 QPS),或排查历史问题(如上周三 QPS 突增原因)。
原理:应用打印包含“时间、接口、状态码”的结构化日志,通过 ELK(Elasticsearch+Logstash+Kibana)或 Flink 分析日志,计算 QPS。
实战:ELK 统计离线 QPS
-
应用打印结构化日志(Logback 配置):
<!-- logback-spring.xml --> <configuration> <!-- 输出 JSON 格式日志到文件(按天切割) --> <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> <maxHistory>30</maxHistory> <!-- 保留 30 天日志 --> </rollingPolicy> <!-- 使用 LogstashEncoder 输出 JSON 格式 --> <encoder class="net.logstash.logback.encoder.LogstashEncoder"> <includeMdcKeyName>requestPath</includeMdcKeyName> <!-- 包含请求路径 --> <includeMdcKeyName>requestTime</includeMdcKeyName> <!-- 包含请求时间戳 --> <includeMdcKeyName>statusCode</includeMdcKeyName> <!-- 包含响应状态码 --> </encoder> </appender> <root level="INFO"> <appender-ref ref="JSON_FILE" /> </root> </configuration> -
MDC 埋点记录请求信息(Filter 实现):
@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); // 记录响应状态码 MDC.put("statusCode", String.valueOf(response.getStatus())); } 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 格式日志 json { source => "message" # 从 message 字段解析 JSON } # 转换请求时间戳为 Elasticsearch 支持的格式 date { match => ["requestTime", "yyyy-MM-dd HH:mm:ss"] target => "@timestamp" # 写入 Elasticsearch 的时间字段 } # 过滤无效日志(如 DEBUG 级别) if [level] != "INFO" { drop {} } } output { # 输出到 Elasticsearch elasticsearch { hosts => ["192.168.0.102:9200"] # Elasticsearch 地址 index => "order-request-%{+YYYY.MM.dd}" # 索引名(按天拆分) } } -
Kibana 分析 QPS:
- 进入 Kibana 的“Discover”页面,选择
order-request-*索引; - 进入“Visualize”页面,创建“柱状图”:
- X 轴:选择“@timestamp”,设置间隔为“1 秒”;
- Y 轴:选择“文档数”(即每秒请求数,对应 QPS);
- 生成 QPS 趋势图,可筛选特定时间段(如昨天秒杀时段)查看峰值。
- 进入 Kibana 的“Discover”页面,选择
踩坑经验:
- 日志切割:必须按天/按大小切割日志,避免单个文件超过 10GB,导致 Logstash 读取缓慢;
- 字段清洗:过滤 DEBUG 日志、无效请求日志(如健康检查),减少 Elasticsearch 存储压力。
方法 5:数据库层辅助统计(间接,适合排查 DB 瓶颈)
适用场景:当 QPS 突增导致数据库压力大时,通过数据库指标间接判断应用 QPS(如 MySQL 的连接数、慢查询数)。
原理:数据库请求数与应用 QPS 正相关(如 1 个订单请求对应 2 次 DB 查询),可通过 DB 指标反推应用 QPS。
实战:MySQL 统计连接数和慢查询
-
查看 MySQL 实时连接数与查询数:
-- 查看当前连接数(QPS 高时连接数会同步增长) show status like 'Threads_connected'; -- 查看累计查询数(计算 DB 层 QPS:(当前值 - 10 秒前值) / 10) show status like 'Queries'; -
配置慢查询日志(my.cnf):
slow_query_log = 1 # 开启慢查询日志 slow_query_log_file = /var/log/mysql/slow.log # 慢查询日志路径 long_query_time = 1 # 超过 1 秒的查询记录为慢查询 log_queries_not_using_indexes = 1 # 记录未使用索引的查询(辅助优化) -
分析慢查询与 QPS 的关系:
当应用 QPS 突增时,慢查询数会同步增长(如秒杀时 QPS 从 1000 涨到 5000,慢查询从 10 次/秒涨到 100 次/秒)。通过mysqldumpslow工具分析慢查询日志,可定位瓶颈 SQL:# 查看最耗时的 10 条慢查询 mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
踩坑经验:
- 间接统计有误差:DB QPS ≠ 应用 QPS(1 个应用请求可能对应多个 DB 查询),仅能作为辅助判断;
- 避免频繁执行
show status:该命令会占用 DB 资源,建议每 10 秒执行一次,而非实时查询。
三、十年经验总结:QPS 统计的选型指南和避坑清单
1. 选型指南(按场景选方法)
| 需求场景 | 推荐方法 | 优点 | 缺点 |
|---|---|---|---|
| 实时全局 QPS 监控 | 网关层(Nginx/Gateway)+ Prometheus | 全局视角、实时性高 | 配置复杂(多节点需汇总) |
| 单服务接口级 QPS 统计 | 应用层 AOP + Micrometer | 细粒度、侵入性低 | 分布式场景需汇总数据 |
| 离线容量评估 | ELK 日志分析 | 可回溯、数据完整 | 实时性差(延迟分钟级) |
| 排查 DB 瓶颈 | 数据库层辅助统计 | 无需应用埋点 | 误差大,仅能间接判断 |
2. 避坑清单(我踩过的坑,你别再踩)
- 别统计无效请求:过滤健康检查(/actuator/health)、爬虫请求(User-Agent 含 spider),避免 QPS 虚高;
- 并发安全要保证:计数必须用
AtomicLong或 Micrometer 的Counter,禁止用普通long变量; - 平衡实时性与性能:Prometheus 拉取间隔设为 1-5 秒,AOP 仅统计核心接口,避免过度消耗资源;
- 多节点数据汇总:分布式网关或多实例应用,需将 QPS 数据推到统一平台(如 Prometheus),避免单节点偏差;
- 结合业务上下文:QPS 标准需关联场景(如秒杀 QPS 阈值远高于日常),避免“唯 QPS 论”。
最后:QPS 统计的本质是“为决策服务”
十年开发经验告诉我,很多人会陷入“追求精确 QPS”的误区——实际上,QPS 统计的核心目的是“判断系统是否能扛住流量、是否需要扩容”,而非追求“精确到个位数的数值”。
比如秒杀场景,只要统计出 QPS 超过 4000(系统阈值),就该扩容,至于是 4001 还是 4002,差别不大。关键是选对统计方法,避开无效请求、并发安全、数据偏差这些坑,让 QPS 数据真正能指导决策。
下次再有人问你“你们公司 QPS 怎么统计的”,别只说“用了 Prometheus”,把场景、方法、踩过的坑讲清楚——这才是资深开发该有的深度。
除非注明,否则均为李锋镝的博客原创文章,转载必须以链接形式标明本文链接
文章评论