|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Nginx 的access_log会记录每一次请求,配合ngx_http_stub_status_module模块,能快速统计 QPS。
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;
}
}
}
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 秒执行一次):
whiletrue; 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
@Component
publicclassQpsStatisticsFilterimplementsGlobalFilter, Ordered{
// 存储接口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);
});
}, 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
publicintgetOrder(){
return -1; // 过滤器优先级:数字越小越先执行
}
}
-
网关统计会包含 “健康检查请求”(如 /actuator/health),需要过滤:在方法中加filterif (path.startsWith("/actuator")) return chain.filter(exchange);。 -
分布式网关(多节点)需汇总 QPS,可把数据推到 Prometheus,避免单节点统计不准。
-
适用场景:需要统计单个服务的接口级 QPS(如订单服务的 /create 接口),或方法级 QPS(如 Service 层的 createOrder 方法)。 -
原理:用 AOP 或 Filter 拦截请求 / 方法,记录请求数,按秒计算 QPS(适合 Java 应用)。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
@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);
}
});
}, 0, 1, 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);
并发安全:必须用AtomicLong计数,避免long变量的线程安全问题;
<!-- 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# 开启响应时间分位数统计
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端口)
-
导入 Prometheus 数据源,写 QPS 查询语句:sum(rate(order_create_qps_total[1m])) by (application)(1 分钟内的平均 QPS); -
配置告警:当 QPS>5000 时,发送邮件 / 钉钉告警。
-
适用场景:需要离线统计 QPS(如分析昨天秒杀的峰值 QPS),或排查历史问题(如上周三 QPS 突增的原因)。 -
原理:应用打印请求日志(包含时间、接口、状态码),用 ELK(Elasticsearch+Logstash+Kibana)或 Flink 分析日志,计算 QPS。
<!-- 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>
@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 的 “Discover”,选择 order-request-*索引; -
用 “Visualize” 创建柱状图,X 轴选 “时间(1 秒间隔)”,Y 轴选 “文档数”(即每秒请求数),就是 QPS 趋势图。
-
日志切割:按天切割日志,避免单个日志文件太大(如超过 10GB),导致 Logstash 读取缓慢; -
字段清洗:过滤掉无用日志(如 DEBUG 级别的日志),减少 Elasticsearch 的存储压力。
-
适用场景:当 QPS 突增导致数据库压力大时,通过数据库指标间接判断 QPS(如 MySQL 的连接数、慢查询数)。 -
原理:数据库的请求数和应用 QPS 正相关(如 1 个订单请求对应 2 次 DB 查询),可通过 DB 指标反推应用 QPS。
-- 查看当前连接数(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秒的查询记录为慢查询
show status:该命令会占用 DB 资源,建议每 10 秒执行一次,而非实时执行。
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-
别统计无效请求:过滤健康检查(/actuator/health)、爬虫请求(User-Agent 包含 spider),避免 QPS 虚高; -
并发安全要保证:计数用 AtomicLong或Counter(Micrometer),避免用long变量导致计数不准; -
实时性和性能平衡:Prometheus 拉取间隔设为 1-5 秒,AOP 埋点只统计核心接口,避免过度消耗资源; -
多节点数据汇总:分布式网关或多实例应用,需将 QPS 数据推到统一监控平台(如 Prometheus),避免单节点统计偏差; -
结合业务上下文:QPS 统计要关联业务场景(如秒杀时的 QPS 和日常 QPS 标准不同),避免 “唯 QPS 论”。
往期推荐
我司使用了两年的高效日志打印工具,非常好用!
Redis与本地缓存组合使用...
老司机总结的12条SQL优化方案(非常实用)
中毒太深!离开Spring我居然连最基本的接口都不会写了。。。
开源项目|Java开发身份证号码识别系统
SpringBoot + minio实现分片上传、秒传、续传

