你们公司的 QPS 是怎么统计出来的?这 5 种常见方法我踩过一半的坑

你们公司的 QPS 是怎么统计出来的?这 5 种常见方法我踩过一半的坑

(八年 Java 开发实战:从业务场景到代码落地,附避坑指南)

开篇:那次因 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。

  1. 配置 Nginxnginx.conf):
ini 复制代码
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;
        }
    }
}
  1. 查看实时 QPS

    访问http://192.168.0.100:8080/nginx-status,会显示:

yaml 复制代码
Active connections: 200
server accepts handled requests
 10000  10000  80000
Reading: 0 Writing: 10 Waiting: 190
  • QPS 计算requests/时间,比如 10 秒内请求 80000 次,QPS=8000。

  • 工具脚本:写个 Shell 脚本定时统计(每 1 秒执行一次):

bash 复制代码
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=$((next - current))
    echo "当前QPS: $qps"
done
实战 2:Spring Cloud Gateway 统计 QPS(Java 微服务)

如果用 Spring Cloud Gateway,可通过自定义过滤器统计 QPS:

typescript 复制代码
@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);
  • 分布式网关(多节点)需汇总 QPS,可把数据推到 Prometheus,避免单节点统计不准。

方法 2:应用层埋点(细粒度,适合单服务接口统计)

适用场景 :需要统计单个服务的接口级 QPS(如订单服务的 /create 接口),或方法级 QPS(如 Service 层的 createOrder 方法)。
原理:用 AOP 或 Filter 拦截请求 / 方法,记录请求数,按秒计算 QPS(适合 Java 应用)。

实战:Spring AOP 统计接口 QPS
  1. 引入依赖(Spring Boot 项目):
xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
  1. 自定义切面(统计 Controller 接口 QPS):
less 复制代码
@Aspect
@Component
@Slf4j
public class ApiQpsAspect {
    // 存储接口QPS:key=接口名(如com.example.OrderController.createOrder),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中判断响应状态码,只统计 200/300 的有效请求;

  • 结合响应时间:在环绕通知中记录方法执行时间,同时统计 "QPS + 平均响应时间":

ini 复制代码
// 记录响应时间
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 替代 AOP 减少侵入。

方法 3:监控工具统计(实时可视化,适合运维监控)

适用场景 :需要实时可视化 QPS、历史趋势分析、告警(如 QPS 超过阈值自动发告警),主流方案是Prometheus + Grafana
原理:应用埋点暴露指标(如 QPS、响应时间),Prometheus 定时拉取指标,Grafana 展示图表。

实战:Spring Boot + Prometheus + Grafana 统计 QPS
  1. 引入依赖
xml 复制代码
<!-- 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>
  1. 配置 Prometheusapplication.yml):
yaml 复制代码
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 # 开启响应时间分位数统计
  1. 埋点统计 QPS (用 Micrometer 的MeterRegistry):
kotlin 复制代码
@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";
    }
}
  1. 配置 Prometheus 拉取指标prometheus.yml):
yaml 复制代码
scrape_configs:
  - job_name: 'order-service'
    scrape_interval: 1s # 每秒拉取一次(实时性高)
    static_configs:
      - targets: ['192.168.0.101:8080'] # 应用地址(暴露的actuator端口)
  1. Grafana 配置图表
  • 导入 Prometheus 数据源,写 QPS 查询语句:sum(rate(order_create_qps_total[1m])) by (application)(1 分钟内的平均 QPS);

  • 配置告警:当 QPS>5000 时,发送邮件 / 钉钉告警。

踩坑经验

  • 拉取间隔:scrape_interval不要设太小(如 < 100ms),会增加应用和 Prometheus 的压力;
  • 指标命名:按 "业务 + 接口 + 指标类型" 命名(如order_create_qps),避免和其他指标冲突。

方法 4:日志分析统计(离线,适合容量评估)

适用场景 :需要离线统计 QPS(如分析昨天秒杀的峰值 QPS),或排查历史问题(如上周三 QPS 突增的原因)。
原理:应用打印请求日志(包含时间、接口、状态码),用 ELK(Elasticsearch+Logstash+Kibana)或 Flink 分析日志,计算 QPS。

实战:ELK 统计离线 QPS
  1. 应用打印结构化日志(用 Logback):
xml 复制代码
<!-- 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>
  1. MDC 埋点记录请求信息
scala 复制代码
@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();
        }
    }
}
  1. Logstash 收集日志到 Elasticsearchlogstash.conf):
ini 复制代码
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", "yyyy-MM-dd HH:mm:ss"]
        target => "@timestamp"
    }
}

output {
    elasticsearch {
        hosts => ["192.168.0.102:9200"] # Elasticsearch地址
        index => "order-request-%{+YYYY.MM.dd}" # 索引名
    }
}
  1. Kibana 分析 QPS
  • 进入 Kibana 的 "Discover",选择order-request-*索引;

  • 用 "Visualize" 创建柱状图,X 轴选 "时间(1 秒间隔)",Y 轴选 "文档数"(即每秒请求数),就是 QPS 趋势图。

