最近接手了一个棘手的生产问题:公司核心业务系统每天都会出现间歇性的页面无法打开情况。作为经历过多次性能调优的老手,我深知这类问题往往不是单一因素导致的,需要系统性地排查。经过初步观察,问题呈现以下特征:
这种"软性"性能问题最让人头疼——它不像服务器宕机那样明显,但确实影响业务。我决定从请求链路的最前端开始,逐步向下排查。
首先在Nginx层面增加耗时统计,这是定位性能瓶颈的第一步。不同于常规的access日志,我们需要精确记录各阶段耗时:
nginx复制http {
# 定制化日志格式(关键字段说明):
# rt=$request_time 请求总耗时(秒,毫秒精度)
# uct=$upstream_connect_time 连接后端服务器耗时
# uht=$upstream_header_time 接收后端首字节耗时
# urt=$upstream_response_time 后端处理总耗时
log_format api_timed '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time uct=$upstream_connect_time '
'uht=$upstream_header_time urt=$upstream_response_time';
# 慢请求限流配置(预防雪崩)
limit_req_zone $binary_remote_addr zone=slow:10m rate=1r/s;
}
这个配置的精妙之处在于:
配置生效后,通过以下命令实时监控:
bash复制# 实时跟踪日志(-f参数)
tail -f /var/log/nginx/api_timed.log
# 按耗时排序分析(awk+sort黄金组合)
awk '{print $NF, $0}' access.log | sort -nr | head -10
这里有个实用技巧:$NF表示最后一个字段(即耗时值),通过这种排序方式可以快速定位最耗时的请求。在实际分析中,我发现/dsp-console/v2/report接口平均耗时达到2.3秒,明显超出合理范围。
经验提示:当uct时间异常高时,可能是网络或连接池问题;当urt高而uct正常时,通常是应用层处理慢。
Nginx日志指向后端处理慢,而经验告诉我,这很可能是数据库问题。我们系统使用Druid连接池,通过以下配置开启慢SQL监控:
java复制@Bean
public StatFilter logSlowSql() {
StatFilter statFilter = new StatFilter();
statFilter.setMergeSql(true); // 合并相似SQL
statFilter.setSlowSqlMillis(300); // 超过300ms视为慢查询
statFilter.setLogSlowSql(true); // 记录日志
return statFilter;
}
关键参数说明:
mergeSql:将相同模式SQL合并统计(防止参数不同导致的统计分散)slowSqlMillis:根据业务特点设置,OLTP系统建议200-500mslogSlowSql:会输出执行耗时、参数等完整信息通过grep分析日志文件:
bash复制# 查找最近3天的慢查询(-a处理二进制日志)
grep -a 'slow sql' con2026-01-0*.log
分析结果中发现一条高频慢查询:
sql复制SELECT id, offer_name FROM t_offer
WHERE status = 1 AND create_time > '2026-01-01'
ORDER BY update_time DESC LIMIT 1000
使用EXPLAIN ANALYZE获取执行计划:
sql复制EXPLAIN ANALYZE
SELECT id, offer_name FROM t_offer
WHERE status = 1 AND create_time > '2026-01-01'
ORDER BY update_time DESC LIMIT 1000;
执行计划显示:
code复制-> Sort: update_time DESC (cost=2874.32 rows=28743)
-> Filter: ((status = 1) and (create_time > '2026-01-01'))
-> Table scan on t_offer (cost=2874.32 rows=28743)
问题诊断:
针对上述问题,设计复合索引:
sql复制ALTER TABLE t_offer ADD INDEX idx_status_ctime_utime
(status, create_time, update_time);
索引设计考量:
优化后执行计划:
code复制-> Index range scan on t_offer using idx_status_ctime_utime
(cost=623.12 rows=1243)
-> Filesort: update_time DESC (cost=1243.12 rows=1243)
进一步优化:
sql复制SELECT id, offer_name FROM t_offer
WHERE status = 1 AND create_time > '2026-01-01'
ORDER BY update_time DESC LIMIT 1000;
改写为:
sql复制SELECT t.id, t.offer_name FROM (
SELECT id FROM t_offer
WHERE status = 1 AND create_time > '2026-01-01'
ORDER BY update_time DESC LIMIT 1000
) tmp JOIN t_offer t ON tmp.id = t.id;
优化原理:
对于深度分页问题:
sql复制-- 低效写法
SELECT * FROM t_order ORDER BY id LIMIT 10000, 20;
-- 优化方案
SELECT * FROM t_order WHERE id > 10000 ORDER BY id LIMIT 20;
使用"游标分页"替代传统分页,避免OFFSET带来的性能损耗。
| 指标 | 优化前 | 优化后 | 提升幅度 |
|---|---|---|---|
| 平均响应时间 | 2300ms | 320ms | 85% |
| 95分位耗时 | 4500ms | 600ms | 86% |
| 数据库QPS | 120 | 350 | 191% |
| CPU使用率 | 75% | 45% | 40% |
Prometheus监控指标:
yaml复制- name: db_slow_queries
query: |
sum(rate(druid_slow_sql_count[1m])) by (sql)
- name: db_query_duration
query: |
histogram_quantile(0.95, sum(rate(druid_sql_execute_time_bucket[1m])) by (le))
告警规则配置:
yaml复制groups:
- name: db.rules
rules:
- alert: SlowQueryIncrease
expr: rate(druid_slow_sql_count[5m]) > 5
for: 10m
过度索引陷阱:
OR条件优化:
sql复制-- 低效写法
SELECT * FROM users WHERE age > 30 OR salary > 10000;
-- 优化方案
SELECT * FROM users WHERE age > 30
UNION
SELECT * FROM users WHERE salary > 10000;
EXPLAIN关键指标解读:
type列:至少达到range级别,避免ALLrows列:估算扫描行数,应尽量小Extra列:警惕Using filesort、Using temporary连接池配置建议:
properties复制# 最佳实践配置
druid.initialSize=5
druid.maxActive=20
druid.maxWait=1000
druid.minIdle=5
druid.timeBetweenEvictionRunsMillis=60000
事务优化原则:
这套优化方案实施后,系统稳定性显著提升。最让我意外的是,原本只以为是数据库问题,实际上通过全链路分析发现了从Nginx配置到应用代码的多层次优化点。这也再次验证了性能优化必须要有系统化思维。