李锋镝的博客

  • 首页
  • 时间轴
  • 评论区显眼包🔥
  • 左邻右舍
  • 博友圈
  • 关于我
    • 关于我
    • 另一个网站
    • 我的导航站
    • 网站地图
    • 赞助
  • 留言
  • 🚇开往
Destiny
自是人生长恨水长东
  1. 首页
  2. 其他
  3. 正文

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

2025年10月10日 259点热度 0人点赞 0条评论

开篇:那次因 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. 配置 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;
        }
    }
    }
  2. 查看实时 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。
  3. 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

  1. 引入依赖(Spring Boot 项目):

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
  2. 自定义切面(统计 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

  1. 引入依赖:

    <!-- 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>
  2. 配置 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)
  3. 埋点统计 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";
    }
    }
  4. 配置 Prometheus 拉取指标(prometheus.yml):

    scrape_configs:
    - job_name: 'order-service' # 任务名(自定义)
    scrape_interval: 1s # 拉取间隔(1秒/次,实时性高)
    static_configs:
      - targets: ['192.168.0.101:8080'] # 应用地址(Actuator 暴露的端口)
  5. 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 时,通过邮件/钉钉发送告警通知。

踩坑经验:

  • 拉取间隔:scrape_interval 不宜过小(如 < 100ms),否则会增加应用与 Prometheus 的性能压力;
  • 指标命名:需遵循“业务+接口+指标类型”的格式(如 order_create_qps),避免与其他指标冲突。

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

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

实战:ELK 统计离线 QPS

  1. 应用打印结构化日志(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>
  2. 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();
        }
    }
    }
  3. 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}" # 索引名(按天拆分)
    }
    }
  4. Kibana 分析 QPS:

    • 进入 Kibana 的“Discover”页面,选择 order-request-* 索引;
    • 进入“Visualize”页面,创建“柱状图”:
    • X 轴:选择“@timestamp”,设置间隔为“1 秒”;
    • Y 轴:选择“文档数”(即每秒请求数,对应 QPS);
    • 生成 QPS 趋势图,可筛选特定时间段(如昨天秒杀时段)查看峰值。

踩坑经验:

  • 日志切割:必须按天/按大小切割日志,避免单个文件超过 10GB,导致 Logstash 读取缓慢;
  • 字段清洗:过滤 DEBUG 日志、无效请求日志(如健康检查),减少 Elasticsearch 存储压力。

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

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

实战:MySQL 统计连接数和慢查询

  1. 查看 MySQL 实时连接数与查询数:

    -- 查看当前连接数(QPS 高时连接数会同步增长)
    show status like 'Threads_connected';
    -- 查看累计查询数(计算 DB 层 QPS:(当前值 - 10 秒前值) / 10)
    show status like 'Queries';
  2. 配置慢查询日志(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 # 记录未使用索引的查询(辅助优化)
  3. 分析慢查询与 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. 避坑清单(我踩过的坑,你别再踩)

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

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

十年开发经验告诉我,很多人会陷入“追求精确 QPS”的误区——实际上,QPS 统计的核心目的是“判断系统是否能扛住流量、是否需要扩容”,而非追求“精确到个位数的数值”。

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

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

除非注明,否则均为李锋镝的博客原创文章,转载必须以链接形式标明本文链接

本文链接:https://www.lifengdi.com/others/4515

相关文章

  • Python 3.12并发革命:大厂争相升级的底层逻辑与实战指南
  • 免费开源的在线手绘风格白板工具——Excalidraw
  • 跨平台版本管理神器,开发者的环境配置救星:vfox
  • 一次 Git Rebase 事故,让我彻底明白 Rebase 和 Merge 的区别
  • 为什么 SpringBoot 宁可挨骂也要干掉 spring.factories?
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
标签: QPS 并发 高并发
最后更新:2025年11月21日

李锋镝

既然选择了远方,便只顾风雨兼程。

打赏 点赞
< 上一篇
下一篇 >

文章评论

1 2 3 4 5 6 7 8 9 11 12 13 14 15 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 46 47 48 49 50 51 52 53 54 55 57 58 60 61 62 63 64 65 66 67 69 72 74 76 77 78 79 80 81 82 85 86 87 90 92 93 94 95 96 97 98 99
取消回复

我是人间惆怅客,知君何事泪纵横,断肠声里忆平生。

那年今日(04月14日)

  • 2010年:中国青海玉树大地震
  • 1894年:托马斯·爱迪生展示了其新发明活动电影放映机
  • 1629年:荷兰物理学家克里斯蒂安·惠更斯出生
  • 1578年:西班牙国王腓力三世出生
  • 605年:隋炀帝下令开凿大运河
  • 更多历史事件
最新 热点 随机
最新 热点 随机
Everything Claude Code 详细使用文档 配置Jackson使用字段而不是getter/setter来序列化和反序列化 这个域名注册整整十年了,十年时间,真快啊 Claude Code全维度实战指南:从入门到精通,解锁AI编程新范式 Apollo配置中心中的protalDB的作用是什么 org.apache.ibatis.plugin.Interceptor类详细介绍及使用
AI时代,个人技术博客的出路在哪里?使用WireGuard在Ubuntu 24.04系统搭建VPN这个域名注册整整十年了,十年时间,真快啊WordPress实现用户评论等级排行榜插件WordPress网站换了个字体,差点儿把样式换崩了做了一个WordPress文章热力图插件
开发者必懂的 AI 向量入门:从数学基础到实战应用 分代ZGC这么牛?底层原理是什么? 图解 | 原来这就是网络 使用springboot结合AI生成视频 Java枚举梳理总结一 Excel2016右键新建工作表,打开时提示“因为文件格式或文件扩展名无效。请确定文件未损坏,并且文件扩展名与文件的格式匹配。”的解决办法
标签聚合
设计模式 ElasticSearch docker 多线程 SpringBoot JAVA AI 分布式 MySQL JVM Spring SQL 架构 K8s IDEA WordPress 数据库 AI编程 Redis 日常
友情链接
  • Blogs·CN
  • Honesty
  • Mr.Sun的博客
  • 临窗旋墨
  • 哥斯拉
  • 彬红茶日记
  • 志文工作室
  • 懋和道人
  • 拾趣博客导航
  • 搬砖日记
  • 旧时繁华
  • 林羽凡
  • 瓦匠个人小站
  • 皮皮社
  • 知向前端
  • 蜗牛工作室
  • 韩小韩博客
  • 风渡言

COPYRIGHT © 2026 lifengdi.com. ALL RIGHTS RESERVED.

域名年龄

Theme Kratos Made By Dylan

津ICP备2024022503号-3

京公网安备11011502039375号