踩坑经验

  • 日志切割:按天切割日志,避免单个日志文件太大(如超过 10GB),导致 Logstash 读取缓慢;
  • 字段清洗:过滤掉无用日志(如 DEBUG 级别的日志),减少 Elasticsearch 的存储压力。

方法 5:数据库层辅助统计(间接,适合排查 DB 瓶颈)

适用场景 :当 QPS 突增导致数据库压力大时,通过数据库指标间接判断 QPS(如 MySQL 的连接数、慢查询数)。
原理:数据库的请求数和应用 QPS 正相关(如 1 个订单请求对应 2 次 DB 查询),可通过 DB 指标反推应用 QPS。

实战:MySQL 统计连接数和慢查询
  1. 查看 MySQL 实时连接数
sql 复制代码
-- 查看当前连接数(QPS高时连接数会增长)
show status like 'Threads_connected';
-- 查看每秒查询数(DB层QPS,可反推应用QPS)
show status like 'Queries';
  1. 配置慢查询日志my.cnf):
ini 复制代码
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1 # 超过1秒的查询记录为慢查询
  1. 分析慢查询与 QPS 的关系

    当应用 QPS 突增时,慢查询数会同步增长(如秒杀时 QPS 从 1000 涨到 5000,慢查询从 10 / 秒涨到 100 / 秒),可通过慢查询日志定位瓶颈 SQL。

踩坑经验

  • 间接统计有误差:DB QPS ≠ 应用 QPS(1 个应用请求可能对应多个 DB 查询),只能作为辅助判断;
  • 避免频繁执行show status:该命令会占用 DB 资源,建议每 10 秒执行一次,而非实时执行。

三、八年经验总结:QPS 统计的选型指南和避坑清单

1. 选型指南(按场景选方法)

需求场景 推荐方法 优点 缺点
实时全局 QPS 监控 网关层(Nginx/Gateway)+ Prometheus 全局视角、实时性高 配置复杂(多节点需汇总)
单服务接口级 QPS 统计 应用层 AOP + Micrometer 细粒度、侵入性低 分布式场景需汇总数据
离线容量评估 ELK 日志分析 可回溯、数据完整 实时性差(延迟分钟级)
排查 DB 瓶颈 数据库层辅助统计 无需应用埋点 误差大,只能间接判断

2. 避坑清单(我踩过的坑,你别再踩)

  1. 别统计无效请求:过滤健康检查(/actuator/health)、爬虫请求(User-Agent 包含 spider),避免 QPS 虚高;
  2. 并发安全要保证 :计数用AtomicLongCounter(Micrometer),避免用long变量导致计数不准;
  3. 实时性和性能平衡:Prometheus 拉取间隔设为 1-5 秒,AOP 埋点只统计核心接口,避免过度消耗资源;
  4. 多节点数据汇总:分布式网关或多实例应用,需将 QPS 数据推到统一监控平台(如 Prometheus),避免单节点统计偏差;
  5. 结合业务上下文:QPS 统计要关联业务场景(如秒杀时的 QPS 和日常 QPS 标准不同),避免 "唯 QPS 论"。

最后:QPS 统计的本质是 "为决策服务"

八年开发下来,我发现很多人陷入 "追求精确 QPS" 的误区 ------ 其实 QPS 统计的核心目的是 "判断系统是否能扛住流量,是否需要扩容",而非追求 "精确到个位数的 QPS"。

比如秒杀场景,只要统计出 QPS 超过 4000(系统阈值),就该扩容,至于是 4001 还是 4002,差别不大。关键是选对统计方法,避开无效请求、并发安全、数据偏差这些坑,让 QPS 数据能真正指导决策。

下次有人问你 "你们公司 QPS 怎么统计的",别只说 "用了 Prometheus",把场景、方法、踩过的坑讲清楚 ------ 这才是八年开发该有的深度。

相关推荐
似水流年流不尽思念几秒前
描述一下 Spring Bean 的生命周期 ?
后端·面试
带刺的坐椅13 分钟前
老码农教你:Solon + EasyExcel 导出工具
java·excel·solon·easyexcel
pany17 分钟前
体验一款编程友好的显示器
前端·后端·程序员
Java水解23 分钟前
深度剖析【Spring】事务:万字详解,彻底掌握传播机制与事务原理
后端·spring
迷知悟道23 分钟前
java面向对象的四大核心特征之继承---超详细(保姆级)
java
lixn24 分钟前
深入理解JVM字节码:invokedynamic
java·jvm
数据智能老司机27 分钟前
探索Java 全新的线程模型——结构化并发
java·性能优化·架构
顾林海27 分钟前
网络江湖的两大护法:TCP与UDP的爱恨情仇
网络协议·面试·性能优化
数据智能老司机28 分钟前
探索Java 全新的线程模型——作用域值
java·性能优化·架构
开始学java29 分钟前
继承树追溯
后端