大数跨境
0
0

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

你们公司的QPS是怎么统计出来的?这5种常见方法我踩过一半的坑! 终码一生
2025-09-22
1
点击“终码一生”,关注,置顶公众号
每日技术干货,第一时间送达!
三年前做电商秒杀项目,运维同学说 “网关 QPS 已经到 8000 了,赶紧扩容”,但我查应用监控却显示 “接口 QPS 才 3000”—— 两边数据差了一倍多,最后发现是网关统计时把 “健康检查请求” 也算进去了,白扩容了 3 台服务器。
作为 Java 的老开发,我太清楚 QPS 统计的重要性:它是判断系统承载能力、决定是否扩容的核心依据,统计不准会导致 “要么资源浪费,要么系统雪崩”。今天就从 业务场景、技术原理、核心代码、踩坑经验 四个维度,拆解 5 种常见的 QPS 统计方法,帮你避开我曾踩过的坑。
图片
01
不同业务场景
图片
在讲方法前,得先搞清楚 “你要统计什么粒度的 QPS”—— 不同场景关注的重点完全不同:
业务场景
统计粒度
核心需求
电商秒杀
单个接口(如 /order/seckill)
实时性(秒级更新)、准确性(排除无效请求)
微服务集群监控
服务维度(如订单服务)
全局视角(所有接口汇总)、低侵入
接口性能优化
方法级(如 createOrder 方法)
细粒度(定位慢方法)、结合响应时间
离线容量评估
全天 / 峰值时段汇总
数据完整性(不丢日志)、可回溯
图片
02
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。
配置 Nginxnginx.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
serveracceptshandledrequests
10000  10000  80000
Reading: 0 Writing: 10 Waiting:190
  • QPS 计算requests/时间,比如 10 秒内请求 80000 次,QPS=8000。
  • 工具脚本:写个 Shell 脚本定时统计(每 1 秒执行一次):
whiletruedo
    # 取当前请求数
    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:
@Component
publicclassQpsStatisticsFilterimplementsGlobalFilterOrdered{
    // 存储接口QPS:key=接口路径,value=原子计数器
    privatefinal Map<String, AtomicLong> pathQpsMap = new ConcurrentHashMap<>();

    // 定时1秒清零计数器(避免数值过大)
    @PostConstruct
    publicvoidinit(){
        ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
        executor.scheduleAtFixedRate(() -> {
            // 遍历所有接口,打印QPS后清零
            pathQpsMap.forEach((path, counter) -> {
                long qps = counter.getAndSet(0);
                log.info("接口[{}] QPS: {}", path, qps);
            });
        }, 01, 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
    publicintgetOrder(){
        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
引入依赖(Spring Boot 项目):
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
自定义切面(统计 Controller 接口 QPS):
@Aspect
@Component
@Slf4j
publicclassApiQpsAspect{
    // 存储接口QPS:key=接口名(如com.example.OrderController.createOrder),value=计数器
    privatefinal Map<String, AtomicLong> apiQpsMap = new ConcurrentHashMap<>();

    // 定时1秒打印QPS并清零
    @PostConstruct
    publicvoidscheduleQpsPrint(){
        Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {
            apiQpsMap.forEach((api, counter) -> {
                long qps = counter.getAndSet(0);
                if (qps > 0) { // 只打印有请求的接口
                    log.info("[QPS统计] 接口: {}, QPS: {}", api, qps);
                }
            });
        }, 01, TimeUnit.SECONDS);
    }

    // 切入点:拦截所有Controller方法
    @Pointcut("execution(* com.example.*.controller..*(..))")
    publicvoidapiPointcut(){}

    // 环绕通知:统计请求数
    @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 + 平均响应时间”:
// 记录响应时间
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);
方法 3:监控工具统计(实时可视化,适合运维监控)
并发安全:必须用AtomicLong计数,避免long变量的线程安全问题
实战: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# 暴露/prometheus端点
metrics:
    tags:
      application:${spring.application.name}# 给指标加服务名标签
    distribution:
      percentiles-histogram:
        http:
          server:
            requests:true# 开启响应时间分位数统计
埋点统计 QPS(用 Micrometer 的MeterRegistry):
@RestController
@RequestMapping("/order")
publicclassOrderController{
    // 注入MeterRegistry
    privatefinal MeterRegistry meterRegistry;

    @Autowired
    publicOrderController(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.yml
scrape_configs:
  -job_name:'order-service'
    scrape_interval:1s# 每秒拉取一次(实时性高)
    static_configs:
      -targets:['192.168.0.101:8080']# 应用地址(暴露的actuator端口)
Grafana 配置图表:
  • 导入 Prometheus 数据源,写 QPS 查询语句:sum(rate(order_create_qps_total[1m])) by (application)(1 分钟内的平均 QPS);
  • 配置告警:当 QPS>5000 时,发送邮件 / 钉钉告警。
方法 4:日志分析统计(离线,适合容量评估)
  • 适用场景:需要离线统计 QPS(如分析昨天秒杀的峰值 QPS),或排查历史问题(如上周三 QPS 突增的原因)。
  • 原理:应用打印请求日志(包含时间、接口、状态码),用 ELK(Elasticsearch+Logstash+Kibana)或 Flink 分析日志,计算 QPS。
实战:ELK 统计离线 QPS
应用打印结构化日志(用 Logback):
<!-- logback-spring.xml -->
<appendername="JSON_FILE"class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>/var/log/order-service/request.log</file>
    <rollingPolicyclass="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <fileNamePattern>/var/log/order-service/request.%d{yyyy-MM-dd}.log</fileNamePattern>
    </rollingPolicy>
    <!-- 输出JSON格式日志 -->
    <encoderclass="net.logstash.logback.encoder.LogstashEncoder">
        <includeMdcKeyName>requestPath</includeMdcKeyName>
        <includeMdcKeyName>requestTime</includeMdcKeyName>
    </encoder>
</appender>

<rootlevel="INFO">
    <appender-refref="JSON_FILE" />
</root>
MDC 埋点记录请求信息:
@Component
publicclassRequestLogFilterextendsOncePerRequestFilter{
    @Override
    protectedvoiddoFilterInternal(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.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""yyyy-MM-dd HH:mm:ss"]
        target => "@timestamp"
    }
}

output {
    elasticsearch {
        hosts => ["192.168.0.102:9200"] # Elasticsearch地址
        index => "order-request-%{+YYYY.MM.dd}" # 索引名
    }
}
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 统计连接数和慢查询
查看 MySQL 实时连接数:
-- 查看当前连接数(QPS高时连接数会增长)
showstatuslike'Threads_connected';
-- 查看每秒查询数(DB层QPS,可反推应用QPS)
showstatuslike'Queries';
my.cnf
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1 # 超过1秒的查询记录为慢查询
分析慢查询与 QPS 的关系: 当应用 QPS 突增时,慢查询数会同步增长(如秒杀时 QPS 从 1000 涨到 5000,慢查询从 10 / 秒涨到 100 / 秒),可通过慢查询日志定位瓶颈 SQL。
踩坑经验:
间接统计有误差:DB QPS ≠ 应用 QPS(1 个应用请求可能对应多个 DB 查询),只能作为辅助判断;
避免频繁执行show status:该命令会占用 DB 资源,建议每 10 秒执行一次,而非实时执行。
图片
03
经验总结
图片
1. 选型指南(按场景选方法)
需求场景
推荐方法
优点
缺点
实时全局 QPS 监控
网关层(Nginx/Gateway)+ Prometheus
全局视角、实时性高
配置复杂(多节点需汇总)
单服务接口级 QPS 统计
应用层 AOP + Micrometer
细粒度、侵入性低
分布式场景需汇总数据
离线容量评估
ELK 日志分析
可回溯、数据完整
实时性差(延迟分钟级)
排查 DB 瓶颈
数据库层辅助统计
无需应用埋点
误差大,只能间接判断
2. 避坑清单(我踩过的坑,你别再踩)
  1. 别统计无效请求:过滤健康检查(/actuator/health)、爬虫请求(User-Agent 包含 spider),避免 QPS 虚高;
  2. 并发安全要保证:计数用AtomicLongCounterMicrometer),避免用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”,把场景、方法、踩过的坑讲清楚 —— 这才是多年开发该有的深度。
END
PS:防止找不到本篇文章,可以收藏点赞,方便翻阅查找哦。



往期推荐



我司使用了两年的高效日志打印工具,非常好用!

Redis与本地缓存组合使用...

老司机总结的12条SQL优化方案(非常实用)

中毒太深!离开Spring我居然连最基本的接口都不会写了。。。

开源项目|Java开发身份证号码识别系统

SpringBoot + minio实现分片上传、秒传、续传


【声明】内容源于网络
0
0
终码一生
开发者聚集地。分享Java相关开发技术(JVM,多线程,高并发,性能调优等),开源项目,常见开发问题和前沿科技资讯!
内容 1876
粉丝 0
终码一生 开发者聚集地。分享Java相关开发技术(JVM,多线程,高并发,性能调优等),开源项目,常见开发问题和前沿科技资讯!
总阅读79
粉丝0
内容1.9k